/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package privdata

import (
	"fmt"

	"github.com/golang/protobuf/proto"
	"github.com/hyperledger/fabric-protos-go/common"
	"github.com/hyperledger/fabric-protos-go/ledger/rwset"
	"github.com/hyperledger/fabric-protos-go/ledger/rwset/kvrwset"
	"github.com/hyperledger/fabric-protos-go/msp"
	"github.com/hyperledger/fabric-protos-go/peer"
	"github.com/hyperledger/fabric/core/ledger"
	"github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/rwsetutil"
	privdatacommon "github.com/hyperledger/fabric/gossip/privdata/common"
)

type txValidationFlags []uint8

type blockFactory struct {
	channelID     string
	transactions  [][]byte
	metadataSize  int
	lacksMetadata bool
	invalidTxns   map[int]struct{}
}

func (bf *blockFactory) AddTxn(txID string, nsName string, hash []byte, collections ...string) *blockFactory {
	return bf.AddTxnWithEndorsement(txID, nsName, hash, "", true, collections...)
}

func (bf *blockFactory) AddReadOnlyTxn(txID string, nsName string, hash []byte, collections ...string) *blockFactory {
	return bf.AddTxnWithEndorsement(txID, nsName, hash, "", false, collections...)
}

func (bf *blockFactory) AddTxnWithEndorsement(txID string, nsName string, hash []byte, org string, hasWrites bool, collections ...string) *blockFactory {
	txn := &peer.Transaction{
		Actions: []*peer.TransactionAction{
			{},
		},
	}
	nsRWSet := sampleNsRwSet(nsName, hash, collections...)
	if !hasWrites {
		nsRWSet = sampleReadOnlyNsRwSet(nsName, hash, collections...)
	}
	txrws := rwsetutil.TxRwSet{
		NsRwSets: []*rwsetutil.NsRwSet{nsRWSet},
	}

	b, err := txrws.ToProtoBytes()
	if err != nil {
		panic(err)
	}
	ccAction := &peer.ChaincodeAction{
		Results: b,
	}

	ccActionBytes, err := proto.Marshal(ccAction)
	if err != nil {
		panic(err)
	}
	pRespPayload := &peer.ProposalResponsePayload{
		Extension: ccActionBytes,
	}

	respPayloadBytes, err := proto.Marshal(pRespPayload)
	if err != nil {
		panic(err)
	}

	ccPayload := &peer.ChaincodeActionPayload{
		Action: &peer.ChaincodeEndorsedAction{
			ProposalResponsePayload: respPayloadBytes,
		},
	}

	if org != "" {
		sID := &msp.SerializedIdentity{Mspid: org, IdBytes: []byte(fmt.Sprintf("p0%s", org))}
		b, _ := proto.Marshal(sID)
		ccPayload.Action.Endorsements = []*peer.Endorsement{
			{
				Endorser: b,
			},
		}
	}

	ccPayloadBytes, err := proto.Marshal(ccPayload)
	if err != nil {
		panic(err)
	}

	txn.Actions[0].Payload = ccPayloadBytes
	txBytes, _ := proto.Marshal(txn)

	cHdr := &common.ChannelHeader{
		TxId:      txID,
		Type:      int32(common.HeaderType_ENDORSER_TRANSACTION),
		ChannelId: bf.channelID,
	}
	cHdrBytes, _ := proto.Marshal(cHdr)
	commonPayload := &common.Payload{
		Header: &common.Header{
			ChannelHeader: cHdrBytes,
		},
		Data: txBytes,
	}

	payloadBytes, _ := proto.Marshal(commonPayload)
	envp := &common.Envelope{
		Payload: payloadBytes,
	}
	envelopeBytes, _ := proto.Marshal(envp)

	bf.transactions = append(bf.transactions, envelopeBytes)
	return bf
}

func (bf *blockFactory) create() *common.Block {
	defer func() {
		*bf = blockFactory{channelID: bf.channelID}
	}()
	block := &common.Block{
		Header: &common.BlockHeader{
			Number: 1,
		},
		Data: &common.BlockData{
			Data: bf.transactions,
		},
	}

	if bf.lacksMetadata {
		return block
	}
	block.Metadata = &common.BlockMetadata{
		Metadata: make([][]byte, common.BlockMetadataIndex_TRANSACTIONS_FILTER+1),
	}
	if bf.metadataSize > 0 {
		block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] = make([]uint8, bf.metadataSize)
	} else {
		block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER] = make([]uint8, len(block.Data.Data))
	}

	for txSeqInBlock := range bf.invalidTxns {
		block.Metadata.Metadata[common.BlockMetadataIndex_TRANSACTIONS_FILTER][txSeqInBlock] = uint8(peer.TxValidationCode_INVALID_ENDORSER_TRANSACTION)
	}

	return block
}

func (bf *blockFactory) withoutMetadata() *blockFactory {
	bf.lacksMetadata = true
	return bf
}

func (bf *blockFactory) withMetadataSize(mdSize int) *blockFactory {
	bf.metadataSize = mdSize
	return bf
}

func (bf *blockFactory) withInvalidTxns(sequences ...int) *blockFactory {
	bf.invalidTxns = make(map[int]struct{})
	for _, seq := range sequences {
		bf.invalidTxns[seq] = struct{}{}
	}
	return bf
}

func sampleNsRwSet(ns string, hash []byte, collections ...string) *rwsetutil.NsRwSet {
	nsRwSet := &rwsetutil.NsRwSet{NameSpace: ns,
		KvRwSet: sampleKvRwSet(),
	}
	for _, col := range collections {
		nsRwSet.CollHashedRwSets = append(nsRwSet.CollHashedRwSets, sampleCollHashedRwSet(col, hash, true))
	}
	return nsRwSet
}

func sampleReadOnlyNsRwSet(ns string, hash []byte, collections ...string) *rwsetutil.NsRwSet {
	nsRwSet := &rwsetutil.NsRwSet{NameSpace: ns,
		KvRwSet: sampleKvRwSet(),
	}
	for _, col := range collections {
		nsRwSet.CollHashedRwSets = append(nsRwSet.CollHashedRwSets, sampleCollHashedRwSet(col, hash, false))
	}
	return nsRwSet
}

func sampleKvRwSet() *kvrwset.KVRWSet {
	rqi1 := &kvrwset.RangeQueryInfo{StartKey: "k0", EndKey: "k9", ItrExhausted: true}
	rwsetutil.SetRawReads(rqi1, []*kvrwset.KVRead{
		{Key: "k1", Version: &kvrwset.Version{BlockNum: 1, TxNum: 1}},
		{Key: "k2", Version: &kvrwset.Version{BlockNum: 1, TxNum: 2}},
	})

	rqi2 := &kvrwset.RangeQueryInfo{StartKey: "k00", EndKey: "k90", ItrExhausted: true}
	rwsetutil.SetMerkelSummary(rqi2, &kvrwset.QueryReadsMerkleSummary{MaxDegree: 5, MaxLevel: 4, MaxLevelHashes: [][]byte{[]byte("Hash-1"), []byte("Hash-2")}})
	return &kvrwset.KVRWSet{
		Reads:            []*kvrwset.KVRead{{Key: "key1", Version: &kvrwset.Version{BlockNum: 1, TxNum: 1}}},
		RangeQueriesInfo: []*kvrwset.RangeQueryInfo{rqi1},
		Writes:           []*kvrwset.KVWrite{{Key: "key2", IsDelete: false, Value: []byte("value2")}},
	}
}

func sampleCollHashedRwSet(collectionName string, hash []byte, hasWrites bool) *rwsetutil.CollHashedRwSet {
	collHashedRwSet := &rwsetutil.CollHashedRwSet{
		CollectionName: collectionName,
		HashedRwSet: &kvrwset.HashedRWSet{
			HashedReads: []*kvrwset.KVReadHash{
				{KeyHash: []byte("Key-1-hash"), Version: &kvrwset.Version{BlockNum: 1, TxNum: 2}},
				{KeyHash: []byte("Key-2-hash"), Version: &kvrwset.Version{BlockNum: 2, TxNum: 3}},
			},
		},
		PvtRwSetHash: hash,
	}
	if hasWrites {
		collHashedRwSet.HashedRwSet.HashedWrites = []*kvrwset.KVWriteHash{
			{KeyHash: []byte("Key-3-hash"), ValueHash: []byte("value-3-hash"), IsDelete: false},
			{KeyHash: []byte("Key-4-hash"), ValueHash: []byte("value-4-hash"), IsDelete: true},
		}
	}
	return collHashedRwSet
}

func extractCollectionConfig(configPackage *peer.CollectionConfigPackage, collectionName string) *peer.CollectionConfig {
	for _, config := range configPackage.Config {
		switch cconf := config.Payload.(type) {
		case *peer.CollectionConfig_StaticCollectionConfig:
			if cconf.StaticCollectionConfig.Name == collectionName {
				return config
			}
		default:
			return nil
		}
	}
	return nil
}

type pvtDataFactory struct {
	data []*ledger.TxPvtData
}

func (df *pvtDataFactory) addRWSet() *pvtDataFactory {
	seqInBlock := uint64(len(df.data))
	df.data = append(df.data, &ledger.TxPvtData{
		SeqInBlock: seqInBlock,
		WriteSet:   &rwset.TxPvtReadWriteSet{},
	})
	return df
}

func (df *pvtDataFactory) addNSRWSet(namespace string, collections ...string) *pvtDataFactory {
	nsrws := &rwset.NsPvtReadWriteSet{
		Namespace: namespace,
	}
	for _, col := range collections {
		nsrws.CollectionPvtRwset = append(nsrws.CollectionPvtRwset, &rwset.CollectionPvtReadWriteSet{
			CollectionName: col,
			Rwset:          []byte("rws-pre-image"),
		})
	}
	df.data[len(df.data)-1].WriteSet.NsPvtRwset = append(df.data[len(df.data)-1].WriteSet.NsPvtRwset, nsrws)
	return df
}

func (df *pvtDataFactory) create() []*ledger.TxPvtData {
	defer func() {
		df.data = nil
	}()
	return df.data
}

type digestsAndSourceFactory struct {
	d2s     dig2sources
	lastDig *privdatacommon.DigKey
}

func (f *digestsAndSourceFactory) mapDigest(dig *privdatacommon.DigKey) *digestsAndSourceFactory {
	f.lastDig = dig
	return f
}

func (f *digestsAndSourceFactory) toSources(peers ...string) *digestsAndSourceFactory {
	if f.d2s == nil {
		f.d2s = make(dig2sources)
	}
	var endorsements []*peer.Endorsement
	for _, p := range peers {
		endorsements = append(endorsements, &peer.Endorsement{
			Endorser: []byte(p),
		})
	}
	f.d2s[*f.lastDig] = endorsements
	return f
}

func (f *digestsAndSourceFactory) create() dig2sources {
	return f.d2s
}
