/*
Copyright Hitachi, Ltd. 2023 All rights reserved.

SPDX-License-Identifier: Apache-2.0
*/

package verify

import (
	"bytes"
	"fmt"
	"path/filepath"

	"github.com/hyperledger/fabric-lib-go/common/metrics/disabled"
	"github.com/hyperledger/fabric-protos-go-apiv2/common"
	"github.com/hyperledger/fabric/common/ledger/blkstorage"
	"github.com/hyperledger/fabric/core/ledger/kvledger"
	"github.com/hyperledger/fabric/internal/fileutil"
	"github.com/hyperledger/fabric/internal/ledgerutil/jsonrw"
	"github.com/hyperledger/fabric/protoutil"
	"github.com/pkg/errors"
)

const (
	ledgersDataDirName      = "ledgersData"
	blockResultJsonFilename = "blocks.json"

	errorDataHashMismatch = "DataHash mismatch"
	errorPrevHashMismatch = "PreviousHash mismatch"
)

// VerifyLedger - Verifies the integrity of ledgers in a single peer
// This function verifies the ledgers specified in blockStorePath, specifically the integrity of
// the hash value (DataHash) and the previous hash value (PreviousHash) in every block.
// It creates a directory in outputDir for each ledger and stores the check result as a JSON file
// in the directory.
func VerifyLedger(blockStorePath string, outputDir string) (bool, error) {
	// Save whether any error is detected
	ledgerErrorFound := false

	// Get the block store provider
	blockStoreProvider, err := getBlockStoreProvider(blockStorePath)
	if err != nil {
		return false, err
	}
	defer blockStoreProvider.Close()

	// Retrieve the list of the ledgers (i.e. channels) available
	list, err := blockStoreProvider.List()
	if err != nil {
		return false, err
	}

	// Iterate over the channels
	for _, ledgerId := range list {
		// Create Output Directory
		outputDirName := fmt.Sprintf("%s_verification_result", ledgerId)
		outputDirPath := filepath.Join(outputDir, outputDirName)

		empty, err := fileutil.CreateDirIfMissing(outputDirPath)
		if err != nil {
			return false, err
		}
		if !empty {
			return false, errors.Errorf("%s already exists in %s. Choose a different location or remove the existing results. Aborting verify", outputDirName, outputDir)
		}

		// Open block provider
		store, err := blockStoreProvider.Open(ledgerId)
		if err != nil {
			return false, err
		}
		defer store.Shutdown()

		info, err := store.GetBlockchainInfo()
		if err != nil {
			return false, err
		}

		firstBlockNumber := getFirstBlockNumberInLedger(info)

		iterator, err := store.RetrieveBlocks(firstBlockNumber)
		if err != nil {
			return false, err
		}
		defer iterator.Close()

		// Open the result JSON for blocks
		blockResult, err := newBlockResultOutput(outputDirPath)
		if err != nil {
			return false, err
		}
		defer blockResult.writer.Close()

		err = blockResult.writer.OpenList()
		if err != nil {
			return false, err
		}

		var previousHash []byte
		for i := firstBlockNumber; i < info.GetHeight(); i++ {
			// Retrieve the next block
			result, err := iterator.Next()
			if err != nil {
				return false, err
			}
			if result == nil {
				break
			}
			block, ok := result.(*common.Block)
			if !ok {
				return false, errors.Errorf("Cannot decode the block %d", i)
			}

			if block.Header.Number != i {
				return false, errors.Errorf("The next block is expected to be %d but got %d", i, block.Header.Number)
			}

			// Perform checks for this block
			checkResult, err := checkBlock(block, previousHash)
			if err != nil {
				return false, err
			}

			// If any error is found, flag as so, and vice versa
			if !checkResult.Valid {
				ledgerErrorFound = true
				blockResult.erroneousBlocks += 1

				// Record the error details in the JSON
				err = blockResult.writer.AddEntry(checkResult)
				if err != nil {
					return false, err
				}
			} else {
				blockResult.validBlocks += 1
			}

			// Remember the header hash to compare it with the "previous hash" in the next block
			previousHash = protoutil.BlockHeaderHash(block.Header)
		}

		err = blockResult.writer.CloseList()
		if err != nil {
			return false, err
		}
	}

	// Returns true only if no error is found in all the channels
	return !ledgerErrorFound, nil
}

// getFirstBlockNumberInLedger - Gets the number of the first block in the ledger.
// Usually it should be zero, but when the ledger is started from a snapshot,
// it should be the last block number in the snapshot.
func getFirstBlockNumberInLedger(info *common.BlockchainInfo) uint64 {
	snapshotInfo := info.GetBootstrappingSnapshotInfo()
	if snapshotInfo == nil {
		return uint64(0)
	}

	return snapshotInfo.GetLastBlockInSnapshot() + uint64(1)
}

// getBlockStoreProvider - Gets a default block store provider to access the peer's block store
func getBlockStoreProvider(fsPath string) (*blkstorage.BlockStoreProvider, error) {
	// Format path to block store
	blockStorePath := kvledger.BlockStorePath(filepath.Join(fsPath, ledgersDataDirName))
	isEmpty, err := fileutil.DirEmpty(blockStorePath)
	if err != nil {
		return nil, err
	}
	if isEmpty {
		return nil, errors.Errorf("provided path %s is empty. Aborting verify", fsPath)
	}
	// Default fields for block store provider
	conf := blkstorage.NewConf(blockStorePath, 0)
	indexConfig := &blkstorage.IndexConfig{
		AttrsToIndex: []blkstorage.IndexableAttr{
			blkstorage.IndexableAttrBlockNum,
			blkstorage.IndexableAttrBlockHash,
			blkstorage.IndexableAttrTxID,
			blkstorage.IndexableAttrBlockNumTranNum,
		},
	}
	metricsProvider := &disabled.Provider{}
	// Create new block store provider
	blockStoreProvider, err := blkstorage.NewProvider(conf, indexConfig, metricsProvider)
	if err != nil {
		return nil, err
	}

	return blockStoreProvider, nil
}

// blockResultOutput - Stores the number of valid blocks and those of erroneous blocks
// and the JSON writer object
type blockResultOutput struct {
	validBlocks     uint64
	erroneousBlocks uint64
	writer          *jsonrw.JSONFileWriter
}

func newBlockResultOutput(directory string) (*blockResultOutput, error) {
	writer, err := jsonrw.NewJSONFileWriter(filepath.Join(directory, blockResultJsonFilename))
	if err != nil {
		return nil, err
	}

	return &blockResultOutput{
		validBlocks:     0,
		erroneousBlocks: 0,
		writer:          writer,
	}, nil
}

// blockCheckResult - Stores the result of the checks for a block
type blockCheckResult struct {
	BlockNum uint64   `json:"blockNum"`
	Valid    bool     `json:"valid"`
	Errors   []string `json:"errors,omitempty"`
}

// checkBlock - Performs checks a block's hash
func checkBlock(block *common.Block, previousHash []byte) (*blockCheckResult, error) {
	result := blockCheckResult{
		BlockNum: block.Header.Number,
		Valid:    true,
		Errors:   []string{},
	}

	hash, err := protoutil.BlockDataHash(block.Data)
	if err != nil {
		result.Errors = append(result.Errors, err.Error())
	}
	// Check if the hash value of the data matches the hash value field in the header
	if !bytes.Equal(block.Header.DataHash, hash) {
		result.Valid = false
		result.Errors = append(result.Errors, errorDataHashMismatch)
	}
	if len(previousHash) > 0 {
		// Check if the hash value of the previous header matches the prev hash value field in the header
		if !bytes.Equal(block.Header.PreviousHash, previousHash) {
			result.Valid = false
			result.Errors = append(result.Errors, errorPrevHashMismatch)
		}
	}

	return &result, nil
}
