#!/usr/bin/env python3
"""Summarize control opcodes inside manual route-like active object scripts.

Map movement is treated as manual.  This report narrows the post-trigger
control stream without promoting it to an automatic scene transition.  The
interesting scripts are the active object scripts that contain route-like text
and manual movement/object-position opcodes.
"""
from __future__ import annotations

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

from probe_exe_scene_tables import read_sections, va_to_offset
from summarize_object_payload_442c75_callers import decode_stream


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

CONTROL_OPCODES = {"0x11", "0x12", "0x13", "0x24", "0x2f", "0x55", "0x65", "0x66", "0x67", "0x84"}


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


def compact_preview(text: str, limit: int = 220) -> str:
    preview = " / ".join(line.strip() for line in text.splitlines() if line.strip())
    return preview[:limit]


def classify_pattern(commands: list[dict[str, Any]]) -> str:
    opcodes = [command.get("opcodeHex") for command in commands]
    if "0x65" in opcodes and "0x55" in opcodes and "0x67" in opcodes:
        return "door-choice-position-step-position-gate"
    if "0x2f" in opcodes and "0x13" in opcodes and "0x66" in opcodes:
        return "prompt-choice-object-sequence-gate"
    if "0x2f" in opcodes and "0x13" in opcodes and "0x67" in opcodes:
        return "prompt-choice-object-position-gate"
    return "manual-control-other"


def branch_target_summary(exe: bytes, sections: list[dict[str, Any]], target_hex: str | None) -> dict[str, Any] | None:
    if not target_hex:
        return None
    target = int(target_hex, 16)
    stream = decode_stream(exe, sections, target, max_commands=6, max_bytes=0x60)
    commands = [
        {
            "vaHex": command.get("vaHex"),
            "opcodeHex": command.get("opcodeHex"),
            "opcodeName": command.get("opcodeName"),
            "summary": command.get("summary", ""),
            "targetVaHex": command.get("targetVaHex", ""),
        }
        for command in (stream.get("commands") or [])[:4]
    ]
    reset_then_jump = (
        len(commands) >= 2
        and commands[0].get("opcodeHex") == "0x6d"
        and "0x574544=0x00" in (commands[0].get("summary") or "")
        and commands[1].get("opcodeHex") == "0x03"
    )
    return {
        "targetVaHex": target_hex,
        "resetGroupThenJump": reset_then_jump,
        "commands": commands,
    }


def prompt_control_summary(
    exe: bytes,
    sections: list[dict[str, Any]],
    payload_hex: str | None,
    limit: int = 0x220,
) -> dict[str, Any] | None:
    if not payload_hex:
        return None
    payload = int(payload_hex, 16)
    offset = va_to_offset(sections, payload)
    if offset is None:
        return None
    raw = exe[offset: offset + limit]
    markers: list[dict[str, Any]] = []
    for index in range(0, max(0, len(raw) - 3)):
        if raw[index] != 0x40:
            continue
        subopcode = raw[index + 1]
        arg_a = raw[index + 2]
        arg_b = raw[index + 3]
        marker = {
            "offset": index,
            "offsetHex": f"+0x{index:04x}",
            "rawHex": raw[index:index + 4].hex(" "),
            "subopcodeHex": f"0x{subopcode:02x}",
            "argAHex": f"0x{arg_a:02x}",
            "argBHex": f"0x{arg_b:02x}",
        }
        if subopcode == 0x18:
            marker.update({
                "meaning": "choice cursor/result marker",
                "resultOffsetHex": f"+0x{arg_a:02x}",
                "optionCount": arg_b & 0x3F,
                "incrementExisting": bool(arg_b & 0x40),
                "allowCancelSentinel": bool(arg_b & 0x80),
            })
        markers.append(marker)
    choice_markers = [marker for marker in markers if marker.get("subopcodeHex") == "0x18"]
    return {
        "payloadVaHex": payload_hex,
        "markerCount": len(markers),
        "choiceMarkerCount": len(choice_markers),
        "choiceMarkers": choice_markers,
        "firstMarkers": markers[:12],
    }


def command_payload_hex(command: dict[str, Any]) -> str:
    payload_hex = command.get("payloadVaHex") or ""
    if payload_hex:
        return str(payload_hex)
    raw_hex = str(command.get("rawHex") or "")
    parts = raw_hex.split()
    if command.get("opcodeHex") == "0x2f" and len(parts) >= 8:
        payload = int.from_bytes(bytes(int(part, 16) for part in parts[4:8]), "little")
        return f"0x{payload:08x}"
    return ""


def build_summary(manual_review_path: Path, exe_path: Path) -> dict[str, Any]:
    exe = exe_path.read_bytes()
    sections = read_sections(exe)
    review = load_json(manual_review_path)
    route_like = [
        row
        for row in review.get("scripts", [])
        if row.get("classification") == "manual-route-like-prompt-script"
    ]
    rows: list[dict[str, Any]] = []
    opcode_counts: Counter[str] = Counter()
    pattern_counts: Counter[str] = Counter()
    target_counts: Counter[str] = Counter()
    field_counts: Counter[str] = Counter()
    prompt_choice_marker_count = 0
    prompt_wait_count = 0
    for row in route_like:
        commands = [command for command in row.get("commands", []) if command.get("opcodeHex") in CONTROL_OPCODES]
        for command in commands:
            opcode_counts[str(command.get("opcodeHex"))] += 1
            if command.get("opcodeHex") == "0x84":
                prompt_wait_count += 1
            if command.get("targetVaHex"):
                target_counts[str(command["targetVaHex"])] += 1
            if command.get("fieldOffset") is not None:
                field_counts[f"+0x{int(command['fieldOffset']):04x}"] += 1
        pattern = classify_pattern(commands)
        pattern_counts[pattern] += 1
        rows.append({
            "scriptVaHex": row.get("scriptVaHex"),
            "pattern": pattern,
            "textPreview": compact_preview(row.get("textPreview") or ""),
            "routeProofFound": bool(row.get("routeProofFound") or row.get("mapLoaderRefFound")),
            "controlCommands": [
                {
                    "vaHex": command.get("vaHex"),
                    "opcodeHex": command.get("opcodeHex"),
                    "opcodeName": command.get("opcodeName"),
                    "summary": command.get("summary", ""),
                    "rawHex": command.get("rawHex", ""),
                    "payloadVaHex": command_payload_hex(command),
                    "targetVaHex": command.get("targetVaHex", ""),
                    "branchTarget": branch_target_summary(exe, sections, command.get("targetVaHex"))
                    if command.get("opcodeHex") == "0x13"
                    else None,
                    "promptControls": prompt_control_summary(exe, sections, command_payload_hex(command))
                    if command.get("opcodeHex") == "0x2f"
                    else None,
                }
                for command in commands
            ],
        })
        for command in rows[-1]["controlCommands"]:
            if command.get("promptControls"):
                prompt_choice_marker_count += command["promptControls"].get("choiceMarkerCount", 0)

    branch_target_rows = [
        command["branchTarget"]
        for row in rows
        for command in row["controlCommands"]
        if command.get("branchTarget")
    ]
    reset_then_jump_count = sum(1 for item in branch_target_rows if item.get("resetGroupThenJump"))
    return {
        "title": "Manual Route Control Opcode Review",
        "summary": {
            "manualMovementAssumption": True,
            "sceneAutoTransitionClaim": False,
            "routeLikePromptScriptCount": len(route_like),
            "routeProofCount": sum(1 for row in rows if row["routeProofFound"]),
            "opcode13BranchTargetCount": len(branch_target_rows),
            "opcode13BranchTargetResetThenJumpCount": reset_then_jump_count,
            "promptCompletionWaitCount": prompt_wait_count,
            "promptChoiceMarkerCount": prompt_choice_marker_count,
            "handlerEvidence": {
                "objectVmDispatcher": "0x00402321",
                "conditionalBranchOpcode13": "0x0040353e",
                "compareHelper": "0x0040370a",
                "promptTextOpcode2f": "0x00405c79",
                "promptWaitOpcode84": "0x0040ae7e",
                "promptCallback": "0x0041b66d",
                "promptChoiceSubopcode18": "0x0041d61b",
            },
            "controlOpcodeCounts": dict(sorted(opcode_counts.items())),
            "patternCounts": dict(sorted(pattern_counts.items())),
            "targetOperandCounts": dict(sorted(target_counts.items())),
            "fieldOperandCounts": dict(sorted(field_counts.items())),
            "interpretation": [
                "0x2f mode 3 starts a prompt/text payload and advances by +0x08.",
                "0x84 is the prompt-completion wait gate that holds the object VM until the prompt callback clears.",
                "Prompt subopcode 0x18 records a choice cursor/result byte; route-like samples use marker 40 18 3a 02.",
                "0x13 appears after the prompt wait and branches by compare-helper result, commonly checking choice result +0x3a == 1.",
                "0x12 writes a dword field; in route-like samples it targets +0x00ec, the active object script pointer slot candidate.",
                "0x11 writes a field; in the door sample it targets +0x00e8, matching the active object x-position word candidate.",
                "0x24 advances by +4 and is retained as an action/evaluate command, not a map-loader proof.",
                "0x55/0x65/0x66/0x67 are manual position/object gates and are the strongest current manual route-like evidence.",
                "In all route-like samples, the 0x13 target decodes to group/slot reset and jump-back, not to a target map loader.",
            ],
        },
        "rows": rows,
        "nonClaims": [
            "No route-like script reaches the resource command VM map-loader opcode 0x10.",
            "No route-like script writes an explicit target map/root in the decoded stream.",
            "These are manual trigger scripts, not scene-driven automatic map transitions.",
        ],
    }


def html_doc(report: dict[str, Any]) -> str:
    s = report["summary"]
    table_rows = []
    for row in report["rows"]:
        commands = "<br>".join(
            f"<code>{html.escape(command['vaHex'] or '')}</code> "
            f"<code>{html.escape(command['opcodeHex'] or '')}</code> "
            f"{html.escape(command['summary'])}"
            + (
                "<br><small>"
                f"branch target {html.escape(command['branchTarget']['targetVaHex'])}: "
                f"reset+jump={command['branchTarget']['resetGroupThenJump']}"
                "</small>"
                if command.get("branchTarget")
                else ""
            )
            + (
                "<br><small>"
                + f"prompt {html.escape(command['promptControls']['payloadVaHex'])}: "
                + f"choice markers {command['promptControls']['choiceMarkerCount']}/"
                + f"{command['promptControls']['markerCount']}"
                + (
                    " · "
                    + ", ".join(
                        html.escape(marker.get("rawHex", ""))
                        for marker in command["promptControls"].get("choiceMarkers", [])[:3]
                    )
                    if command["promptControls"].get("choiceMarkers")
                    else ""
                )
                + "</small>"
                if command.get("promptControls")
                else ""
            )
            for command in row["controlCommands"]
        )
        table_rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['scriptVaHex'])}</code></td>"
            f"<td>{html.escape(row['pattern'])}</td>"
            f"<td>{html.escape(row['textPreview'])}</td>"
            f"<td>{commands}</td>"
            f"<td>{row['routeProofFound']}</td>"
            "</tr>"
        )
    return f"""<!doctype html>
<html lang=\"ko\">
<head>
  <meta charset=\"utf-8\">
  <title>{html.escape(report['title'])}</title>
  <style>
    body {{ font-family: system-ui, sans-serif; margin: 24px; color: #17202a; }}
    table {{ width: 100%; border-collapse: collapse; }}
    th, td {{ border: 1px solid #d7dde6; padding: 8px; vertical-align: top; }}
    th {{ background: #eef2f6; }}
    code {{ background: #f3f5f7; padding: 1px 3px; border-radius: 3px; }}
    .metrics {{ display: grid; grid-template-columns: repeat(auto-fit, minmax(190px, 1fr)); gap: 8px; }}
    .metric {{ background: #f6f8fb; border: 1px solid #d7dde6; padding: 10px; border-radius: 6px; }}
  </style>
</head>
<body>
  <h1>{html.escape(report['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\">route-like prompt scripts: <strong>{s['routeLikePromptScriptCount']}</strong></div>
    <div class=\"metric\">route proof count: <strong>{s['routeProofCount']}</strong></div>
    <div class=\"metric\">0x13 reset+jump targets: <strong>{s['opcode13BranchTargetResetThenJumpCount']}/{s['opcode13BranchTargetCount']}</strong></div>
    <div class=\"metric\">0x84 prompt waits: <strong>{s['promptCompletionWaitCount']}</strong></div>
    <div class=\"metric\">prompt choice markers: <strong>{s['promptChoiceMarkerCount']}</strong></div>
  </div>
  <h2>Interpretation</h2>
  <ul>{''.join(f'<li>{html.escape(item)}</li>' for item in s['interpretation'])}</ul>
  <h2>Handler Evidence</h2>
  <pre>{html.escape(json.dumps(s['handlerEvidence'], ensure_ascii=False, indent=2))}</pre>
  <h2>Non-Claims</h2>
  <ul>{''.join(f'<li>{html.escape(item)}</li>' for item in report['nonClaims'])}</ul>
  <table>
    <thead><tr><th>script</th><th>pattern</th><th>preview</th><th>control commands</th><th>route proof</th></tr></thead>
    <tbody>{''.join(table_rows)}</tbody>
  </table>
  <script>
    window.manualRouteControlOpcodeReview = {{
      manualMovementAssumption: true,
      sceneAutoTransitionClaim: false,
      routeLikePromptScriptCount: {s['routeLikePromptScriptCount']},
      routeProofCount: {s['routeProofCount']},
      opcode13BranchTargetResetThenJumpCount: {s['opcode13BranchTargetResetThenJumpCount']},
      promptCompletionWaitCount: {s['promptCompletionWaitCount']},
      promptChoiceMarkerCount: {s['promptChoiceMarkerCount']},
      handlerEvidence: {json.dumps(s['handlerEvidence'], ensure_ascii=False)},
      controlOpcodeCounts: {json.dumps(s['controlOpcodeCounts'], ensure_ascii=False)}
    }};
  </script>
</body>
</html>
"""


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


if __name__ == "__main__":
    main()
