#!/usr/bin/env python3
"""Validate submitted external proof records for the blocked route."""
from __future__ import annotations

import argparse
import html
import json
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parent))

from scan_savedata_slots import scan_search_roots, synthetic_diagnostic_hashes


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
SOURCE = "map1_01a"
TARGET = "map2_02d"
CURRENT_SELECTOR = "2:0"
CURRENT_SELECTED_POINTER = "0x00540714"
SELECTED_POINTER_GLOBAL = "0x0059de30"
EXPECTED_SAVE_SIZE = 1274
ACCEPTED_TRACE_POINTS = {"0x0059e348", "0x0040c675", "0x00542b0c", "0x005428c4"}
STRICT_CANDIDATE_SIDES = {"top", "bottom", "left", "right"}


def load_json(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def normalize_hex(value: Any) -> str | None:
    if value is None:
        return None
    text = str(value).strip().lower()
    if not text:
        return None
    try:
        return f"0x{int(text, 16):08x}"
    except ValueError:
        return text


def bool_true(value: Any) -> bool:
    return value is True


def bool_false(value: Any) -> bool:
    return value is False


def field_maps(record: dict) -> list[str]:
    maps = record.get("fieldMaps") or record.get("maps") or []
    return [str(item) for item in maps if isinstance(item, str)]


def is_int_tile(tile: Any) -> bool:
    return isinstance(tile, dict) and isinstance(tile.get("x"), int) and isinstance(tile.get("y"), int)


def proof_records(payload: Any) -> tuple[list[dict], str]:
    if isinstance(payload, list):
        return [row for row in payload if isinstance(row, dict)], "record-list"
    if not isinstance(payload, dict):
        return [], "unsupported-payload"
    if isinstance(payload.get("inputId"), str):
        return [payload], "single-record"
    for key in ("submittedRecords", "records", "proofRecords"):
        rows = payload.get(key)
        if isinstance(rows, list):
            return [row for row in rows if isinstance(row, dict)], key
    if payload.get("templateStatus") == "no-external-proof-submitted" or payload.get("requiredInputs"):
        return [], "template-only"
    return [], "no-records"


def check_row(check_id: str, passed: bool, detail: str) -> dict:
    return {"id": check_id, "passed": bool(passed), "detail": detail}


def resolve_record_path(record: dict) -> Path | None:
    raw_path = record.get("path") or record.get("artifactPath")
    if not isinstance(raw_path, str) or not raw_path.strip():
        return None
    path_text = raw_path.split("::", 1)[0]
    path = Path(path_text)
    if not path.is_absolute():
        path = ROOT / path
    return path


def scan_savedata_path(record: dict) -> dict | None:
    path = resolve_record_path(record)
    if path is None:
        return None
    exe = ROOT / "Hwanse2.exe"
    maps_index = OUT / "maps_runtime.js"
    rows = scan_search_roots(
        [path],
        exe if exe.exists() else None,
        maps_index if maps_index.exists() else None,
        synthetic_diagnostic_hashes(),
    )
    valid_rows = [row for row in rows if row.get("valid")]
    if "::" in str(record.get("path") or ""):
        member = str(record.get("path")).split("::", 1)[1]
        matched = [
            row for row in valid_rows
            if row.get("archiveMember") == member or str(row.get("path") or "").endswith(f"::{member}")
        ]
        if matched:
            return matched[0]
    return (valid_rows or rows or [None])[0]


def validate_savedata_record(record: dict) -> dict:
    checks: list[dict] = []
    maps = set(field_maps(record))
    actual = scan_savedata_path(record)
    actual_path = resolve_record_path(record)
    checks.extend([
        check_row("input-id", record.get("inputId") == "real-selector-2-0-save", "inputId must be real-selector-2-0-save"),
        check_row("captured-from-gameplay", bool_true(record.get("capturedFromGameplay")), "capturedFromGameplay must be true"),
        check_row("record-synthetic-false", bool_false(record.get("syntheticDiagnostic")), "syntheticDiagnostic must be false"),
        check_row("record-selector", record.get("selector") == CURRENT_SELECTOR, "record selector must be 2:0"),
        check_row(
            "record-selected-pointer",
            normalize_hex(record.get("selectedPointerHex")) == CURRENT_SELECTED_POINTER,
            "record selectedPointerHex must be 0x00540714",
        ),
        check_row(
            "record-route-maps",
            {SOURCE, TARGET}.issubset(maps),
            "record fieldMaps must include map1_01a and map2_02d",
        ),
    ])
    if actual_path is None:
        checks.append(check_row("artifact-path-present", False, "path or artifactPath is required for savedata proof"))
    else:
        checks.append(check_row("artifact-path-exists", actual_path.exists(), f"{actual_path} must exist"))
    if actual:
        checks.extend([
            check_row("actual-valid-savedata", actual.get("valid") is True, actual.get("error") or "savedata parsed successfully"),
            check_row("actual-size", actual.get("size") == EXPECTED_SAVE_SIZE, f"size={actual.get('size')} expected={EXPECTED_SAVE_SIZE}"),
            check_row("actual-selector", actual.get("selectorMatchesCurrent") is True, f"selector={actual.get('selector')}"),
            check_row(
                "actual-selected-pointer",
                actual.get("selectedPointerMatchesCurrent") is True,
                f"selectedPointerHex={actual.get('selectedPointerHex')}",
            ),
            check_row("actual-route-pair", actual.get("routePairCovered") is True, f"fieldMaps={actual.get('fieldMaps') or []}"),
            check_row("actual-synthetic-false", actual.get("syntheticDiagnostic") is False, actual.get("syntheticDiagnosticReason") or "not diagnostic"),
            check_row("actual-real-route-evidence", actual.get("realRouteEvidenceCandidate") is True, "actual scan must produce real route evidence candidate"),
        ])
    accepted = all(row["passed"] for row in checks)
    return {
        "inputId": "real-selector-2-0-save",
        "accepted": accepted,
        "status": "accepted" if accepted else "rejected",
        "checks": checks,
        "actualScanRow": actual or {},
    }


def observed_trace_addresses(record: dict) -> set[str]:
    rows = record.get("observedTracePoints") or record.get("tracePoints") or []
    addresses: set[str] = set()
    for row in rows:
        if isinstance(row, dict):
            value = row.get("addressHex") or row.get("address") or row.get("vaHex")
            normalized = normalize_hex(value)
            if normalized:
                addresses.add(normalized)
    return addresses


def validate_runtime_trace_record(record: dict) -> dict:
    addresses = observed_trace_addresses(record)
    selected_root_ok = (
        bool_true(record.get("selectedRootExecutionRefFound"))
        or bool_true(record.get("equivalentSelectedRootProofFound"))
        or normalize_hex(record.get("selectedRootHex")) == CURRENT_SELECTED_POINTER
    )
    checks = [
        check_row("input-id", record.get("inputId") == "normal-route-runtime-trace", "inputId must be normal-route-runtime-trace"),
        check_row("normal-route-path", bool_true(record.get("normalRoutePath")), "normalRoutePath must be true"),
        check_row("diagnostic-run-false", bool_false(record.get("diagnosticRun")), "diagnosticRun must be false"),
        check_row(
            "selected-pointer-global",
            normalize_hex(record.get("selectedPointerGlobalVaHex")) == SELECTED_POINTER_GLOBAL,
            "selectedPointerGlobalVaHex must be 0x0059de30",
        ),
        check_row(
            "selected-pointer-value",
            normalize_hex(record.get("selectedPointerValueHex")) == CURRENT_SELECTED_POINTER,
            "selectedPointerValueHex must be 0x00540714",
        ),
        check_row("selected-root-proof", selected_root_ok, "selected-root execution ref or equivalent proof must be present"),
        check_row(
            "accepted-trace-point",
            bool(addresses & ACCEPTED_TRACE_POINTS),
            f"observed trace points must include one of {sorted(ACCEPTED_TRACE_POINTS)}",
        ),
    ]
    accepted = all(row["passed"] for row in checks)
    return {
        "inputId": "normal-route-runtime-trace",
        "accepted": accepted,
        "status": "accepted" if accepted else "rejected",
        "checks": checks,
        "observedTracePointAddresses": sorted(addresses),
    }


def validate_strict_hotspot_record(record: dict) -> dict:
    selector_only = record.get("selectorOnlySceneListAdjacency")
    if selector_only is None:
        selector_only = record.get("selectorOnly")
    checks = [
        check_row("input-id", record.get("inputId") == "strict-source-hotspot", "inputId must be strict-source-hotspot"),
        check_row("source-map", record.get("sourceMap") == SOURCE, "sourceMap must be map1_01a"),
        check_row("target-map", record.get("targetMap") == TARGET, "targetMap must be map2_02d"),
        check_row("candidate-side", record.get("candidateSide") in STRICT_CANDIDATE_SIDES, "candidateSide must be top, bottom, left, or right"),
        check_row("source-tile", is_int_tile(record.get("sourceTile")), "sourceTile.x/y must be integers"),
        check_row("target-spawn", is_int_tile(record.get("targetSpawn")), "targetSpawn.x/y must be integers"),
        check_row("strict-proof", bool_true(record.get("strictSourceHotspotProofFound")), "strictSourceHotspotProofFound must be true"),
        check_row("tile-proof", bool_true(record.get("tileHotspotConfirmed")), "tileHotspotConfirmed must be true"),
        check_row("not-selector-only", selector_only is not True, "selector-only scene-list adjacency must not be the only evidence"),
        check_row("supporting-artifact", bool(record.get("supportingArtifact")), "supportingArtifact must be provided"),
    ]
    accepted = all(row["passed"] for row in checks)
    return {
        "inputId": "strict-source-hotspot",
        "accepted": accepted,
        "status": "accepted" if accepted else "rejected",
        "checks": checks,
    }


def validate_record(record: dict) -> dict:
    input_id = record.get("inputId")
    if input_id == "real-selector-2-0-save":
        return validate_savedata_record(record)
    if input_id == "normal-route-runtime-trace":
        return validate_runtime_trace_record(record)
    if input_id == "strict-source-hotspot":
        return validate_strict_hotspot_record(record)
    return {
        "inputId": input_id,
        "accepted": False,
        "status": "rejected",
        "checks": [check_row("known-input-id", False, "inputId must be one of real-selector-2-0-save, normal-route-runtime-trace, strict-source-hotspot")],
    }


def build_summary(proof_path: Path) -> dict:
    payload = load_json(proof_path)
    records, mode = proof_records(payload)
    results = [validate_record(record) for record in records]
    accepted = [row for row in results if row.get("accepted")]
    status = "accepted-external-proof-present" if accepted else ("template-only" if mode == "template-only" else "no-accepted-external-proof")
    return {
        "schemaVersion": 1,
        "source": SOURCE,
        "target": TARGET,
        "proofPath": str(proof_path),
        "inputMode": mode,
        "recordCount": len(records),
        "acceptedRecordCount": len(accepted),
        "proofFound": bool(accepted),
        "externalProofValidationProofFound": bool(accepted),
        "promotionStatus": "external-proof-ready-for-refresh" if accepted else "blocked",
        "validationStatus": status,
        "acceptedInputIds": [row.get("inputId") for row in accepted],
        "recordResults": results,
        "notPromotionByItself": True,
        "nextStep": (
            "Run tools/refresh_savedata_route_proof.py for accepted savedata, or refresh the specific "
            "external proof packet and route gate after adding the accepted trace/hotspot artifact."
            if accepted
            else "No accepted external proof record is present. Route promotion remains blocked."
        ),
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Route Promotion External Proof Validation",
        "",
        f"- route: `{summary['source']} -> {summary['target']}`",
        f"- proof path: `{summary['proofPath']}`",
        f"- input mode: `{summary['inputMode']}`",
        f"- validation status: `{summary['validationStatus']}`",
        f"- proof found: {summary['proofFound']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        f"- record count: {summary['recordCount']}",
        f"- accepted record count: {summary['acceptedRecordCount']}",
        f"- not promotion by itself: {summary['notPromotionByItself']}",
        f"- next step: {summary['nextStep']}",
        "",
        "## Records",
        "",
    ]
    if not summary.get("recordResults"):
        lines.append("- no submitted external proof records")
        lines.append("")
        return "\n".join(lines)
    for result in summary.get("recordResults") or []:
        lines.extend([
            f"### {result.get('inputId')}",
            "",
            f"- status: `{result.get('status')}`",
            f"- accepted: {result.get('accepted')}",
            "",
            "| check | passed | detail |",
            "| --- | --- | --- |",
        ])
        for check in result.get("checks") or []:
            lines.append(f"| `{check.get('id')}` | {check.get('passed')} | {check.get('detail')} |")
        lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    esc = lambda value: html.escape(str(value))
    sections = []
    if not summary.get("recordResults"):
        sections.append("<p>no submitted external proof records</p>")
    for result in summary.get("recordResults") or []:
        rows = "".join(
            "<tr>"
            f"<td><code>{esc(check.get('id'))}</code></td>"
            f"<td>{esc(check.get('passed'))}</td>"
            f"<td>{esc(check.get('detail'))}</td>"
            "</tr>"
            for check in result.get("checks") or []
        )
        sections.append(
            "<section>"
            f"<h2>{esc(result.get('inputId'))}</h2>"
            f"<p>status <code>{esc(result.get('status'))}</code>; accepted {esc(result.get('accepted'))}</p>"
            "<table><thead><tr><th>check</th><th>passed</th><th>detail</th></tr></thead>"
            f"<tbody>{rows}</tbody></table>"
            "</section>"
        )
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Route Promotion External Proof Validation</title>",
        "  <style>body{margin:24px;background:#101010;color:#eee;font:14px system-ui,sans-serif}table{border-collapse:collapse;width:100%}th,td{border:1px solid #333;padding:6px 8px;vertical-align:top}th{background:#1d1d1d}code{color:#f5d76e}section{margin-top:20px}</style>",
        "</head>",
        "<body>",
        "  <h1>Route Promotion External Proof Validation</h1>",
        f"  <p>route <code>{esc(summary['source'])} -&gt; {esc(summary['target'])}</code>; "
        f"proof path <code>{esc(summary['proofPath'])}</code>.</p>",
        f"  <p>input mode <code>{esc(summary['inputMode'])}</code>; validation status "
        f"<code>{esc(summary['validationStatus'])}</code>; proof found {esc(summary['proofFound'])}; "
        f"promotion status <code>{esc(summary['promotionStatus'])}</code>.</p>",
        f"  <p>record count {esc(summary['recordCount'])}; accepted record count "
        f"{esc(summary['acceptedRecordCount'])}; not promotion by itself {esc(summary['notPromotionByItself'])}.</p>",
        f"  <p>{esc(summary['nextStep'])}</p>",
        *sections,
        "</body>",
        "</html>",
        "",
    ])


def write_outputs(summary: dict, out_dir: Path) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "route_promotion_external_proof_validation.json").write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    (out_dir / "route_promotion_external_proof_validation.html").write_text(html_page(summary), encoding="utf-8")


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument(
        "proof_json",
        nargs="?",
        type=Path,
        default=OUT / "route_promotion_external_proof_template.json",
        help="submitted proof JSON record/list/wrapper; defaults to the generated template",
    )
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--json", action="store_true", help="print validation summary instead of writing reports")
    parser.add_argument(
        "--require-accepted",
        action="store_true",
        help="exit with status 1 when the submitted proof JSON has no accepted record",
    )
    args = parser.parse_args()

    proof_path = args.proof_json
    if not proof_path.is_absolute():
        proof_path = ROOT / proof_path
    summary = build_summary(proof_path)
    if args.json:
        print(json.dumps(summary, ensure_ascii=False, indent=2))
        if args.require_accepted and not summary["proofFound"]:
            raise SystemExit(1)
        return
    write_outputs(summary, args.out_dir)
    print(f"wrote external proof validation -> {args.out_dir / 'route_promotion_external_proof_validation.html'}")
    if args.require_accepted and not summary["proofFound"]:
        raise SystemExit(1)


if __name__ == "__main__":
    main()
