/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package compare

import (
	"bufio"
	"bytes"
	"container/heap"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"os"
	"path/filepath"
	"strings"

	"github.com/hyperledger/fabric/common/ledger/util"
	"github.com/hyperledger/fabric/core/ledger/kvledger"
	"github.com/hyperledger/fabric/core/ledger/kvledger/txmgmt/privacyenabledstate"
	"github.com/hyperledger/fabric/internal/fileutil"
	"github.com/pkg/errors"
	"google.golang.org/protobuf/proto"
)

const (
	// AllPubDiffsByKey - Filename for the json output that contains all public differences ordered by key
	AllPubDiffsByKey = "all_pub_diffs_by_key.json"
	// AllPvtDiffsByKey - Filename for the json output that contains all private differences ordered by key
	AllPvtDiffsByKey = "all_pvt_diffs_by_key.json"
	// FirstDiffsByHeight - Filename for the json output that contains the first n differences ordered by height
	FirstDiffsByHeight = "first_diffs_by_height.json"
)

// Compare - Compares two ledger snapshots and outputs the differences in snapshot records
// This function will throw an error if the output directory already exist in the outputDirLoc
// Function will return count of -1 if the public state and private state hashes are the same
func Compare(snapshotDir1 string, snapshotDir2 string, outputDirLoc string, firstDiffs int) (count int, outputDirPath string, err error) {
	// firstRecords - Slice of diffRecords that stores found differences based on block height, used to generate first n differences output file
	firstRecords := &firstRecords{records: &diffRecordHeap{}, limit: firstDiffs}

	// Check the hashes between two files
	hashPath1 := filepath.Join(snapshotDir1, kvledger.SnapshotSignableMetadataFileName)
	hashPath2 := filepath.Join(snapshotDir2, kvledger.SnapshotSignableMetadataFileName)

	equalPub, equalPvt, channelName, blockHeight, err := hashesEqual(hashPath1, hashPath2)
	if err != nil {
		return 0, "", err
	}
	// Snapshot public and private hashes are the same
	if equalPub && equalPvt {
		return -1, "", nil
	}

	// Output directory creation
	outputDirName := fmt.Sprintf("%s_%d_comparison", channelName, blockHeight)
	outputDirPath = filepath.Join(outputDirLoc, outputDirName)

	empty, err := fileutil.CreateDirIfMissing(outputDirPath)
	if err != nil {
		return 0, "", err
	}
	if !empty {
		switch outputDirLoc {
		case ".":
			outputDirLoc = "the current directory"
		case "..":
			outputDirLoc = "the parent directory"
		}
		return 0, "", errors.Errorf("%s already exists in %s. Choose a different location or remove the existing results. Aborting compare", outputDirName, outputDirLoc)
	}

	// Generate all public data differences between snapshots
	if !equalPub {
		snapshotPubReader1, err := privacyenabledstate.NewSnapshotReader(snapshotDir1,
			privacyenabledstate.PubStateDataFileName, privacyenabledstate.PubStateMetadataFileName)
		if err != nil {
			return 0, "", err
		}
		snapshotPubReader2, err := privacyenabledstate.NewSnapshotReader(snapshotDir2,
			privacyenabledstate.PubStateDataFileName, privacyenabledstate.PubStateMetadataFileName)
		if err != nil {
			return 0, "", err
		}
		outputPubFileWriter, err := findAndWriteDifferences(outputDirPath, AllPubDiffsByKey, channelName, false, snapshotPubReader1, snapshotPubReader2, firstDiffs, firstRecords)
		if err != nil {
			return 0, "", err
		}
		count += outputPubFileWriter.count
	}

	// Generate all private data differences between snapshots
	if !equalPvt {
		snapshotPvtReader1, err := privacyenabledstate.NewSnapshotReader(snapshotDir1,
			privacyenabledstate.PvtStateHashesFileName, privacyenabledstate.PvtStateHashesMetadataFileName)
		if err != nil {
			return 0, "", err
		}
		snapshotPvtReader2, err := privacyenabledstate.NewSnapshotReader(snapshotDir2,
			privacyenabledstate.PvtStateHashesFileName, privacyenabledstate.PvtStateHashesMetadataFileName)
		if err != nil {
			return 0, "", err
		}
		outputPvtFileWriter, err := findAndWriteDifferences(outputDirPath, AllPvtDiffsByKey, channelName, true, snapshotPvtReader1, snapshotPvtReader2, firstDiffs, firstRecords)
		if err != nil {
			return 0, "", err
		}
		count += outputPvtFileWriter.count
	}

	// Generate early differences output file
	if firstDiffs != 0 {
		firstDiffsOutputFileWriter, err := newJSONFileWriter(filepath.Join(outputDirPath, FirstDiffsByHeight), channelName)
		if err != nil {
			return 0, "", err
		}
		for _, r := range firstRecords.getAllRecords() {
			firstDiffsOutputFileWriter.addRecord(*r)
		}
		err = firstDiffsOutputFileWriter.close()
		if err != nil {
			return 0, "", err
		}
	}

	return count, outputDirPath, nil
}

// Finds the differing records between two snapshot data files using SnapshotReaders and saves differences
// to an output file. Simultaneously, keep track of the first n differences.
func findAndWriteDifferences(outputDirPath string, outputFilename string, channelName string, hashed bool,
	snapshotReader1 *privacyenabledstate.SnapshotReader, snapshotReader2 *privacyenabledstate.SnapshotReader,
	firstDiffs int, firstRecords *firstRecords) (outputFileWriter *jsonArrayFileWriter, err error) {
	// Create the output file
	outputFileWriter, err = newJSONFileWriter(filepath.Join(outputDirPath, outputFilename), channelName)
	if err != nil {
		return nil, err
	}

	// Read each snapshot record  to begin looking for differences
	namespace1, snapshotRecord1, err := snapshotReader1.Next()
	if err != nil {
		return nil, err
	}
	namespace2, snapshotRecord2, err := snapshotReader2.Next()
	if err != nil {
		return nil, err
	}

	// Main snapshot record comparison loop
	for snapshotRecord1 != nil && snapshotRecord2 != nil {

		// nsKeys used for comparing snapshot records
		key1 := &nsKey{namespace: namespace1, key: snapshotRecord1.Key}
		key2 := &nsKey{namespace: namespace2, key: snapshotRecord2.Key}

		// Determine the difference in records by comparing nsKeys
		switch nsKeyCompare(key1, key2) {

		case 0: // Keys are the same, look for a difference in records
			if !(proto.Equal(snapshotRecord1, snapshotRecord2)) {
				// Keys are the same but records are different
				diffRecord, err := newDiffRecord(namespace1, hashed, snapshotRecord1, snapshotRecord2)
				if err != nil {
					return nil, err
				}
				// Add difference to output JSON file
				err = outputFileWriter.addRecord(*diffRecord)
				if err != nil {
					return nil, err
				}
				if firstDiffs != 0 {
					firstRecords.addRecord(diffRecord)
				}
			}
			// Advance both snapshot readers
			namespace1, snapshotRecord1, err = snapshotReader1.Next()
			if err != nil {
				return nil, err
			}
			namespace2, snapshotRecord2, err = snapshotReader2.Next()
			if err != nil {
				return nil, err
			}

		case 1: // Key 1 is bigger, snapshot 1 is missing a record
			// Snapshot 2 has the missing record, add missing to output JSON file
			diffRecord, err := newDiffRecord(namespace2, hashed, nil, snapshotRecord2)
			if err != nil {
				return nil, err
			}
			// Add missing record to output JSON file
			err = outputFileWriter.addRecord(*diffRecord)
			if err != nil {
				return nil, err
			}
			if firstDiffs != 0 {
				firstRecords.addRecord(diffRecord)
			}
			// Advance the second snapshot reader
			namespace2, snapshotRecord2, err = snapshotReader2.Next()
			if err != nil {
				return nil, err
			}

		case -1: // Key 2 is bigger, snapshot 2 is missing a record
			// Snapshot 1 has the missing record, add missing to output JSON file
			diffRecord, err := newDiffRecord(namespace1, hashed, snapshotRecord1, nil)
			if err != nil {
				return nil, err
			}
			// Add missing record to output JSON file
			err = outputFileWriter.addRecord(*diffRecord)
			if err != nil {
				return nil, err
			}
			if firstDiffs != 0 {
				firstRecords.addRecord(diffRecord)
			}
			// Advance the first snapshot reader
			namespace1, snapshotRecord1, err = snapshotReader1.Next()
			if err != nil {
				return nil, err
			}

		default:
			panic("unexpected code path: bug")
		}
	}

	// Check for tailing records
	switch {

	case snapshotRecord1 != nil: // Snapshot 2 is missing a record
		for snapshotRecord1 != nil {
			// Add missing to output JSON file
			diffRecord, err := newDiffRecord(namespace1, hashed, snapshotRecord1, nil)
			if err != nil {
				return nil, err
			}
			err = outputFileWriter.addRecord(*diffRecord)
			if err != nil {
				return nil, err
			}
			if firstDiffs != 0 {
				firstRecords.addRecord(diffRecord)
			}
			namespace1, snapshotRecord1, err = snapshotReader1.Next()
			if err != nil {
				return nil, err
			}
		}

	case snapshotRecord2 != nil: // Snapshot 1 is missing a record
		for snapshotRecord2 != nil {
			// Add missing to output JSON file
			diffRecord, err := newDiffRecord(namespace2, hashed, nil, snapshotRecord2)
			if err != nil {
				return nil, err
			}
			err = outputFileWriter.addRecord(*diffRecord)
			if err != nil {
				return nil, err
			}
			if firstDiffs != 0 {
				firstRecords.addRecord(diffRecord)
			}
			namespace2, snapshotRecord2, err = snapshotReader2.Next()
			if err != nil {
				return nil, err
			}
		}
	}

	err = outputFileWriter.close()
	if err != nil {
		return nil, err
	}

	return outputFileWriter, nil
}

// firstRecords is a struct used to hold only the earliest records up to a given limit
// Basically a max heap with a size limit
type firstRecords struct {
	records *diffRecordHeap
	limit   int
}

func (s *firstRecords) addRecord(r *diffRecord) {
	if s.limit == 0 {
		return
	}
	// Limit not reached, can still add elements
	if len(*s.records) < s.limit {
		heap.Push(s.records, r)
		// Limit reached, check new record against root
	} else {
		// Only add records earlier than the root
		if r.earlierThan((*s.records)[0]) {
			heap.Push(s.records, r)
			heap.Pop(s.records)
		}
	}
}

// Returns list of diffRecords currently in heap sorted from earliest to latest, will empty the heap
func (s *firstRecords) getAllRecords() []*diffRecord {
	n := len(*s.records)
	t := make([]*diffRecord, n)
	for i := 0; i < n; i++ {
		r := heap.Pop(s.records)
		t[n-i-1] = r.(*diffRecord)
	}
	return t
}

// diffRecordHeap implements heap interface where later records have higher priority than earlier records
type diffRecordHeap []*diffRecord

func (s diffRecordHeap) Len() int {
	return len(s)
}

func (s diffRecordHeap) Swap(i, j int) {
	s[i], s[j] = s[j], s[i]
}

func (s diffRecordHeap) Less(i, j int) bool {
	return !(s[i]).earlierThan(s[j])
}

func (s *diffRecordHeap) Push(x interface{}) {
	*s = append(*s, x.(*diffRecord))
}

func (s *diffRecordHeap) Pop() interface{} {
	popped := (*s)[len(*s)-1]
	*s = (*s)[0 : len(*s)-1]
	return popped
}

// diffRecord represents a diverging record in json
type diffRecord struct {
	Namespace string          `json:"namespace,omitempty"`
	Key       string          `json:"key,omitempty"`
	Hashed    bool            `json:"hashed"`
	Record1   *snapshotRecord `json:"snapshotrecord1"`
	Record2   *snapshotRecord `json:"snapshotrecord2"`
}

// Creates a new diffRecord
func newDiffRecord(namespace string, hashed bool, record1 *privacyenabledstate.SnapshotRecord,
	record2 *privacyenabledstate.SnapshotRecord) (*diffRecord, error) {
	var s1, s2 *snapshotRecord = nil, nil // snapshot records
	var k string                          // key
	var err error

	// Snapshot2 has a missing record
	if record1 != nil {
		k = bytesToString(record1.Key, hashed)
		s1, err = newSnapshotRecord(record1, hashed)
		if err != nil {
			return nil, err
		}
	}
	// Snapshot1 has a missing record
	if record2 != nil {
		k = bytesToString(record2.Key, hashed)
		s2, err = newSnapshotRecord(record2, hashed)
		if err != nil {
			return nil, err
		}
	}

	return &diffRecord{
		Namespace: namespace,
		Key:       k,
		Hashed:    hashed,
		Record1:   s1,
		Record2:   s2,
	}, nil
}

// Get height from a diffRecord
func (d *diffRecord) getHeight() (blockNum uint64, txNum uint64) {
	r := earlierSSRecord(d.Record1, d.Record2)
	return r.BlockNum, r.TxNum
}

// Returns true if d is an earlier diffRecord than e
func (d *diffRecord) earlierThan(e *diffRecord) bool {
	dBlockNum, dTxNum := d.getHeight()
	eBlockNum, eTxNum := e.getHeight()

	if dBlockNum == eBlockNum {
		return dTxNum <= eTxNum
	}
	return dBlockNum < eBlockNum
}

// snapshotRecord represents the data of a snapshot record in json
type snapshotRecord struct {
	Value    string `json:"value"`
	BlockNum uint64 `json:"blockNum"`
	TxNum    uint64 `json:"txNum"`
}

// Returns the snapshotRecord with the earlier height
func earlierSSRecord(r1 *snapshotRecord, r2 *snapshotRecord) *snapshotRecord {
	if r1 == nil {
		return r2
	}
	if r2 == nil {
		return r1
	}
	// Determine earlier record by block height
	if r1.BlockNum < r2.BlockNum {
		return r1
	}
	if r2.BlockNum < r1.BlockNum {
		return r2
	}
	// Record block heights are the same, determine earlier transaction
	if r1.TxNum < r2.TxNum {
		return r1
	}
	return r2
}

// Creates a new SnapshotRecord
func newSnapshotRecord(record *privacyenabledstate.SnapshotRecord, hashed bool) (*snapshotRecord, error) {
	blockNum, txNum, err := heightFromBytes(record.Version)
	if err != nil {
		return nil, err
	}

	return &snapshotRecord{
		Value:    bytesToString(record.Value, hashed),
		BlockNum: blockNum,
		TxNum:    txNum,
	}, nil
}

// Converts byte slice to string, respects hashed data integrity
// If data is hashed, converts byte slice to hexadecimal string encoding
func bytesToString(v []byte, h bool) string {
	if h {
		return hex.EncodeToString(v)
	} else {
		return string(v)
	}
}

// Obtain the block height and transaction height of a snapshot from its version bytes
func heightFromBytes(b []byte) (uint64, uint64, error) {
	blockNum, n1, err := util.DecodeOrderPreservingVarUint64(b)
	if err != nil {
		return 0, 0, err
	}
	txNum, _, err := util.DecodeOrderPreservingVarUint64(b[n1:])
	if err != nil {
		return 0, 0, err
	}

	return blockNum, txNum, nil
}

// nsKey is used to compare between snapshot records using both the namespace and key
type nsKey struct {
	namespace string
	key       []byte
}

// Compares two nsKeys
// Returns:
// -1 if k1 > k2
// 1 if k1 < k2
// 0 if k1 == k2
func nsKeyCompare(k1, k2 *nsKey) int {
	res := strings.Compare(k1.namespace, k2.namespace)
	if res != 0 {
		return res
	}
	return bytes.Compare(k1.key, k2.key)
}

// Extracts metadata from provided filepath
func readMetadata(fpath string) (*kvledger.SnapshotSignableMetadata, error) {
	var mdata kvledger.SnapshotSignableMetadata

	// Open file
	f, err := os.ReadFile(fpath)
	if err != nil {
		return nil, err
	}
	// Unmarshal bytes
	err = json.Unmarshal(f, &mdata)
	if err != nil {
		return nil, err
	}

	return &mdata, nil
}

// Compares hashes of snapshots to determine if they can be compared, then returns channel name and block height for the output directory name
// Return values:
// equalPub - True if snapshot public data hashes are the same, false otherwise. If true, public differences will not be generated.
// equalPvt - True if snapshot private data hashes are the same, false otherwise. If true, private differences will not be generated.
// chName - Channel name shared between snapshots, used to name output directory. If channel names are not the same, no comparison is made.
// lastBN - Block height shared between snapshots, used to name output directory. If block heights are not the same, no comparison is made.
func hashesEqual(fpath1 string, fpath2 string) (equalPub bool, equalPvt bool, chName string, lastBN uint64, err error) {
	var mdata1, mdata2 *kvledger.SnapshotSignableMetadata

	// Read metadata from snapshot metadata filepaths
	mdata1, err = readMetadata(fpath1)
	if err != nil {
		return false, false, "", 0, err
	}
	mdata2, err = readMetadata(fpath2)
	if err != nil {
		return false, false, "", 0, err
	}

	if mdata1.ChannelName != mdata2.ChannelName {
		return false, false, "", 0, errors.Errorf("the supplied snapshots appear to be non-comparable. Channel names do not match."+
			"\nSnapshot1 channel name: %s\nSnapshot2 channel name: %s", mdata1.ChannelName, mdata2.ChannelName)
	}

	if mdata1.LastBlockNumber != mdata2.LastBlockNumber {
		return false, false, "", 0, errors.Errorf("the supplied snapshots appear to be non-comparable. Last block numbers do not match."+
			"\nSnapshot1 last block number: %v\nSnapshot2 last block number: %v", mdata1.LastBlockNumber, mdata2.LastBlockNumber)
	}

	if mdata1.LastBlockHashInHex != mdata2.LastBlockHashInHex {
		return false, false, "", 0, errors.Errorf("the supplied snapshots appear to be non-comparable. Last block hashes do not match."+
			"\nSnapshot1 last block hash: %s\nSnapshot2 last block hash: %s", mdata1.LastBlockHashInHex, mdata2.LastBlockHashInHex)
	}

	if mdata1.StateDBType != mdata2.StateDBType {
		return false, false, "", 0, errors.Errorf("the supplied snapshots appear to be non-comparable. State db types do not match."+
			"\nSnapshot1 state db type: %s\nSnapshot2 state db type: %s", mdata1.StateDBType, mdata2.StateDBType)
	}

	pubDataHash1 := mdata1.FilesAndHashes[privacyenabledstate.PubStateDataFileName]
	pubMdataHash1 := mdata1.FilesAndHashes[privacyenabledstate.PubStateMetadataFileName]
	pvtDataHash1 := mdata1.FilesAndHashes[privacyenabledstate.PvtStateHashesFileName]
	pvtMdataHash1 := mdata1.FilesAndHashes[privacyenabledstate.PvtStateHashesMetadataFileName]

	pubDataHash2 := mdata2.FilesAndHashes[privacyenabledstate.PubStateDataFileName]
	pubMdataHash2 := mdata2.FilesAndHashes[privacyenabledstate.PubStateMetadataFileName]
	pvtDataHash2 := mdata2.FilesAndHashes[privacyenabledstate.PvtStateHashesFileName]
	pvtMdataHash2 := mdata2.FilesAndHashes[privacyenabledstate.PvtStateHashesMetadataFileName]

	equalPub = pubDataHash1 == pubDataHash2 && pubMdataHash1 == pubMdataHash2
	equalPvt = pvtDataHash1 == pvtDataHash2 && pvtMdataHash1 == pvtMdataHash2
	return equalPub, equalPvt, mdata1.ChannelName, mdata1.LastBlockNumber, nil
}

// jsonArrayFileWriter writes a list of diffRecords to a json file
type jsonArrayFileWriter struct {
	file               *os.File
	buffer             *bufio.Writer
	encoder            *json.Encoder
	firstRecordWritten bool
	count              int
}

func newJSONFileWriter(filePath string, ledgerid string) (*jsonArrayFileWriter, error) {
	f, err := os.OpenFile(filePath, os.O_WRONLY|os.O_CREATE, 0o644)
	if err != nil {
		return nil, err
	}

	b := bufio.NewWriter(f)
	// Opening json brace, ledgerid, and beginning of diffRecord list
	openingStr := fmt.Sprintf("{\n\"ledgerid\":\"%s\",\n\"diffRecords\":[\n", ledgerid)
	_, err = b.Write([]byte(openingStr))
	if err != nil {
		return nil, err
	}

	return &jsonArrayFileWriter{
		file:    f,
		buffer:  b,
		encoder: json.NewEncoder(b),
	}, nil
}

func (w *jsonArrayFileWriter) addRecord(r interface{}) error {
	// Add commas for records after the first in the list
	if w.firstRecordWritten {
		_, err := w.buffer.Write([]byte(",\n"))
		if err != nil {
			return err
		}
	} else {
		w.firstRecordWritten = true
	}

	err := w.encoder.Encode(r)
	if err != nil {
		return err
	}
	w.count++

	return nil
}

func (w *jsonArrayFileWriter) close() error {
	_, err := w.buffer.Write([]byte("]\n}\n"))
	if err != nil {
		return err
	}

	err = w.buffer.Flush()
	if err != nil {
		return err
	}

	err = w.file.Sync()
	if err != nil {
		return err
	}

	err = fileutil.SyncParentDir(w.file.Name())
	if err != nil {
		return err
	}

	return w.file.Close()
}
