use criterion::{BenchmarkId, Criterion, Throughput, criterion_group, criterion_main};
use std::hint::black_box;

use betex::{
    book::Book,
    book::protocol::command::{Command, CommandKind, MarketState, Persistence, Side, TimeInForce},
    types::*,
};

const MARKET_ID: MarketId = MarketId(1);

fn account_id() -> AccountId {
    AccountId::from(1000)
}

fn place_back_cmd(
    order_id: OrderId,
    runner_id: RunnerId,
    odds: OddsX10000,
    stake: Money,
) -> Command {
    Command {
        correlation_id: None,
        metadata: None,
        market_id: MARKET_ID,
        kind: CommandKind::PlaceOrder {
            runner_id,
            account_id: account_id(),
            client_order_id: Some(ClientOrderId(order_id.0)),
            side: Side::Back,
            odds,
            stake,
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    }
}

fn place_lay_cmd(
    order_id: OrderId,
    runner_id: RunnerId,
    odds: OddsX10000,
    stake: Money,
) -> Command {
    Command {
        correlation_id: None,
        metadata: None,
        market_id: MARKET_ID,
        kind: CommandKind::PlaceOrder {
            runner_id,
            account_id: account_id(),
            client_order_id: Some(ClientOrderId(order_id.0)),
            side: Side::Lay,
            odds,
            stake,
            persistence: Persistence::Persist,
            time_in_force: TimeInForce::Gtc,
        },
    }
}

fn cancel_cmd(order_id: OrderId) -> Command {
    Command {
        correlation_id: None,
        metadata: None,
        market_id: MARKET_ID,
        kind: CommandKind::CancelOrder {
            account_id: account_id(),
            order_id,
        },
    }
}

fn set_state_cmd(state: MarketState) -> Command {
    Command {
        correlation_id: None,
        metadata: None,
        market_id: MARKET_ID,
        kind: CommandKind::SetMarketState { state },
    }
}

fn set_live_phase_cmd() -> Command {
    Command {
        correlation_id: None,
        metadata: None,
        market_id: MARKET_ID,
        kind: CommandKind::SetMarketPhase {
            phase: MarketPhase::Live,
        },
    }
}

fn apply_cmd_and_events(book: &mut Book, cmd: &Command) {
    let (events, _) = black_box(book.handle(black_box(cmd))).unwrap();
    book.apply_all_events(&black_box(events));
}

fn create_two_runner_book() -> Book {
    Book::new_two_runner_with_kind_and_capacity(
        MARKET_ID,
        MarketKind::InPlayCapable,
        RunnerId(1),
        RunnerId(2),
        20_000,
    )
}

fn create_multi_runner_book(runner_ids: impl IntoIterator<Item = RunnerId>) -> Book {
    Book::new_multi_runner_with_kind_and_capacity(
        MARKET_ID,
        MarketKind::InPlayCapable,
        runner_ids,
        20_000,
    )
}

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

    for (book_type, n) in [
        ("two_runner", 100usize),
        ("two_runner", 200),
        ("two_runner", 500),
        ("multi_runner", 100),
        ("multi_runner", 200),
        ("multi_runner", 500),
    ] {
        let book_type_str = book_type.to_string();
        group.throughput(Throughput::Elements(n as u64));
        group.bench_with_input(BenchmarkId::new(book_type, n), &n, |b, n| {
            b.iter(|| {
                let mut book = if book_type_str == "two_runner" {
                    create_two_runner_book()
                } else {
                    create_multi_runner_book([RunnerId(1), RunnerId(2)])
                };
                for i in 0..*n {
                    let odds = OddsX10000(10_100 + (i as u32 * 100));
                    let cmd = place_back_cmd(OrderId(1 + i as u64), RunnerId(1), odds, Money(100));
                    apply_cmd_and_events(&mut book, &cmd);
                }
            });
        });
    }
    group.finish();
}

fn bench_simple_full_match(c: &mut Criterion) {
    let mut group = c.benchmark_group("simple_full_match");
    group.throughput(Throughput::Elements(1));

    for book_type in ["two_runner", "multi_runner"] {
        group.bench_with_input(
            BenchmarkId::from_parameter(book_type),
            &book_type,
            |b, _| {
                b.iter(|| {
                    let mut book = match book_type {
                        "two_runner" => create_two_runner_book(),
                        "multi_runner" => create_multi_runner_book([RunnerId(1), RunnerId(2)]),
                        _ => unreachable!(),
                    };
                    let setup_cmd =
                        place_back_cmd(OrderId(1), RunnerId(1), OddsX10000(20_000), Money(100));
                    apply_cmd_and_events(&mut book, &setup_cmd);
                    let match_cmd =
                        place_lay_cmd(OrderId(2), RunnerId(1), OddsX10000(20_000), Money(100));
                    apply_cmd_and_events(&mut book, &match_cmd);
                });
            },
        );
    }
    group.finish();
}

fn bench_match_through_levels(c: &mut Criterion) {
    let mut group = c.benchmark_group("match_through_levels");
    group.throughput(Throughput::Elements(1));

    for book_type in ["two_runner", "multi_runner"] {
        group.bench_with_input(
            BenchmarkId::from_parameter(book_type),
            &book_type,
            |b, _| {
                b.iter(|| {
                    let mut book = match book_type {
                        "two_runner" => create_two_runner_book(),
                        "multi_runner" => create_multi_runner_book([RunnerId(1), RunnerId(2)]),
                        _ => unreachable!(),
                    };
                    let base_odds = 10_100;
                    for i in 0..25 {
                        let odds = OddsX10000(base_odds + (i as u32 * 100));
                        for j in 0..2 {
                            let cmd = place_back_cmd(
                                OrderId(1 + i * 2 + j as u64),
                                RunnerId(1),
                                odds,
                                Money(100),
                            );
                            apply_cmd_and_events(&mut book, &cmd);
                        }
                    }
                    let total_stake = Money(25 * 2 * 100);
                    let match_cmd =
                        place_lay_cmd(OrderId(100), RunnerId(1), OddsX10000(35_000), total_stake);
                    apply_cmd_and_events(&mut book, &match_cmd);
                });
            },
        );
    }
    group.finish();
}

fn bench_partial_match(c: &mut Criterion) {
    let mut group = c.benchmark_group("partial_match");
    group.throughput(Throughput::Elements(1));

    for book_type in ["two_runner", "multi_runner"] {
        group.bench_with_input(
            BenchmarkId::from_parameter(book_type),
            &book_type,
            |b, _| {
                b.iter(|| {
                    let mut book = match book_type {
                        "two_runner" => create_two_runner_book(),
                        "multi_runner" => create_multi_runner_book([RunnerId(1), RunnerId(2)]),
                        _ => unreachable!(),
                    };
                    let setup_cmd =
                        place_back_cmd(OrderId(1), RunnerId(1), OddsX10000(20_000), Money(1000));
                    apply_cmd_and_events(&mut book, &setup_cmd);
                    let match_cmd =
                        place_lay_cmd(OrderId(2), RunnerId(1), OddsX10000(20_000), Money(300));
                    apply_cmd_and_events(&mut book, &match_cmd);
                });
            },
        );
    }
    group.finish();
}

fn bench_cancel_order(c: &mut Criterion) {
    let mut group = c.benchmark_group("cancel_order");
    group.throughput(Throughput::Elements(1));

    for book_type in ["two_runner", "multi_runner"] {
        group.bench_with_input(
            BenchmarkId::from_parameter(book_type),
            &book_type,
            |b, _| {
                b.iter(|| {
                    let mut book = match book_type {
                        "two_runner" => create_two_runner_book(),
                        "multi_runner" => create_multi_runner_book([RunnerId(1), RunnerId(2)]),
                        _ => unreachable!(),
                    };
                    for i in 0..100 {
                        let odds = OddsX10000(10_100 + (i as u32 * 100));
                        let cmd =
                            place_back_cmd(OrderId(1 + i as u64), RunnerId(1), odds, Money(100));
                        apply_cmd_and_events(&mut book, &cmd);
                    }
                    let cancel_cmd = cancel_cmd(OrderId(50));
                    apply_cmd_and_events(&mut book, &cancel_cmd);
                });
            },
        );
    }
    group.finish();
}

fn bench_tworunner_implied_match(c: &mut Criterion) {
    let mut group = c.benchmark_group("tworunner_implied_match");
    group.throughput(Throughput::Elements(1));

    group.bench_function("two_runner", |b| {
        b.iter(|| {
            let mut book = create_two_runner_book();
            let setup_cmd = place_back_cmd(OrderId(1), RunnerId(1), OddsX10000(20_000), Money(100));
            apply_cmd_and_events(&mut book, &setup_cmd);
            let implied_match_cmd =
                place_back_cmd(OrderId(2), RunnerId(2), OddsX10000(20_000), Money(100));
            apply_cmd_and_events(&mut book, &implied_match_cmd);
        });
    });
    group.finish();
}

fn bench_market_state_transition(c: &mut Criterion) {
    let mut group = c.benchmark_group("market_state_transition");
    group.throughput(Throughput::Elements(1));

    for book_type in ["two_runner", "multi_runner"] {
        group.bench_with_input(
            BenchmarkId::from_parameter(book_type),
            &book_type,
            |b, _| {
                b.iter(|| {
                    let mut book = match book_type {
                        "two_runner" => create_two_runner_book(),
                        "multi_runner" => create_multi_runner_book([RunnerId(1), RunnerId(2)]),
                        _ => unreachable!(),
                    };
                    for i in 0..50 {
                        let odds = OddsX10000(10_100 + (i as u32 * 100));
                        let cmd =
                            place_back_cmd(OrderId(1 + i as u64), RunnerId(1), odds, Money(100));
                        apply_cmd_and_events(&mut book, &cmd);
                    }
                    apply_cmd_and_events(&mut book, &set_state_cmd(MarketState::Suspended));
                    apply_cmd_and_events(&mut book, &set_live_phase_cmd());
                    apply_cmd_and_events(&mut book, &set_state_cmd(MarketState::Closed));
                });
            },
        );
    }
    group.finish();
}

fn bench_complex_realistic_scenario(c: &mut Criterion) {
    let mut group = c.benchmark_group("complex_realistic_scenario");
    group.throughput(Throughput::Elements(1));

    let runner_ids = [
        RunnerId(1),
        RunnerId(2),
        RunnerId(3),
        RunnerId(4),
        RunnerId(5),
    ];

    group.bench_function("multi_runner", |b| {
        b.iter(|| {
            let mut book = create_multi_runner_book(runner_ids);
            let mut order_id_counter = 1u64;

            for &runner_id in &runner_ids {
                for side in [Side::Back, Side::Lay] {
                    for level in 0..10 {
                        let odds = OddsX10000(10_100 + level as u32 * 100);
                        let cmd = Command {
                            correlation_id: None,
                            metadata: None,
                            market_id: MARKET_ID,
                            kind: CommandKind::PlaceOrder {
                                runner_id,
                                account_id: account_id(),
                                client_order_id: Some(ClientOrderId(order_id_counter)),
                                side,
                                odds,
                                stake: Money(100),
                                persistence: Persistence::Persist,
                                time_in_force: TimeInForce::Gtc,
                            },
                        };
                        apply_cmd_and_events(&mut book, &cmd);
                        order_id_counter += 1;
                    }
                }
            }

            let test_cmd = Command {
                correlation_id: None,
                metadata: None,
                market_id: MARKET_ID,
                kind: CommandKind::PlaceOrder {
                    runner_id: RunnerId(1),
                    account_id: account_id(),
                    client_order_id: Some(ClientOrderId(1000)),
                    side: Side::Lay,
                    odds: OddsX10000(12_000),
                    stake: Money(600),
                    persistence: Persistence::Persist,
                    time_in_force: TimeInForce::Gtc,
                },
            };
            apply_cmd_and_events(&mut book, &test_cmd);
        });
    });
    group.finish();
}

criterion_group!(
    book_benchmarks,
    bench_insert_no_match,
    bench_simple_full_match,
    bench_match_through_levels,
    bench_partial_match,
    bench_cancel_order,
    bench_tworunner_implied_match,
    bench_market_state_transition,
    bench_complex_realistic_scenario,
);
criterion_main!(book_benchmarks);
