/*
Copyright 2021 IBM All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package gateway

import (
	"io"

	gp "github.com/hyperledger/fabric-protos-go-apiv2/gateway"
	ab "github.com/hyperledger/fabric-protos-go-apiv2/orderer"
	"github.com/hyperledger/fabric-protos-go-apiv2/peer"
	"github.com/hyperledger/fabric/core/aclmgmt/resources"
	"github.com/hyperledger/fabric/internal/pkg/gateway/event"
	"github.com/hyperledger/fabric/internal/pkg/gateway/ledger"
	"github.com/hyperledger/fabric/protoutil"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"google.golang.org/protobuf/proto"
)

// ChaincodeEvents supplies a stream of responses, each containing all the events emitted by the requested chaincode
// for a specific block. The streamed responses are ordered by ascending block number. Responses are only returned for
// blocks that contain the requested events, while blocks not containing any of the requested events are skipped. The
// events within each response message are presented in the same order that the transactions that emitted them appear
// within the block.
func (gs *Server) ChaincodeEvents(signedRequest *gp.SignedChaincodeEventsRequest, stream gp.Gateway_ChaincodeEventsServer) error {
	if len(signedRequest.GetRequest()) == 0 {
		return status.Error(codes.InvalidArgument, "a chaincode events request is required")
	}

	request := &gp.ChaincodeEventsRequest{}
	if err := proto.Unmarshal(signedRequest.GetRequest(), request); err != nil {
		return status.Errorf(codes.InvalidArgument, "invalid chaincode events request: %v", err)
	}

	signedData := &protoutil.SignedData{
		Data:      signedRequest.GetRequest(),
		Identity:  request.GetIdentity(),
		Signature: signedRequest.GetSignature(),
	}
	if err := gs.policy.CheckACL(resources.Gateway_ChaincodeEvents, request.GetChannelId(), signedData); err != nil {
		return status.Error(codes.PermissionDenied, err.Error())
	}

	ledger, err := gs.ledgerProvider.Ledger(request.GetChannelId())
	if err != nil {
		return status.Error(codes.NotFound, err.Error())
	}

	startBlock, err := chaincodeEventsStartBlock(ledger, request)
	if err != nil {
		return err
	}

	isMatch := chaincodeEventMatcher(request)

	ledgerIter, err := ledger.GetBlocksIterator(startBlock)
	if err != nil {
		return status.Error(codes.Aborted, err.Error())
	}

	eventsIter := event.NewChaincodeEventsIterator(ledgerIter)
	defer eventsIter.Close()

	for {
		response, err := eventsIter.Next()
		if err != nil {
			return status.Error(codes.Aborted, err.Error())
		}

		var matchingEvents []*peer.ChaincodeEvent
		for _, event := range response.GetEvents() {
			if isMatch(event) {
				matchingEvents = append(matchingEvents, event)
			}
		}

		if len(matchingEvents) == 0 {
			continue
		}

		response.Events = matchingEvents

		if err := stream.Send(response); err != nil {
			if err == io.EOF {
				// Stream closed by the client
				return status.Error(codes.Canceled, err.Error())
			}
			return err
		}
	}
}

func chaincodeEventMatcher(request *gp.ChaincodeEventsRequest) func(event *peer.ChaincodeEvent) bool {
	chaincodeID := request.GetChaincodeId()
	previousTransactionID := request.GetAfterTransactionId()

	if len(previousTransactionID) == 0 {
		return func(event *peer.ChaincodeEvent) bool {
			return event.GetChaincodeId() == chaincodeID
		}
	}

	passedPreviousTransaction := false

	return func(event *peer.ChaincodeEvent) bool {
		if !passedPreviousTransaction {
			if event.TxId == previousTransactionID {
				passedPreviousTransaction = true
			}
			return false
		}

		return event.GetChaincodeId() == chaincodeID
	}
}

func chaincodeEventsStartBlock(ledger ledger.Ledger, request *gp.ChaincodeEventsRequest) (uint64, error) {
	afterTransactionID := request.GetAfterTransactionId()
	if len(afterTransactionID) > 0 {
		if block, err := ledger.GetBlockByTxID(afterTransactionID); err == nil {
			return block.GetHeader().GetNumber(), nil
		}
	}

	return startBlockFromLedgerPosition(ledger, request.GetStartPosition())
}

func startBlockFromLedgerPosition(ledger ledger.Ledger, position *ab.SeekPosition) (uint64, error) {
	switch seek := position.GetType().(type) {
	case nil:
	case *ab.SeekPosition_NextCommit:
	case *ab.SeekPosition_Specified:
		return seek.Specified.GetNumber(), nil
	default:
		return 0, status.Errorf(codes.InvalidArgument, "invalid start position type: %T", seek)
	}

	ledgerInfo, err := ledger.GetBlockchainInfo()
	if err != nil {
		return 0, status.Error(codes.Aborted, err.Error())
	}

	return ledgerInfo.GetHeight(), nil
}
