//! Block and transaction validation rules.
//!
//! Implements consensus rules for validating:
//! - Transaction structure and signatures
//! - Block structure and proof of work
//! - UTXO spending rules
//! - Script execution (simplified)

use crate::utxo::{UtxoEntry, UtxoError, UtxoSet};
use synor_types::{
    block::{Block, BlockHeader},
    transaction::{Outpoint, ScriptType, Transaction, TxInput},
    Amount, Hash256,
};
use thiserror::Error;

/// Transaction validator.
pub struct TransactionValidator {
    /// Maximum transaction size.
    max_tx_size: usize,
    /// Minimum relay fee rate (sompi per byte).
    min_fee_rate: u64,
}

impl TransactionValidator {
    /// Creates a new transaction validator with default settings.
    pub fn new() -> Self {
        TransactionValidator {
            max_tx_size: crate::MAX_TRANSACTION_SIZE,
            min_fee_rate: 1, // 1 sompi per byte
        }
    }

    /// Creates a validator with custom settings.
    pub fn with_settings(max_tx_size: usize, min_fee_rate: u64) -> Self {
        TransactionValidator {
            max_tx_size,
            min_fee_rate,
        }
    }

    /// Validates a transaction's structure (without UTXO checks).
    pub fn validate_structure(&self, tx: &Transaction) -> Result<(), ValidationError> {
        // Check version
        if tx.version == 0 || tx.version > 2 {
            return Err(ValidationError::InvalidVersion(tx.version));
        }

        // Coinbase specific checks
        if tx.is_coinbase() {
            return self.validate_coinbase_structure(tx);
        }

        // Must have at least one input
        if tx.inputs.is_empty() {
            return Err(ValidationError::NoInputs);
        }

        // Must have at least one output
        if tx.outputs.is_empty() {
            return Err(ValidationError::NoOutputs);
        }

        // Check for duplicate inputs
        let mut seen_inputs = std::collections::HashSet::new();
        for input in &tx.inputs {
            if !seen_inputs.insert(input.previous_output) {
                return Err(ValidationError::DuplicateInput(input.previous_output));
            }
        }

        // Validate outputs
        let mut total_output = Amount::ZERO;
        for output in &tx.outputs {
            // Check for zero amount (except OP_RETURN)
            if output.amount == Amount::ZERO
                && output.script_pubkey.script_type != ScriptType::OpReturn
            {
                return Err(ValidationError::ZeroValueOutput);
            }

            // Check for overflow
            total_output = total_output
                .checked_add(output.amount)
                .ok_or(ValidationError::OutputOverflow)?;
        }

        // Check total doesn't exceed max supply
        if total_output.as_sompi() > Amount::MAX_SUPPLY {
            return Err(ValidationError::ExceedsMaxSupply);
        }

        Ok(())
    }

    /// Validates coinbase transaction structure.
    fn validate_coinbase_structure(&self, tx: &Transaction) -> Result<(), ValidationError> {
        // Coinbase must have exactly one input
        if tx.inputs.len() != 1 {
            return Err(ValidationError::InvalidCoinbase(
                "Must have exactly one input".to_string(),
            ));
        }

        // Input must be null outpoint
        if !tx.inputs[0].is_coinbase() {
            return Err(ValidationError::InvalidCoinbase(
                "Input must have null outpoint".to_string(),
            ));
        }

        // Must have at least one output
        if tx.outputs.is_empty() {
            return Err(ValidationError::NoOutputs);
        }

        // Coinbase script must not be too long
        if tx.inputs[0].signature_script.len() > 100 {
            return Err(ValidationError::InvalidCoinbase(
                "Script too long".to_string(),
            ));
        }

        Ok(())
    }

    /// Validates a transaction against the UTXO set.
    pub fn validate_against_utxos(
        &self,
        tx: &Transaction,
        utxo_set: &UtxoSet,
        current_daa_score: u64,
    ) -> Result<Amount, ValidationError> {
        if tx.is_coinbase() {
            return Ok(Amount::ZERO); // Coinbase doesn't spend UTXOs
        }

        let mut total_input = Amount::ZERO;

        for input in &tx.inputs {
            // Get the UTXO being spent
            let utxo = utxo_set
                .get(&input.previous_output)
                .ok_or(ValidationError::UtxoNotFound(input.previous_output))?;

            // Check maturity
            if !utxo.is_mature(current_daa_score) {
                return Err(ValidationError::ImmatureCoinbase(input.previous_output));
            }

            // Verify signature (simplified - in production would execute script)
            self.verify_input_script(input, &utxo)?;

            total_input = total_input
                .checked_add(utxo.amount())
                .ok_or(ValidationError::InputOverflow)?;
        }

        let total_output = tx.total_output();

        // Check input >= output (difference is fee)
        if total_input < total_output {
            return Err(ValidationError::InsufficientInputs {
                input: total_input,
                output: total_output,
            });
        }

        let fee = total_input.saturating_sub(total_output);

        // Check minimum fee
        let tx_size = tx.weight();
        let min_fee = Amount::from_sompi(tx_size * self.min_fee_rate);
        if fee < min_fee {
            return Err(ValidationError::InsufficientFee {
                provided: fee,
                required: min_fee,
            });
        }

        Ok(fee)
    }

    /// Verifies an input's signature script against the UTXO.
    fn verify_input_script(
        &self,
        input: &TxInput,
        utxo: &UtxoEntry,
    ) -> Result<(), ValidationError> {
        // Simplified verification - in production would:
        // 1. Parse the signature script
        // 2. Execute against the UTXO's script pubkey
        // 3. Verify signatures

        let script_pubkey = utxo.script_pubkey();

        match script_pubkey.script_type {
            ScriptType::P2PKH | ScriptType::P2pkhPqc => {
                // For P2PKH, signature script should contain signature + pubkey
                if input.signature_script.len() < 64 {
                    return Err(ValidationError::InvalidSignature(
                        "Signature script too short".to_string(),
                    ));
                }
                // In production: extract pubkey, hash it, compare to script_pubkey.data
                // Then verify signature
            }
            ScriptType::P2SH | ScriptType::P2shPqc => {
                // For P2SH, signature script contains actual script + signatures
                if input.signature_script.is_empty() {
                    return Err(ValidationError::InvalidSignature(
                        "Empty signature script".to_string(),
                    ));
                }
                // In production: deserialize script, hash it, compare, execute
            }
            ScriptType::OpReturn => {
                return Err(ValidationError::SpendingOpReturn);
            }
        }

        Ok(())
    }
}

impl Default for TransactionValidator {
    fn default() -> Self {
        Self::new()
    }
}

/// Block validator.
pub struct BlockValidator {
    /// Transaction validator.
    tx_validator: TransactionValidator,
    /// Maximum block mass.
    max_block_mass: u64,
    /// Maximum transactions per block.
    max_transactions: usize,
}

impl BlockValidator {
    /// Creates a new block validator.
    pub fn new() -> Self {
        BlockValidator {
            tx_validator: TransactionValidator::new(),
            max_block_mass: crate::MAX_BLOCK_MASS,
            max_transactions: synor_types::block::MAX_TRANSACTIONS,
        }
    }

    /// Validates a block header.
    pub fn validate_header(&self, header: &BlockHeader) -> Result<(), ValidationError> {
        // Check version
        if header.version == 0 {
            return Err(ValidationError::InvalidVersion(header.version as u16));
        }

        // Check timestamp is not too far in future
        let now = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .unwrap()
            .as_millis() as u64;

        let header_timestamp = header.timestamp.as_millis();
        if header_timestamp > now + 2 * 60 * 60 * 1000 {
            // 2 hours in future
            return Err(ValidationError::TimestampTooFar(header_timestamp));
        }

        // Check parents
        if header.parents.is_empty() && !header.block_id().is_zero() {
            // Only genesis can have no parents
            return Err(ValidationError::NoParents);
        }

        // Check for duplicate parents
        let mut seen_parents = std::collections::HashSet::new();
        for parent in &header.parents {
            if !seen_parents.insert(*parent) {
                return Err(ValidationError::DuplicateParent(*parent));
            }
        }

        Ok(())
    }

    /// Validates proof of work.
    pub fn validate_pow(
        &self,
        header: &BlockHeader,
        target: Hash256,
    ) -> Result<(), ValidationError> {
        let hash = header.block_id();

        // Check hash is below target
        if hash > target {
            return Err(ValidationError::InsufficientPow { hash, target });
        }

        Ok(())
    }

    /// Validates a complete block.
    pub fn validate_block(
        &self,
        block: &Block,
        utxo_set: &UtxoSet,
        expected_reward: Amount,
    ) -> Result<Amount, ValidationError> {
        // Validate header
        self.validate_header(&block.header)?;

        // Check transaction count
        if block.body.transactions.len() > self.max_transactions {
            return Err(ValidationError::TooManyTransactions {
                count: block.body.transactions.len(),
                max: self.max_transactions,
            });
        }

        // First transaction must be coinbase
        if block.body.transactions.is_empty() {
            return Err(ValidationError::MissingCoinbase);
        }

        if !block.body.transactions[0].is_coinbase() {
            return Err(ValidationError::FirstTxNotCoinbase);
        }

        // No other transactions can be coinbase
        for tx in block.body.transactions.iter().skip(1) {
            if tx.is_coinbase() {
                return Err(ValidationError::MultipleCoinbase);
            }
        }

        // Validate all transactions
        let mut total_fees = Amount::ZERO;
        let mut total_mass = 0u64;

        for (i, tx) in block.body.transactions.iter().enumerate() {
            // Validate structure
            self.tx_validator.validate_structure(tx)?;

            // Validate against UTXOs (skip coinbase)
            if i > 0 {
                let fee = self.tx_validator.validate_against_utxos(
                    tx,
                    utxo_set,
                    block.header.daa_score,
                )?;
                total_fees = total_fees.saturating_add(fee);
            }

            // Track mass
            total_mass += tx.weight();
        }

        // Check block mass
        if total_mass > self.max_block_mass {
            return Err(ValidationError::BlockMassExceeded {
                mass: total_mass,
                max: self.max_block_mass,
            });
        }

        // Validate coinbase output
        let coinbase = &block.body.transactions[0];
        let coinbase_output = coinbase.total_output();
        let max_coinbase = expected_reward.saturating_add(total_fees);

        if coinbase_output > max_coinbase {
            return Err(ValidationError::InvalidCoinbaseAmount {
                output: coinbase_output,
                max: max_coinbase,
            });
        }

        // Verify merkle root
        let computed_merkle = block.body.merkle_root();
        if computed_merkle != block.header.merkle_root {
            return Err(ValidationError::InvalidMerkleRoot {
                expected: block.header.merkle_root,
                computed: computed_merkle,
            });
        }

        Ok(total_fees)
    }
}

impl Default for BlockValidator {
    fn default() -> Self {
        Self::new()
    }
}

/// Validation errors.
#[derive(Debug, Error)]
pub enum ValidationError {
    #[error("Invalid version: {0}")]
    InvalidVersion(u16),

    #[error("Transaction has no inputs")]
    NoInputs,

    #[error("Transaction has no outputs")]
    NoOutputs,

    #[error("Block has no parents")]
    NoParents,

    #[error("Duplicate input: {0}")]
    DuplicateInput(Outpoint),

    #[error("Duplicate parent: {0}")]
    DuplicateParent(Hash256),

    #[error("Zero value output (non-OP_RETURN)")]
    ZeroValueOutput,

    #[error("Output amount overflow")]
    OutputOverflow,

    #[error("Input amount overflow")]
    InputOverflow,

    #[error("Total output exceeds max supply")]
    ExceedsMaxSupply,

    #[error("Invalid coinbase transaction: {0}")]
    InvalidCoinbase(String),

    #[error("UTXO not found: {0}")]
    UtxoNotFound(Outpoint),

    #[error("Immature coinbase: {0}")]
    ImmatureCoinbase(Outpoint),

    #[error("Insufficient inputs: have {input}, need {output}")]
    InsufficientInputs { input: Amount, output: Amount },

    #[error("Insufficient fee: provided {provided}, required {required}")]
    InsufficientFee { provided: Amount, required: Amount },

    #[error("Invalid signature: {0}")]
    InvalidSignature(String),

    #[error("Cannot spend OP_RETURN output")]
    SpendingOpReturn,

    #[error("Timestamp too far in future: {0}")]
    TimestampTooFar(u64),

    #[error("Insufficient proof of work: hash {hash}, target {target}")]
    InsufficientPow { hash: Hash256, target: Hash256 },

    #[error("Too many transactions: {count} (max {max})")]
    TooManyTransactions { count: usize, max: usize },

    #[error("Missing coinbase transaction")]
    MissingCoinbase,

    #[error("First transaction must be coinbase")]
    FirstTxNotCoinbase,

    #[error("Multiple coinbase transactions")]
    MultipleCoinbase,

    #[error("Block mass exceeded: {mass} (max {max})")]
    BlockMassExceeded { mass: u64, max: u64 },

    #[error("Invalid coinbase amount: output {output}, max {max}")]
    InvalidCoinbaseAmount { output: Amount, max: Amount },

    #[error("Invalid merkle root: expected {expected}, computed {computed}")]
    InvalidMerkleRoot {
        expected: Hash256,
        computed: Hash256,
    },

    #[error("UTXO error: {0}")]
    UtxoError(#[from] UtxoError),
}

#[cfg(test)]
mod tests {
    use super::*;
    use synor_types::transaction::{ScriptPubKey, SubnetworkId};
    use synor_types::TxOutput;

    fn make_p2pkh_output(amount: u64) -> TxOutput {
        TxOutput::new(Amount::from_sompi(amount), ScriptPubKey::p2pkh(&[0u8; 32]))
    }

    #[test]
    fn test_validate_empty_tx() {
        let validator = TransactionValidator::new();

        let tx = Transaction {
            version: 1,
            inputs: vec![],
            outputs: vec![make_p2pkh_output(1000)],
            lock_time: 0,
            subnetwork_id: SubnetworkId::default(),
            gas: 0,
            payload: vec![],
        };

        let result = validator.validate_structure(&tx);
        assert!(matches!(result, Err(ValidationError::NoInputs)));
    }

    #[test]
    fn test_validate_no_outputs() {
        let validator = TransactionValidator::new();

        let tx = Transaction {
            version: 1,
            inputs: vec![TxInput::new(
                Outpoint::new(Hash256::ZERO, 0),
                vec![0u8; 100],
            )],
            outputs: vec![],
            lock_time: 0,
            subnetwork_id: SubnetworkId::default(),
            gas: 0,
            payload: vec![],
        };

        let result = validator.validate_structure(&tx);
        assert!(matches!(result, Err(ValidationError::NoOutputs)));
    }

    #[test]
    fn test_validate_invalid_version() {
        let validator = TransactionValidator::new();

        let tx = Transaction {
            version: 0,
            inputs: vec![TxInput::new(
                Outpoint::new(Hash256::ZERO, 0),
                vec![0u8; 100],
            )],
            outputs: vec![make_p2pkh_output(1000)],
            lock_time: 0,
            subnetwork_id: SubnetworkId::default(),
            gas: 0,
            payload: vec![],
        };

        let result = validator.validate_structure(&tx);
        assert!(matches!(result, Err(ValidationError::InvalidVersion(0))));
    }

    #[test]
    fn test_validate_valid_coinbase() {
        let validator = TransactionValidator::new();

        let tx = Transaction::coinbase(
            vec![make_p2pkh_output(50_000_000_000)],
            b"Synor Genesis".to_vec(),
        );

        let result = validator.validate_structure(&tx);
        assert!(result.is_ok());
    }

    #[test]
    fn test_validate_duplicate_input() {
        let validator = TransactionValidator::new();

        let outpoint = Outpoint::new(Hash256::blake3(b"test"), 0);
        let tx = Transaction {
            version: 1,
            inputs: vec![
                TxInput::new(outpoint, vec![0u8; 100]),
                TxInput::new(outpoint, vec![0u8; 100]), // Duplicate
            ],
            outputs: vec![make_p2pkh_output(1000)],
            lock_time: 0,
            subnetwork_id: SubnetworkId::default(),
            gas: 0,
            payload: vec![],
        };

        let result = validator.validate_structure(&tx);
        assert!(matches!(result, Err(ValidationError::DuplicateInput(_))));
    }
}
