#!/usr/bin/env python3
"""Exact C1 anchor replay using the pinned AITuner trace/worker/SLO paths."""

from __future__ import annotations

import argparse
import dataclasses
import hashlib
import json
import math
import os
import sys
import time
from pathlib import Path
from typing import Any


AITUNER_ROOT = Path(os.environ.get("AITUNER_ROOT", Path(__file__).resolve().parents[2]))
sys.path.insert(0, str(AITUNER_ROOT / "src"))
os.environ.setdefault("AITUNER_CODEX_BASE_URL", "http://127.0.0.1:1")

from aituner.slo import evaluate_request, summarize_evaluations  # noqa: E402
from aituner.spec import load_study_spec  # noqa: E402
from aituner.trace import load_trace_requests, select_requests_for_threshold  # noqa: E402
from aituner.worker import _probe_drain_deadline, _replay_requests  # noqa: E402


def atomic_json(path: Path, payload: Any) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    tmp = path.with_suffix(path.suffix + ".tmp")
    tmp.write_text(json.dumps(payload, sort_keys=True, indent=2) + "\n")
    os.replace(tmp, path)


def sha256_file(path: Path) -> str:
    h = hashlib.sha256()
    with path.open("rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def numeric(values: list[float | int | None]) -> dict[str, Any]:
    finite = [float(x) for x in values if x is not None and math.isfinite(float(x))]
    return {
        "n": len(values), "finite_n": len(finite), "missing_n": len(values) - len(finite),
        "min": min(finite) if finite else None, "max": max(finite) if finite else None,
        "distinct_n": len(set(finite)),
    }


def load_selected(study_path: Path, anchor: float):
    study = load_study_spec(study_path)
    window, requests = load_trace_requests(study, study_spec_path=study_path)
    selected = select_requests_for_threshold(requests, threshold=anchor)
    return study, window, requests, selected


def selected_summary(selected, duration_s: float, tp: int) -> dict[str, Any]:
    ids = "\n".join(item.row_id for item in selected)
    arrival = "\n".join(f"{item.arrival_s:.12f}" for item in selected)
    lengths = "\n".join(str(item.prompt_tokens_hint) for item in selected)
    return {
        "count": len(selected),
        "offered_req_s": len(selected) / duration_s,
        "offered_req_s_per_gpu": len(selected) / duration_s / tp,
        "request_id_order_sha256": hashlib.sha256(ids.encode()).hexdigest(),
        "arrival_order_sha256": hashlib.sha256(arrival.encode()).hexdigest(),
        "raw_length_order_sha256": hashlib.sha256(lengths.encode()).hexdigest(),
        "arrival_s": numeric([item.arrival_s for item in selected]),
        "raw_input_tokens": numeric([item.prompt_tokens_hint for item in selected]),
        "long_gt4096": sum(int(item.prompt_tokens_hint or 0) > 4096 for item in selected),
    }


def run_replay(args: argparse.Namespace, *, warmup: bool) -> dict[str, Any]:
    study_path = Path(args.study)
    study, window, _requests, selected = load_selected(study_path, args.anchor)
    if warmup:
        first = selected[:16]
        if not any(int(item.prompt_tokens_hint or 0) > 4096 for item in first):
            long_item = next(item for item in selected if int(item.prompt_tokens_hint or 0) > 4096)
            first = [*selected[:15], long_item]
        first = sorted({item.row_id: item for item in first}.values(), key=lambda item: item.arrival_s)
        if len(first) < 16:
            raise RuntimeError("warmup set has fewer than 16 unique requests")
        start = first[0].arrival_s
        selected = [dataclasses.replace(item, arrival_s=item.arrival_s - start) for item in first]

    duration_s = float(window.window_end - window.window_start)
    interval_start_mono_ns = time.monotonic_ns()
    interval_start_wall_ns = time.time_ns()
    outcomes, early_stopped, early_stop_reason = _replay_requests(
        selected,
        base_url=args.base_url,
        timeout_s=study.engine.request_timeout_s,
        max_concurrency=study.trace.max_concurrency,
        target_pass_rate=(
            0.0
            if warmup or args.disable_slo_early_stop
            else study.slo.target_pass_rate
        ),
        max_lag_s=study.trace.early_stop_max_lag_s,
        max_elapsed_s=(
            120.0 if warmup else _probe_drain_deadline(
                selected, study.slo, ceiling=study.trace.early_stop_max_elapsed_s
            )
        ),
        evaluate_outcome=lambda outcome: evaluate_request(outcome, study.slo),
        drain_inflight_on_early_stop=True,
    )
    interval_end_mono_ns = time.monotonic_ns()
    interval_end_wall_ns = time.time_ns()
    evaluations, slo_summary = summarize_evaluations(outcomes, study.slo)
    by_id = {item.row_id: item for item in selected}
    details = []
    for outcome, evaluation in zip(outcomes, evaluations):
        request = by_id[outcome.request_id]
        details.append({
            "request_id": outcome.request_id,
            "sampling_u": request.sampling_u,
            "arrival_s": request.arrival_s,
            "raw_input_tokens": request.prompt_tokens_hint,
            "success": outcome.success,
            "ttft_ms": outcome.ttft_ms,
            "tpot_ms": outcome.tpot_ms,
            "completion_tokens": outcome.completion_tokens,
            "completion_tokens_source": outcome.completion_tokens_source,
            "completed_mono_ns": outcome.completed_mono_ns,
            "completed_elapsed_s": (
                (outcome.completed_mono_ns - interval_start_mono_ns) / 1e9
                if outcome.completed_mono_ns is not None else None
            ),
            "slo_pass": evaluation.passed,
            "reasons": evaluation.reasons,
            "error": outcome.error,
        })
    out = Path(args.result_dir)
    out.mkdir(parents=True, exist_ok=True)
    with (out / "requests.jsonl").open("w") as f:
        for item in details:
            f.write(json.dumps(item, sort_keys=True) + "\n")
    summary = selected_summary(selected, duration_s, args.tp)
    exact = sum(
        item.success and item.completion_tokens_source == "usage" and item.completion_tokens == 128
        for item in outcomes
    )
    result = {
        "schema": 1,
        "kind": "warmup" if warmup else "anchor",
        "cell": args.cell,
        "anchor": args.anchor,
        "tp": args.tp,
        "mns": args.mns,
        "study_sha256": sha256_file(study_path),
        "interval": {
            "start_mono_ns": interval_start_mono_ns, "end_mono_ns": interval_end_mono_ns,
            "start_wall_ns": interval_start_wall_ns, "end_wall_ns": interval_end_wall_ns,
            "elapsed_s": (interval_end_mono_ns - interval_start_mono_ns) / 1e9,
        },
        "selection": summary,
        "observed_count": len(outcomes),
        "exact_output_count": exact,
        "slo_pass_count": slo_summary["slo_pass_count"],
        "pass_rate": slo_summary["slo_pass_rate"],
        "feasible": bool(slo_summary["feasible"]),
        "early_stopped": early_stopped,
        "early_stop_reason": early_stop_reason,
        "slo_early_stop_disabled": bool(args.disable_slo_early_stop),
        "ttft_ms": numeric([item.ttft_ms for item in outcomes]),
        "tpot_ms": numeric([item.tpot_ms for item in outcomes]),
        "invariants": {
            "selected_nonempty": bool(selected),
            "outcomes_cover_selected": len(outcomes) == len(selected),
            "exact_output_or_failed": all(
                (not item.success) or (
                    item.completion_tokens_source == "usage" and item.completion_tokens == 128
                ) for item in outcomes
            ),
            "raw_lengths_present": all(item.prompt_tokens_hint is not None for item in selected),
            "arrival_nondecreasing": all(
                b.arrival_s >= a.arrival_s for a, b in zip(selected, selected[1:])
            ),
            "warmup_16": (len(outcomes) >= 16 if warmup else True),
            "warmup_exact_16": (exact >= 16 if warmup else True),
            "warmup_long": (
                any(int(item.prompt_tokens_hint or 0) > 4096 for item in selected)
                if warmup else True
            ),
        },
    }
    atomic_json(out / "result.json", result)
    print(json.dumps({k: result[k] for k in ("cell", "anchor", "kind", "pass_rate", "feasible")}))
    if not all(result["invariants"].values()):
        raise RuntimeError(f"client invariants failed: {result['invariants']}")
    return result


def preflight(args: argparse.Namespace) -> None:
    ground = json.loads(Path(args.ground_truth).read_text())
    studies = {1: Path(args.primary_study), 2: Path(args.primary_study), 4: Path(args.tp4_study)}
    loaded = {}
    mismatches = []
    values = []
    for cell in ground["cells"]:
        tp = int(cell["tensor_parallel_size"])
        if tp not in loaded:
            _study, _window, requests, _selected = load_selected(studies[tp], 0.0)
            loaded[tp] = requests
        for historical in cell["probe_history"]:
            selected = select_requests_for_threshold(
                loaded[tp], threshold=float(historical["sampling_u"])
            )
            values.append(len(selected))
            if len(selected) != int(historical["request_count"]):
                mismatches.append({
                    "cell": cell["cell_id"], "anchor": historical["sampling_u"],
                    "expected": historical["request_count"], "actual": len(selected),
                })
    result = {
        "schema": 1, "observations": len(values), "mismatches": mismatches,
        "request_counts": numeric(values),
        "invariants": {"observations_92": len(values) == 92, "counts_match": not mismatches},
    }
    atomic_json(Path(args.out), result)
    print(json.dumps(result, sort_keys=True))
    if not all(result["invariants"].values()):
        raise RuntimeError("preflight count reconstruction failed")


def parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser()
    sub = p.add_subparsers(dest="command", required=True)
    pf = sub.add_parser("preflight")
    pf.add_argument("--ground-truth", required=True)
    pf.add_argument("--primary-study", required=True)
    pf.add_argument("--tp4-study", required=True)
    pf.add_argument("--out", required=True)
    for name in ("warmup", "run-anchor"):
        q = sub.add_parser(name)
        q.add_argument("--study", required=True)
        q.add_argument("--cell", required=True)
        q.add_argument("--anchor", type=float, required=True)
        q.add_argument("--tp", type=int, required=True)
        q.add_argument("--mns", type=int, required=True)
        q.add_argument("--base-url", required=True)
        q.add_argument("--result-dir", required=True)
        q.add_argument("--disable-slo-early-stop", action="store_true")
    return p


def main() -> None:
    args = parser().parse_args()
    if args.command == "preflight":
        preflight(args)
    else:
        run_replay(args, warmup=args.command == "warmup")


if __name__ == "__main__":
    main()
