/*
Copyright IBM Corp. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/

package blkstorage

import (
	"fmt"
	"os"
	"path"
	"strconv"

	"github.com/hyperledger/fabric/internal/fileutil"
)

// ResetBlockStore drops the block storage index and truncates the blocks files for all channels/ledgers to genesis blocks
func ResetBlockStore(blockStorageDir string) error {
	if err := DeleteBlockStoreIndex(blockStorageDir); err != nil {
		return err
	}
	conf := &Conf{blockStorageDir: blockStorageDir}
	chainsDir := conf.getChainsDir()
	chainsDirExists, err := pathExists(chainsDir)
	if err != nil {
		return err
	}
	if !chainsDirExists {
		logger.Infof("Dir [%s] missing... exiting", chainsDir)
		return nil
	}
	ledgerIDs, err := fileutil.ListSubdirs(chainsDir)
	if err != nil {
		return err
	}
	if len(ledgerIDs) == 0 {
		logger.Info("No ledgers found.. exiting")
		return nil
	}
	logger.Infof("Found ledgers - %s", ledgerIDs)
	for _, ledgerID := range ledgerIDs {
		ledgerDir := conf.getLedgerBlockDir(ledgerID)
		if err := recordHeightIfGreaterThanPreviousRecording(ledgerDir); err != nil {
			return err
		}
		if err := resetToGenesisBlk(ledgerDir); err != nil {
			return err
		}
	}
	return nil
}

// DeleteBlockStoreIndex deletes block store index file
func DeleteBlockStoreIndex(blockStorageDir string) error {
	conf := &Conf{blockStorageDir: blockStorageDir}
	indexDir := conf.getIndexDir()
	logger.Infof("Dropping all contents under the index dir [%s]... if present", indexDir)
	return fileutil.RemoveContents(indexDir)
}

func resetToGenesisBlk(ledgerDir string) error {
	logger.Infof("Resetting ledger [%s] to genesis block", ledgerDir)
	lastFileNum, err := retrieveLastFileSuffix(ledgerDir)
	logger.Infof("lastFileNum = [%d]", lastFileNum)
	if err != nil {
		return err
	}
	if lastFileNum < 0 {
		return nil
	}
	zeroFilePath, genesisBlkEndOffset, err := retrieveGenesisBlkOffsetAndMakeACopy(ledgerDir)
	if err != nil {
		return err
	}
	for lastFileNum > 0 {
		filePath := deriveBlockfilePath(ledgerDir, lastFileNum)
		logger.Infof("Deleting file number = [%d]", lastFileNum)
		if err := os.Remove(filePath); err != nil {
			return err
		}
		lastFileNum--
	}
	logger.Infof("Truncating file [%s] to offset [%d]", zeroFilePath, genesisBlkEndOffset)
	return os.Truncate(zeroFilePath, genesisBlkEndOffset)
}

func retrieveGenesisBlkOffsetAndMakeACopy(ledgerDir string) (string, int64, error) {
	blockfilePath := deriveBlockfilePath(ledgerDir, 0)
	blockfileStream, err := newBlockfileStream(ledgerDir, 0, 0)
	if err != nil {
		return "", -1, err
	}
	genesisBlockBytes, _, err := blockfileStream.nextBlockBytesAndPlacementInfo()
	if err != nil {
		return "", -1, err
	}
	endOffsetGenesisBlock := blockfileStream.currentOffset
	blockfileStream.close()

	if err := assertIsGenesisBlock(genesisBlockBytes); err != nil {
		return "", -1, err
	}
	// just for an extra safety make a backup of genesis block
	if err := os.WriteFile(path.Join(ledgerDir, "__backupGenesisBlockBytes"), genesisBlockBytes, 0o640); err != nil {
		return "", -1, err
	}
	logger.Infof("Genesis block backed up. Genesis block info file [%s], offset [%d]", blockfilePath, endOffsetGenesisBlock)
	return blockfilePath, endOffsetGenesisBlock, nil
}

func assertIsGenesisBlock(blockBytes []byte) error {
	block, err := deserializeBlock(blockBytes)
	if err != nil {
		return err
	}
	if block.Header.Number != 0 || block.Header.GetDataHash() == nil {
		return fmt.Errorf("The supplied bytes are not of genesis block. blockNum=%d, blockHash=%x", block.Header.Number, block.Header.GetDataHash())
	}
	return nil
}

func pathExists(path string) (bool, error) {
	_, err := os.Stat(path)
	if os.IsNotExist(err) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	return true, nil
}

const (
	fileNamePreRestHt = "__preResetHeight"
)

// recordHeightIfGreaterThanPreviousRecording creates a file "__preResetHeight" in the ledger's
// directory. This file contains human readable string for the current block height. This function
// only overwrites this information if the current block height is higher than the one recorded in
// the existing file (if present). This helps in achieving fail-safe behaviour of reset utility
func recordHeightIfGreaterThanPreviousRecording(ledgerDir string) error {
	logger.Infof("Preparing to record current height for ledger at [%s]", ledgerDir)
	blkfilesInfo, err := constructBlockfilesInfo(ledgerDir)
	if err != nil {
		return err
	}
	logger.Infof("Loaded current info from blockfiles %#v", blkfilesInfo)
	preResetHtFile := path.Join(ledgerDir, fileNamePreRestHt)
	exists, err := pathExists(preResetHtFile)
	logger.Infof("preResetHtFile already exists? = %t", exists)
	if err != nil {
		return err
	}
	previuoslyRecordedHt := uint64(0)
	if exists {
		htBytes, err := os.ReadFile(preResetHtFile)
		if err != nil {
			return err
		}
		if previuoslyRecordedHt, err = strconv.ParseUint(string(htBytes), 10, 64); err != nil {
			return err
		}
		logger.Infof("preResetHtFile contains height = %d", previuoslyRecordedHt)
	}
	currentHt := blkfilesInfo.lastPersistedBlock + 1
	if currentHt > previuoslyRecordedHt {
		logger.Infof("Recording current height [%d]", currentHt)
		return os.WriteFile(preResetHtFile,
			[]byte(strconv.FormatUint(currentHt, 10)),
			0o640,
		)
	}
	logger.Infof("Not recording current height [%d] since this is less than previously recorded height [%d]",
		currentHt, previuoslyRecordedHt)

	return nil
}

// LoadPreResetHeight searches the preResetHeight files for the specified ledgers and
// returns a map of channelname to the last recorded block height during one of the reset operations.
func LoadPreResetHeight(blockStorageDir string, ledgerIDs []string) (map[string]uint64, error) {
	logger.Debug("Loading Pre-reset heights")
	preResetFilesMap, err := preResetHtFiles(blockStorageDir, ledgerIDs)
	if err != nil {
		return nil, err
	}
	m := map[string]uint64{}
	for ledgerID, filePath := range preResetFilesMap {
		bytes, err := os.ReadFile(filePath)
		if err != nil {
			return nil, err
		}
		previuoslyRecordedHt, err := strconv.ParseUint(string(bytes), 10, 64)
		if err != nil {
			return nil, err
		}
		m[ledgerID] = previuoslyRecordedHt
	}
	if len(m) > 0 {
		logger.Infof("Pre-reset heights loaded: %v", m)
	}
	return m, nil
}

// ClearPreResetHeight deletes the files that contain the last recorded reset heights for the specified ledgers
func ClearPreResetHeight(blockStorageDir string, ledgerIDs []string) error {
	logger.Info("Clearing Pre-reset heights")
	preResetFilesMap, err := preResetHtFiles(blockStorageDir, ledgerIDs)
	if err != nil {
		return err
	}
	for _, filePath := range preResetFilesMap {
		if err := os.Remove(filePath); err != nil {
			return err
		}
	}
	logger.Info("Cleared off Pre-reset heights")
	return nil
}

func preResetHtFiles(blockStorageDir string, ledgerIDs []string) (map[string]string, error) {
	if len(ledgerIDs) == 0 {
		logger.Info("No active channels passed")
		return nil, nil
	}
	conf := &Conf{blockStorageDir: blockStorageDir}
	chainsDir := conf.getChainsDir()
	chainsDirExists, err := pathExists(chainsDir)
	if err != nil {
		return nil, err
	}
	if !chainsDirExists {
		logger.Infof("Dir [%s] missing... exiting", chainsDir)
		return nil, err
	}
	m := map[string]string{}
	for _, ledgerID := range ledgerIDs {
		ledgerDir := conf.getLedgerBlockDir(ledgerID)
		file := path.Join(ledgerDir, fileNamePreRestHt)
		exists, err := pathExists(file)
		if err != nil {
			return nil, err
		}
		if !exists {
			continue
		}
		m[ledgerID] = file
	}
	return m, nil
}
