//! GHOSTDAG benchmarks.
//!
//! Benchmarks the core GHOSTDAG operations:
//! - Block insertion with GHOSTDAG data calculation
//! - Blue score computation
//! - Merge set partitioning
//! - Selected chain traversal
//! - Reachability queries
//!
//! Run with: cargo bench -p synor-dag

use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::sync::Arc;
use synor_dag::{BlockDag, BlockId, GhostdagManager, ReachabilityStore};
use synor_types::Hash256;

/// Helper to create deterministic block IDs.
fn make_block_id(n: u64) -> BlockId {
    let mut bytes = [0u8; 32];
    bytes[..8].copy_from_slice(&n.to_le_bytes());
    Hash256::from_bytes(bytes)
}

/// Creates a test DAG setup.
fn setup_dag() -> (Arc<BlockDag>, Arc<ReachabilityStore>, GhostdagManager) {
    let genesis = make_block_id(0);
    let dag = Arc::new(BlockDag::new(genesis, 0));
    let reachability = Arc::new(ReachabilityStore::new(genesis));
    let ghostdag = GhostdagManager::new(dag.clone(), reachability.clone());
    (dag, reachability, ghostdag)
}

/// Builds a linear chain of n blocks.
fn build_linear_chain(
    n: usize,
    dag: &Arc<BlockDag>,
    reachability: &Arc<ReachabilityStore>,
    ghostdag: &GhostdagManager,
) -> Vec<BlockId> {
    let genesis = dag.genesis();
    let mut blocks = vec![genesis];

    for i in 1..=n {
        let block = make_block_id(i as u64);
        let parent = blocks[i - 1];

        dag.insert_block(block, vec![parent], i as u64 * 100)
            .unwrap();
        reachability.add_block(block, parent, &[parent]).unwrap();
        ghostdag.add_block(block, &[parent]).unwrap();
        blocks.push(block);
    }

    blocks
}

/// Builds a DAG with specified width (parallel chains).
fn build_wide_dag(
    depth: usize,
    width: usize,
    dag: &Arc<BlockDag>,
    reachability: &Arc<ReachabilityStore>,
    ghostdag: &GhostdagManager,
) -> Vec<Vec<BlockId>> {
    let genesis = dag.genesis();
    let mut layers: Vec<Vec<BlockId>> = vec![vec![genesis]];

    for d in 1..=depth {
        let mut layer = Vec::new();

        for w in 0..width {
            let block_num = (d * 1000 + w) as u64;
            let block = make_block_id(block_num);

            // Connect to all blocks in previous layer (up to MAX_BLOCK_PARENTS)
            let parents: Vec<_> = layers[d - 1].to_vec();
            let selected_parent = parents[0];

            dag.insert_block(block, parents.clone(), block_num * 100)
                .unwrap();
            reachability
                .add_block(block, selected_parent, &parents)
                .unwrap();
            ghostdag.add_block(block, &parents).unwrap();

            layer.push(block);
        }

        layers.push(layer);
    }

    layers
}

// ==================== Block Insertion Benchmarks ====================

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

    for chain_len in [10, 50, 100, 500] {
        group.throughput(Throughput::Elements(1));
        group.bench_with_input(
            BenchmarkId::from_parameter(chain_len),
            &chain_len,
            |b, &n| {
                b.iter_batched(
                    || {
                        let (dag, reachability, ghostdag) = setup_dag();
                        // Pre-build the chain except last block
                        let blocks = build_linear_chain(n - 1, &dag, &reachability, &ghostdag);
                        (dag, reachability, ghostdag, blocks)
                    },
                    |(dag, reachability, ghostdag, blocks)| {
                        // Benchmark inserting the final block
                        let new_block = make_block_id(n as u64);
                        let parent = *blocks.last().unwrap();

                        dag.insert_block(new_block, vec![parent], n as u64 * 100)
                            .unwrap();
                        reachability
                            .add_block(new_block, parent, &[parent])
                            .unwrap();
                        black_box(ghostdag.add_block(new_block, &[parent]).unwrap())
                    },
                    criterion::BatchSize::SmallInput,
                )
            },
        );
    }

    group.finish();
}

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

    // Test with different DAG widths
    for width in [2, 4, 8, 16] {
        group.throughput(Throughput::Elements(1));
        group.bench_with_input(BenchmarkId::from_parameter(width), &width, |b, &w| {
            b.iter_batched(
                || {
                    let (dag, reachability, ghostdag) = setup_dag();
                    // Pre-build DAG with specified width
                    let layers = build_wide_dag(10, w, &dag, &reachability, &ghostdag);
                    (dag, reachability, ghostdag, layers)
                },
                |(dag, reachability, ghostdag, layers)| {
                    // Benchmark inserting a new block on top
                    let new_block = make_block_id(99999);
                    let parents: Vec<_> = layers.last().unwrap().to_vec();
                    let selected_parent = parents[0];

                    dag.insert_block(new_block, parents.clone(), 99999 * 100)
                        .unwrap();
                    reachability
                        .add_block(new_block, selected_parent, &parents)
                        .unwrap();
                    black_box(ghostdag.add_block(new_block, &parents).unwrap())
                },
                criterion::BatchSize::SmallInput,
            )
        });
    }

    group.finish();
}

// ==================== Blue Score Benchmarks ====================

fn blue_score_calculation(c: &mut Criterion) {
    let (dag, reachability, ghostdag) = setup_dag();
    let blocks = build_linear_chain(100, &dag, &reachability, &ghostdag);
    let tip = *blocks.last().unwrap();

    c.bench_function("blue_score_get", |b| {
        b.iter(|| black_box(ghostdag.get_blue_score(&tip).unwrap()))
    });
}

fn blue_score_batch(c: &mut Criterion) {
    let (dag, reachability, ghostdag) = setup_dag();
    let blocks = build_linear_chain(100, &dag, &reachability, &ghostdag);

    c.bench_function("blue_score_batch_100", |b| {
        b.iter(|| {
            for block in &blocks {
                black_box(ghostdag.get_blue_score(block).unwrap());
            }
        })
    });
}

// ==================== Selected Chain Benchmarks ====================

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

    for chain_len in [10, 50, 100, 500] {
        group.bench_with_input(
            BenchmarkId::from_parameter(chain_len),
            &chain_len,
            |b, &n| {
                let (dag, reachability, ghostdag) = setup_dag();
                let blocks = build_linear_chain(n, &dag, &reachability, &ghostdag);
                let tip = *blocks.last().unwrap();

                b.iter(|| black_box(ghostdag.get_selected_chain(&tip).unwrap()))
            },
        );
    }

    group.finish();
}

// ==================== Reachability Benchmarks ====================

fn reachability_ancestor_check(c: &mut Criterion) {
    let (dag, reachability, ghostdag) = setup_dag();
    let blocks = build_linear_chain(100, &dag, &reachability, &ghostdag);
    let tip = *blocks.last().unwrap();
    let genesis = dag.genesis();

    c.bench_function("reachability_is_ancestor_100", |b| {
        b.iter(|| black_box(reachability.is_ancestor(&genesis, &tip).unwrap()))
    });
}

fn reachability_anticone_check(c: &mut Criterion) {
    let (dag, reachability, ghostdag) = setup_dag();

    // Build a forked DAG
    let genesis = dag.genesis();

    // First chain
    let block1 = make_block_id(1);
    let block2 = make_block_id(2);
    dag.insert_block(block1, vec![genesis], 100).unwrap();
    dag.insert_block(block2, vec![block1], 200).unwrap();
    reachability.add_block(block1, genesis, &[genesis]).unwrap();
    reachability.add_block(block2, block1, &[block1]).unwrap();
    ghostdag.add_block(block1, &[genesis]).unwrap();
    ghostdag.add_block(block2, &[block1]).unwrap();

    // Second chain (parallel)
    let block3 = make_block_id(3);
    let block4 = make_block_id(4);
    dag.insert_block(block3, vec![genesis], 150).unwrap();
    dag.insert_block(block4, vec![block3], 250).unwrap();
    reachability.add_block(block3, genesis, &[genesis]).unwrap();
    reachability.add_block(block4, block3, &[block3]).unwrap();
    ghostdag.add_block(block3, &[genesis]).unwrap();
    ghostdag.add_block(block4, &[block3]).unwrap();

    // block2 and block4 are in each other's anticone
    c.bench_function("reachability_is_anticone", |b| {
        b.iter(|| black_box(reachability.is_anticone(&block2, &block4).unwrap()))
    });
}

// ==================== Merge Set Benchmarks ====================

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

    for width in [2, 4, 8] {
        group.bench_with_input(BenchmarkId::from_parameter(width), &width, |b, &w| {
            b.iter_batched(
                || {
                    let (dag, reachability, ghostdag) = setup_dag();
                    let layers = build_wide_dag(5, w, &dag, &reachability, &ghostdag);
                    (dag, reachability, ghostdag, layers)
                },
                |(dag, reachability, ghostdag, layers)| {
                    // Insert a new block that merges multiple parents
                    let new_block = make_block_id(99999);
                    let parents: Vec<_> = layers.last().unwrap().to_vec();
                    let selected_parent = parents[0];

                    dag.insert_block(new_block, parents.clone(), 99999 * 100)
                        .unwrap();
                    reachability
                        .add_block(new_block, selected_parent, &parents)
                        .unwrap();
                    let data = ghostdag.add_block(new_block, &parents).unwrap();
                    black_box(data.merge_set_size())
                },
                criterion::BatchSize::SmallInput,
            )
        });
    }

    group.finish();
}

// ==================== k-Cluster Benchmarks ====================

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

    for k in [3, 10, 18] {
        group.bench_with_input(BenchmarkId::from_parameter(k), &k, |b, &k_val| {
            b.iter_batched(
                || {
                    let genesis = make_block_id(0);
                    let dag = Arc::new(BlockDag::new(genesis, 0));
                    let reachability = Arc::new(ReachabilityStore::new(genesis));
                    let ghostdag =
                        GhostdagManager::with_k(dag.clone(), reachability.clone(), k_val);
                    build_wide_dag(5, k_val as usize, &dag, &reachability, &ghostdag);
                    (dag, reachability, ghostdag)
                },
                |(_dag, _reachability, ghostdag)| {
                    // Check k-cluster property
                    black_box(ghostdag.k())
                },
                criterion::BatchSize::SmallInput,
            )
        });
    }

    group.finish();
}

// ==================== GHOSTDAG Data Access ====================

fn ghostdag_data_access(c: &mut Criterion) {
    let (dag, reachability, ghostdag) = setup_dag();
    let blocks = build_linear_chain(100, &dag, &reachability, &ghostdag);
    let tip = *blocks.last().unwrap();

    c.bench_function("ghostdag_get_data", |b| {
        b.iter(|| black_box(ghostdag.get_data(&tip).unwrap()))
    });

    c.bench_function("ghostdag_get_selected_parent", |b| {
        b.iter(|| black_box(ghostdag.get_selected_parent(&tip).unwrap()))
    });

    c.bench_function("ghostdag_is_blue", |b| {
        b.iter(|| black_box(ghostdag.is_blue(&tip)))
    });
}

// ==================== DAG Operations ====================

fn dag_block_lookup(c: &mut Criterion) {
    let (dag, reachability, ghostdag) = setup_dag();
    let blocks = build_linear_chain(100, &dag, &reachability, &ghostdag);
    let tip = *blocks.last().unwrap();

    c.bench_function("dag_get_block", |b| {
        b.iter(|| black_box(dag.get_block(&tip)))
    });

    c.bench_function("dag_get_parents", |b| {
        b.iter(|| black_box(dag.get_parents(&tip)))
    });

    c.bench_function("dag_get_children", |b| {
        b.iter(|| black_box(dag.get_children(&tip)))
    });
}

fn dag_tips_query(c: &mut Criterion) {
    let (dag, reachability, ghostdag) = setup_dag();
    build_wide_dag(10, 4, &dag, &reachability, &ghostdag);

    c.bench_function("dag_get_tips", |b| b.iter(|| black_box(dag.tips())));
}

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

criterion_group!(
    insertion_benches,
    block_insertion_linear,
    block_insertion_wide,
);

criterion_group!(score_benches, blue_score_calculation, blue_score_batch,);

criterion_group!(chain_benches, selected_chain_traversal,);

criterion_group!(
    reachability_benches,
    reachability_ancestor_check,
    reachability_anticone_check,
);

criterion_group!(merge_benches, merge_set_calculation, k_cluster_validation,);

criterion_group!(
    data_access_benches,
    ghostdag_data_access,
    dag_block_lookup,
    dag_tips_query,
);

criterion_main!(
    insertion_benches,
    score_benches,
    chain_benches,
    reachability_benches,
    merge_benches,
    data_access_benches
);
