//! DEEPFALL balance-by-bot harness. Runs sweeps over `deepfall-core`, writes CSV/JSON
//! to `--out`, and prints the headline findings. Mirrors `combat-sim`'s shape.
//!
//! Sweeps:
//!   1. strategy   — all 24 seat-permutations of the 4 bots × seeds → exit-dominance.
//!   2. combo      — each of the 36 Lineage×Calling combos as a focal "main" vs a
//!                   random field → asymmetry fairness.
//!   3. pacing/feel — game length, victory margin, feel-bad rate (from sweep 1).
//!   4. depth      — which floors' rooms get claimed, and by parties drafted when.

use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::fs;
use std::path::Path;

use deepfall_core::config::Config;
use deepfall_core::content::{Calling, Lineage, ALL_CALLINGS, ALL_LINEAGES};
use deepfall_core::controller::{Strategy, ALL_STRATEGIES};
use deepfall_core::game::Game;

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let out = arg_value(&args, "--out").unwrap_or_else(|| "out".to_string());
    let seeds: u64 = arg_value(&args, "--seeds").and_then(|s| s.parse().ok()).unwrap_or(400);
    let cfg_path = arg_value(&args, "--config");

    let cfg = match &cfg_path {
        Some(p) => {
            let txt = fs::read_to_string(p).expect("read config");
            serde_json::from_str(&txt).expect("parse config")
        }
        None => Config::default(),
    };

    // `--players N` runs a quick player-count scaling diagnostic instead of the full
    // sweeps (used to ground the rulebook's 6-player rules in data).
    if let Some(n) = arg_value(&args, "--players").and_then(|s| s.parse::<usize>().ok()) {
        for pc in [4usize, 5, 6, n].iter().copied().collect::<std::collections::BTreeSet<_>>() {
            diagnose_players(&cfg, seeds, pc);
        }
        return;
    }

    fs::create_dir_all(&out).expect("create out dir");
    println!("DEEPFALL sweeps  seeds={seeds}  out={out}  config={}", cfg_path.as_deref().unwrap_or("<default>"));
    println!("{}", "=".repeat(72));

    let strat = strategy_sweep(&cfg, seeds, &out);
    let combo = combo_sweep(&cfg, seeds, &out);
    write_sample_game(&cfg, &out);

    // Headline summary JSON (consumed by analysis/analyze.py).
    let summary = serde_json::json!({
        "seeds": seeds,
        "config": cfg_value(&cfg),
        "strategy_winrates": strat.winrates,
        "strategy_dominant": strat.dominant,
        "mean_rounds": strat.mean_rounds,
        "rounds_min": strat.rounds_min,
        "rounds_max": strat.rounds_max,
        "mean_margin_frac": strat.mean_margin_frac,
        "feelbad_per_game": strat.feelbad_per_game,
        "exit_mix": strat.exit_mix,
        "combo_best": combo.best,
        "combo_worst": combo.worst,
        "combo_spread": combo.spread,
        "combo_flags": combo.flags,
        "depth_claims": strat.depth_claims,
        "deep_claim_drafted_round_share": strat.deep_drafted_share,
    });
    fs::write(Path::new(&out).join("summary.json"), serde_json::to_string_pretty(&summary).unwrap())
        .unwrap();

    print_headline(&strat, &combo);
}

// ---------------------------------------------------------------------------------
// Sweep 1: strategy (exit-dominance) + pacing/feel/depth metrics
// ---------------------------------------------------------------------------------

struct StratOut {
    winrates: BTreeMap<String, f64>,
    dominant: bool,
    mean_rounds: f64,
    rounds_min: u32,
    rounds_max: u32,
    mean_margin_frac: f64,
    feelbad_per_game: f64,
    exit_mix: BTreeMap<String, u64>,
    depth_claims: BTreeMap<String, u64>,
    deep_drafted_share: f64,
}

fn strategy_sweep(cfg: &Config, seeds: u64, out: &str) -> StratOut {
    let perms = permutations(&ALL_STRATEGIES);
    let mut wins: BTreeMap<Strategy, u64> = BTreeMap::new();
    let mut games_per_strat: BTreeMap<Strategy, u64> = BTreeMap::new();
    let mut total_games = 0u64;
    let mut sum_rounds = 0u64;
    let mut rounds_min = u32::MAX;
    let mut rounds_max = 0u32;
    let mut sum_margin_frac = 0.0;
    let mut sum_feelbad = 0u64;
    let mut exits: BTreeMap<&str, u64> = BTreeMap::new();
    let mut depth_claims: BTreeMap<u32, u64> = BTreeMap::new();
    let mut deep_claims_total = 0u64;
    let mut deep_claims_round1 = 0u64;

    let mut games_csv = String::from(
        "seed,perm,rounds,winner_strategy,winner_glory,margin,margin_frac,feelbad,worthy_ends,cashouts\n",
    );

    for seed in 0..seeds {
        for (pi_perm, perm) in perms.iter().enumerate() {
            let roster: Vec<(Strategy, Option<(Lineage, Calling)>)> =
                perm.iter().map(|s| (*s, None)).collect();
            let r = Game::new(cfg.clone(), seed.wrapping_mul(1000) + pi_perm as u64, &roster).run();
            total_games += 1;
            sum_rounds += r.rounds as u64;
            rounds_min = rounds_min.min(r.rounds);
            rounds_max = rounds_max.max(r.rounds);

            let win_strat = r.players[r.winner].strategy;
            *wins.entry(win_strat).or_default() += 1;
            for p in &r.players {
                *games_per_strat.entry(p.strategy).or_default() += 1;
            }

            let winner_glory = r.players[r.winner].glory;
            let margin_frac = if winner_glory > 0.0 { r.margin / winner_glory } else { 0.0 };
            sum_margin_frac += margin_frac;

            let feelbad: u32 = r.players.iter().map(|p| p.takes_no_laststand).sum();
            let worthy: u32 = r.players.iter().map(|p| p.worthy_ends).sum();
            let cashouts: u32 = r.players.iter().map(|p| p.cashouts).sum();
            sum_feelbad += feelbad as u64;
            *exits.entry("worthy_end").or_default() += worthy as u64;
            *exits.entry("cash_out").or_default() += cashouts as u64;
            *exits.entry("taken_feelbad").or_default() += feelbad as u64;

            for c in &r.claims {
                *depth_claims.entry(c.depth).or_default() += 1;
                if c.depth >= cfg.floors.saturating_sub(1) {
                    deep_claims_total += 1;
                    if c.drafted_round <= 1 {
                        deep_claims_round1 += 1;
                    }
                }
            }

            // Sample only a slice of permutations into the per-game CSV to keep it lean.
            if pi_perm == 0 {
                let _ = writeln!(
                    games_csv,
                    "{seed},{pi_perm},{},{},{:.2},{:.2},{:.3},{feelbad},{worthy},{cashouts}",
                    r.rounds,
                    win_strat.name(),
                    winner_glory,
                    r.margin,
                    margin_frac,
                );
            }
        }
    }

    fs::write(Path::new(out).join("games.csv"), games_csv).unwrap();

    // Win-rate table.
    let mut winrates_csv = String::from("strategy,games,wins,winrate\n");
    let mut winrates = BTreeMap::new();
    for s in ALL_STRATEGIES {
        let g = *games_per_strat.get(&s).unwrap_or(&0);
        let w = *wins.get(&s).unwrap_or(&0);
        let wr = if g > 0 { w as f64 / g as f64 } else { 0.0 };
        let _ = writeln!(winrates_csv, "{},{g},{w},{:.4}", s.name(), wr);
        winrates.insert(s.name().to_string(), wr);
    }
    fs::write(Path::new(out).join("strategy_winrates.csv"), winrates_csv).unwrap();

    // Depth-claims table.
    let mut dc_csv = String::from("depth,claims\n");
    let mut depth_claims_named = BTreeMap::new();
    for (d, n) in &depth_claims {
        let _ = writeln!(dc_csv, "{d},{n}");
        depth_claims_named.insert(format!("floor{d}"), *n);
    }
    fs::write(Path::new(out).join("depth_claims.csv"), dc_csv).unwrap();

    let max_wr = winrates.values().cloned().fold(0.0_f64, f64::max);
    let min_wr = winrates.values().cloned().fold(1.0_f64, f64::min);
    // "Dominant" if any strategy clears 40% (1.6x the 25% fair share) or one is starved.
    let dominant = max_wr > 0.40 || min_wr < 0.12;

    let exit_mix = exits.iter().map(|(k, v)| (k.to_string(), *v)).collect();
    let deep_drafted_share = if deep_claims_total > 0 {
        deep_claims_round1 as f64 / deep_claims_total as f64
    } else {
        0.0
    };

    StratOut {
        winrates,
        dominant,
        mean_rounds: sum_rounds as f64 / total_games as f64,
        rounds_min,
        rounds_max,
        mean_margin_frac: sum_margin_frac / total_games as f64,
        feelbad_per_game: sum_feelbad as f64 / total_games as f64,
        exit_mix,
        depth_claims: depth_claims_named,
        deep_drafted_share,
    }
}

// ---------------------------------------------------------------------------------
// Player-count scaling diagnostic
// ---------------------------------------------------------------------------------

fn diagnose_players(base: &Config, seeds: u64, players: usize) {
    // Linear board scaling: rooms-per-floor = player count (keeps rooms-per-capita and
    // thus contention roughly constant). Maw dread-push is scaled by 4/players so the
    // larger table's extra collective Dread doesn't crash the game length.
    let mut cfg = base.clone();
    cfg.rooms_per_floor = players as u32;
    cfg.maw_dread_push = base.maw_dread_push * 4.0 / players as f64;

    let mut sum_rounds = 0u64;
    let (mut rmin, mut rmax) = (u32::MAX, 0u32);
    let mut sum_margin = 0.0;
    let mut seat_wins = vec![0u64; players];
    let mut feelbad = 0u64;
    let mut worthy = 0u64;
    let mut cashout = 0u64;
    let mut games = 0u64;

    for seed in 0..seeds {
        let roster: Vec<(Strategy, Option<(Lineage, Calling)>)> =
            (0..players).map(|i| (ALL_STRATEGIES[i % 4], None)).collect();
        let r = Game::new(cfg.clone(), seed.wrapping_mul(131), &roster).run();
        games += 1;
        sum_rounds += r.rounds as u64;
        rmin = rmin.min(r.rounds);
        rmax = rmax.max(r.rounds);
        let wg = r.players[r.winner].glory;
        sum_margin += if wg > 0.0 { r.margin / wg } else { 0.0 };
        seat_wins[r.winner] += 1;
        feelbad += r.players.iter().map(|p| p.takes_no_laststand as u64).sum::<u64>();
        worthy += r.players.iter().map(|p| p.worthy_ends as u64).sum::<u64>();
        cashout += r.players.iter().map(|p| p.cashouts as u64).sum::<u64>();
    }

    let g = games as f64;
    println!(
        "\n{players}P  (rooms/floor={}, dread_push={:.3})",
        cfg.rooms_per_floor, cfg.maw_dread_push
    );
    println!(
        "   length: mean {:.1} rounds ({rmin}-{rmax})   margin: {:.1}%   feelbad: {:.3}/game",
        sum_rounds as f64 / g,
        sum_margin / g * 100.0,
        feelbad as f64 / g
    );
    let seat_pct: Vec<String> = seat_wins
        .iter()
        .enumerate()
        .map(|(i, w)| format!("P{i}:{:.0}%", *w as f64 / g * 100.0))
        .collect();
    let fair = 100.0 / players as f64;
    println!("   seat win% (fair={fair:.0}%): {}", seat_pct.join(" "));
    println!(
        "   exits/game: cashout {:.1}, worthy {:.2}, feelbad {:.2}",
        cashout as f64 / g,
        worthy as f64 / g,
        feelbad as f64 / g
    );
}

// ---------------------------------------------------------------------------------
// Sweep 2: combo asymmetry
// ---------------------------------------------------------------------------------

struct ComboOut {
    best: (String, f64),
    worst: (String, f64),
    spread: f64,
    flags: Vec<String>,
}

fn combo_sweep(cfg: &Config, seeds: u64, out: &str) -> ComboOut {
    // Each combo "mains" (fixed) one focal seat vs a random field; all Balanced bots.
    // Focal seat rotates by seed for turn-order fairness. 4 players.
    let games_per_combo = seeds.max(200);
    let mut rows: Vec<(Lineage, Calling, f64, u64)> = Vec::new();

    for l in ALL_LINEAGES {
        for c in ALL_CALLINGS {
            let mut wins = 0u64;
            for seed in 0..games_per_combo {
                let focal_seat = (seed % 4) as usize;
                // The focal "mains" its combo as a generalist (Balanced); the field is
                // a realistic mix of the four strategies with random combos, rotated for
                // turn-order fairness. (All-Balanced fields under-credit worthy-capable
                // combos, because Balanced under-uses the Worthy End.)
                let roster: Vec<(Strategy, Option<(Lineage, Calling)>)> = (0..4)
                    .map(|seat| {
                        if seat == focal_seat {
                            (Strategy::Balanced, Some((l, c)))
                        } else {
                            (ALL_STRATEGIES[(seat + seed as usize) % 4], None)
                        }
                    })
                    .collect();
                let seed_full = seed
                    .wrapping_mul(7919)
                    .wrapping_add((l as u64) << 8)
                    .wrapping_add(c as u64);
                let r = Game::new(cfg.clone(), seed_full, &roster).run();
                if r.winner == focal_seat {
                    wins += 1;
                }
            }
            let wr = wins as f64 / games_per_combo as f64;
            rows.push((l, c, wr, games_per_combo));
        }
    }

    let mean = rows.iter().map(|r| r.2).sum::<f64>() / rows.len() as f64;

    let mut csv = String::from("lineage,calling,games,wins_frac,vs_mean\n");
    for (l, c, wr, g) in &rows {
        let _ = writeln!(csv, "{},{},{g},{:.4},{:.2}", l.name(), c.name(), wr, wr / mean);
    }
    fs::write(Path::new(out).join("combo_winrates.csv"), csv).unwrap();

    let best = rows.iter().cloned().fold(rows[0].clone(), |a, b| if b.2 > a.2 { b } else { a });
    let worst = rows.iter().cloned().fold(rows[0].clone(), |a, b| if b.2 < a.2 { b } else { a });

    // Fair share in a 4p game is 0.25. Flag combos that beat 1.5x mean or fall under 0.5x.
    let mut flags = Vec::new();
    for (l, c, wr, _) in &rows {
        if *wr > mean * 1.5 {
            flags.push(format!("STRONG {} {} ({:.1}% = {:.2}x mean)", l.name(), c.name(), wr * 100.0, wr / mean));
        } else if *wr < mean * 0.5 {
            flags.push(format!("WEAK   {} {} ({:.1}% = {:.2}x mean)", l.name(), c.name(), wr * 100.0, wr / mean));
        }
    }

    ComboOut {
        best: (format!("{} {}", best.0.name(), best.1.name()), best.2),
        worst: (format!("{} {}", worst.0.name(), worst.1.name()), worst.2),
        spread: best.2 - worst.2,
        flags,
    }
}

// ---------------------------------------------------------------------------------
// One full game serialized for inspection
// ---------------------------------------------------------------------------------

fn write_sample_game(cfg: &Config, out: &str) {
    let roster: Vec<(Strategy, Option<(Lineage, Calling)>)> =
        ALL_STRATEGIES.iter().map(|s| (*s, None)).collect();
    let r = Game::new(cfg.clone(), 7, &roster).run();
    let players: Vec<_> = r
        .players
        .iter()
        .map(|p| {
            serde_json::json!({
                "id": p.id,
                "strategy": p.strategy.name(),
                "combo": p.combo.map(|(l, c)| format!("{} {}", l.name(), c.name())),
                "glory": p.glory,
                "setbacks": p.setbacks,
                "cashouts": p.cashouts,
                "worthy_ends": p.worthy_ends,
                "taken_feelbad": p.takes_no_laststand,
                "mean_cashout_depth": p.mean_cashout_depth,
            })
        })
        .collect();
    let j = serde_json::json!({
        "seed": 7,
        "rounds": r.rounds,
        "winner": r.winner,
        "margin": r.margin,
        "players": players,
        "total_claims": r.claims.len(),
    });
    fs::write(Path::new(out).join("sample_game.json"), serde_json::to_string_pretty(&j).unwrap()).unwrap();
}

// ---------------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------------

fn print_headline(s: &StratOut, c: &ComboOut) {
    println!("\nSTRATEGY WIN-RATES (fair share = 25%)");
    for (name, wr) in &s.winrates {
        let bar = "#".repeat((*wr * 80.0) as usize);
        println!("  {:<10} {:>5.1}%  {}", name, wr * 100.0, bar);
    }
    println!("  dominant-strategy flag: {}", if s.dominant { "⚠ YES" } else { "no — balanced band" });

    println!("\nPACING & FEEL");
    println!("  game length: mean {:.1} rounds  (min {}, max {})", s.mean_rounds, s.rounds_min, s.rounds_max);
    println!("  victory margin: mean {:.1}% of winner's score", s.mean_margin_frac * 100.0);
    println!("  feel-bad (Taken w/o Last Stand): {:.2} per game", s.feelbad_per_game);
    println!("  exit population: {:?}", s.exit_mix);

    println!("\nDEEP-CONTENT REACHABILITY");
    println!("  claims by floor: {:?}", s.depth_claims);
    println!("  share of deep-floor claims by round-1 parties: {:.1}%  (high = deep game is game-start-only)", s.deep_drafted_share * 100.0);

    println!("\nCOMBO ASYMMETRY (36 combos; fair ≈ 25%)");
    println!("  strongest: {} at {:.1}%", c.best.0, c.best.1 * 100.0);
    println!("  weakest:   {} at {:.1}%", c.worst.0, c.worst.1 * 100.0);
    println!("  spread (best-worst): {:.1} pts", c.spread * 100.0);
    if c.flags.is_empty() {
        println!("  no combos flagged outside [0.5x, 1.5x] of mean ✓");
    } else {
        println!("  flags:");
        for f in &c.flags {
            println!("    {f}");
        }
    }
    println!("\n{}", "=".repeat(72));
}

fn arg_value(args: &[String], key: &str) -> Option<String> {
    args.iter().position(|a| a == key).and_then(|i| args.get(i + 1)).cloned()
}

/// All permutations of a 4-element strategy array (24).
fn permutations(items: &[Strategy; 4]) -> Vec<Vec<Strategy>> {
    let mut out = Vec::new();
    let v: Vec<Strategy> = items.to_vec();
    permute(&v, 0, &mut v.clone(), &mut out);
    out
}

fn permute(orig: &[Strategy], k: usize, cur: &mut Vec<Strategy>, out: &mut Vec<Vec<Strategy>>) {
    if k == orig.len() {
        out.push(cur.clone());
        return;
    }
    for i in k..cur.len() {
        cur.swap(k, i);
        permute(orig, k + 1, cur, out);
        cur.swap(k, i);
    }
}

fn cfg_value(cfg: &Config) -> serde_json::Value {
    serde_json::to_value(cfg).unwrap_or(serde_json::Value::Null)
}
