use chrono::NaiveDate;
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput};
use std::io::Write;
use std::path::Path;
use std::time::Duration;
use tempfile::{NamedTempFile, TempDir};
use timebomb::config::{CliOverrides, Config};
use timebomb::diff::parse_unified_diff;
use timebomb::scanner::{build_regex, is_binary, scan, scan_content, scan_str};
use timebomb::snooze::{append_reason, snooze_line};

// ── Fixture builders ──────────────────────────────────────────────────────────

fn fixed_today() -> NaiveDate {
    NaiveDate::from_ymd_opt(2025, 6, 1).unwrap()
}

fn default_config() -> Config {
    Config::default()
}

/// Build a file with `total_lines` lines where annotations appear every
/// `annotation_every` lines. Pass `usize::MAX` to get zero annotations.
fn build_content(total_lines: usize, annotation_every: usize) -> String {
    let mut out = String::with_capacity(total_lines * 60);
    for i in 0..total_lines {
        if annotation_every < usize::MAX && i % annotation_every == 0 {
            // Half expired, half future — realistic mix
            let date = if i % 2 == 0 {
                "2020-01-01"
            } else {
                "2099-01-01"
            };
            let tag = ["TODO", "FIXME", "HACK", "TEMP", "REMOVEME"][i % 5];
            out.push_str(&format!(
                "    // {}[{}]: annotation number {}\n",
                tag, date, i
            ));
        } else {
            out.push_str(&format!("    let x_{} = {};\n", i, i));
        }
    }
    out
}

/// Build a file where every single line is an annotation.
fn build_dense_content(lines: usize) -> String {
    let mut out = String::with_capacity(lines * 60);
    for i in 0..lines {
        let date = if i % 2 == 0 {
            "2020-01-01"
        } else {
            "2099-01-01"
        };
        out.push_str(&format!("    // TODO[{}]: dense annotation {}\n", date, i));
    }
    out
}

/// Build content that has NO `[` characters at all — exercises the pre-filter
/// fast path exclusively.
fn build_no_bracket_content(lines: usize) -> String {
    let mut out = String::with_capacity(lines * 50);
    for i in 0..lines {
        out.push_str(&format!("    let variable_{} = {};\n", i, i));
    }
    out
}

/// Build content with `[` characters but no valid timebomb annotations —
/// exercises the regex on lines that pass the byte pre-filter but don't match.
fn build_bracket_no_annotation_content(lines: usize) -> String {
    let mut out = String::with_capacity(lines * 60);
    for i in 0..lines {
        if i % 3 == 0 {
            out.push_str(&format!("    let arr[{}] = value;\n", i));
        } else if i % 3 == 1 {
            out.push_str(&format!("    // TODO: fix this [issue {}]\n", i));
        } else {
            out.push_str(&format!("    let x = foo[{}];\n", i));
        }
    }
    out
}

// ── scan_content benchmarks ───────────────────────────────────────────────────

fn bench_scan_content_no_annotations(c: &mut Criterion) {
    let cfg = default_config();
    let regex = build_regex(&cfg).unwrap();
    let today = fixed_today();
    let path = Path::new("bench/fake.rs");

    // No `[` at all — best case for the pre-filter
    let content_no_bracket = build_no_bracket_content(10_000);
    // Has `[` but no valid annotations — exercises regex on filtered lines
    let content_bracket = build_bracket_no_annotation_content(10_000);

    let mut group = c.benchmark_group("scan_content/no_annotations");
    group.throughput(Throughput::Elements(10_000));

    group.bench_function("no_bracket_10k_lines", |b| {
        b.iter(|| scan_content(&content_no_bracket, path, &regex, &cfg, today).unwrap())
    });

    group.bench_function("bracket_no_annotation_10k_lines", |b| {
        b.iter(|| scan_content(&content_bracket, path, &regex, &cfg, today).unwrap())
    });

    group.finish();
}

fn bench_scan_content_sparse(c: &mut Criterion) {
    let cfg = default_config();
    let regex = build_regex(&cfg).unwrap();
    let today = fixed_today();
    let path = Path::new("bench/fake.rs");

    // Annotation every 2000 lines → 5 annotations in 10k lines
    let content = build_content(10_000, 2_000);

    let mut group = c.benchmark_group("scan_content/sparse");
    group.throughput(Throughput::Elements(10_000));

    group.bench_function("5_annotations_10k_lines", |b| {
        b.iter(|| scan_content(&content, path, &regex, &cfg, today).unwrap())
    });

    group.finish();
}

fn bench_scan_content_density(c: &mut Criterion) {
    let cfg = default_config();
    let regex = build_regex(&cfg).unwrap();
    let today = fixed_today();
    let path = Path::new("bench/fake.rs");

    let mut group = c.benchmark_group("scan_content/by_density");

    for &(total, every, label) in &[
        (1_000, usize::MAX, "1k_lines_0_annotations"),
        (1_000, 100, "1k_lines_10_annotations"),
        (1_000, 10, "1k_lines_100_annotations"),
        (1_000, 1, "1k_lines_1000_annotations"),
        (10_000, usize::MAX, "10k_lines_0_annotations"),
        (10_000, 1_000, "10k_lines_10_annotations"),
        (10_000, 100, "10k_lines_100_annotations"),
        (10_000, 10, "10k_lines_1000_annotations"),
    ] {
        let content = build_content(total, every);
        group.throughput(Throughput::Elements(total as u64));
        group.bench_with_input(
            BenchmarkId::from_parameter(label),
            &content,
            |b, content| b.iter(|| scan_content(content, path, &regex, &cfg, today).unwrap()),
        );
    }

    group.finish();
}

fn bench_scan_content_dense(c: &mut Criterion) {
    let cfg = default_config();
    let regex = build_regex(&cfg).unwrap();
    let today = fixed_today();
    let path = Path::new("bench/fake.rs");

    // Every line is an annotation
    let content_500 = build_dense_content(500);
    let content_2000 = build_dense_content(2_000);

    let mut group = c.benchmark_group("scan_content/dense");

    group.throughput(Throughput::Elements(500));
    group.bench_function("every_line_500_lines", |b| {
        b.iter(|| scan_content(&content_500, path, &regex, &cfg, today).unwrap())
    });

    group.throughput(Throughput::Elements(2_000));
    group.bench_function("every_line_2k_lines", |b| {
        b.iter(|| scan_content(&content_2000, path, &regex, &cfg, today).unwrap())
    });

    group.finish();
}

// ── scan_str benchmark (includes regex compile) ───────────────────────────────

fn bench_scan_str_includes_regex_compile(c: &mut Criterion) {
    let cfg = default_config();
    let today = fixed_today();
    let path = Path::new("bench/fake.rs");
    let content = build_content(1_000, 200); // 5 annotations in 1k lines

    let mut group = c.benchmark_group("scan_str");
    group.throughput(Throughput::Elements(1_000));

    // scan_str compiles the regex on every call — establishes the cost of
    // NOT caching the regex (the wrong pattern, as a baseline comparison).
    group.bench_function("1k_lines_5_annotations_with_regex_compile", |b| {
        b.iter(|| scan_str(&content, path, &cfg, today).unwrap())
    });

    // Same workload but regex pre-compiled — shows the amortised cost.
    let regex = build_regex(&cfg).unwrap();
    group.bench_function("1k_lines_5_annotations_precompiled_regex", |b| {
        b.iter(|| scan_content(&content, path, &regex, &cfg, today).unwrap())
    });

    group.finish();
}

// ── build_regex benchmark ─────────────────────────────────────────────────────

fn bench_build_regex(c: &mut Criterion) {
    let cfg_default = default_config();

    // Config with 5 triggers
    let cfg_5_tags = Config {
        triggers: vec![
            "TODO".into(),
            "FIXME".into(),
            "HACK".into(),
            "TEMP".into(),
            "REMOVEME".into(),
        ],
        ..Config::default()
    };

    // Config with a single trigger — shorter alternation
    let cfg_1_tag = Config {
        triggers: vec!["TODO".into()],
        ..Config::default()
    };

    // Config with many triggers — longer alternation
    let cfg_10_tags = Config {
        triggers: vec![
            "TODO".into(),
            "FIXME".into(),
            "HACK".into(),
            "TEMP".into(),
            "REMOVEME".into(),
            "DEBT".into(),
            "STOPSHIP".into(),
            "WORKAROUND".into(),
            "DEPRECATED".into(),
            "BUG".into(),
        ],
        ..Config::default()
    };

    let mut group = c.benchmark_group("build_regex");

    group.bench_function("default_5_tags", |b| {
        b.iter(|| build_regex(&cfg_default).unwrap())
    });

    group.bench_function("1_tag", |b| b.iter(|| build_regex(&cfg_1_tag).unwrap()));

    group.bench_function("5_tags", |b| b.iter(|| build_regex(&cfg_5_tags).unwrap()));

    group.bench_function("10_tags", |b| b.iter(|| build_regex(&cfg_10_tags).unwrap()));

    group.finish();
}

// ── is_binary benchmark ───────────────────────────────────────────────────────

fn bench_is_binary(c: &mut Criterion) {
    // Write two temp files: one plain text, one with a null byte near the end
    // of the 8 KB window. We time the full open+read+check cycle.
    let mut text_file = NamedTempFile::new().unwrap();
    text_file.write_all(&vec![b'a'; 8192]).unwrap();
    text_file.flush().unwrap();

    let mut binary_file = NamedTempFile::new().unwrap();
    let mut buf = vec![b'a'; 8192];
    buf[4096] = 0u8; // null byte in the middle
    binary_file.write_all(&buf).unwrap();
    binary_file.flush().unwrap();

    let text_path = text_file.path().to_path_buf();
    let binary_path = binary_file.path().to_path_buf();

    let mut group = c.benchmark_group("is_binary");

    group.bench_function("text_file_8kb", |b| {
        b.iter(|| is_binary(&text_path).unwrap())
    });

    group.bench_function("binary_file_8kb", |b| {
        b.iter(|| is_binary(&binary_path).unwrap())
    });

    group.finish();
}

// ── per-line micro-benchmarks ─────────────────────────────────────────────────

/// Benchmark the cost of a single regex match on a line known to contain an
/// annotation vs a line that doesn't. Isolates the per-line regex cost.
fn bench_per_line_regex(c: &mut Criterion) {
    let cfg = default_config();
    let regex = build_regex(&cfg).unwrap();
    let today = fixed_today();
    let path = Path::new("bench/fake.rs");

    // Single-line inputs
    let annotation_line = "    // TODO[2020-01-01]: remove this legacy code path\n";
    let plain_todo_line = "    // TODO: fix this later\n";
    let code_line = "    let result = compute_value(input, threshold);\n";
    let bracket_line = "    let arr = values[index];\n"; // has `[` but no annotation

    let mut group = c.benchmark_group("per_line");

    group.bench_function("annotation_line", |b| {
        b.iter(|| scan_content(annotation_line, path, &regex, &cfg, today).unwrap())
    });

    group.bench_function("plain_todo_no_date", |b| {
        b.iter(|| scan_content(plain_todo_line, path, &regex, &cfg, today).unwrap())
    });

    group.bench_function("code_line_no_bracket", |b| {
        b.iter(|| scan_content(code_line, path, &regex, &cfg, today).unwrap())
    });

    group.bench_function("bracket_line_no_annotation", |b| {
        b.iter(|| scan_content(bracket_line, path, &regex, &cfg, today).unwrap())
    });

    group.finish();
}

// ── fuse_regex_pattern benchmark ─────────────────────────────────────────────

fn bench_fuse_regex_pattern(c: &mut Criterion) {
    let cfg = default_config();

    c.bench_function("fuse_regex_pattern_build_string", |b| {
        b.iter(|| cfg.fuse_regex_pattern())
    });
}

// ── end-to-end scan() benchmark ───────────────────────────────────────────────

/// Write `n` synthetic Rust source files into a tempdir and benchmark the full
/// `scan()` call including directory walk, binary detection, and rayon parallel
/// map. Files contain a realistic mix of annotations (1 per 100 lines).
fn build_scan_tempdir(file_count: usize) -> TempDir {
    let dir = TempDir::new().unwrap();
    for i in 0..file_count {
        let content = build_content(200, 100); // 200-line file, ~2 annotations each
        let path = dir.path().join(format!("module_{}.rs", i));
        std::fs::write(&path, content).unwrap();
    }
    dir
}

fn bench_scan_end_to_end(c: &mut Criterion) {
    let today = fixed_today();
    let cfg = default_config();

    let mut group = c.benchmark_group("scan/end_to_end");

    for &n in &[10usize, 50, 200] {
        let dir = build_scan_tempdir(n);
        group.throughput(Throughput::Elements(n as u64));
        group.bench_with_input(
            BenchmarkId::from_parameter(format!("{}_files", n)),
            &dir,
            |b, dir| b.iter(|| scan(dir.path(), &cfg, today).unwrap()),
        );
    }

    group.finish();
}

// ── parse_unified_diff benchmark ──────────────────────────────────────────────

fn build_diff(hunk_count: usize) -> String {
    let mut out = String::with_capacity(hunk_count * 120);
    out.push_str("--- a/src/main.rs\n+++ b/src/main.rs\n");
    for i in 0..hunk_count {
        let start = i * 10 + 1;
        out.push_str(&format!("@@ -{start},3 +{start},4 @@\n"));
        out.push_str("-old line\n+new line A\n+new line B\n context\n");
    }
    out
}

fn bench_parse_unified_diff(c: &mut Criterion) {
    let diff_500 = build_diff(500);
    let diff_5000 = build_diff(5_000);

    let mut group = c.benchmark_group("parse_unified_diff");

    group.throughput(Throughput::Elements(500));
    group.bench_function("500_hunks", |b| b.iter(|| parse_unified_diff(&diff_500)));

    group.throughput(Throughput::Elements(5_000));
    group.bench_function("5000_hunks", |b| b.iter(|| parse_unified_diff(&diff_5000)));

    group.finish();
}

// ── snooze_line / append_reason micro-benchmarks ──────────────────────────────

fn bench_snooze_line(c: &mut Criterion) {
    let new_date = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap();

    let line_match = "    // TODO[2020-01-01]: remove this legacy shim\n";
    let line_no_match = "    let result = compute(input);\n";
    let line_multi = "    // TODO[2020-01-01]: first // FIXME[2021-03-01]: second\n";

    let mut group = c.benchmark_group("snooze_line");
    group.bench_function("match", |b| b.iter(|| snooze_line(line_match, new_date)));
    group.bench_function("no_match", |b| {
        b.iter(|| snooze_line(line_no_match, new_date))
    });
    group.bench_function("multi_annotation", |b| {
        b.iter(|| snooze_line(line_multi, new_date))
    });
    group.finish();
}

fn bench_append_reason(c: &mut Criterion) {
    let line_with_annotation = "    // TODO[2020-01-01]: remove after upgrade\n";
    let line_without = "    let x = compute();\n";
    let reason = "blocked on upstream library release";

    let mut group = c.benchmark_group("append_reason");
    group.bench_function("with_annotation", |b| {
        b.iter(|| append_reason(line_with_annotation, reason))
    });
    group.bench_function("without_annotation", |b| {
        b.iter(|| append_reason(line_without, reason))
    });
    group.finish();
}

// ── config::load_config TOML parse benchmark ──────────────────────────────────

fn bench_load_config(c: &mut Criterion) {
    let dir = TempDir::new().unwrap();
    let toml = r#"
triggers = ["TODO", "FIXME", "HACK", "TEMP", "REMOVEME"]
exclude = [
    "target/**", "vendor/**", "*.min.js",
    "node_modules/**", "dist/**", ".git/**",
]
extensions = ["rs", "py", "go", "ts", "tsx", "js", "sql", "yaml"]
fuse_days = 14
max_detonated = 0
max_ticking = 5
"#;
    std::fs::write(dir.path().join(".timebomb.toml"), toml).unwrap();
    let overrides = CliOverrides::default();

    c.bench_function("load_config/toml_parse", |b| {
        b.iter(|| timebomb::config::load_config(dir.path(), &overrides).unwrap())
    });
}

// ── rayon scaling benchmark ───────────────────────────────────────────────────

fn bench_rayon_scaling(c: &mut Criterion) {
    let today = fixed_today();
    let cfg = default_config();
    let dir = build_scan_tempdir(100);

    let mut group = c.benchmark_group("scan/rayon_scaling");
    group.throughput(Throughput::Elements(100));

    group.bench_function("1_thread", |b| {
        let pool = rayon::ThreadPoolBuilder::new()
            .num_threads(1)
            .build()
            .unwrap();
        b.iter(|| pool.install(|| scan(dir.path(), &cfg, today).unwrap()))
    });

    group.bench_function("default_threads", |b| {
        b.iter(|| scan(dir.path(), &cfg, today).unwrap())
    });

    group.finish();
}

// ── registration ─────────────────────────────────────────────────────────────

criterion_group! {
    name = benches;
    config = Criterion::default()
        .warm_up_time(Duration::from_secs(1))
        .measurement_time(Duration::from_secs(5))
        .sample_size(50);
    targets =
        bench_scan_content_no_annotations,
        bench_scan_content_sparse,
        bench_scan_content_density,
        bench_scan_content_dense,
        bench_scan_str_includes_regex_compile,
        bench_build_regex,
        bench_is_binary,
        bench_per_line_regex,
        bench_fuse_regex_pattern,
        bench_scan_end_to_end,
        bench_parse_unified_diff,
        bench_snooze_line,
        bench_append_reason,
        bench_load_config,
        bench_rayon_scaling,
}
criterion_main!(benches);
