#!/usr/bin/env python3
"""Smoke-check STANDARD AI against saved battles that were already AI-vs-AI."""

import argparse
import csv
import json
import os
import sqlite3
import subprocess
import sys
import tempfile
from pathlib import Path


REPO = Path(__file__).resolve().parents[8]
DEFAULT_DB = Path(os.environ.get("EAGLE0_REAL_BATTLE_DB", "")) if os.environ.get("EAGLE0_REAL_BATTLE_DB") else None
DEFAULT_OUT = Path(__file__).resolve().parent / "experimental_results"


def run(args):
    result = subprocess.run(args, text=True, capture_output=True, check=False)
    if result.returncode != 0:
        raise RuntimeError(
            f"command failed ({result.returncode}): {' '.join(map(str, args))}\n"
            f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
        )
    return result


def read_battle_rows(db):
    conn = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
    rows = conn.execute(
        """
        SELECT action_seq, new_battle_proto
        FROM action_results
        WHERE new_battle_proto IS NOT NULL
        ORDER BY action_seq
        """
    ).fetchall()
    result = []
    for action_seq, battle_blob in rows:
        state_row = conn.execute(
            """
            SELECT boundary_action_seq, payload
            FROM state_snapshots
            WHERE boundary_action_seq < ?
            ORDER BY boundary_action_seq DESC
            LIMIT 1
            """,
            (action_seq,),
        ).fetchone()
        if state_row is None:
            continue
        result.append((action_seq, battle_blob, state_row[0], state_row[1]))
    conn.close()
    return result


def shardok_results_desc(db, shardok_game_id):
    conn = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
    rows = conn.execute(
        """
        SELECT payload
        FROM shardok_results
        WHERE shardok_game_id = ?
        ORDER BY action_seq DESC
        """,
        (shardok_game_id,),
    ).fetchall()
    conn.close()
    return [row[0] for row in rows]


def shardok_action_views(db, shardok_game_id):
    conn = sqlite3.connect(f"file:{db}?mode=ro", uri=True)
    rows = conn.execute(
        """
        SELECT seq, payload
        FROM shardok_player_results
        WHERE shardok_game_id = ? AND faction_id = -1
        ORDER BY seq
        """,
        (shardok_game_id,),
    ).fetchall()
    conn.close()
    return rows


def side_troop_totals(player_totals, player_sides):
    attacker_troops = 0
    defender_troops = 0
    sides = player_sides.split(";")
    for totals in player_totals:
        player_id = int(totals["player_id"])
        troop_count = int(totals["troop_count"])
        if player_id < len(sides) and sides[player_id] == "defender":
            defender_troops += troop_count
        else:
            attacker_troops += troop_count
    return attacker_troops, defender_troops


def parse_sim_csv(stdout):
    lines = [line for line in stdout.splitlines() if line and not line.startswith("ActionPointDistances")]
    header_index = next(i for i, line in enumerate(lines) if line.startswith("config,state_file,"))
    reader = csv.DictReader(lines[header_index : header_index + 2])
    return next(reader)


def write_csv(path, rows):
    columns = [
        "action_seq",
        "shardok_game_id",
        "attacker_factions",
        "defender_factions",
        "real_winner",
        "sim_winner",
        "winner_match",
        "real_round",
        "sim_round",
        "real_attacker_troops",
        "real_defender_troops",
        "sim_attacker_troops",
        "sim_defender_troops",
    ]
    with path.open("w", newline="") as f:
        writer = csv.DictWriter(f, fieldnames=columns, extrasaction="ignore", lineterminator="\n")
        writer.writeheader()
        writer.writerows(rows)


def write_md(path, rows, min_match_rate):
    matched = sum(1 for row in rows if row["winner_match"] == "true")
    rate = matched / len(rows) if rows else 0.0
    lines = [
        "# AI-vs-AI Real Battle Smoke Test",
        "",
        "This smoke test reruns saved battles where neither side used faction `3` or `4`,",
        "then compares STANDARD-vs-STANDARD winner side with the winner side recorded by",
        "the original Shardok battle. It is intentionally a broad similarity check rather",
        "than an exact replay assertion.",
        "",
        f"- Checked battles: {len(rows)}",
        f"- Winner-side matches: {matched}",
        f"- Match rate: {rate:.0%}",
        f"- Required match rate: {min_match_rate:.0%}",
        "",
        "| Action seq | Factions | Real winner | Sim winner | Real troops | Sim troops |",
        "| ---: | --- | --- | --- | ---: | ---: |",
    ]
    for row in rows:
        factions = f"{row['attacker_factions']} vs {row['defender_factions']}"
        real_troops = f"{row['real_attacker_troops']} vs {row['real_defender_troops']}"
        sim_troops = f"{row['sim_attacker_troops']} vs {row['sim_defender_troops']}"
        lines.append(
            f"| {row['action_seq']} | {factions} | {row['real_winner']} | "
            f"{row['sim_winner']} | {real_troops} | {sim_troops} |"
        )
    path.write_text("\n".join(lines) + "\n")


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--db", type=Path, default=DEFAULT_DB, help="Path to extracted game.db")
    parser.add_argument("--limit", type=int, default=8, help="Maximum AI-vs-AI battles to smoke-check; 0 means all")
    parser.add_argument("--min-match-rate", type=float, default=0.75)
    parser.add_argument("--out-dir", type=Path, default=DEFAULT_OUT)
    parser.add_argument(
        "--extractor",
        type=Path,
        default=REPO / "bazel-bin/src/main/cpp/net/eagle0/shardok/ai_battle_simulator/real_battle_config_extractor",
    )
    parser.add_argument(
        "--metadata",
        type=Path,
        default=REPO / "bazel-bin/src/main/cpp/net/eagle0/shardok/ai_battle_simulator/real_battle_smoke_metadata",
    )
    parser.add_argument(
        "--simulator",
        type=Path,
        default=REPO / "bazel-bin/src/main/cpp/net/eagle0/shardok/ai_battle_simulator/ai_battle_simulator_main",
    )
    args = parser.parse_args()

    if args.db is None or not args.db.exists():
        raise RuntimeError("Pass --db=/path/to/game.db or set EAGLE0_REAL_BATTLE_DB")

    args.out_dir.mkdir(parents=True, exist_ok=True)
    rows = []
    with tempfile.TemporaryDirectory(prefix="eagle0-ai-vs-ai-smoke-") as tmp:
        tmpdir = Path(tmp)
        for action_seq, battle_blob, boundary_seq, state_blob in read_battle_rows(args.db):
            battle_path = tmpdir / f"{action_seq}_battle.pb"
            state_path = tmpdir / f"{action_seq}_state_{boundary_seq}.pb"
            battle_path.write_bytes(battle_blob)
            state_path.write_bytes(state_blob)

            metadata = json.loads(run([str(args.metadata), f"--battle={battle_path}"]).stdout)
            if not metadata["ai_vs_ai"]:
                continue
            decisive_metadata = None
            for index, result_blob in enumerate(shardok_results_desc(args.db, metadata["shardok_game_id"])):
                result_path = tmpdir / f"{action_seq}_result_{index}.pb"
                result_path.write_bytes(result_blob)
                candidate_metadata = json.loads(
                    run([str(args.metadata), f"--battle={battle_path}", f"--result={result_path}"]).stdout
                )
                if candidate_metadata["real_winner"] in ("attacker", "defender"):
                    decisive_metadata = candidate_metadata
                    break
            if decisive_metadata is None:
                continue
            metadata = decisive_metadata
            view_paths = []
            for seq, view_blob in shardok_action_views(args.db, metadata["shardok_game_id"]):
                view_path = tmpdir / f"{action_seq}_view_{seq}.pb"
                view_path.write_bytes(view_blob)
                view_paths.append(view_path)
            real_attacker_troops = "n/a"
            real_defender_troops = "n/a"
            if view_paths:
                view_args = [str(args.metadata), f"--battle={battle_path}"]
                view_args.extend(f"--action-view={path}" for path in view_paths)
                view_metadata = json.loads(run(view_args).stdout)
                real_attacker_troops, real_defender_troops = side_troop_totals(
                    view_metadata["player_totals"], metadata["player_sides"]
                )

            config_path = tmpdir / f"{action_seq}_standard.json"
            run(
                [
                    str(args.extractor),
                    f"--battle={battle_path}",
                    f"--state={state_path}",
                    f"--output={config_path}",
                    "--seed=1",
                    "--max-rounds=31",
                    "--attacker-scoring=STANDARD",
                    "--defender-scoring=STANDARD",
                ]
            )
            sim = parse_sim_csv(
                run([str(args.simulator), f"--config={config_path}", "--summary=csv", "--quiet"]).stdout
            )
            rows.append(
                {
                    "action_seq": action_seq,
                    "shardok_game_id": metadata["shardok_game_id"],
                    "attacker_factions": metadata["attacker_factions"],
                    "defender_factions": metadata["defender_factions"],
                    "real_winner": metadata["real_winner"],
                    "sim_winner": sim["winner"],
                    "winner_match": "true" if metadata["real_winner"] == sim["winner"] else "false",
                    "real_round": metadata["real_round"],
                    "sim_round": sim["total_rounds"],
                    "real_attacker_troops": real_attacker_troops,
                    "real_defender_troops": real_defender_troops,
                    "sim_attacker_troops": sim["attacker_surviving_troops"],
                    "sim_defender_troops": sim["defender_surviving_troops"],
                }
            )
            print(
                f"{action_seq}: real={metadata['real_winner']} sim={sim['winner']} "
                f"match={rows[-1]['winner_match']}",
                flush=True,
            )
            if args.limit > 0 and len(rows) >= args.limit:
                break

    if not rows:
        raise RuntimeError("No AI-vs-AI saved battles were available for the smoke test")

    write_csv(args.out_dir / "standard_scoring_ai_vs_ai_smoke.csv", rows)
    write_md(args.out_dir / "standard_scoring_ai_vs_ai_smoke.md", rows, args.min_match_rate)
    match_rate = sum(1 for row in rows if row["winner_match"] == "true") / len(rows)
    if match_rate < args.min_match_rate:
        raise RuntimeError(f"AI-vs-AI smoke match rate {match_rate:.0%} is below {args.min_match_rate:.0%}")


if __name__ == "__main__":
    try:
        main()
    except Exception as exc:
        print(f"ERROR: {exc}", file=sys.stderr)
        sys.exit(1)
