/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package policies

import (
	"bytes"
	"fmt"

	cb "github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric/msp"
	"github.com/hyperledger/fabric/protoutil"
	"go.uber.org/zap/zapcore"
	"google.golang.org/protobuf/proto"
)

type ImplicitMetaPolicy struct {
	Threshold   int
	SubPolicies []Policy

	// Only used for logging
	managers      map[string]*ManagerImpl
	SubPolicyName string
}

// NewImplicitMetaPolicy creates a new policy based on the policy bytes
func NewImplicitMetaPolicy(data []byte, managers map[string]*ManagerImpl) (*ImplicitMetaPolicy, error) {
	definition := &cb.ImplicitMetaPolicy{}
	if err := proto.Unmarshal(data, definition); err != nil {
		return nil, fmt.Errorf("Error unmarshalling to ImplicitMetaPolicy: %s", err)
	}

	subPolicies := make([]Policy, len(managers))

	i := 0
	for _, manager := range managers {
		subPolicies[i], _ = manager.GetPolicy(definition.SubPolicy)
		i++
	}

	var threshold int

	switch definition.Rule {
	case cb.ImplicitMetaPolicy_ANY:
		threshold = 1
	case cb.ImplicitMetaPolicy_ALL:
		threshold = len(subPolicies)
	case cb.ImplicitMetaPolicy_MAJORITY:
		threshold = len(subPolicies)/2 + 1
	}

	// In the special case that there are no policies, consider 0 to be a majority or any
	if len(subPolicies) == 0 {
		threshold = 0
	}

	return &ImplicitMetaPolicy{
		SubPolicies:   subPolicies,
		Threshold:     threshold,
		managers:      managers,
		SubPolicyName: definition.SubPolicy,
	}, nil
}

// EvaluateSignedData takes a set of SignedData and evaluates whether this set of signatures satisfies the policy
func (imp *ImplicitMetaPolicy) EvaluateSignedData(signatureSet []*protoutil.SignedData) error {
	logger.Debugf("This is an implicit meta policy, it will trigger other policy evaluations, whose failures may be benign")
	remaining := imp.Threshold

	defer func() {
		if remaining != 0 {
			// This log message may be large and expensive to construct, so worth checking the log level
			if logger.IsEnabledFor(zapcore.DebugLevel) {
				var b bytes.Buffer
				b.WriteString(fmt.Sprintf("Evaluation Failed: Only %d policies were satisfied, but needed %d of [ ", imp.Threshold-remaining, imp.Threshold))
				for m := range imp.managers {
					b.WriteString(m)
					b.WriteString("/")
					b.WriteString(imp.SubPolicyName)
					b.WriteString(" ")
				}
				b.WriteString("]")
				logger.Debugf(b.String())
			}
		}
	}()

	for _, policy := range imp.SubPolicies {
		if policy.EvaluateSignedData(signatureSet) == nil {
			remaining--
			if remaining == 0 {
				return nil
			}
		}
	}
	if remaining == 0 {
		return nil
	}
	return fmt.Errorf("implicit policy evaluation failed - %d sub-policies were satisfied, but this policy requires %d of the '%s' sub-policies to be satisfied", imp.Threshold-remaining, imp.Threshold, imp.SubPolicyName)
}

// EvaluateIdentities takes an array of identities and evaluates whether
// they satisfy the policy
func (imp *ImplicitMetaPolicy) EvaluateIdentities(identities []msp.Identity) error {
	logger.Debugf("This is an implicit meta policy, it will trigger other policy evaluations, whose failures may be benign")
	remaining := imp.Threshold

	defer func() {
		// This log message may be large and expensive to construct, so worth checking the log level
		if remaining == 0 {
			return
		}
		if !logger.IsEnabledFor(zapcore.DebugLevel) {
			return
		}

		var b bytes.Buffer
		b.WriteString(fmt.Sprintf("Evaluation Failed: Only %d policies were satisfied, but needed %d of [ ", imp.Threshold-remaining, imp.Threshold))
		for m := range imp.managers {
			b.WriteString(m)
			b.WriteString("/")
			b.WriteString(imp.SubPolicyName)
			b.WriteString(" ")
		}
		b.WriteString("]")
		logger.Debugf(b.String())
	}()

	for _, policy := range imp.SubPolicies {
		if policy.EvaluateIdentities(identities) == nil {
			remaining--
			if remaining == 0 {
				return nil
			}
		}
	}
	if remaining == 0 {
		return nil
	}
	return fmt.Errorf("implicit policy evaluation failed - %d sub-policies were satisfied, but this policy requires %d of the '%s' sub-policies to be satisfied", imp.Threshold-remaining, imp.Threshold, imp.SubPolicyName)
}
