// Copyright (c) 2017-2026 Lethean (https://lt.hn)
//
// Licensed under the European Union Public Licence (EUPL) version 1.2.
// SPDX-License-Identifier: EUPL-1.2

package consensus

import (
	"fmt"
	"slices"

	coreerr "dappco.re/go/core/log"

	"dappco.re/go/core/blockchain/config"
	"dappco.re/go/core/blockchain/types"
)

// IsPoS returns true if the block flags indicate a Proof-of-Stake block.
// Bit 0 of the flags byte is the PoS indicator.
func IsPoS(flags uint8) bool {
	return flags&1 != 0
}

// CheckTimestamp validates a block's timestamp against future limits and
// the median of recent timestamps.
func CheckTimestamp(blockTimestamp uint64, flags uint8, adjustedTime uint64, recentTimestamps []uint64) error {
	// Future time limit.
	limit := config.BlockFutureTimeLimit
	if IsPoS(flags) {
		limit = config.PosBlockFutureTimeLimit
	}
	if blockTimestamp > adjustedTime+limit {
		return coreerr.E("CheckTimestamp", fmt.Sprintf("%d > %d + %d",
			blockTimestamp, adjustedTime, limit), ErrTimestampFuture)
	}

	// Median check — only when we have enough history.
	if uint64(len(recentTimestamps)) < config.TimestampCheckWindow {
		return nil
	}

	median := medianTimestamp(recentTimestamps)
	if blockTimestamp < median {
		return coreerr.E("CheckTimestamp", fmt.Sprintf("%d < median %d",
			blockTimestamp, median), ErrTimestampOld)
	}

	return nil
}

// medianTimestamp returns the median of a slice of timestamps.
func medianTimestamp(timestamps []uint64) uint64 {
	sorted := make([]uint64, len(timestamps))
	copy(sorted, timestamps)
	slices.Sort(sorted)

	n := len(sorted)
	if n == 0 {
		return 0
	}
	return sorted[n/2]
}

func expectedMinerTxVersion(forks []config.HardFork, height uint64) uint64 {
	switch {
	case config.IsHardForkActive(forks, config.HF5, height):
		return types.VersionPostHF5
	case config.IsHardForkActive(forks, config.HF4Zarcanum, height):
		return types.VersionPostHF4
	case config.IsHardForkActive(forks, config.HF1, height):
		return types.VersionPreHF4
	default:
		return types.VersionInitial
	}
}

// ValidateMinerTx checks the structure of a coinbase (miner) transaction.
// For PoW blocks: exactly 1 input (TxInputGenesis). For PoS blocks: exactly
// 2 inputs (TxInputGenesis + stake input).
func ValidateMinerTx(tx *types.Transaction, height uint64, forks []config.HardFork) error {
	expectedVersion := expectedMinerTxVersion(forks, height)
	if tx.Version != expectedVersion {
		return coreerr.E("ValidateMinerTx", fmt.Sprintf("version %d invalid at height %d (expected %d)",
			tx.Version, height, expectedVersion), ErrMinerTxVersion)
	}
	if tx.Version >= types.VersionPostHF5 {
		currentFork := config.VersionAtHeight(forks, height)
		if tx.HardforkID != currentFork {
			return coreerr.E("ValidateMinerTx", fmt.Sprintf("hardfork id %d does not match active fork %d at height %d",
				tx.HardforkID, currentFork, height), ErrMinerTxVersion)
		}
	}

	if len(tx.Vin) == 0 {
		return coreerr.E("ValidateMinerTx", "no inputs", ErrMinerTxInputs)
	}

	// First input must be TxInputGenesis.
	gen, ok := tx.Vin[0].(types.TxInputGenesis)
	if !ok {
		return coreerr.E("ValidateMinerTx", "first input is not txin_gen", ErrMinerTxInputs)
	}
	if gen.Height != height {
		return coreerr.E("ValidateMinerTx", fmt.Sprintf("got %d, expected %d", gen.Height, height), ErrMinerTxHeight)
	}

	// PoW blocks: exactly 1 input. PoS: exactly 2.
	if len(tx.Vin) == 1 {
		// PoW — valid.
	} else if len(tx.Vin) == 2 {
		// PoS — second input must be a spend input.
		switch tx.Vin[1].(type) {
		case types.TxInputToKey:
			// Pre-HF4 PoS.
		case types.TxInputZC:
			hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
			if !hf4Active {
				return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs)
			}
		default:
			return coreerr.E("ValidateMinerTx", "invalid PoS stake input type", ErrMinerTxInputs)
		}
	} else {
		return coreerr.E("ValidateMinerTx", fmt.Sprintf("%d inputs (expected 1 or 2)", len(tx.Vin)), ErrMinerTxInputs)
	}

	return nil
}

// ValidateBlockReward checks that the miner transaction outputs do not
// exceed the expected reward (base reward + fees for pre-HF4).
//
// Post-HF4 miner transactions may use Zarcanum outputs, so the validator
// sums both transparent amounts and the encoded Zarcanum amount field.
func ValidateBlockReward(minerTx *types.Transaction, height, blockSize, medianSize, totalFees uint64, forks []config.HardFork) error {
	base := BaseReward(height)
	reward, err := BlockReward(base, blockSize, medianSize)
	if err != nil {
		return err
	}

	hf4Active := config.IsHardForkActive(forks, config.HF4Zarcanum, height)
	expected := MinerReward(reward, totalFees, hf4Active)

	// Sum miner tx outputs.
	var outputSum uint64
	for _, vout := range minerTx.Vout {
		switch out := vout.(type) {
		case types.TxOutputBare:
			outputSum += out.Amount
		case types.TxOutputZarcanum:
			outputSum += out.EncryptedAmount
		}
	}

	if outputSum > expected {
		return coreerr.E("ValidateBlockReward", fmt.Sprintf("outputs %d > expected %d", outputSum, expected), ErrRewardMismatch)
	}

	return nil
}

// expectedBlockMajorVersion returns the expected block major version for a
// given height and fork schedule. This maps hardfork eras to block versions:
//
//	HF0 (genesis) -> 0
//	HF1           -> 1
//	HF3           -> 2
//	HF4+          -> 3
func expectedBlockMajorVersion(forks []config.HardFork, height uint64) uint8 {
	if config.IsHardForkActive(forks, config.HF4Zarcanum, height) {
		return config.CurrentBlockMajorVersion // 3
	}
	if config.IsHardForkActive(forks, config.HF3, height) {
		return config.HF3BlockMajorVersion // 2
	}
	if config.IsHardForkActive(forks, config.HF1, height) {
		return config.HF1BlockMajorVersion // 1
	}
	return config.BlockMajorVersionInitial // 0
}

// checkBlockVersion validates that the block's major version matches
// what is expected at the given height in the fork schedule.
func checkBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
	expected := expectedBlockMajorVersion(forks, height)
	if majorVersion != expected {
		return coreerr.E("CheckBlockVersion", fmt.Sprintf("got %d, want %d at height %d",
			majorVersion, expected, height), ErrBlockMajorVersion)
	}
	return nil
}

// CheckBlockVersion validates that the block's major version matches the
// expected version for the supplied height and fork schedule.
func CheckBlockVersion(majorVersion uint8, forks []config.HardFork, height uint64) error {
	return checkBlockVersion(majorVersion, forks, height)
}

// ValidateBlock performs full consensus validation on a block. It checks
// the block version, timestamp, miner transaction structure, and reward.
// Transaction semantic validation for regular transactions should be done
// separately via ValidateTransaction for each tx in the block.
func ValidateBlock(blk *types.Block, height, blockSize, medianSize, totalFees, adjustedTime uint64,
	recentTimestamps []uint64, forks []config.HardFork) error {

	// Block major version check.
	if err := checkBlockVersion(blk.MajorVersion, forks, height); err != nil {
		return err
	}

	// Timestamp validation.
	if err := CheckTimestamp(blk.Timestamp, blk.Flags, adjustedTime, recentTimestamps); err != nil {
		return err
	}

	// Miner transaction structure.
	if err := ValidateMinerTx(&blk.MinerTx, height, forks); err != nil {
		return err
	}

	// Block reward.
	if err := ValidateBlockReward(&blk.MinerTx, height, blockSize, medianSize, totalFees, forks); err != nil {
		return err
	}

	return nil
}

// IsPreHardforkFreeze reports whether the given height falls within the
// pre-hardfork transaction freeze window for the specified fork version.
// The freeze window is the PreHardforkTxFreezePeriod blocks immediately
// before the fork activation height (inclusive).
//
// For a fork with activation height H (active at heights > H):
//
//	freeze applies at heights (H - period + 1) .. H
//
// Returns false if the fork version is not found or if the activation height
// is too low for a meaningful freeze window.
func IsPreHardforkFreeze(forks []config.HardFork, version uint8, height uint64) bool {
	activationHeight, ok := config.HardforkActivationHeight(forks, version)
	if !ok {
		return false
	}

	// A fork at height 0 means active from genesis — no freeze window.
	if activationHeight == 0 {
		return false
	}

	// Guard against underflow: if activation height < period, freeze starts at 1.
	freezeStart := uint64(1)
	if activationHeight >= config.PreHardforkTxFreezePeriod {
		freezeStart = activationHeight - config.PreHardforkTxFreezePeriod + 1
	}

	return height >= freezeStart && height <= activationHeight
}

// ValidateTransactionInBlock performs transaction validation including the
// pre-hardfork freeze check. This wraps ValidateTransaction with an
// additional check: during the freeze window before HF5, non-coinbase
// transactions are rejected.
func ValidateTransactionInBlock(tx *types.Transaction, txBlob []byte, forks []config.HardFork, height uint64) error {
	// Pre-hardfork freeze: reject non-coinbase transactions in the freeze window.
	if !isCoinbase(tx) && IsPreHardforkFreeze(forks, config.HF5, height) {
		return coreerr.E("ValidateTransactionInBlock", fmt.Sprintf("height %d is within HF5 freeze window", height), ErrPreHardforkFreeze)
	}

	return ValidateTransaction(tx, txBlob, forks, height)
}
