"""Analyze phase markers for final score-18 loss chains."""

from __future__ import annotations

import argparse
import json
import sys
from collections import Counter, defaultdict
from pathlib import Path
from statistics import mean
from typing import Any


SCRIPT_DIR = Path(__file__).resolve().parent
if str(SCRIPT_DIR) not in sys.path:
    sys.path.insert(0, str(SCRIPT_DIR))

from compare_setup_routes import route_for_record


Trace = dict[str, Any]


def read_records(path: Path) -> list[Trace]:
    records = []
    with path.open("r", encoding="utf-8") as handle:
        for line in handle:
            line = line.strip()
            if line:
                records.append(json.loads(line))
    return records


def write_json(path: Path, payload: dict[str, Any]) -> None:
    path.parent.mkdir(parents=True, exist_ok=True)
    path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")


def loss_signature(record: Trace) -> str:
    return (
        f"{record.get('pattern')};"
        f"x={record.get('final_ball_x')};"
        f"y={record.get('final_ball_y')};"
        f"dy={record.get('final_dy')};"
        f"score_after={record.get('self_score')}-{record.get('opponent_score')}"
    )


def compact_chain(records: list[Trace]) -> str:
    return "|".join(
        loss_signature(record)
        for record in records
        if record.get("outcome") == "loss"
    )


def score_before(record: Trace) -> str:
    if record.get("outcome") == "loss":
        return f"{record.get('self_score')}-{int(record.get('opponent_score')) - 1}"
    if record.get("outcome") == "win":
        return f"{int(record.get('self_score')) - 1}-{record.get('opponent_score')}"
    return "unknown"


def compact_row(row: Trace | None) -> dict[str, Any] | None:
    if row is None:
        return None
    target = row.get("target_y")
    paddle = row.get("self_paddle_y")
    return {
        "step": row.get("step"),
        "score": f"{row.get('self_score')}-{row.get('opponent_score')}",
        "ball_x": row.get("ball_x"),
        "ball_y": row.get("ball_y"),
        "dx": row.get("dx"),
        "dy": row.get("dy"),
        "target_y": target,
        "self_paddle_y": paddle,
        "paddle_error": round(float(paddle) - float(target), 2)
        if target is not None and paddle is not None
        else None,
        "action_name": row.get("action_name"),
        "mode": row.get("mode"),
    }


def first_row(window: list[Trace], predicate) -> Trace | None:
    return next((row for row in window if predicate(row)), None)


def active_right_indices(window: list[Trace]) -> list[int]:
    return [
        index
        for index, row in enumerate(window)
        if row.get("dx") is not None
        and row["dx"] > 0
        and row.get("ball_x", 0) >= 120
        and row.get("ball_y", 0) != 0
        and row.get("ball_x") not in (0, 205)
        and row.get("mode") != "inactive"
    ]


def row_bucket(row: Trace | None) -> str:
    if row is None:
        return "missing"
    target = row.get("target_y")
    paddle = row.get("self_paddle_y")
    score = row.get("score")
    if score is None:
        score = f"{row.get('self_score')}-{row.get('opponent_score')}"
    error = None
    if target is not None and paddle is not None:
        error = round(float(paddle) - float(target), 1)
    return (
        f"x={row.get('ball_x')};y={row.get('ball_y')};"
        f"dx={row.get('dx')};dy={row.get('dy')};"
        f"target={target};pad={paddle};err={error};"
        f"action={row.get('action_name')};mode={row.get('mode')};"
        f"score={score}"
    )


def sample_offset(record: Trace, anchor: str, offset: int) -> Trace | None:
    window = record.get("window") or []
    indices = active_right_indices(window)
    if not indices:
        return None
    base = indices[0] if anchor == "first_right" else indices[-1]
    index = base + offset
    return window[index] if 0 <= index < len(window) else None


def reset_signature(window: list[Trace], limit: int = 10) -> str:
    inactive = [
        row
        for row in window
        if row.get("mode") == "inactive"
        or (row.get("ball_x") in (0, 205) or row.get("ball_y") == 0)
    ][:limit]
    return "|".join(
        f"{row.get('self_paddle_y')}:{(row.get('action_name') or '?')[:1]}"
        for row in inactive
    )


def summarize_seed(seed: int, records: list[Trace]) -> dict[str, Any]:
    losses = [record for record in records if record.get("outcome") == "loss"]
    first_loss = losses[0]
    previous_points = [record for record in records if record["point_index"] < first_loss["point_index"]]
    window = first_loss.get("window") or []
    first_active = first_row(
        window,
        lambda row: row.get("ball_x") not in (0, 205) and row.get("ball_y") not in (0, None),
    )
    first_right = first_row(
        window,
        lambda row: row.get("dx") is not None
        and row["dx"] > 0
        and row.get("ball_x", 0) >= 120
        and row.get("ball_y", 0) != 0
        and row.get("ball_x") not in (0, 205)
        and row.get("mode") != "inactive",
    )
    first_left = first_row(
        window,
        lambda row: row.get("dx") is not None
        and row["dx"] < 0
        and row.get("ball_x", 0) >= 120
        and row.get("ball_y", 0) != 0,
    )
    return {
        "seed": seed,
        "final_score": records[-1].get("score_diff") if records else None,
        "chain_key": compact_chain(records),
        "first_loss_point": first_loss.get("point_index"),
        "first_loss_step": first_loss.get("step"),
        "first_loss_before_score": score_before(first_loss),
        "first_loss_signature": loss_signature(first_loss),
        "first_loss_route": route_for_record(first_loss),
        "previous_points": [
            {
                "point_index": record.get("point_index"),
                "outcome": record.get("outcome"),
                "signature": "win" if record.get("outcome") == "win" else loss_signature(record),
                "score_after": f"{record.get('self_score')}-{record.get('opponent_score')}",
            }
            for record in previous_points
        ],
        "reset_signature": reset_signature(window),
        "first_active": compact_row(first_active),
        "first_left": compact_row(first_left),
        "first_right": compact_row(first_right),
        "first_right_offsets": {
            str(offset): compact_row(sample_offset(first_loss, "first_right", offset))
            for offset in (-8, -4, 0, 4, 8, 12, 16, 20)
        },
        "last_right_offsets": {
            str(offset): compact_row(sample_offset(first_loss, "last_right", offset))
            for offset in (-16, -12, -8, -4, 0)
        },
    }


def top_counts(counter: Counter[str], limit: int = 8) -> list[dict[str, Any]]:
    return [{"value": value, "count": count} for value, count in counter.most_common(limit)]


def summarize_group(members: list[dict[str, Any]]) -> dict[str, Any]:
    return {
        "count": len(members),
        "seeds": [member["seed"] for member in members],
        "chain_key": members[0]["chain_key"] if members else "",
        "first_loss_points": dict(Counter(member["first_loss_point"] for member in members)),
        "first_loss_before_scores": dict(Counter(member["first_loss_before_score"] for member in members)),
        "first_loss_routes": top_counts(Counter(member["first_loss_route"] for member in members)),
        "reset_signatures": top_counts(Counter(member["reset_signature"] for member in members)),
        "first_active_buckets": top_counts(Counter(row_bucket(member["first_active"]) for member in members)),
        "first_left_buckets": top_counts(Counter(row_bucket(member["first_left"]) for member in members)),
        "first_right_buckets": top_counts(Counter(row_bucket(member["first_right"]) for member in members)),
        "first_right_offset_buckets": {
            str(offset): top_counts(
                Counter(row_bucket(member["first_right_offsets"][str(offset)]) for member in members)
            )
            for offset in (-8, -4, 0, 4, 8, 12, 16, 20)
        },
        "last_right_offset_buckets": {
            str(offset): top_counts(
                Counter(row_bucket(member["last_right_offsets"][str(offset)]) for member in members)
            )
            for offset in (-16, -12, -8, -4, 0)
        },
        "mean_first_loss_step": mean(member["first_loss_step"] for member in members),
    }


def summarize(path: Path, final_score: int) -> dict[str, Any]:
    by_seed: dict[int, list[Trace]] = defaultdict(list)
    for record in read_records(path):
        by_seed[int(record["seed"])].append(record)

    seed_summaries = [
        summarize_seed(seed, records)
        for seed, records in sorted(by_seed.items())
        if records and records[-1].get("score_diff") == final_score
    ]

    grouped: dict[str, list[dict[str, Any]]] = defaultdict(list)
    for summary in seed_summaries:
        grouped[summary["chain_key"]].append(summary)

    chain_groups = [
        summarize_group(members)
        for _chain, members in sorted(grouped.items(), key=lambda item: (-len(item[1]), item[0]))
    ]
    return {
        "source": str(path),
        "final_score": final_score,
        "seed_count": len(seed_summaries),
        "interpretation": (
            "Summarizes family markers visible before the first loss in each final-score chain. "
            "Offsets are relative to the first or last active right-moving frame in that first-loss point."
        ),
        "chain_groups": chain_groups,
        "seeds": seed_summaries,
    }


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("jsonl", type=Path)
    parser.add_argument("--final-score", type=int, default=18)
    parser.add_argument("--output", type=Path)
    args = parser.parse_args()

    payload = summarize(args.jsonl, args.final_score)
    text = json.dumps(payload, indent=2, sort_keys=True)
    if args.output:
        write_json(args.output, payload)
    print(text)


if __name__ == "__main__":
    main()
