#!/usr/bin/env python3
"""Attach original-runtime battle effect captures to the local oracle manifest.

The web reference side can be generated automatically, but original-game
captures have to be produced externally.  This helper keeps that manual step
small: point it at a directory of original PNG captures and it updates
``data/battle_effect_capture_manifest.json`` with matching ``originalCapture``
paths.

Accepted source layouts for each oracle row are:

- ``<source-root>/<safe-row-id>/``
- ``<source-root>/<safe-row-id>/original/``
- ``<source-root>/<raw-row-id>/``
- ``<source-root>/<raw-row-id>/original/``

Where ``safe-row-id`` is the same sanitized directory name used by
``tools/capture_battle_effect_web_reference.py``.
"""

from __future__ import annotations

import argparse
import json
import shutil
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
DATA = ROOT / "data"
CAPTURE_ROOT = ROOT / "captures" / "battle_effect_pixel_oracle"
PLAN = OUT / "battle_effect_pixel_oracle_plan.json"
MANIFEST = DATA / "battle_effect_capture_manifest.json"


def read_json(path: Path) -> dict[str, Any]:
    return json.loads(path.read_text(encoding="utf-8"))


def safe_row_dir(value: str | None) -> str:
    text = str(value or "row")
    return "".join(ch if ch.isalnum() or ch in "._-" else "_" for ch in text).strip("._") or "row"


def display_path(path: Path) -> str:
    try:
        return str(path.relative_to(ROOT))
    except ValueError:
        return str(path)


def resolve_path(value: str | None) -> Path | None:
    if not value:
        return None
    path = Path(value)
    if not path.is_absolute():
        path = ROOT / path
    return path


def collect_pngs(path: Path | None) -> list[Path]:
    if path is None or not path.exists():
        return []
    if path.is_file():
        return [path] if path.suffix.lower() == ".png" else []
    return sorted(item for item in path.iterdir() if item.is_file() and item.suffix.lower() == ".png")


def plan_rows(scope: str) -> list[dict[str, Any]]:
    plan = read_json(PLAN)
    if scope == "first-pass":
        rows = plan.get("recommendedFirstPassRows") or []
    elif scope == "full":
        rows = plan.get("fullOracleRows") or []
    else:
        raise ValueError(f"unknown scope: {scope}")
    if not rows:
        raise RuntimeError(f"{PLAN} has no rows for scope {scope!r}")
    return rows


def load_manifest() -> dict[str, Any]:
    if MANIFEST.exists():
        return read_json(MANIFEST)
    return {
        "version": 1,
        "kind": "hwanse-battle-effect-capture-manifest",
        "note": "Local-only manifest. Add webReferenceCapture and originalCapture paths, then run compare.",
        "captures": [],
    }


def manifest_by_id(manifest: dict[str, Any]) -> dict[str, dict[str, Any]]:
    rows: dict[str, dict[str, Any]] = {}
    captures = manifest.get("captures")
    if isinstance(captures, list):
        for item in captures:
            if isinstance(item, dict) and item.get("id"):
                rows[str(item["id"])] = dict(item)
    elif isinstance(captures, dict):
        for key, item in captures.items():
            if isinstance(item, dict):
                rows[str(key)] = {"id": str(key), **item}
    return rows


def candidate_source_paths(source_root: Path, row_id: str) -> list[Path]:
    safe = safe_row_dir(row_id)
    return [
        source_root / safe / "original",
        source_root / safe,
        source_root / row_id / "original",
        source_root / row_id,
    ]


def find_source_capture(source_root: Path, row_id: str) -> Path | None:
    for candidate in candidate_source_paths(source_root, row_id):
        if collect_pngs(candidate):
            return candidate
    return None


def copy_capture(source: Path, row_id: str) -> Path:
    destination = CAPTURE_ROOT / safe_row_dir(row_id) / "original"
    destination.mkdir(parents=True, exist_ok=True)
    for old in destination.glob("*.png"):
        old.unlink()
    if source.is_file():
        shutil.copy2(source, destination / source.name)
    else:
        for item in collect_pngs(source):
            shutil.copy2(item, destination / item.name)
    return destination


def expected_web_frame_count(entry: dict[str, Any]) -> int:
    web_path = resolve_path(entry.get("webReferenceCapture"))
    frames = collect_pngs(web_path)
    if frames:
        return len(frames)
    ticks = entry.get("captureTicks")
    if isinstance(ticks, list):
        return len(ticks)
    return 0


def update_manifest(args: argparse.Namespace) -> dict[str, Any]:
    source_root = args.source_root.resolve()
    if not source_root.exists():
        raise FileNotFoundError(f"source root does not exist: {source_root}")

    rows = plan_rows(args.scope)
    manifest = load_manifest()
    by_id = manifest_by_id(manifest)

    imported = []
    missing = []
    mismatches = []
    for row in rows:
        row_id = str(row["id"])
        entry = by_id.get(row_id, {"id": row_id})
        entry.setdefault("ownerKey", row.get("ownerKey"))
        entry.setdefault("skillIdHex", row.get("skillIdHex"))
        entry.setdefault("levelOrFixed", row.get("levelOrFixed"))
        entry.setdefault("skillName", row.get("skillName") or "")

        source = find_source_capture(source_root, row_id)
        if source is None:
            missing.append(row_id)
            by_id[row_id] = entry
            continue

        capture_path = copy_capture(source, row_id) if args.copy else source
        entry["originalCapture"] = display_path(capture_path)
        original_count = len(collect_pngs(capture_path))
        web_count = expected_web_frame_count(entry)
        count_matches = not web_count or original_count == web_count
        if not count_matches:
            mismatches.append(
                {
                    "id": row_id,
                    "originalFrameCount": original_count,
                    "expectedFrameCount": web_count,
                    "originalCapture": entry["originalCapture"],
                    "webReferenceCapture": entry.get("webReferenceCapture") or "",
                }
            )
        imported.append(
            {
                "id": row_id,
                "originalCapture": entry["originalCapture"],
                "originalFrameCount": original_count,
                "expectedFrameCount": web_count,
                "frameCountMatches": count_matches,
            }
        )
        by_id[row_id] = entry

    payload = {
        "version": manifest.get("version", 1),
        "kind": manifest.get("kind", "hwanse-battle-effect-capture-manifest"),
        "note": manifest.get(
            "note",
            "Local-only manifest. Add originalCapture paths, then run tools/compare_battle_effect_pixel_oracle.py.",
        ),
        "captures": sorted(by_id.values(), key=lambda item: str(item.get("id") or "")),
    }

    if not args.dry_run:
        DATA.mkdir(parents=True, exist_ok=True)
        MANIFEST.write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")

    return {
        "scope": args.scope,
        "sourceRoot": display_path(source_root),
        "manifestPath": display_path(MANIFEST),
        "dryRun": args.dry_run,
        "copy": args.copy,
        "expectedRows": len(rows),
        "importedRows": len(imported),
        "missingRows": len(missing),
        "frameCountMismatches": len(mismatches),
        "imported": imported,
        "missing": missing,
        "mismatches": mismatches,
    }


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("source_root", type=Path, help="Directory containing original runtime PNG captures")
    parser.add_argument("--scope", choices=["first-pass", "full"], default="first-pass")
    parser.add_argument("--copy", action="store_true", help="Copy matched PNGs into ignored captures/battle_effect_pixel_oracle")
    parser.add_argument("--dry-run", action="store_true", help="Print what would be imported without updating the manifest")
    parser.add_argument("--strict", action="store_true", help="Exit nonzero if any expected row is missing or has a frame-count mismatch")
    args = parser.parse_args()

    report = update_manifest(args)
    print(json.dumps(report, ensure_ascii=False, indent=2))
    if args.strict and (report["missingRows"] or report["frameCountMismatches"]):
        return 1
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
