//! Consensus benchmarks for Synor.
//!
//! Benchmarks transaction validation, block validation, and UTXO operations.
//! Run with: cargo bench -p synor-consensus

use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use synor_consensus::{
    utxo::{UtxoDiff, UtxoEntry, UtxoSet},
    validation::{BlockValidator, TransactionValidator},
};
use synor_types::{
    block::{Block, BlockBody, BlockHeader},
    transaction::{Outpoint, ScriptPubKey, SubnetworkId, Transaction, TxInput, TxOutput},
    Amount, Hash256, Timestamp,
};

// ==================== Test Data Helpers ====================

/// Creates a deterministic hash.
fn make_hash(n: u64) -> Hash256 {
    let mut bytes = [0u8; 32];
    bytes[..8].copy_from_slice(&n.to_le_bytes());
    Hash256::from_bytes(bytes)
}

/// Creates a test outpoint.
fn make_outpoint(tx_n: u64, index: u32) -> Outpoint {
    Outpoint::new(make_hash(tx_n), index)
}

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

/// Creates a UTXO entry.
fn make_utxo_entry(amount: u64, daa_score: u64, is_coinbase: bool) -> UtxoEntry {
    UtxoEntry::new(make_p2pkh_output(amount), daa_score, is_coinbase)
}

/// Creates a valid regular transaction with given input/output counts.
fn make_transaction(input_count: usize, output_count: usize) -> Transaction {
    let inputs: Vec<TxInput> = (0..input_count)
        .map(|i| TxInput::new(make_outpoint(i as u64, 0), vec![0u8; 100]))
        .collect();

    let amount_per_output = 1_000_000_000u64 / output_count as u64;
    let outputs: Vec<TxOutput> = (0..output_count)
        .map(|_| make_p2pkh_output(amount_per_output))
        .collect();

    Transaction {
        version: 1,
        inputs,
        outputs,
        lock_time: 0,
        subnetwork_id: SubnetworkId::default(),
        gas: 0,
        payload: vec![],
    }
}

/// Creates a coinbase transaction.
fn make_coinbase(reward: u64) -> Transaction {
    Transaction::coinbase(vec![make_p2pkh_output(reward)], b"benchmark".to_vec())
}

/// Creates a populated UTXO set with n entries.
fn make_utxo_set(n: usize) -> UtxoSet {
    let set = UtxoSet::new();
    for i in 0..n {
        let outpoint = make_outpoint(i as u64, 0);
        let entry = make_utxo_entry(1_000_000_000, 100, false);
        set.add(outpoint, entry).unwrap();
    }
    set
}

/// Creates a test block header.
fn make_block_header(parent: Hash256) -> BlockHeader {
    BlockHeader {
        version: 1,
        parents: vec![parent],
        merkle_root: Hash256::ZERO,
        accepted_id_merkle_root: Hash256::ZERO,
        utxo_commitment: Hash256::ZERO,
        timestamp: Timestamp::now(),
        bits: 0x1d00ffff,
        nonce: 0,
        daa_score: 1000,
        blue_score: 1000.into(),
        blue_work: Hash256::ZERO,
        pruning_point: Hash256::ZERO,
    }
}

/// Creates a test block with transactions.
fn make_block(tx_count: usize) -> Block {
    let mut transactions = vec![make_coinbase(50_000_000_000)];
    for _ in 1..tx_count {
        transactions.push(make_transaction(1, 2));
    }

    let body = BlockBody { transactions };
    let merkle_root = body.merkle_root();

    let mut header = make_block_header(Hash256::ZERO);
    header.merkle_root = merkle_root;

    Block { header, body }
}

// ==================== Transaction Validation Benchmarks ====================

fn tx_structure_validation(c: &mut Criterion) {
    let mut group = c.benchmark_group("tx_structure_validation");
    let validator = TransactionValidator::new();

    for (inputs, outputs) in [(1, 1), (2, 2), (5, 5), (10, 10)] {
        let tx = make_transaction(inputs, outputs);
        let id = format!("{}in_{}out", inputs, outputs);

        group.throughput(Throughput::Elements(1));
        group.bench_with_input(BenchmarkId::new("validate", &id), &tx, |b, tx| {
            b.iter(|| black_box(validator.validate_structure(tx)))
        });
    }

    group.finish();
}

fn tx_coinbase_validation(c: &mut Criterion) {
    let validator = TransactionValidator::new();
    let coinbase = make_coinbase(50_000_000_000);

    c.bench_function("tx_coinbase_validation", |b| {
        b.iter(|| black_box(validator.validate_structure(&coinbase)))
    });
}

fn tx_utxo_validation(c: &mut Criterion) {
    let mut group = c.benchmark_group("tx_utxo_validation");
    let validator = TransactionValidator::new();

    for input_count in [1, 2, 5, 10] {
        // Create UTXO set with the UTXOs we'll spend
        let utxo_set = UtxoSet::new();
        for i in 0..input_count {
            let outpoint = make_outpoint(i as u64, 0);
            let entry = make_utxo_entry(1_000_000_000, 100, false);
            utxo_set.add(outpoint, entry).unwrap();
        }

        let tx = make_transaction(input_count, 2);

        group.throughput(Throughput::Elements(1));
        group.bench_with_input(
            BenchmarkId::from_parameter(input_count),
            &(tx, utxo_set),
            |b, (tx, utxo_set)| {
                b.iter(|| black_box(validator.validate_against_utxos(tx, utxo_set, 1000)))
            },
        );
    }

    group.finish();
}

// ==================== Block Validation Benchmarks ====================

fn block_header_validation(c: &mut Criterion) {
    let validator = BlockValidator::new();
    let header = make_block_header(Hash256::ZERO);

    c.bench_function("block_header_validation", |b| {
        b.iter(|| black_box(validator.validate_header(&header)))
    });
}

fn block_pow_validation(c: &mut Criterion) {
    let validator = BlockValidator::new();
    let header = make_block_header(Hash256::ZERO);

    // Target that the block will pass
    let target = Hash256::from_bytes([0xff; 32]);

    c.bench_function("block_pow_validation", |b| {
        b.iter(|| black_box(validator.validate_pow(&header, target)))
    });
}

fn block_full_validation(c: &mut Criterion) {
    let mut group = c.benchmark_group("block_full_validation");
    let validator = BlockValidator::new();

    for tx_count in [1, 5, 10, 50] {
        let block = make_block(tx_count);

        // Create UTXO set for the transactions
        let utxo_set = UtxoSet::new();
        for tx in block.body.transactions.iter().skip(1) {
            for input in &tx.inputs {
                let entry = make_utxo_entry(1_000_000_000, 100, false);
                let _ = utxo_set.add(input.previous_output, entry);
            }
        }

        let expected_reward = Amount::from_sompi(50_000_000_000);

        group.throughput(Throughput::Elements(tx_count as u64));
        group.bench_with_input(
            BenchmarkId::from_parameter(tx_count),
            &(block, utxo_set, expected_reward),
            |b, (block, utxo_set, reward)| {
                b.iter(|| black_box(validator.validate_block(block, utxo_set, *reward)))
            },
        );
    }

    group.finish();
}

// ==================== UTXO Set Benchmarks ====================

fn utxo_lookup(c: &mut Criterion) {
    let mut group = c.benchmark_group("utxo_lookup");

    for set_size in [100, 1000, 10000, 100000] {
        let utxo_set = make_utxo_set(set_size);
        let target = make_outpoint((set_size / 2) as u64, 0);

        group.bench_with_input(
            BenchmarkId::from_parameter(set_size),
            &(utxo_set, target),
            |b, (set, target)| b.iter(|| black_box(set.get(target))),
        );
    }

    group.finish();
}

fn utxo_contains(c: &mut Criterion) {
    let mut group = c.benchmark_group("utxo_contains");

    for set_size in [100, 1000, 10000, 100000] {
        let utxo_set = make_utxo_set(set_size);
        let target = make_outpoint((set_size / 2) as u64, 0);

        group.bench_with_input(
            BenchmarkId::from_parameter(set_size),
            &(utxo_set, target),
            |b, (set, target)| b.iter(|| black_box(set.contains(target))),
        );
    }

    group.finish();
}

fn utxo_add(c: &mut Criterion) {
    let mut group = c.benchmark_group("utxo_add");

    for set_size in [100, 1000, 10000] {
        group.bench_with_input(BenchmarkId::from_parameter(set_size), &set_size, |b, &n| {
            b.iter_batched(
                || make_utxo_set(n),
                |set| {
                    let outpoint = make_outpoint(999999, 0);
                    let entry = make_utxo_entry(1_000_000_000, 100, false);
                    black_box(set.add(outpoint, entry))
                },
                criterion::BatchSize::SmallInput,
            )
        });
    }

    group.finish();
}

fn utxo_remove(c: &mut Criterion) {
    let mut group = c.benchmark_group("utxo_remove");

    for set_size in [100, 1000, 10000] {
        group.bench_with_input(BenchmarkId::from_parameter(set_size), &set_size, |b, &n| {
            b.iter_batched(
                || {
                    let set = make_utxo_set(n);
                    let target = make_outpoint((n / 2) as u64, 0);
                    (set, target)
                },
                |(set, target)| black_box(set.remove(&target)),
                criterion::BatchSize::SmallInput,
            )
        });
    }

    group.finish();
}

// ==================== UTXO Diff Benchmarks ====================

fn utxo_diff_apply(c: &mut Criterion) {
    let mut group = c.benchmark_group("utxo_diff_apply");

    for diff_size in [10, 50, 100, 500] {
        group.bench_with_input(
            BenchmarkId::from_parameter(diff_size),
            &diff_size,
            |b, &n| {
                b.iter_batched(
                    || {
                        // Create UTXO set with entries to remove
                        let set = make_utxo_set(n);

                        // Create diff that removes half and adds half
                        let mut diff = UtxoDiff::new();

                        // Remove first half
                        for i in 0..(n / 2) {
                            diff.remove(make_outpoint(i as u64, 0));
                        }

                        // Add new entries
                        for i in n..(n + n / 2) {
                            diff.add(
                                make_outpoint(i as u64, 0),
                                make_utxo_entry(500_000_000, 100, false),
                            );
                        }

                        (set, diff)
                    },
                    |(set, diff)| black_box(set.apply_diff(&diff)),
                    criterion::BatchSize::SmallInput,
                )
            },
        );
    }

    group.finish();
}

fn utxo_diff_merge(c: &mut Criterion) {
    let mut group = c.benchmark_group("utxo_diff_merge");

    for diff_size in [10, 50, 100] {
        group.bench_with_input(
            BenchmarkId::from_parameter(diff_size),
            &diff_size,
            |b, &n| {
                b.iter_batched(
                    || {
                        let mut diff1 = UtxoDiff::new();
                        let mut diff2 = UtxoDiff::new();

                        for i in 0..n {
                            diff1.add(
                                make_outpoint(i as u64, 0),
                                make_utxo_entry(100_000_000, 100, false),
                            );
                        }

                        for i in 0..(n / 2) {
                            diff2.remove(make_outpoint(i as u64, 0));
                        }
                        for i in n..(n * 2) {
                            diff2.add(
                                make_outpoint(i as u64, 0),
                                make_utxo_entry(200_000_000, 100, false),
                            );
                        }

                        (diff1, diff2)
                    },
                    |(mut diff1, diff2)| {
                        diff1.merge(diff2);
                        black_box(diff1)
                    },
                    criterion::BatchSize::SmallInput,
                )
            },
        );
    }

    group.finish();
}

fn utxo_create_tx_diff(c: &mut Criterion) {
    let mut group = c.benchmark_group("utxo_create_tx_diff");

    for input_count in [1, 2, 5, 10] {
        // Create UTXO set with UTXOs to spend
        let utxo_set = UtxoSet::new();
        for i in 0..input_count {
            let outpoint = make_outpoint(i as u64, 0);
            let entry = make_utxo_entry(1_000_000_000, 100, false);
            utxo_set.add(outpoint, entry).unwrap();
        }

        let tx = make_transaction(input_count, 2);

        group.throughput(Throughput::Elements(1));
        group.bench_with_input(
            BenchmarkId::from_parameter(input_count),
            &(utxo_set, tx),
            |b, (set, tx)| b.iter(|| black_box(set.create_transaction_diff(tx, 1000))),
        );
    }

    group.finish();
}

// ==================== UTXO Selection Benchmarks ====================

fn utxo_get_balance(c: &mut Criterion) {
    use synor_types::{Address, Network};

    // Create a test address from a dummy Ed25519 public key
    let dummy_pubkey = [0u8; 32];
    let address = Address::from_ed25519_pubkey(Network::Mainnet, &dummy_pubkey);

    c.bench_function("utxo_get_balance_1000", |b| {
        // For this benchmark, we use an empty set since address matching
        // requires proper script parsing
        let utxo_set = make_utxo_set(1000);

        b.iter(|| black_box(utxo_set.get_balance(&address, Network::Mainnet)))
    });
}

// ==================== Batch Validation Benchmarks ====================

fn batch_tx_validation(c: &mut Criterion) {
    let mut group = c.benchmark_group("batch_tx_validation");
    let validator = TransactionValidator::new();

    for batch_size in [10, 50, 100] {
        let transactions: Vec<_> = (0..batch_size).map(|_| make_transaction(2, 2)).collect();

        group.throughput(Throughput::Elements(batch_size as u64));
        group.bench_with_input(
            BenchmarkId::from_parameter(batch_size),
            &transactions,
            |b, txs| {
                b.iter(|| {
                    for tx in txs {
                        black_box(validator.validate_structure(tx)).ok();
                    }
                })
            },
        );
    }

    group.finish();
}

// ==================== Criterion Groups ====================

criterion_group!(
    tx_validation_benches,
    tx_structure_validation,
    tx_coinbase_validation,
    tx_utxo_validation,
);

criterion_group!(
    block_validation_benches,
    block_header_validation,
    block_pow_validation,
    block_full_validation,
);

criterion_group!(
    utxo_set_benches,
    utxo_lookup,
    utxo_contains,
    utxo_add,
    utxo_remove,
);

criterion_group!(
    utxo_diff_benches,
    utxo_diff_apply,
    utxo_diff_merge,
    utxo_create_tx_diff,
);

criterion_group!(misc_benches, utxo_get_balance, batch_tx_validation,);

criterion_main!(
    tx_validation_benches,
    block_validation_benches,
    utxo_set_benches,
    utxo_diff_benches,
    misc_benches,
);
