/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package lifecycle

import (
	"fmt"
	"sort"
	"strconv"
	"sync"

	lb "github.com/hyperledger/fabric-protos-go/peer/lifecycle"
	"github.com/hyperledger/fabric/common/chaincode"
	"github.com/hyperledger/fabric/common/util"
	"github.com/hyperledger/fabric/core/chaincode/persistence"
	"github.com/hyperledger/fabric/core/container/externalbuilder"
	"github.com/hyperledger/fabric/core/ledger"
	"github.com/hyperledger/fabric/protoutil"

	"github.com/pkg/errors"
)

type LocalChaincodeInfo struct {
	Definition  *ChaincodeDefinition
	Approved    bool
	InstallInfo *ChaincodeInstallInfo
}

type ChaincodeInstallInfo struct {
	PackageID string
	Type      string
	Path      string
	Label     string
}

type CachedChaincodeDefinition struct {
	Definition  *ChaincodeDefinition
	Approved    bool
	InstallInfo *ChaincodeInstallInfo

	// Hashes is the list of hashed keys in the implicit collection referring to this definition.
	// These hashes are determined by the current sequence number of chaincode definition.  When dirty,
	// these hashes will be empty, and when not, they will be populated.
	Hashes []string
}

type ChannelCache struct {
	Chaincodes map[string]*CachedChaincodeDefinition

	// InterestingHashes is a map of hashed key names to the chaincode name which they affect.
	// These are to be used for the state listener, to mark chaincode definitions dirty when
	// a write is made into the implicit collection for this org.  Interesting hashes are
	// added when marking a definition clean, and deleted when marking it dirty.
	InterestingHashes map[string]string
}

// MetadataHandler is the interface through which the cache drives
// metadata updates for listeners such as gossip and service discovery
type MetadataHandler interface {
	InitializeMetadata(channel string, chaincodes chaincode.MetadataSet)
	UpdateMetadata(channel string, chaincodes chaincode.MetadataSet)
}

type Cache struct {
	definedChaincodes map[string]*ChannelCache
	Resources         *Resources
	MyOrgMSPID        string

	// mutex serializes lifecycle operations globally for the peer.  It will cause a lifecycle update
	// in one channel to potentially affect the throughput of another.  However, relative to standard
	// transactions, lifecycle updates should be quite rare, and this is a RW lock so in general, there
	// should not be contention in the normal case.  Because chaincode package installation is a peer global
	// event, by synchronizing at a peer global level, we drastically simplify accounting for which
	// chaincodes are installed and which channels that installed chaincode is currently in use on.
	mutex sync.RWMutex

	// localChaincodes is a map from the hash of the locally installed chaincode's proto
	// encoded hash (yes, the hash of the hash), to a set of channels, to a set of chaincode
	// definitions which reference this local installed chaincode hash.
	localChaincodes map[string]*LocalChaincode
	eventBroker     *EventBroker
	MetadataHandler MetadataHandler

	chaincodeCustodian *ChaincodeCustodian
}

type LocalChaincode struct {
	Info       *ChaincodeInstallInfo
	References map[string]map[string]*CachedChaincodeDefinition
}

// ToInstalledChaincode converts a LocalChaincode to an InstalledChaincode,
// which is returned by lifecycle queries.
func (l *LocalChaincode) ToInstalledChaincode() *chaincode.InstalledChaincode {
	references := l.createMetadataMapFromReferences()
	return &chaincode.InstalledChaincode{
		PackageID:  l.Info.PackageID,
		Label:      l.Info.Label,
		References: references,
	}
}

// createMetadataMapFromReferences returns a map of channel name to a slice
// of chaincode metadata for the chaincode definitions that reference a
// specific local chaincode. This function should only be called by code that
// holds the lock on the cache.
func (l *LocalChaincode) createMetadataMapFromReferences() map[string][]*chaincode.Metadata {
	references := map[string][]*chaincode.Metadata{}
	for channel, chaincodeMap := range l.References {
		metadata := []*chaincode.Metadata{}
		for cc, cachedDefinition := range chaincodeMap {
			metadata = append(metadata, &chaincode.Metadata{
				Name:    cc,
				Version: cachedDefinition.Definition.EndorsementInfo.Version,
			})
		}
		references[channel] = metadata
	}
	return references
}

func NewCache(resources *Resources, myOrgMSPID string, metadataManager MetadataHandler, custodian *ChaincodeCustodian, ebMetadata *externalbuilder.MetadataProvider) *Cache {
	return &Cache{
		chaincodeCustodian: custodian,
		definedChaincodes:  map[string]*ChannelCache{},
		localChaincodes:    map[string]*LocalChaincode{},
		Resources:          resources,
		MyOrgMSPID:         myOrgMSPID,
		eventBroker:        NewEventBroker(resources.ChaincodeStore, resources.PackageParser, ebMetadata),
		MetadataHandler:    metadataManager,
	}
}

// InitializeLocalChaincodes should be called once after cache creation (timing doesn't matter,
// though already installed chaincodes will not be invokable until it it completes).  Ideally,
// this would be part of the constructor, but, we cannot rely on the chaincode store being created
// before the cache is created.
func (c *Cache) InitializeLocalChaincodes() error {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	ccPackages, err := c.Resources.ChaincodeStore.ListInstalledChaincodes()
	if err != nil {
		return errors.WithMessage(err, "could not list installed chaincodes")
	}

	for _, ccPackage := range ccPackages {
		ccPackageBytes, err := c.Resources.ChaincodeStore.Load(ccPackage.PackageID)
		if err != nil {
			return errors.WithMessagef(err, "could not load chaincode with package ID '%s'", ccPackage.PackageID)
		}
		parsedCCPackage, err := c.Resources.PackageParser.Parse(ccPackageBytes)
		if err != nil {
			return errors.WithMessagef(err, "could not parse chaincode with package ID '%s'", ccPackage.PackageID)
		}
		c.handleChaincodeInstalledWhileLocked(true, parsedCCPackage.Metadata, ccPackage.PackageID)
	}

	logger.Infof("Initialized lifecycle cache with %d already installed chaincodes", len(c.localChaincodes))
	for channelID, chaincodeCache := range c.definedChaincodes {
		approved, installed, runnable := 0, 0, 0
		for _, cachedChaincode := range chaincodeCache.Chaincodes {
			if cachedChaincode.Approved {
				approved++
			}
			if cachedChaincode.InstallInfo != nil {
				installed++
			}
			if cachedChaincode.Approved && cachedChaincode.InstallInfo != nil {
				runnable++
			}
		}

		logger.Infof("Initialized lifecycle cache for channel '%s' with %d chaincodes runnable (%d approved, %d installed)", channelID, runnable, approved, installed)
	}

	return nil
}

// Name returns the name of the listener
func (c *Cache) Name() string {
	return "lifecycle cache listener"
}

// Initialize will populate the set of currently committed chaincode definitions
// for a channel into the cache.  Note, it this looks like a bit of a DRY violation
// with respect to 'Update', but, the error handling is quite different and attempting
// to factor out the common pieces results in a net total of more code.
func (c *Cache) Initialize(channelID string, qe ledger.SimpleQueryExecutor) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()

	publicState := &SimpleQueryExecutorShim{
		Namespace:           LifecycleNamespace,
		SimpleQueryExecutor: qe,
	}

	metadatas, err := c.Resources.Serializer.DeserializeAllMetadata(NamespacesName, publicState)
	if err != nil {
		return errors.WithMessage(err, "could not query namespace metadata")
	}

	dirtyChaincodes := map[string]struct{}{}

	for namespace, metadata := range metadatas {
		switch metadata.Datatype {
		case ChaincodeDefinitionType:
			dirtyChaincodes[namespace] = struct{}{}
		default:
			// non-chaincode
		}
	}

	return c.update(true, channelID, dirtyChaincodes, qe)
}

// HandleChaincodeInstalled should be invoked whenever a new chaincode is installed
func (c *Cache) HandleChaincodeInstalled(md *persistence.ChaincodePackageMetadata, packageID string) {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	c.handleChaincodeInstalledWhileLocked(false, md, packageID)
}

func (c *Cache) handleChaincodeInstalledWhileLocked(initializing bool, md *persistence.ChaincodePackageMetadata, packageID string) {
	// it would be nice to get this value from the serialization package, but it was not obvious
	// how to expose this in a nice way, so we manually compute it.
	encodedCCHash := protoutil.MarshalOrPanic(&lb.StateData{
		Type: &lb.StateData_String_{String_: packageID},
	})
	hashOfCCHash := string(util.ComputeSHA256(encodedCCHash))
	localChaincode, ok := c.localChaincodes[hashOfCCHash]
	if !ok {
		localChaincode = &LocalChaincode{
			References: map[string]map[string]*CachedChaincodeDefinition{},
		}
		c.localChaincodes[hashOfCCHash] = localChaincode
		c.chaincodeCustodian.NotifyInstalled(packageID)
	}
	localChaincode.Info = &ChaincodeInstallInfo{
		PackageID: packageID,
		Type:      md.Type,
		Path:      md.Path,
		Label:     md.Label,
	}
	for channelID, channelCache := range localChaincode.References {
		for chaincodeName, cachedChaincode := range channelCache {
			cachedChaincode.InstallInfo = localChaincode.Info
			logger.Infof("Installed chaincode with package ID '%s' now available on channel %s for chaincode definition %s:%s", packageID, channelID, chaincodeName, cachedChaincode.Definition.EndorsementInfo.Version)
			c.chaincodeCustodian.NotifyInstalledAndRunnable(packageID)
		}
	}

	if !initializing {
		c.eventBroker.ProcessInstallEvent(localChaincode)
		c.handleMetadataUpdates(localChaincode)
	}
}

// HandleStateUpdates is required to implement the ledger state listener interface.  It applies
// any state updates to the cache.
func (c *Cache) HandleStateUpdates(trigger *ledger.StateUpdateTrigger) error {
	c.mutex.Lock()
	defer c.mutex.Unlock()
	channelID := trigger.LedgerID
	updates, ok := trigger.StateUpdates[LifecycleNamespace]
	if !ok {
		return errors.Errorf("no state updates for promised namespace _lifecycle")
	}

	dirtyChaincodes := map[string]struct{}{}

	for _, publicUpdate := range updates.PublicUpdates {
		matches := SequenceMatcher.FindStringSubmatch(publicUpdate.Key)
		if len(matches) != 2 {
			continue
		}

		dirtyChaincodes[matches[1]] = struct{}{}
	}

	channelCache, ok := c.definedChaincodes[channelID]

	// if the channel cache does not yet exist, there are no interesting hashes, so skip
	if ok {
		for collection, privateUpdates := range updates.CollHashUpdates {
			matches := ImplicitCollectionMatcher.FindStringSubmatch(collection)
			if len(matches) != 2 {
				// This is not an implicit collection
				continue
			}

			if matches[1] != c.MyOrgMSPID {
				// This is not our implicit collection
				continue
			}

			for _, privateUpdate := range privateUpdates {
				chaincodeName, ok := channelCache.InterestingHashes[string(privateUpdate.KeyHash)]
				if ok {
					dirtyChaincodes[chaincodeName] = struct{}{}
				}
			}
		}
	}

	err := c.update(false, channelID, dirtyChaincodes, trigger.PostCommitQueryExecutor)
	if err != nil {
		return errors.WithMessage(err, "error updating cache")
	}

	return nil
}

// InterestedInNamespaces is required to implement the ledger state listener interface
func (c *Cache) InterestedInNamespaces() []string {
	return []string{LifecycleNamespace}
}

// StateCommitDone is required to implement the ledger state listener interface
func (c *Cache) StateCommitDone(channelName string) {
	// NOTE: It's extremely tempting to acquire the write lock in HandleStateUpdate
	// and release it here, however, this is asking for a deadlock.  In particular,
	// because the 'write lock' on the state is only held for a short period
	// between HandleStateUpdate and StateCommitDone, it's possible (in fact likely)
	// that a chaincode invocation will acquire a read-lock on the world state, then attempt
	// to get chaincode info from the cache, resulting in a deadlock.  So, we choose
	// potential inconsistency between the cache and the world state which the callers
	// must detect and cope with as necessary.  Note, the cache will always be _at least_
	// as current as the committed state.
	c.eventBroker.ApproveOrDefineCommitted(channelName)
}

// ChaincodeInfo returns the chaincode definition and its install info.
// An error is returned only if either the channel or the chaincode do not exist.
func (c *Cache) ChaincodeInfo(channelID, name string) (*LocalChaincodeInfo, error) {
	if name == LifecycleNamespace {
		ac, ok := c.Resources.ChannelConfigSource.GetStableChannelConfig(channelID).ApplicationConfig()
		if !ok {
			return nil, errors.Errorf("application config does not exist for channel '%s'", channelID)
		}
		if !ac.Capabilities().LifecycleV20() {
			return nil, errors.Errorf("cannot use _lifecycle without V2_0 application capabilities enabled for channel '%s'", channelID)
		}
		return c.getLifecycleSCCChaincodeInfo(channelID)
	}

	c.mutex.RLock()
	defer c.mutex.RUnlock()
	channelChaincodes, ok := c.definedChaincodes[channelID]
	if !ok {
		return nil, errors.Errorf("unknown channel '%s'", channelID)
	}

	cachedChaincode, ok := channelChaincodes.Chaincodes[name]
	if !ok {
		return nil, errors.Errorf("unknown chaincode '%s' for channel '%s'", name, channelID)
	}

	return &LocalChaincodeInfo{
		Definition:  cachedChaincode.Definition,
		InstallInfo: cachedChaincode.InstallInfo,
		Approved:    cachedChaincode.Approved,
	}, nil
}

func (c *Cache) getLifecycleSCCChaincodeInfo(channelID string) (*LocalChaincodeInfo, error) {
	policyBytes, err := c.Resources.LifecycleEndorsementPolicyAsBytes(channelID)
	if err != nil {
		return nil, err
	}

	return &LocalChaincodeInfo{
		Definition: &ChaincodeDefinition{
			ValidationInfo: &lb.ChaincodeValidationInfo{
				ValidationParameter: policyBytes,
			},
			Sequence: 1,
		},
		Approved:    true,
		InstallInfo: &ChaincodeInstallInfo{},
	}, nil
}

// ListInstalledChaincodes returns a slice containing all of the information
// about the installed chaincodes.
func (c *Cache) ListInstalledChaincodes() []*chaincode.InstalledChaincode {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	installedChaincodes := []*chaincode.InstalledChaincode{}
	for _, lc := range c.localChaincodes {
		if lc.Info == nil {
			// the update function adds an entry to localChaincodes
			// even if it isn't yet installed
			continue
		}
		installedChaincodes = append(installedChaincodes, lc.ToInstalledChaincode())
	}

	return installedChaincodes
}

// GetInstalledChaincode returns all of the information about a specific
// installed chaincode.
func (c *Cache) GetInstalledChaincode(packageID string) (*chaincode.InstalledChaincode, error) {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	for _, lc := range c.localChaincodes {
		if lc.Info == nil {
			// the update function adds an entry to localChaincodes
			// even if it isn't yet installed
			continue
		}
		if lc.Info.PackageID == packageID {
			return lc.ToInstalledChaincode(), nil
		}
	}

	return nil, errors.Errorf("could not find chaincode with package id '%s'", packageID)
}

// update should only be called with the write lock already held
func (c *Cache) update(initializing bool, channelID string, dirtyChaincodes map[string]struct{}, qe ledger.SimpleQueryExecutor) error {
	channelCache, ok := c.definedChaincodes[channelID]
	if !ok {
		channelCache = &ChannelCache{
			Chaincodes:        map[string]*CachedChaincodeDefinition{},
			InterestingHashes: map[string]string{},
		}
		c.definedChaincodes[channelID] = channelCache
	}

	publicState := &SimpleQueryExecutorShim{
		Namespace:           LifecycleNamespace,
		SimpleQueryExecutor: qe,
	}

	orgState := &PrivateQueryExecutorShim{
		Namespace:  LifecycleNamespace,
		Collection: ImplicitCollectionNameForOrg(c.MyOrgMSPID),
		State:      qe,
	}

	for name := range dirtyChaincodes {
		logger.Infof("Updating cached definition for chaincode '%s' on channel '%s'", name, channelID)
		cachedChaincode, ok := channelCache.Chaincodes[name]
		if !ok {
			cachedChaincode = &CachedChaincodeDefinition{}
			channelCache.Chaincodes[name] = cachedChaincode
		}

		for _, hash := range cachedChaincode.Hashes {
			delete(channelCache.InterestingHashes, hash)
		}

		exists, chaincodeDefinition, err := c.Resources.ChaincodeDefinitionIfDefined(name, publicState)
		if err != nil {
			return errors.WithMessagef(err, "could not get chaincode definition for '%s' on channel '%s'", name, channelID)
		}

		if !exists {
			// the chaincode definition was deleted, this is currently not
			// possible, but there should be no problems with that.
			delete(channelCache.Chaincodes, name)
			continue
		}

		privateName := fmt.Sprintf("%s#%d", name, chaincodeDefinition.Sequence)
		hashKey := FieldKey(ChaincodeSourcesName, privateName, "PackageID")
		hashOfCCHash, err := orgState.GetStateHash(hashKey)
		if err != nil {
			return errors.WithMessagef(err, "could not check opaque org state for chaincode source hash for '%s' on channel '%s'", name, channelID)
		}

		localChaincode, ok := c.localChaincodes[string(hashOfCCHash)]
		if !ok {
			localChaincode = &LocalChaincode{
				References: map[string]map[string]*CachedChaincodeDefinition{},
			}
			c.localChaincodes[string(hashOfCCHash)] = localChaincode
		}

		if !initializing {
			// check for existing local chaincodes that reference this chaincode
			// name on this channel
			for _, lc := range c.localChaincodes {
				if ref, ok := lc.References[channelID][name]; ok {
					if ref.InstallInfo == nil {
						continue
					}
					if localChaincode.Info != nil {
						if ref.InstallInfo.PackageID == localChaincode.Info.PackageID {
							continue
						}
					}

					// remove existing local chaincode reference, which referred to a
					// previous chaincode definition
					delete(lc.References[channelID], name)
					if len(lc.References[channelID]) == 0 {
						delete(lc.References, channelID)

						// check to see if this "local" chaincode is installed (an entry
						// is added into local chaincodes for active chaincode definition
						// references regardless of whether the peer has a chaincode
						// package installed)
						if lc.Info == nil {
							continue
						}

						// finally, check to see if this chaincode is referenced in any
						// channel. if not, stop the chaincode here
						if len(lc.References) == 0 {
							logger.Debugf("chaincode package with label %s is no longer referenced and will be stopped", lc.Info.Label)
							c.chaincodeCustodian.NotifyStoppable(lc.Info.PackageID)
						}
					}
				}
			}
		}

		cachedChaincode.Definition = chaincodeDefinition
		cachedChaincode.Approved = false

		cachedChaincode.Hashes = []string{
			string(util.ComputeSHA256([]byte(MetadataKey(NamespacesName, privateName)))),
			string(util.ComputeSHA256([]byte(FieldKey(NamespacesName, privateName, "EndorsementInfo")))),
			string(util.ComputeSHA256([]byte(FieldKey(NamespacesName, privateName, "ValidationInfo")))),
			string(util.ComputeSHA256([]byte(FieldKey(NamespacesName, privateName, "Collections")))),
			string(util.ComputeSHA256([]byte(FieldKey(ChaincodeSourcesName, privateName, "PackageID")))),
		}

		for _, hash := range cachedChaincode.Hashes {
			channelCache.InterestingHashes[hash] = name
		}

		ok, err = c.Resources.Serializer.IsSerialized(NamespacesName, privateName, chaincodeDefinition.Parameters(), orgState)

		if err != nil {
			return errors.WithMessagef(err, "could not check opaque org state for '%s' on channel '%s'", name, channelID)
		}
		if !ok {
			logger.Debugf("Channel %s for chaincode definition %s:%s does not have our org's approval", channelID, name, chaincodeDefinition.EndorsementInfo.Version)
			continue
		}

		cachedChaincode.Approved = true

		isLocalPackage, err := c.Resources.Serializer.IsMetadataSerialized(ChaincodeSourcesName, privateName, &ChaincodeLocalPackage{}, orgState)
		if err != nil {
			return errors.WithMessagef(err, "could not check opaque org state for chaincode source for '%s' on channel '%s'", name, channelID)
		}

		if !isLocalPackage {
			logger.Debugf("Channel %s for chaincode definition %s:%s does not have a chaincode source defined", channelID, name, chaincodeDefinition.EndorsementInfo.Version)
			continue
		}

		cachedChaincode.InstallInfo = localChaincode.Info
		if localChaincode.Info != nil {
			logger.Infof("Chaincode with package ID '%s' now available on channel %s for chaincode definition %s:%s", localChaincode.Info.PackageID, channelID, name, cachedChaincode.Definition.EndorsementInfo.Version)
			c.chaincodeCustodian.NotifyInstalledAndRunnable(localChaincode.Info.PackageID)
		} else {
			logger.Debugf("Chaincode definition for chaincode '%s' on channel '%s' is approved, but not installed", name, channelID)
		}

		channelReferences, ok := localChaincode.References[channelID]
		if !ok {
			channelReferences = map[string]*CachedChaincodeDefinition{}
			localChaincode.References[channelID] = channelReferences
		}

		channelReferences[name] = cachedChaincode

		if !initializing {
			c.eventBroker.ProcessApproveOrDefineEvent(channelID, name, cachedChaincode)
		}
	}

	if !initializing {
		c.handleMetadataUpdatesForChannel(channelID)
	}

	return nil
}

// RegisterListener registers an event listener for receiving an event when a chaincode becomes invokable
func (c *Cache) RegisterListener(channelID string, listener ledger.ChaincodeLifecycleEventListener) {
	c.eventBroker.RegisterListener(channelID, listener)
}

func (c *Cache) InitializeMetadata(channel string) {
	c.mutex.RLock()
	defer c.mutex.RUnlock()

	ms, err := c.retrieveChaincodesMetadataSetWhileLocked(channel)
	if err != nil {
		logger.Warningf("no metadata found on channel '%s', err %s", channel, err)
		return
	}

	c.MetadataHandler.InitializeMetadata(channel, ms)
}

func (c *Cache) retrieveChaincodesMetadataSetWhileLocked(channelID string) (chaincode.MetadataSet, error) {
	channelChaincodes, ok := c.definedChaincodes[channelID]
	if !ok {
		return nil, errors.Errorf("unknown channel '%s'", channelID)
	}

	keys := make([]string, 0, len(channelChaincodes.Chaincodes))
	for name := range channelChaincodes.Chaincodes {
		keys = append(keys, name)
	}
	sort.Strings(keys)

	metadataSet := chaincode.MetadataSet{}
	for _, name := range keys {
		def := channelChaincodes.Chaincodes[name]

		// report the sequence as the version to service discovery since
		// the version is no longer required to change when updating any
		// part of the chaincode definition
		metadataSet = append(metadataSet,
			chaincode.Metadata{
				Name:              name,
				Version:           strconv.FormatInt(def.Definition.Sequence, 10),
				Policy:            def.Definition.ValidationInfo.ValidationParameter,
				CollectionsConfig: def.Definition.Collections,
				Approved:          def.Approved,
				Installed:         def.InstallInfo != nil,
			},
		)
	}

	// get the chaincode info for _lifecycle
	lc, err := c.getLifecycleSCCChaincodeInfo(channelID)
	if err != nil {
		return nil, err
	}

	// add it to the metadataset so _lifecycle can also be queried
	// via service discovery
	metadataSet = append(metadataSet,
		chaincode.Metadata{
			Name:      LifecycleNamespace,
			Version:   strconv.FormatInt(lc.Definition.Sequence, 10),
			Policy:    lc.Definition.ValidationInfo.ValidationParameter,
			Approved:  lc.Approved,
			Installed: lc.InstallInfo != nil,
		},
	)

	return metadataSet, nil
}

func (c *Cache) handleMetadataUpdates(localChaincode *LocalChaincode) {
	for channelID := range localChaincode.References {
		c.handleMetadataUpdatesForChannel(channelID)
	}
}

func (c *Cache) handleMetadataUpdatesForChannel(channelID string) {
	ms, err := c.retrieveChaincodesMetadataSetWhileLocked(channelID)
	if err != nil {
		logger.Warningf("no metadata found on channel '%s': %s", channelID, err)
		return
	}

	c.MetadataHandler.UpdateMetadata(channelID, ms)
}
