#!/usr/bin/env python3
"""Run AZS DPS model at Naxx 20/25/30 min; write results.json + results.md."""
from __future__ import annotations
import json
from pathlib import Path
from lib import core
from lib.core import (
    ASSUMPTIONS, GEAR, FORMULA, RUN_LENGTHS_SEC, NAXX_FINAL_SEC,
    NAXX_BOSSES, ENDLESS_CADENCE_SEC, set_run_length, mix_weights_for_length, mix,
)
from lib.builds_whr import ALL as WHR
from lib.builds_rest import ALL as REST

ROOT = Path(__file__).resolve().parent
ALL_FNS = WHR + REST


def _ser_breakdown(b):
    return {"label": b.label, "dps": round(float(b.dps), 1), "note": b.note}


def _eval_all():
    return [fn() for fn in ALL_FNS]


def _build_row(it, rank, w_wave, w_boss, run_sec, bosses_meta):
    mixed = mix(it.wave_dps, it.boss_dps, w_wave, w_boss)
    total = mixed * run_sec
    return {
        "class": it.class_name,
        "id": it.build_id,
        "name": it.name,
        "tree": it.tree,
        "talents": it.talents,
        "item_set": it.item_set,
        "key_choices": it.key_choices,
        "wave_dps": round(it.wave_dps, 1),
        "boss_dps": round(it.boss_dps, 1),
        "mixed_dps": round(mixed, 1),
        "total_damage": round(total, 0),
        "mix_wave_weight": round(w_wave, 4),
        "mix_boss_weight": round(w_boss, 4),
        "rank_in_class": rank,
        "breakdown": [_ser_breakdown(b) for b in it.breakdown],
        "assumptions_used": it.assumptions_used,
        "unverified": it.unverified,
        "bosses_in_window": bosses_meta,
    }


def _rank_class(items):
    items = sorted(items, key=lambda x: x["mixed_dps"], reverse=True)
    for i, it in enumerate(items):
        it["rank_in_class"] = i + 1
    return items


def _by_class_best(builds):
    by = {}
    for b in builds:
        by.setdefault(b["class"], []).append(b)
    out = []
    for cls, items in sorted(by.items()):
        items = _rank_class(items)
        best, runner = items[0], items[1] if len(items) > 1 else None
        out.append({
            "class": cls,
            "best": best["id"],
            "best_name": best["name"],
            "best_mixed": best["mixed_dps"],
            "best_wave": best["wave_dps"],
            "best_boss": best["boss_dps"],
            "best_total_damage": best["total_damage"],
            "item_set": best["item_set"],
            "talents": best["talents"],
            "key_choices": best["key_choices"],
            "breakdown": best["breakdown"],
            "runner_up": runner["id"] if runner else None,
            "runner_name": runner["name"] if runner else None,
            "runner_mixed": runner["mixed_dps"] if runner else None,
            "runner_wave": runner["wave_dps"] if runner else None,
            "runner_boss": runner["boss_dps"] if runner else None,
            "margin_pct": round(100 * (best["mixed_dps"] - runner["mixed_dps"]) / runner["mixed_dps"], 1)
                if runner and runner["mixed_dps"] else None,
            "assumptions_used": best["assumptions_used"],
            "unverified": best["unverified"],
            "mix_wave_weight": best["mix_wave_weight"],
            "mix_boss_weight": best["mix_boss_weight"],
        })
    return out


def sim_length(run_sec: float):
    set_run_length(run_sec)
    results = _eval_all()
    builds = []
    by_cls = {}
    for r in results:
        w_wave, w_boss, bosses, boss_time = mix_weights_for_length(run_sec, r.boss_dps)
        bosses_meta = [{
            "name": b["name"], "at": b["at"], "hp": b["hp"],
            "final": bool(b.get("final")), "endless": bool(b.get("endless")),
        } for b in bosses]
        row = _build_row(r, 0, w_wave, w_boss, run_sec, bosses_meta)
        builds.append(row)
        by_cls.setdefault(r.class_name, []).append(row)
    ranked = []
    for cls, items in sorted(by_cls.items()):
        ranked.extend(_rank_class(items))
    # global rank
    ranked_global = sorted(ranked, key=lambda x: -x["mixed_dps"])
    for i, b in enumerate(ranked_global):
        b["rank_overall"] = i + 1
    return {
        "run_length_sec": run_sec,
        "run_length_min": round(run_sec / 60, 1),
        "boss_schedule_note": (
            "Scheduled Naxx bosses every 4 min at 4/8/12/16/20 CONFIRMED "
            "(azerothsurvivorsgame.com/zones/, wiki HP). "
            f"Endless after {NAXX_FINAL_SEC:.0f}s every {ENDLESS_CADENCE_SEC:.0f}s ASSUMED (Orion; API nb timer undocumented)."
        ),
        "builds": ranked_global,
        "by_class_best": _by_class_best(ranked_global),
    }


def main():
    length_blocks = [sim_length(T) for T in RUN_LENGTHS_SEC]
    # Default / legacy top-level = 20 min (Naxx final arrival)
    primary = length_blocks[0]
    unverified = []
    for b in primary["builds"]:
        unverified.extend(b["unverified"])
    seen, uniq = set(), []
    for u in unverified:
        if u not in seen:
            seen.add(u)
            uniq.append(u)

    out = {
        "assumptions": ASSUMPTIONS,
        "gear_template": GEAR,
        "formula": FORMULA,
        "set_source": "https://azerothsurvivors.app/items/",
        "talent_source": "https://azerothsurvivors.app/talents/",
        "spell_source": "https://azerothsurvivors.app/data/ability-comparisons/<id>.json",
        "map": "Naxxramas",
        "run_lengths_sec": list(RUN_LENGTHS_SEC),
        "naxx_bosses_scheduled": NAXX_BOSSES,
        "boss_cadence": {
            "scheduled": "CONFIRMED every 4 minutes at 4:00, 8:00, 12:00, 16:00, 20:00 (Kel'Thuzad final)",
            "scheduled_sources": [
                "https://azerothsurvivorsgame.com/zones/",
                "https://azeroth-survivors.org/wiki/naxxramas/",
                "AZS_GetUpcomingBosses / AZS_SCHEDULE (addon API)",
            ],
            "endless_after_20": (
                f"ASSUMED every {ENDLESS_CADENCE_SEC:.0f}s (Orion). "
                "API: upcoming list empty in endless; AZS_RUN.nb countdown exists; interval not published. "
                "Combat log + saved_runs did not contradict or confirm."
            ),
            "orion_claim": "after the 20 minute mark bosses come every 4 minutes",
            "orion_vs_data": (
                "4-minute cadence CONFIRMED for scheduled phase (4→20). "
                "After 20:00 (endless) cadence ASSUMED 4 minutes — not contradicted."
            ),
        },
        "hunter_recalibration": {
            "source": "logs/saved_runs.json (deduped runTime+totalDamage)",
            "run": "Gates of Ahn'Qiraj, Hunter L33, 637.16s, 21977678 dmg, 34493 DPS",
            "shares": {
                "Aimed Shot": 0.6062,
                "Rapid Fire": 0.2384,
                "Frost Trap": 0.0542,
                "Multi-Shot": 0.0259,
                "Auto Shot": 0.0246,
                "Amulet Inferno Frost Trap": 0.0138,
            },
            "aimed_hits_crits": {"hits": 3471, "crits": 1768, "crit_rate": 0.509},
            "note": "NOT Naxx. Old model folded RF into Aimed free-casts so RF own DPS ≈ 0; now RF is an explicit proc damage line.",
            "before_mixed_approx": 9866.4,
        },
        "default_run_length_sec": primary["run_length_sec"],
        "builds": primary["builds"],
        "by_class_best": primary["by_class_best"],
        "by_length": {str(int(b["run_length_sec"])): b for b in length_blocks},
        "unverified": uniq,
    }

    # Attach after-calibration hunter mixed at 20 for the changelog field
    for b in primary["builds"]:
        if b["id"] == "hunter_kill_shot":
            out["hunter_recalibration"]["after_mixed_20min"] = b["mixed_dps"]
            out["hunter_recalibration"]["after_wave_20min"] = b["wave_dps"]
            out["hunter_recalibration"]["after_boss_20min"] = b["boss_dps"]
            break

    (ROOT / "results.json").write_text(json.dumps(out, indent=2))

    lines = [
        "# AZS DPS model results — Naxxramas 20 / 25 / 30 min",
        "",
        "Formula: " + FORMULA,
        "",
        "## Boss cadence",
        "",
        "- **Scheduled (CONFIRMED):** Anub'Rekhan 4:00 · Noth 8:00 · Patchwerk 12:00 · Sapphiron 16:00 · Kel'Thuzad 20:00.",
        "  Sources: azerothsurvivorsgame.com/zones/, azeroth-survivors.org/wiki/naxxramas/ (HP), AZS_GetUpcomingBosses.",
        f"- **Endless after 20:00 (ASSUMED):** every {ENDLESS_CADENCE_SEC:.0f}s (Orion). Addon API exposes `AZS_RUN.nb` but not the interval. Logs did not contradict.",
        "",
        "## Hunter recalibration (Gates of AQ — not Naxx)",
        "",
        "- Best run: L33 Gates of Ahn'Qiraj, 637s, **34.5k DPS**; Aimed 61%, Rapid Fire **24% (proc)**, Frost Trap 5.4% + Amulet 1.4%, Multi 2.6%, Auto 2.5%.",
        f"- Kill Shot mixed @20min: **{out['hunter_recalibration'].get('before_mixed_approx')} → {out['hunter_recalibration'].get('after_mixed_20min')}** (RF now an explicit damage line).",
        "",
    ]
    for block in length_blocks:
        T = int(block["run_length_sec"])
        lines += [
            f"## Best per class @ {block['run_length_min']:.0f} min ({T}s)",
            "",
            "| Class | Best | Mixed | Wave | Boss | Total dmg | Mix W/B | Runner-up | Margin |",
            "|---|---|---:|---:|---:|---:|---|---|---:|",
        ]
        for row in block["by_class_best"]:
            lines.append(
                f"| {row['class']} | {row['best_name']} | {row['best_mixed']} | {row['best_wave']} | "
                f"{row['best_boss']} | {row['best_total_damage']:,.0f} | "
                f"{row['mix_wave_weight']:.2f}/{row['mix_boss_weight']:.2f} | "
                f"{row['runner_name']} | {row['margin_pct']}% |"
            )
        lines += ["", f"### All builds @ {block['run_length_min']:.0f} min", ""]
        for b in block["builds"]:
            lines.append(
                f"- {b['class']} | {b['name']} | mixed {b['mixed_dps']} | wave {b['wave_dps']} | "
                f"boss {b['boss_dps']} | total {b['total_damage']:,.0f} | rank {b['rank_in_class']}"
            )
        lines.append("")

    lines += ["## Unverified", ""] + [f"- {u}" for u in uniq]
    (ROOT / "results.md").write_text("\n".join(lines))
    print("\n".join(lines[:80]))
    print("...")
    print(f"Wrote results.json / results.md for lengths {list(RUN_LENGTHS_SEC)}")


if __name__ == "__main__":
    main()
