use bitcoin::{
    Amount, Script, Transaction, TxIn, TxOut, WitnessVersion, policy::MAX_STANDARD_TX_SIGOPS_COST,
    taproot::LeafVersion,
};
use brk_types::{Height, OutputType, SigOps};

use super::super::ComputedTx;
use super::{input, sigops::ComputedSigOps};
use crate::TxFeatureFlags;
use crate::processor::txout::ProcessedOutput;

// Deterministic policy snapshots, not consensus activation heights.
const LAST_V2_POLICY_HEIGHT: u32 = 863_500;
const FIRST_V29_POLICY_HEIGHT: u32 = 892_500;
const FIRST_V30_POLICY_HEIGHT: u32 = 921_000;
// rust-bitcoin 0.32 still exposes Core's previous 82-byte policy value.
const MIN_STANDARD_TX_NONWITNESS_SIZE: u32 = 65;
const MAX_EXECUTED_LEGACY_SIGOP_COST: u32 = 10_000;
const MAX_SCRIPT_SIG_SIZE: usize = 1_650;
const MAX_P2SH_SIGOPS: usize = 15;
const MAX_V29_OP_RETURN_SCRIPT_BYTES: usize = 83;
const MAX_V30_OP_RETURN_SCRIPT_BYTES: usize = 100_000;
const MAX_P2WSH_SCRIPT_BYTES: usize = 3_600;
const MAX_P2WSH_STACK_ITEMS: usize = 100;
const MAX_WITNESS_STACK_ITEM_BYTES: usize = 80;
const MAX_STANDARD_BARE_MULTISIG_SIGOP_COST: u32 =
    3 * bitcoin::constants::WITNESS_SCALE_FACTOR as u32;

#[derive(Default)]
pub(super) struct Accumulator {
    height: u32,
    nonstandard: bool,
    dust_output_count: usize,
    op_return_count: usize,
    op_return_script_bytes: usize,
}

impl Accumulator {
    pub(super) fn new(height: Height) -> Self {
        Self {
            height: height.into(),
            ..Self::default()
        }
    }

    pub(super) fn scan_input(
        &mut self,
        input: &TxIn,
        output_type: OutputType,
        facts: &input::Facts<'_>,
    ) {
        if input.script_sig.len() > MAX_SCRIPT_SIG_SIZE || !facts.script_sig.push_only {
            self.nonstandard = true;
            return;
        }

        self.nonstandard |= match output_type {
            OutputType::P2SH => facts
                .redeem_sigops()
                .is_some_and(|count| count > MAX_P2SH_SIGOPS),
            OutputType::P2A => self.height <= LAST_V2_POLICY_HEIGHT,
            OutputType::P2TR => facts.witness.has_annex,
            OutputType::OpReturn | OutputType::Empty | OutputType::Unknown => true,
            _ => false,
        } || has_nonstandard_witness(output_type, facts);
    }

    pub(super) fn scan_output(&mut self, txout: &TxOut, output: &ProcessedOutput) {
        let script = &txout.script_pubkey;
        self.nonstandard |= match output.output_type {
            OutputType::Empty => true,
            OutputType::Unknown => !is_standard_unknown_witness(script),
            OutputType::P2MS => has_too_many_bare_multisig_keys(output.legacy_sigops),
            OutputType::OpReturn => {
                self.op_return_count += 1;
                self.op_return_script_bytes += script.len();
                false
            }
            _ => false,
        };
        self.dust_output_count += is_dust(txout.value, output.output_type, script) as usize;
    }

    pub(super) fn finish(
        mut self,
        tx: &ComputedTx<'_>,
        sigops: ComputedSigOps,
        flags: &mut TxFeatureFlags,
    ) {
        self.nonstandard |= if self.height < FIRST_V30_POLICY_HEIGHT {
            self.op_return_count > 1 || self.op_return_script_bytes > MAX_V29_OP_RETURN_SCRIPT_BYTES
        } else {
            self.op_return_script_bytes > MAX_V30_OP_RETURN_SCRIPT_BYTES
        };
        self.nonstandard |=
            has_unconditionally_nonstandard_dust(self.height, self.dust_output_count);
        self.nonstandard |= has_nonstandard_header(tx, sigops, self.height);

        if self.nonstandard {
            flags.insert(TxFeatureFlags::UNCONDITIONALLY_NONSTANDARD);
        }
        if self.dust_output_count > 0 {
            flags.insert(TxFeatureFlags::DUST_OUTPUT);
        }
    }
}

pub(super) fn tracks_executed_legacy_sigops(height: Height) -> bool {
    u32::from(height) >= FIRST_V30_POLICY_HEIGHT
}

fn has_nonstandard_header(tx: &ComputedTx, sigops: ComputedSigOps, height: u32) -> bool {
    has_nonstandard_version(tx.tx.version.0, height)
        || tx.weight() > Transaction::MAX_STANDARD_WEIGHT
        || tx.base_size < MIN_STANDARD_TX_NONWITNESS_SIZE
        || u32::from(sigops.total) > MAX_STANDARD_TX_SIGOPS_COST
        || height >= FIRST_V30_POLICY_HEIGHT
            && u32::from(sigops.executed_legacy) > MAX_EXECUTED_LEGACY_SIGOP_COST
}

fn has_unconditionally_nonstandard_dust(height: u32, dust_output_count: usize) -> bool {
    if height < FIRST_V29_POLICY_HEIGHT {
        dust_output_count > 0
    } else {
        dust_output_count > 1
    }
}

fn is_dust(value: Amount, output_type: OutputType, script: &Script) -> bool {
    let threshold = match output_type {
        OutputType::P2PK65 => 672,
        OutputType::P2PK33 => 576,
        OutputType::P2PKH => 546,
        OutputType::P2MS | OutputType::Unknown => {
            return value < script.minimal_non_dust();
        }
        OutputType::P2SH => 540,
        OutputType::OpReturn => return false,
        OutputType::P2WPKH => 294,
        OutputType::P2WSH | OutputType::P2TR => 330,
        OutputType::P2A => 240,
        OutputType::Empty => 471,
    };

    value.to_sat() < threshold
}

fn has_nonstandard_witness(output_type: OutputType, facts: &input::Facts<'_>) -> bool {
    if facts.witness.stack_items == 0 {
        return false;
    }

    match output_type {
        OutputType::P2A => true,
        OutputType::P2WPKH => false,
        OutputType::P2WSH => has_nonstandard_p2wsh_witness(&facts.witness),
        OutputType::P2TR => {
            facts.witness.has_annex || has_nonstandard_taproot_witness(&facts.witness)
        }
        OutputType::P2SH => {
            if facts.redeem_sigops().is_none() {
                return true;
            }
            if facts.redeem_is_p2wsh() {
                has_nonstandard_p2wsh_witness(&facts.witness)
            } else {
                !facts.redeem_is_witness_program()
            }
        }
        _ => true,
    }
}

fn has_nonstandard_p2wsh_witness(witness: &input::WitnessFacts<'_>) -> bool {
    let stack_items = witness.stack_items - 1;
    witness.last.unwrap().len() > MAX_P2WSH_SCRIPT_BYTES
        || stack_items > MAX_P2WSH_STACK_ITEMS
        || witness.max_argument_bytes > MAX_WITNESS_STACK_ITEM_BYTES
}

fn has_nonstandard_taproot_witness(witness: &input::WitnessFacts<'_>) -> bool {
    if witness.stack_items < 2 {
        return false;
    }

    witness.leaf_version.is_none()
        || witness.leaf_version == Some(LeafVersion::TapScript)
            && witness.max_argument_bytes > MAX_WITNESS_STACK_ITEM_BYTES
}

fn has_nonstandard_version(version: i32, height: u32) -> bool {
    let max = if height <= LAST_V2_POLICY_HEIGHT {
        2
    } else {
        3
    };
    !(1..=max).contains(&version)
}

fn is_standard_unknown_witness(script: &Script) -> bool {
    script
        .witness_version()
        .is_some_and(|version| version != WitnessVersion::V0)
}

fn has_too_many_bare_multisig_keys(sigops: SigOps) -> bool {
    u32::from(sigops) > MAX_STANDARD_BARE_MULTISIG_SIGOP_COST
}

#[cfg(test)]
mod tests {
    use bitcoin::{Amount, ScriptBuf, TxIn, Witness};
    use brk_types::{
        AddrBytes, Height, OutputType, P2ABytes, P2PK33Bytes, P2PK65Bytes, P2PKHBytes, P2SHBytes,
        P2TRBytes, P2WPKHBytes, P2WSHBytes, SigOps,
    };

    use super::super::input;
    use super::{
        FIRST_V29_POLICY_HEIGHT, FIRST_V30_POLICY_HEIGHT, LAST_V2_POLICY_HEIGHT,
        has_nonstandard_p2wsh_witness, has_nonstandard_taproot_witness, has_nonstandard_version,
        has_nonstandard_witness, has_too_many_bare_multisig_keys,
        has_unconditionally_nonstandard_dust, is_dust, is_standard_unknown_witness,
        tracks_executed_legacy_sigops,
    };

    #[test]
    fn tracks_executed_sigops_only_when_policy_uses_them() {
        assert!(!tracks_executed_legacy_sigops(Height::from(
            FIRST_V30_POLICY_HEIGHT - 1
        )));
        assert!(tracks_executed_legacy_sigops(Height::from(
            FIRST_V30_POLICY_HEIGHT
        )));
    }

    #[test]
    fn accepts_only_policy_transaction_versions() {
        assert!(has_nonstandard_version(0, LAST_V2_POLICY_HEIGHT));
        assert!(has_nonstandard_version(-1, LAST_V2_POLICY_HEIGHT));
        assert!(!has_nonstandard_version(1, LAST_V2_POLICY_HEIGHT));
        assert!(!has_nonstandard_version(2, LAST_V2_POLICY_HEIGHT));
        assert!(has_nonstandard_version(3, LAST_V2_POLICY_HEIGHT));
        assert!(!has_nonstandard_version(3, LAST_V2_POLICY_HEIGHT + 1));
    }

    #[test]
    fn applies_ephemeral_dust_policy_from_v29() {
        assert!(!has_unconditionally_nonstandard_dust(
            FIRST_V29_POLICY_HEIGHT - 1,
            0
        ));
        assert!(has_unconditionally_nonstandard_dust(
            FIRST_V29_POLICY_HEIGHT - 1,
            1
        ));
        assert!(!has_unconditionally_nonstandard_dust(
            FIRST_V29_POLICY_HEIGHT,
            1
        ));
        assert!(has_unconditionally_nonstandard_dust(
            FIRST_V29_POLICY_HEIGHT,
            2
        ));
    }

    #[test]
    fn uses_exact_dust_thresholds_for_fixed_scripts() {
        let scripts = [
            AddrBytes::from(P2PK65Bytes::from(&[0; 65][..])).to_script_pubkey(),
            AddrBytes::from(P2PK33Bytes::from(&[0; 33][..])).to_script_pubkey(),
            AddrBytes::from(P2PKHBytes::from(&[0; 20][..])).to_script_pubkey(),
            AddrBytes::from(P2SHBytes::from(&[0; 20][..])).to_script_pubkey(),
            AddrBytes::from(P2WPKHBytes::from(&[0; 20][..])).to_script_pubkey(),
            AddrBytes::from(P2WSHBytes::from(&[0; 32][..])).to_script_pubkey(),
            AddrBytes::from(P2TRBytes::from(&[0; 32][..])).to_script_pubkey(),
            AddrBytes::from(P2ABytes::from(&[0; 2][..])).to_script_pubkey(),
        ];
        let thresholds = [672, 576, 546, 540, 294, 330, 330, 240];

        for ((output_type, script), threshold) in OutputType::ADDR_TYPES
            .into_iter()
            .zip(&scripts)
            .zip(thresholds)
        {
            assert_eq!(script.minimal_non_dust().to_sat(), threshold);
            assert!(is_dust(
                Amount::from_sat(threshold - 1),
                output_type,
                script
            ));
            assert!(!is_dust(Amount::from_sat(threshold), output_type, script));
        }

        let empty = ScriptBuf::new();
        assert_eq!(empty.minimal_non_dust().to_sat(), 471);
        assert!(is_dust(Amount::from_sat(470), OutputType::Empty, &empty));
        assert!(!is_dust(Amount::from_sat(471), OutputType::Empty, &empty));
    }

    #[test]
    fn keeps_exact_dust_calculation_for_unknown_scripts() {
        let script = ScriptBuf::from_bytes(vec![0x61; 1_000]);
        let threshold = script.minimal_non_dust().to_sat();

        assert!(is_dust(
            Amount::from_sat(threshold - 1),
            OutputType::Unknown,
            &script
        ));
        assert!(!is_dust(
            Amount::from_sat(threshold),
            OutputType::Unknown,
            &script
        ));
    }

    #[test]
    fn recognizes_future_witness_programs() {
        assert!(is_standard_unknown_witness(
            &ScriptBuf::from_hex(
                "52200000000000000000000000000000000000000000000000000000000000000000"
            )
            .unwrap()
        ));
        assert!(!is_standard_unknown_witness(
            &ScriptBuf::from_hex(
                "00200000000000000000000000000000000000000000000000000000000000000000"
            )
            .unwrap()
        ));
    }

    #[test]
    fn limits_standard_bare_multisig_to_three_keys() {
        assert!(!has_too_many_bare_multisig_keys(SigOps::new(12)));
        assert!(has_too_many_bare_multisig_keys(SigOps::new(16)));
    }

    #[test]
    fn rejects_p2a_witness_stuffing() {
        let input = TxIn {
            witness: Witness::from_slice(&[b"stuffing"]),
            ..TxIn::default()
        };
        let facts = input::analyze(
            &input,
            OutputType::P2A,
            &mut crate::TxFeatureFlags::default(),
        );
        assert!(has_nonstandard_witness(OutputType::P2A, &facts,));
    }

    #[test]
    fn enforces_witness_stack_item_limits() {
        let oversized = [0_u8; 81];
        let input = TxIn {
            witness: Witness::from_slice(&[oversized.as_slice(), [0x51].as_slice()]),
            ..TxIn::default()
        };
        let facts = input::analyze(
            &input,
            OutputType::P2WSH,
            &mut crate::TxFeatureFlags::default(),
        );
        assert!(has_nonstandard_p2wsh_witness(&facts.witness));

        let control_block = [0xc0_u8; 33];
        let input = TxIn {
            witness: Witness::from_slice(&[
                oversized.as_slice(),
                [0x51].as_slice(),
                control_block.as_slice(),
            ]),
            ..TxIn::default()
        };
        let facts = input::analyze(
            &input,
            OutputType::P2TR,
            &mut crate::TxFeatureFlags::default(),
        );
        assert!(has_nonstandard_taproot_witness(&facts.witness));
    }
}
