/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package chaincode

import (
	"io/ioutil"

	"github.com/golang/protobuf/proto"
	pcommon "github.com/hyperledger/fabric-protos-go/common"
	pb "github.com/hyperledger/fabric-protos-go/peer"
	"github.com/hyperledger/fabric/bccsp"
	"github.com/hyperledger/fabric/bccsp/factory"
	"github.com/hyperledger/fabric/common/policydsl"
	"github.com/hyperledger/fabric/core/common/ccpackage"
	"github.com/hyperledger/fabric/internal/pkg/identity"
	mspmgmt "github.com/hyperledger/fabric/msp/mgmt"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/pkg/errors"
	"github.com/spf13/cobra"
)

var (
	chaincodePackageCmd   *cobra.Command
	createSignedCCDepSpec bool
	signCCDepSpec         bool
	instantiationPolicy   string
)

const packageCmdName = "package"

type ccDepSpecFactory func(spec *pb.ChaincodeSpec) (*pb.ChaincodeDeploymentSpec, error)

func defaultCDSFactory(spec *pb.ChaincodeSpec) (*pb.ChaincodeDeploymentSpec, error) {
	return getChaincodeDeploymentSpec(spec, true)
}

// Packager holds the dependencies needed to package
// a chaincode and write it
type Packager struct {
	CDSFactory          ccDepSpecFactory
	ChaincodeCmdFactory *ChaincodeCmdFactory
	Command             *cobra.Command
	Input               *PackageInput
	CryptoProvider      bccsp.BCCSP
}

// PackageInput holds the input parameters for packaging a
// ChaincodeInstallPackage
type PackageInput struct {
	Name                  string
	Version               string
	InstantiationPolicy   string
	CreateSignedCCDepSpec bool
	SignCCDepSpec         bool
	OutputFile            string
	Path                  string
	Type                  string
}

// packageCmd returns the cobra command for packaging chaincode
func packageCmd(cf *ChaincodeCmdFactory, cdsFact ccDepSpecFactory, p *Packager, cryptoProvider bccsp.BCCSP) *cobra.Command {
	chaincodePackageCmd = &cobra.Command{
		Use:       "package [outputfile]",
		Short:     "Package a chaincode",
		Long:      "Package a chaincode and write the package to a file.",
		ValidArgs: []string{"1"},
		RunE: func(cmd *cobra.Command, args []string) error {
			if p == nil {
				// UT will supply its own mock factory
				if cdsFact == nil {
					cdsFact = defaultCDSFactory
				}
				p = &Packager{
					CDSFactory:          cdsFact,
					ChaincodeCmdFactory: cf,
					CryptoProvider:      cryptoProvider,
				}
			}
			p.Command = cmd

			return p.packageChaincode(args)
		},
	}
	flagList := []string{
		"lang",
		"path",
		"ctor",
		"name",
		"version",
		"cc-package",
		"sign",
		"instantiate-policy",
	}
	attachFlags(chaincodePackageCmd, flagList)

	return chaincodePackageCmd
}

// packageChaincode packages the chaincode.
func (p *Packager) packageChaincode(args []string) error {
	if p.Command != nil {
		// Parsing of the command line is done so silence cmd usage
		p.Command.SilenceUsage = true
	}

	if len(args) != 1 {
		return errors.New("output file not specified or invalid number of args (filename should be the only arg)")
	}
	p.setInput(args[0])

	// LSCC package
	return p.packageCC()
}

func (p *Packager) setInput(outputFile string) {
	p.Input = &PackageInput{
		Name:                  chaincodeName,
		Version:               chaincodeVersion,
		InstantiationPolicy:   instantiationPolicy,
		CreateSignedCCDepSpec: createSignedCCDepSpec,
		SignCCDepSpec:         signCCDepSpec,
		OutputFile:            outputFile,
		Path:                  chaincodePath,
		Type:                  chaincodeLang,
	}
}

// packageCC creates the LSCC chaincode packages
// (ChaincodeDeploymentSpec and SignedChaincodeDeploymentSpec)
func (p *Packager) packageCC() error {
	if p.CDSFactory == nil {
		return errors.New("chaincode deployment spec factory not specified")
	}

	var err error
	if p.ChaincodeCmdFactory == nil {
		p.ChaincodeCmdFactory, err = InitCmdFactory(p.Command.Name(), false, false, p.CryptoProvider)
		if err != nil {
			return err
		}
	}
	spec, err := getChaincodeSpec(p.Command)
	if err != nil {
		return err
	}

	cds, err := p.CDSFactory(spec)
	if err != nil {
		return errors.WithMessagef(err, "error getting chaincode code %s", p.Input.Name)
	}

	var bytesToWrite []byte
	if createSignedCCDepSpec {
		bytesToWrite, err = getChaincodeInstallPackage(cds, p.ChaincodeCmdFactory)
		if err != nil {
			return err
		}
	} else {
		bytesToWrite = protoutil.MarshalOrPanic(cds)
	}

	logger.Debugf("Packaged chaincode into deployment spec of size %d, output file %s", len(bytesToWrite), p.Input.OutputFile)
	err = ioutil.WriteFile(p.Input.OutputFile, bytesToWrite, 0700)
	if err != nil {
		logger.Errorf("failed writing deployment spec to file [%s]: [%s]", p.Input.OutputFile, err)
		return err
	}

	return err
}

func getInstantiationPolicy(policy string) (*pcommon.SignaturePolicyEnvelope, error) {
	p, err := policydsl.FromString(policy)
	if err != nil {
		return nil, errors.WithMessagef(err, "invalid policy %s", policy)
	}
	return p, nil
}

// getChaincodeInstallPackage returns either a raw ChaincodeDeploymentSpec or
// a Envelope with ChaincodeDeploymentSpec and (optional) signature
func getChaincodeInstallPackage(cds *pb.ChaincodeDeploymentSpec, cf *ChaincodeCmdFactory) ([]byte, error) {
	var owner identity.SignerSerializer
	// check if we need to sign and set the owner
	if createSignedCCDepSpec && signCCDepSpec {
		if cf.Signer == nil {
			return nil, errors.New("signing identity not found")
		}
		owner = cf.Signer
	}

	ip := instantiationPolicy
	if ip == "" {
		// if an instantiation policy is not given, default
		// to "admin  must sign chaincode instantiation proposals"
		mspid, err := mspmgmt.GetLocalMSP(factory.GetDefault()).GetIdentifier()
		if err != nil {
			return nil, err
		}
		ip = "AND('" + mspid + ".admin')"
	}

	sp, err := getInstantiationPolicy(ip)
	if err != nil {
		return nil, err
	}

	// we get the Envelope of type CHAINCODE_PACKAGE
	objToWrite, err := ccpackage.OwnerCreateSignedCCDepSpec(cds, sp, owner)
	if err != nil {
		return nil, err
	}

	// convert the proto object to bytes
	bytesToWrite, err := proto.Marshal(objToWrite)
	if err != nil {
		return nil, errors.Wrap(err, "error marshalling chaincode package")
	}

	return bytesToWrite, nil
}
