/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package chaincode

import (
	"context"
	"fmt"
	"io"
	"os"
	"sort"
	"strings"

	"github.com/hyperledger/fabric-lib-go/bccsp"
	cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
	pb "github.com/hyperledger/fabric-protos-go-apiv2/peer"
	lb "github.com/hyperledger/fabric-protos-go-apiv2/peer/lifecycle"
	"github.com/hyperledger/fabric/internal/pkg/identity"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
	"github.com/spf13/viper"
	"google.golang.org/protobuf/proto"
)

// CommitReadinessChecker holds the dependencies needed to
// check whether a chaincode definition is ready to be committed
// on a channel (i.e. has approvals from enough organizations to satisfy
// the lifecycle endorsement policy) and receive the list of orgs that
// have approved the definition.
type CommitReadinessChecker struct {
	Command        *cobra.Command
	EndorserClient pb.EndorserClient
	Input          *CommitReadinessCheckInput
	Signer         identity.SignerSerializer
	Writer         io.Writer
}

// CommitReadinessCheckInput holds all of the input parameters for checking
// whether a chaincode definition is ready to be committed ValidationParameter
// bytes is the (marshalled) endorsement policy when using the default
// endorsement and validation plugins.
type CommitReadinessCheckInput struct {
	ChannelID                string
	Name                     string
	Version                  string
	PackageID                string
	Sequence                 int64
	EndorsementPlugin        string
	ValidationPlugin         string
	ValidationParameterBytes []byte
	CollectionConfigPackage  *pb.CollectionConfigPackage
	InitRequired             bool
	PeerAddresses            []string
	TxID                     string
	OutputFormat             string
	InspectionEnabled        bool
}

// Validate the input for a CheckCommitReadiness proposal
func (c *CommitReadinessCheckInput) Validate() error {
	if c.ChannelID == "" {
		return errors.New("The required parameter 'channelID' is empty. Rerun the command with -C flag")
	}

	if c.Name == "" {
		return errors.New("The required parameter 'name' is empty. Rerun the command with -n flag")
	}

	if c.Version == "" {
		return errors.New("The required parameter 'version' is empty. Rerun the command with -v flag")
	}

	if c.Sequence == 0 {
		return errors.New("The required parameter 'sequence' is empty. Rerun the command with --sequence flag")
	}

	return nil
}

// CheckCommitReadinessCmd returns the cobra command for the
// CheckCommitReadiness lifecycle operation
func CheckCommitReadinessCmd(c *CommitReadinessChecker, cryptoProvider bccsp.BCCSP) *cobra.Command {
	chaincodeCheckCommitReadinessCmd := &cobra.Command{
		Use:   "checkcommitreadiness",
		Short: "Check whether a chaincode definition is ready to be committed on a channel.",
		Long:  "Check whether a chaincode definition is ready to be committed on a channel.",
		RunE: func(cmd *cobra.Command, args []string) error {
			if c == nil {
				// set input from CLI flags
				input, err := c.createInput()
				if err != nil {
					return err
				}

				ccInput := &ClientConnectionsInput{
					CommandName:           cmd.Name(),
					EndorserRequired:      true,
					ChannelID:             channelID,
					PeerAddresses:         peerAddresses,
					TLSRootCertFiles:      tlsRootCertFiles,
					ConnectionProfilePath: connectionProfilePath,
					TLSEnabled:            viper.GetBool("peer.tls.enabled"),
				}

				cc, err := NewClientConnections(ccInput, cryptoProvider)
				if err != nil {
					return err
				}

				c = &CommitReadinessChecker{
					Command:        cmd,
					Input:          input,
					EndorserClient: cc.EndorserClients[0],
					Signer:         cc.Signer,
					Writer:         os.Stdout,
				}
			}

			return c.ReadinessCheck()
		},
	}
	flagList := []string{
		"channelID",
		"name",
		"version",
		"sequence",
		"endorsement-plugin",
		"validation-plugin",
		"signature-policy",
		"channel-config-policy",
		"init-required",
		"collections-config",
		"peerAddresses",
		"tlsRootCertFiles",
		"connectionProfile",
		"output",
		"inspect",
	}
	attachFlags(chaincodeCheckCommitReadinessCmd, flagList)

	return chaincodeCheckCommitReadinessCmd
}

// ReadinessCheck submits a CheckCommitReadiness proposal
// and prints the result.
func (c *CommitReadinessChecker) ReadinessCheck() error {
	err := c.Input.Validate()
	if err != nil {
		return err
	}

	if c.Command != nil {
		// Parsing of the command line is done so silence cmd usage
		c.Command.SilenceUsage = true
	}

	proposal, err := c.createProposal(c.Input.TxID)
	if err != nil {
		return errors.WithMessage(err, "failed to create proposal")
	}

	signedProposal, err := signProposal(proposal, c.Signer)
	if err != nil {
		return errors.WithMessage(err, "failed to create signed proposal")
	}

	// checkcommitreadiness currently only supports a single peer
	proposalResponse, err := c.EndorserClient.ProcessProposal(context.Background(), signedProposal)
	if err != nil {
		return errors.WithMessage(err, "failed to endorse proposal")
	}

	if proposalResponse == nil {
		return errors.New("received nil proposal response")
	}

	if proposalResponse.Response == nil {
		return errors.New("received proposal response with nil response")
	}

	if proposalResponse.Response.Status != int32(cb.Status_SUCCESS) {
		return errors.Errorf("query failed with status: %d - %s", proposalResponse.Response.Status, proposalResponse.Response.Message)
	}

	if strings.ToLower(c.Input.OutputFormat) == "json" {
		// Unmarshal the proposal response to add descriptions to mismatch items
		readinessResult := &lb.CheckCommitReadinessResult{}
		err := proto.Unmarshal(proposalResponse.Response.Payload, readinessResult)
		if err != nil {
			return errors.Wrap(err, "failed to unmarshal readiness result")
		}

		if c.Input.InspectionEnabled {
			for org, mismatches := range readinessResult.Mismatches {
				for i, item := range mismatches.Items {
					mismatches.Items[i] = c.mismatchItemWithDescription(item)
				}
				readinessResult.Mismatches[org] = mismatches
			}
		} else {
			// If InspectionEnabled flag is OFF, clear the Mismatches
			readinessResult.Mismatches = nil
		}

		// Marshal back to proposalResponse
		updatedPayload, err := proto.Marshal(readinessResult)
		if err != nil {
			return errors.Wrap(err, "failed to marshal updated readiness result")
		}
		proposalResponse.Response.Payload = updatedPayload

		return printResponseAsJSON(proposalResponse, &lb.CheckCommitReadinessResult{}, c.Writer)
	}
	return c.printResponse(proposalResponse)
}

// mismatchItemWithDescription returns the item with its corresponding description
func (c *CommitReadinessChecker) mismatchItemWithDescription(item string) string {
	descriptions := map[string]string{
		"ChaincodeParameters": "ChaincodeParameters (Check the Sequence, ChaincodeName)",
		"EndorsementInfo":     "EndorsementInfo (Check the Version, InitRequired, EndorsementPlugin)",
		"ValidationInfo":      "ValidationInfo (Check the ValidationParameter, ValidationPlugin)",
		"Collections":         "Collections (Check the Collections)",
	}

	if description, ok := descriptions[item]; ok {
		return description
	}
	return item
}

// printResponse prints the information included in the response
// from the server as human readable plain-text.
func (c *CommitReadinessChecker) printResponse(proposalResponse *pb.ProposalResponse) error {
	result := &lb.CheckCommitReadinessResult{}
	err := proto.Unmarshal(proposalResponse.Response.Payload, result)
	if err != nil {
		return errors.Wrap(err, "failed to unmarshal proposal response's response payload")
	}

	orgs := []string{}
	for org := range result.Approvals {
		orgs = append(orgs, org)
	}
	sort.Strings(orgs)

	fmt.Fprintf(c.Writer, "Chaincode definition for chaincode '%s', version '%s', sequence '%d' on channel '%s' approval status by org:\n", c.Input.Name, c.Input.Version, c.Input.Sequence, c.Input.ChannelID)
	for _, org := range orgs {
		fmt.Fprintf(c.Writer, "%s: %t", org, result.Approvals[org])

		if mismatch, ok := result.Mismatches[org]; ok && c.Input.InspectionEnabled && len(mismatch.Items) > 0 {
			fmt.Fprintf(c.Writer, " (mismatch: [%s])", strings.Join(result.Mismatches[org].Items, ", "))
		}
		fmt.Fprintln(c.Writer)
	}

	return nil
}

// setInput creates the input struct based on the CLI flags
func (c *CommitReadinessChecker) createInput() (*CommitReadinessCheckInput, error) {
	policyBytes, err := createPolicyBytes(signaturePolicy, channelConfigPolicy)
	if err != nil {
		return nil, err
	}

	ccp, err := createCollectionConfigPackage(collectionsConfigFile)
	if err != nil {
		return nil, err
	}

	input := &CommitReadinessCheckInput{
		ChannelID:                channelID,
		Name:                     chaincodeName,
		Version:                  chaincodeVersion,
		PackageID:                packageID,
		Sequence:                 int64(sequence),
		EndorsementPlugin:        endorsementPlugin,
		ValidationPlugin:         validationPlugin,
		ValidationParameterBytes: policyBytes,
		InitRequired:             initRequired,
		CollectionConfigPackage:  ccp,
		PeerAddresses:            peerAddresses,
		OutputFormat:             output,
		InspectionEnabled:        inspectionEnabled,
	}

	return input, nil
}

func (c *CommitReadinessChecker) createProposal(inputTxID string) (*pb.Proposal, error) {
	args := &lb.CheckCommitReadinessArgs{
		Name:                c.Input.Name,
		Version:             c.Input.Version,
		Sequence:            c.Input.Sequence,
		EndorsementPlugin:   c.Input.EndorsementPlugin,
		ValidationPlugin:    c.Input.ValidationPlugin,
		ValidationParameter: c.Input.ValidationParameterBytes,
		InitRequired:        c.Input.InitRequired,
		Collections:         c.Input.CollectionConfigPackage,
	}

	argsBytes, err := proto.Marshal(args)
	if err != nil {
		return nil, err
	}
	ccInput := &pb.ChaincodeInput{Args: [][]byte{[]byte(checkCommitReadinessFuncName), argsBytes}}

	cis := &pb.ChaincodeInvocationSpec{
		ChaincodeSpec: &pb.ChaincodeSpec{
			ChaincodeId: &pb.ChaincodeID{Name: lifecycleName},
			Input:       ccInput,
		},
	}

	creatorBytes, err := c.Signer.Serialize()
	if err != nil {
		return nil, errors.WithMessage(err, "failed to serialize identity")
	}

	proposal, _, err := protoutil.CreateChaincodeProposalWithTxIDAndTransient(cb.HeaderType_ENDORSER_TRANSACTION, c.Input.ChannelID, cis, creatorBytes, inputTxID, nil)
	if err != nil {
		return nil, errors.WithMessage(err, "failed to create ChaincodeInvocationSpec proposal")
	}

	return proposal, nil
}
