#!/usr/bin/env python3
"""Review manual route-like active object scripts.

This report keeps the current assumption explicit: field map movement is manual.
It therefore looks for active object scripts that combine prompt text with
manual movement/object opcodes.  It does not promote a script to a route unless
the decoded command stream reaches the map loader or writes an explicit target.
"""
from __future__ import annotations

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

from probe_exe_scene_tables import read_sections
from summarize_active_object_script_inventory import scan_initializers
from summarize_object_payload_442c75_callers import decode_stream, hex32, is_va


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
EXE = ROOT / "Hwanse2.exe"

MANUAL_OPCODES = {0x55, 0x65, 0x66, 0x67, 0x70}
ROUTE_WORDS = (
    "문을 통과",
    "출발",
    "올라탄다",
    "올라 타본다",
    "돌아갈 수",
    "고향으로",
    "밑으로 내려간다",
)


def text_preview(stream: dict[str, Any]) -> str:
    previews = []
    for ref in stream.get("textPayloadRefs") or []:
        preview = (ref.get("textPreview") or "").strip()
        if preview:
            previews.append(preview)
    return "\n---\n".join(previews)


def script_vas_from_initializers(exe: bytes, sections: list[dict[str, Any]]) -> list[int]:
    initializers = scan_initializers(exe, sections)
    return sorted({
        int(write["value"])
        for init in initializers
        for write in init["ecWrites"]
        if is_va(sections, int(write["value"]))
    })


def command_row(command: dict[str, Any]) -> dict[str, Any]:
    opcode = command.get("opcode")
    return {
        "vaHex": command.get("vaHex"),
        "opcodeHex": command.get("opcodeHex"),
        "opcodeName": command.get("opcodeName"),
        "isManualMovementOpcode": opcode in MANUAL_OPCODES,
        "summary": command.get("summary", ""),
        "rawHex": command.get("rawHex", ""),
        "targetVaHex": command.get("targetVaHex", ""),
        "fieldOffset": command.get("fieldOffset"),
        "literal": command.get("literal"),
        "group": command.get("group"),
        "mode": command.get("mode"),
    }


def classify_script(stream: dict[str, Any], preview: str) -> str:
    opcodes = {command.get("opcode") for command in stream.get("commands") or []}
    if stream.get("routeProofFound") or stream.get("mapLoaderRefFound"):
        return "route-proof"
    if any(word in preview for word in ROUTE_WORDS) and opcodes.intersection(MANUAL_OPCODES):
        return "manual-route-like-prompt-script"
    if opcodes.intersection({0x55, 0x65, 0x67}):
        return "manual-position-control-script"
    if opcodes.intersection(MANUAL_OPCODES):
        return "manual-object-control-script"
    return "non-route-script"


def build_summary(exe_path: Path) -> dict[str, Any]:
    exe = exe_path.read_bytes()
    sections = read_sections(exe)
    rows = []
    opcode_counts: dict[str, int] = {f"0x{opcode:02x}": 0 for opcode in sorted(MANUAL_OPCODES)}
    for script_va in script_vas_from_initializers(exe, sections):
        stream = decode_stream(exe, sections, script_va, max_commands=100, max_bytes=0x500)
        commands = [command_row(command) for command in stream.get("commands") or []]
        manual_commands = [row for row in commands if row["isManualMovementOpcode"]]
        if not manual_commands:
            continue
        for row in manual_commands:
            opcode_counts[row["opcodeHex"]] = opcode_counts.get(row["opcodeHex"], 0) + 1
        preview = text_preview(stream)
        rows.append({
            "scriptVa": script_va,
            "scriptVaHex": hex32(script_va),
            "classification": classify_script(stream, preview),
            "textPreview": preview,
            "manualCommandCount": len(manual_commands),
            "decodedCommandCount": stream.get("decodedCommandCount"),
            "routeProofFound": stream.get("routeProofFound"),
            "mapLoaderRefFound": stream.get("mapLoaderRefFound"),
            "manualCommands": manual_commands,
            "commands": commands,
        })
    class_counts: dict[str, int] = {}
    for row in rows:
        key = row["classification"]
        class_counts[key] = class_counts.get(key, 0) + 1
    route_proof_count = sum(1 for row in rows if row["routeProofFound"] or row["mapLoaderRefFound"])
    return {
        "title": "Manual Route-like Active Object Scripts",
        "summary": {
            "manualMovementAssumption": True,
            "sceneAutoTransitionClaim": False,
            "scriptCount": len(rows),
            "routeProofScriptCount": route_proof_count,
            "classificationCounts": class_counts,
            "manualOpcodeCounts": opcode_counts,
            "newlyGroundedOpcodes": {
                "0x06": "repeat/wait-target",
                "0x6e": "group-slot conditional branch",
                "0x72": "active-object sequence/motion scan",
                "0xd3": "condition/test branch",
            },
        },
        "scripts": rows,
        "nonClaims": [
            "This report does not claim scene-driven automatic map movement.",
            "Manual route-like means a player-triggered object script contains movement/position opcodes or route-like prompt text.",
            "A concrete route still requires map-loader/root/target-map evidence, which is not present in these decoded streams.",
        ],
    }


def html_doc(summary: dict[str, Any]) -> str:
    s = summary["summary"]
    rows = []
    for row in summary["scripts"]:
        preview = html.escape(row["textPreview"][:900])
        manual = "<br>".join(
            f"<code>{html.escape(command['vaHex'] or '')}</code> "
            f"<code>{html.escape(command['opcodeHex'] or '')}</code> "
            f"{html.escape(command['summary'])}"
            for command in row["manualCommands"]
        )
        rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['scriptVaHex'])}</code></td>"
            f"<td>{html.escape(row['classification'])}</td>"
            f"<td><pre>{preview}</pre></td>"
            f"<td>{manual}</td>"
            f"<td>{row['routeProofFound']}</td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang=\"ko\">
<head>
  <meta charset=\"utf-8\">
  <title>{html.escape(summary['title'])}</title>
  <style>
    body {{ font-family: system-ui, sans-serif; margin: 24px; color: #17202a; }}
    table {{ border-collapse: collapse; width: 100%; }}
    th, td {{ border: 1px solid #d7dde6; padding: 8px; vertical-align: top; }}
    th {{ background: #eef2f6; }}
    pre {{ white-space: pre-wrap; margin: 0; max-width: 520px; }}
    code {{ background: #f3f5f7; padding: 1px 3px; border-radius: 3px; }}
    .metrics {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 8px; }}
    .metric {{ background: #f6f8fb; border: 1px solid #d7dde6; padding: 10px; border-radius: 6px; }}
  </style>
</head>
<body>
  <h1>{html.escape(summary['title'])}</h1>
  <div class=\"metrics\">
    <div class=\"metric\">manual movement assumption: <strong>{s['manualMovementAssumption']}</strong></div>
    <div class=\"metric\">scene auto transition claim: <strong>{s['sceneAutoTransitionClaim']}</strong></div>
    <div class=\"metric\">script count: <strong>{s['scriptCount']}</strong></div>
    <div class=\"metric\">route proof script count: <strong>{s['routeProofScriptCount']}</strong></div>
  </div>
  <p>이 페이지는 자동 scene 이동을 주장하지 않는다. 수동 이동/상호작용 object script 중 route-like 프롬프트와 manual movement opcode를 같이 가진 항목만 분리한다.</p>
  <h2>Newly Grounded Opcodes</h2>
  <ul>
    {''.join(f'<li><code>{html.escape(opcode)}</code>: {html.escape(meaning)}</li>' for opcode, meaning in s['newlyGroundedOpcodes'].items())}
  </ul>
  <table>
    <thead><tr><th>script</th><th>classification</th><th>text preview</th><th>manual commands</th><th>route proof</th></tr></thead>
    <tbody>{''.join(rows)}</tbody>
  </table>
  <script>
    window.manualRouteLikeScriptReview = {{
      manualMovementAssumption: true,
      sceneAutoTransitionClaim: false,
      routeProofScriptCount: {s['routeProofScriptCount']},
      newlyGroundedOpcodes: {json.dumps(s['newlyGroundedOpcodes'], ensure_ascii=False)}
    }};
  </script>
</body>
</html>
"""


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=EXE)
    parser.add_argument("--out", type=Path, default=OUT)
    args = parser.parse_args()
    summary = build_summary(args.exe)
    args.out.mkdir(parents=True, exist_ok=True)
    (args.out / "manual_route_like_script_review.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    print(json.dumps(summary["summary"], ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
