/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package endorsement

import (
	"github.com/hyperledger/fabric/common/policies"
	"github.com/hyperledger/fabric/core/common/privdata"
	"github.com/hyperledger/fabric/gossip/api"
	. "github.com/hyperledger/fabric/protos/discovery"
	"github.com/pkg/errors"
)

func principalsFromCollectionConfig(configBytes []byte) (principalSetsByCollectionName, error) {
	principalSetsByCollections := make(principalSetsByCollectionName)
	if len(configBytes) == 0 {
		return principalSetsByCollections, nil
	}
	ccp, err := privdata.ParseCollectionConfig(configBytes)
	if err != nil {
		return nil, errors.Wrapf(err, "invalid collection bytes")
	}
	for _, colConfig := range ccp.Config {
		staticCol := colConfig.GetStaticCollectionConfig()
		if staticCol == nil {
			// Right now we only support static collections, so if we got something else
			// we should refuse to process further
			return nil, errors.Errorf("expected a static collection but got %v instead", colConfig)
		}
		if staticCol.MemberOrgsPolicy == nil {
			return nil, errors.Errorf("MemberOrgsPolicy of %s is nil", staticCol.Name)
		}
		pol := staticCol.MemberOrgsPolicy.GetSignaturePolicy()
		if pol == nil {
			return nil, errors.Errorf("policy of %s is nil", staticCol.Name)
		}
		var principals policies.PrincipalSet
		// We now extract all principals from the policy
		for _, principal := range pol.Identities {
			principals = append(principals, principal)
		}
		principalSetsByCollections[staticCol.Name] = principals
	}
	return principalSetsByCollections, nil
}

type principalSetsByCollectionName map[string]policies.PrincipalSet

// toIdentityFilter converts this principalSetsByCollectionName mapping to a filter
// which accepts or rejects identities of peers.
func (psbc principalSetsByCollectionName) toIdentityFilter(channel string, evaluator principalEvaluator, cc *ChaincodeCall) (identityFilter, error) {
	var principalSets policies.PrincipalSets
	for _, col := range cc.CollectionNames {
		// Each collection we're interested in should exist in the principalSetsByCollectionName mapping.
		// Otherwise, we have no way of computing a filter because we can't locate the principals the peer identities
		// need to satisfy.
		principalSet, exists := psbc[col]
		if !exists {
			return nil, errors.Errorf("collection %s doesn't exist in collection config for chaincode %s", col, cc.Name)
		}
		principalSets = append(principalSets, principalSet)
	}
	return filterForPrincipalSets(channel, evaluator, principalSets), nil
}

// filterForPrincipalSets creates a filter of peer identities out of the given PrincipalSets
func filterForPrincipalSets(channel string, evaluator principalEvaluator, sets policies.PrincipalSets) identityFilter {
	return func(identity api.PeerIdentityType) bool {
		// Iterate over all principal sets and ensure each principal set
		// authorizes the identity.
		for _, principalSet := range sets {
			if !isIdentityAuthorizedByPrincipalSet(channel, evaluator, principalSet, identity) {
				return false
			}
		}
		return true
	}
}

// isIdentityAuthorizedByPrincipalSet returns whether the given identity satisfies some principal out of the given PrincipalSet
func isIdentityAuthorizedByPrincipalSet(channel string, evaluator principalEvaluator, principalSet policies.PrincipalSet, identity api.PeerIdentityType) bool {
	// We look for a principal which authorizes the identity
	// among all principals in the principalSet
	for _, principal := range principalSet {
		err := evaluator.SatisfiesPrincipal(channel, identity, principal)
		if err != nil {
			continue
		}
		// Else, err is nil, so we found a principal which authorized
		// the given identity.
		return true
	}
	return false
}
