//! Transaction types for the Synor blockchain.
//!
//! Synor uses a UTXO model similar to Bitcoin and Kaspa.

use crate::{Address, Amount, Hash256};
use borsh::{BorshDeserialize, BorshSerialize};
use serde::{Deserialize, Serialize};
use std::fmt;
use thiserror::Error;

/// Transaction identifier (hash of the transaction).
pub type TransactionId = Hash256;

/// Outpoint - references a specific output of a previous transaction.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    Serialize,
    Deserialize,
    BorshSerialize,
    BorshDeserialize,
)]
pub struct Outpoint {
    /// The transaction ID containing the output.
    pub txid: TransactionId,
    /// The index of the output within the transaction.
    pub index: u32,
}

impl Outpoint {
    /// Creates a new outpoint.
    pub fn new(txid: TransactionId, index: u32) -> Self {
        Outpoint { txid, index }
    }

    /// Creates a null outpoint (used for coinbase inputs).
    pub fn null() -> Self {
        Outpoint {
            txid: Hash256::ZERO,
            index: u32::MAX,
        }
    }

    /// Returns true if this is a null outpoint.
    pub fn is_null(&self) -> bool {
        self.txid.is_zero() && self.index == u32::MAX
    }
}

impl fmt::Display for Outpoint {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}:{}", self.txid, self.index)
    }
}

/// Transaction input - spends a previous output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct TxInput {
    /// The outpoint being spent.
    pub previous_output: Outpoint,
    /// Signature script (unlocking script).
    /// Contains the signature(s) proving ownership.
    pub signature_script: Vec<u8>,
    /// Sequence number (for relative time-locks, RBF).
    pub sequence: u64,
}

impl TxInput {
    /// Creates a new transaction input.
    pub fn new(previous_output: Outpoint, signature_script: Vec<u8>) -> Self {
        TxInput {
            previous_output,
            signature_script,
            sequence: u64::MAX, // No sequence restrictions by default
        }
    }

    /// Creates a coinbase input.
    pub fn coinbase(extra_data: Vec<u8>) -> Self {
        TxInput {
            previous_output: Outpoint::null(),
            signature_script: extra_data,
            sequence: u64::MAX,
        }
    }

    /// Returns true if this is a coinbase input.
    pub fn is_coinbase(&self) -> bool {
        self.previous_output.is_null()
    }
}

/// Transaction output - creates new spendable value.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct TxOutput {
    /// The amount in sompi.
    pub amount: Amount,
    /// The locking script (defines conditions to spend).
    pub script_pubkey: ScriptPubKey,
}

impl TxOutput {
    /// Creates a new transaction output.
    pub fn new(amount: Amount, script_pubkey: ScriptPubKey) -> Self {
        TxOutput {
            amount,
            script_pubkey,
        }
    }

    /// Creates a P2PKH output (pay to public key hash).
    pub fn p2pkh(amount: Amount, address: &Address) -> Self {
        TxOutput {
            amount,
            script_pubkey: ScriptPubKey::p2pkh(address.payload()),
        }
    }
}

/// Script public key - defines the conditions to spend an output.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct ScriptPubKey {
    /// The script type.
    pub script_type: ScriptType,
    /// The script data (typically a hash).
    pub data: Vec<u8>,
}

impl ScriptPubKey {
    /// Creates a P2PKH script (pay to public key hash).
    pub fn p2pkh(pubkey_hash: &[u8; 32]) -> Self {
        ScriptPubKey {
            script_type: ScriptType::P2PKH,
            data: pubkey_hash.to_vec(),
        }
    }

    /// Creates a P2SH script (pay to script hash).
    pub fn p2sh(script_hash: &[u8; 32]) -> Self {
        ScriptPubKey {
            script_type: ScriptType::P2SH,
            data: script_hash.to_vec(),
        }
    }

    /// Creates an OP_RETURN output (for data embedding).
    pub fn op_return(data: &[u8]) -> Self {
        ScriptPubKey {
            script_type: ScriptType::OpReturn,
            data: data.to_vec(),
        }
    }

    /// Extracts the address from the script if possible.
    pub fn extract_address(&self, network: crate::Network) -> Option<Address> {
        match self.script_type {
            ScriptType::P2PKH | ScriptType::P2pkhPqc => {
                if self.data.len() == 32 {
                    let mut payload = [0u8; 32];
                    payload.copy_from_slice(&self.data);
                    let addr_type = if self.script_type == ScriptType::P2PKH {
                        crate::address::AddressType::P2PKH
                    } else {
                        crate::address::AddressType::P2pkhPqc
                    };
                    Some(Address::from_parts(network, addr_type, payload))
                } else {
                    None
                }
            }
            ScriptType::P2SH | ScriptType::P2shPqc => {
                if self.data.len() == 32 {
                    let mut payload = [0u8; 32];
                    payload.copy_from_slice(&self.data);
                    let addr_type = if self.script_type == ScriptType::P2SH {
                        crate::address::AddressType::P2SH
                    } else {
                        crate::address::AddressType::P2shPqc
                    };
                    Some(Address::from_parts(network, addr_type, payload))
                } else {
                    None
                }
            }
            ScriptType::OpReturn => None,
        }
    }
}

/// Script type enumeration.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    Serialize,
    Deserialize,
    BorshSerialize,
    BorshDeserialize,
)]
#[borsh(use_discriminant = true)]
#[repr(u8)]
pub enum ScriptType {
    /// Pay to Public Key Hash (Ed25519)
    P2PKH = 0,
    /// Pay to Public Key Hash (Hybrid: Ed25519 + Dilithium)
    P2pkhPqc = 1,
    /// Pay to Script Hash
    P2SH = 2,
    /// Pay to Script Hash (Post-Quantum)
    P2shPqc = 3,
    /// OP_RETURN (data carrier, unspendable)
    OpReturn = 4,
}

/// A Synor transaction.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, BorshSerialize, BorshDeserialize)]
pub struct Transaction {
    /// Transaction version.
    pub version: u16,
    /// Transaction inputs.
    pub inputs: Vec<TxInput>,
    /// Transaction outputs.
    pub outputs: Vec<TxOutput>,
    /// Lock time (block height or timestamp).
    pub lock_time: u64,
    /// Subnetwork ID (for future sharding/subnets).
    pub subnetwork_id: SubnetworkId,
    /// Gas limit (for smart contract transactions).
    pub gas: u64,
    /// Payload (for smart contract data).
    pub payload: Vec<u8>,
}

impl Transaction {
    /// Creates a new transaction.
    pub fn new(inputs: Vec<TxInput>, outputs: Vec<TxOutput>) -> Self {
        Transaction {
            version: 1,
            inputs,
            outputs,
            lock_time: 0,
            subnetwork_id: SubnetworkId::NATIVE,
            gas: 0,
            payload: Vec::new(),
        }
    }

    /// Creates a coinbase transaction.
    pub fn coinbase(outputs: Vec<TxOutput>, extra_data: Vec<u8>) -> Self {
        Transaction {
            version: 1,
            inputs: vec![TxInput::coinbase(extra_data)],
            outputs,
            lock_time: 0,
            subnetwork_id: SubnetworkId::COINBASE,
            gas: 0,
            payload: Vec::new(),
        }
    }

    /// Computes the transaction ID (hash).
    pub fn txid(&self) -> TransactionId {
        let serialized = borsh::to_vec(self).expect("Serialization should not fail");
        Hash256::double_blake3(&serialized)
    }

    /// Returns true if this is a coinbase transaction.
    pub fn is_coinbase(&self) -> bool {
        self.inputs.len() == 1 && self.inputs[0].is_coinbase()
    }

    /// Computes the total output value.
    pub fn total_output(&self) -> Amount {
        self.outputs.iter().fold(Amount::ZERO, |acc, output| {
            acc.saturating_add(output.amount)
        })
    }

    /// Returns the transaction weight (for fee calculation).
    pub fn weight(&self) -> u64 {
        let serialized = borsh::to_vec(self).expect("Serialization should not fail");
        serialized.len() as u64
    }

    /// Serializes the transaction for signing (excludes signatures).
    pub fn sighash(&self) -> Hash256 {
        // Create a copy with empty signature scripts
        let mut tx_copy = self.clone();
        for input in &mut tx_copy.inputs {
            input.signature_script.clear();
        }
        let serialized = borsh::to_vec(&tx_copy).expect("Serialization should not fail");
        Hash256::double_blake3(&serialized)
    }
}

impl fmt::Display for Transaction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            f,
            "Transaction {{ id: {}, inputs: {}, outputs: {}, value: {} }}",
            self.txid(),
            self.inputs.len(),
            self.outputs.len(),
            self.total_output()
        )
    }
}

/// Subnetwork identifier.
#[derive(
    Debug,
    Clone,
    Copy,
    PartialEq,
    Eq,
    Hash,
    Serialize,
    Deserialize,
    BorshSerialize,
    BorshDeserialize,
)]
pub struct SubnetworkId([u8; 20]);

impl SubnetworkId {
    /// Native subnetwork (regular transactions).
    pub const NATIVE: SubnetworkId = SubnetworkId([0u8; 20]);

    /// Coinbase subnetwork.
    pub const COINBASE: SubnetworkId =
        SubnetworkId([1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);

    /// Registry subnetwork (for registrations).
    pub const REGISTRY: SubnetworkId =
        SubnetworkId([2, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);

    /// Creates a new subnetwork ID from bytes.
    pub fn from_bytes(bytes: [u8; 20]) -> Self {
        SubnetworkId(bytes)
    }

    /// Returns the bytes of the subnetwork ID.
    pub fn as_bytes(&self) -> &[u8; 20] {
        &self.0
    }
}

impl Default for SubnetworkId {
    fn default() -> Self {
        SubnetworkId::NATIVE
    }
}

/// Errors that can occur with transactions.
#[derive(Debug, Error)]
pub enum TransactionError {
    #[error("Transaction has no inputs")]
    NoInputs,

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

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

    #[error("Invalid signature")]
    InvalidSignature,

    #[error("Double spend detected")]
    DoubleSpend,

    #[error("Insufficient funds: need {need}, have {have}")]
    InsufficientFunds { need: Amount, have: Amount },
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_coinbase_transaction() {
        let output = TxOutput::new(Amount::from_synor(500), ScriptPubKey::p2pkh(&[0u8; 32]));
        let tx = Transaction::coinbase(vec![output], b"Synor Genesis".to_vec());

        assert!(tx.is_coinbase());
        assert_eq!(tx.inputs.len(), 1);
        assert!(tx.inputs[0].is_coinbase());
    }

    #[test]
    fn test_transaction_id() {
        let input = TxInput::new(Outpoint::new(Hash256::blake3(b"prev_tx"), 0), vec![]);
        let output = TxOutput::new(Amount::from_synor(10), ScriptPubKey::p2pkh(&[1u8; 32]));
        let tx = Transaction::new(vec![input], vec![output]);

        let txid = tx.txid();
        assert!(!txid.is_zero());

        // Same transaction should produce same ID
        let txid2 = tx.txid();
        assert_eq!(txid, txid2);
    }

    #[test]
    fn test_outpoint_display() {
        let outpoint = Outpoint::new(Hash256::blake3(b"test"), 5);
        let display = format!("{}", outpoint);
        assert!(display.contains(":5"));
    }

    #[test]
    fn test_total_output() {
        let outputs = vec![
            TxOutput::new(Amount::from_synor(10), ScriptPubKey::p2pkh(&[0u8; 32])),
            TxOutput::new(Amount::from_synor(20), ScriptPubKey::p2pkh(&[1u8; 32])),
            TxOutput::new(Amount::from_synor(5), ScriptPubKey::p2pkh(&[2u8; 32])),
        ];
        let tx = Transaction::new(vec![], outputs);

        assert_eq!(tx.total_output(), Amount::from_synor(35));
    }
}
