#!/usr/bin/env python3
"""Review indirect producers for the status/comment selector byte +0.

Direct absolute writes to 0x004576d8+0 are absent.  The remaining plausible
writer class is an object/text VM command that writes to object+0xa8[offset].
This report scans known object-VM dynamic selector writer opcodes and keeps the
status/menu +0 consumers next to the negative producer evidence.
"""
from __future__ import annotations

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

from probe_exe_scene_tables import offset_to_va, read_sections, va_to_offset


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

STATUS_MENU_MIN = 0x004E8000
STATUS_MENU_MAX = 0x004EC000
STATUS_COMMENT_PAYLOAD_MIN = 0x004E842E
STATUS_COMMENT_PAYLOAD_MAX = 0x004E86A0


def h(value: Any) -> str:
    return html.escape("" if value is None else str(value), quote=True)


def hx(value: int | None, width: int = 8) -> str:
    if value is None:
        return ""
    return f"0x{value:0{width}x}"


def load_json(name: str) -> dict[str, Any]:
    try:
        data = json.loads((OUT / name).read_text(encoding="utf-8"))
    except FileNotFoundError:
        return {}
    return data if isinstance(data, dict) else {}


def section_by_name(sections: list[dict[str, Any]], name: str) -> dict[str, Any]:
    for section in sections:
        if section.get("name") == name:
            return section
    raise RuntimeError(f"missing section {name}")


def root_contexts() -> list[dict[str, Any]]:
    data = load_json("selector_root_structure_review.json")
    roots = data.get("roots")
    rows = []
    if isinstance(roots, list):
        for root in roots:
            if isinstance(root.get("rootVa"), int) and isinstance(root.get("rangeEndVa"), int):
                rows.append(root)
    rows.sort(key=lambda row: int(row["rootVa"]))
    return rows


def context_for_va(roots: list[dict[str, Any]], va: int) -> dict[str, Any] | None:
    for root in roots:
        if int(root["rootVa"]) <= va < int(root["rangeEndVa"]):
            return root
    return None


def classify_opcode(raw: bytes) -> dict[str, Any] | None:
    op = raw[0]
    if op == 0x8C and raw[3] == 0:
        return {
            "opcodeHex": "0x8c",
            "role": "selector-next-enabled-producer",
            "targetOffset": raw[2],
            "selectorOffset": None,
            "sourceStateByte": raw[1],
            "handlerVaHex": "0x0040b55f",
            "meaning": "availability array 0x0059e360/0x0059e370에서 다음 enabled row를 찾아 object+0xa8[target]에 기록",
        }
    if op == 0x8D and raw[3] == 0:
        return {
            "opcodeHex": "0x8d",
            "role": "descriptor-match-selector-producer",
            "targetOffset": raw[2],
            "selectorOffset": None,
            "sourceStateByte": raw[1],
            "handlerVaHex": "0x0040b696",
            "meaning": "active descriptor row +0x48/+0x4a 계열 비교 결과를 object+0xa8[target]에 기록",
        }
    if op == 0x8F and raw[3] == 0:
        return {
            "opcodeHex": "0x8f",
            "role": "global-state-derived-selector-producer",
            "targetOffset": raw[1],
            "selectorOffset": raw[2],
            "sourceStateByte": raw[2],
            "handlerVaHex": "0x0040b84a",
            "meaning": "object+0xa8[selector] 값을 switch로 삼아 0x00457744..49 상태를 읽고 object+0xa8[target]에 기록",
        }
    return None


def scan_dynamic_writers(exe: bytes, sections: list[dict[str, Any]]) -> list[dict[str, Any]]:
    data = section_by_name(sections, ".data")
    start = int(data["raw"])
    end = start + int(data["raw_size"])
    roots = root_contexts()
    rows: list[dict[str, Any]] = []
    for pos in range(start, end - 8):
        if exe[pos] not in {0x8C, 0x8D, 0x8F}:
            continue
        raw = exe[pos : pos + 8]
        decoded = classify_opcode(raw)
        if not decoded:
            continue
        va = offset_to_va(sections, pos)
        if va is None:
            continue
        root = context_for_va(roots, va)
        in_status_menu = STATUS_MENU_MIN <= va < STATUS_MENU_MAX
        in_status_comment = STATUS_COMMENT_PAYLOAD_MIN <= va < STATUS_COMMENT_PAYLOAD_MAX
        rows.append(
            {
                "commandVaHex": hx(va),
                "rawHex": raw.hex(" "),
                **decoded,
                "targetOffsetHex": hx(decoded["targetOffset"], 2),
                "selectorOffsetHex": hx(decoded["selectorOffset"], 2) if decoded["selectorOffset"] is not None else "",
                "targetsStatusCommentSelectorOffset0": decoded["targetOffset"] == 0,
                "inStatusMenuPayloadRange": in_status_menu,
                "inStatusCommentPayloadRange": in_status_comment,
                "rootSelectorKeys": root.get("selectorKeys", []) if root else [],
                "rootVaHex": root.get("rootVaHex") if root else "",
                "rootRangeEndVaHex": root.get("rangeEndVaHex") if root else "",
                "rootClass": root.get("rootClass") if root else "",
                "fieldMaps": root.get("fieldMaps", []) if root else [],
                "linkedCns": root.get("linkedCns", []) if root else [],
            }
        )
    return rows


def source_consumers() -> list[dict[str, Any]]:
    data = load_json("status_menu_vm_table_review.json")
    rows = []
    for table_name in ("opcode13Tables", "opcode14Tables"):
        for row in data.get(table_name, []) if isinstance(data.get(table_name), list) else []:
            if row.get("sourceModeHex") == "0x03" and row.get("sourceOffsetHex") == "0x00":
                rows.append(
                    {
                        "table": table_name,
                        "recordVaHex": row.get("recordVaHex"),
                        "classification": row.get("classification"),
                        "sourceExpression": row.get("sourceExpression"),
                        "itemCount": row.get("itemCount"),
                        "entryTableVaHex": row.get("entryTableVaHex"),
                        "recordRawHex": row.get("recordRawHex"),
                    }
                )
    return rows


def build() -> dict[str, Any]:
    exe = EXE.read_bytes()
    sections = read_sections(exe)
    rows = scan_dynamic_writers(exe, sections)
    target0 = [row for row in rows if row["targetsStatusCommentSelectorOffset0"]]
    target0_status_menu = [row for row in target0 if row["inStatusMenuPayloadRange"]]
    target0_status_comment = [row for row in target0 if row["inStatusCommentPayloadRange"]]
    target_counts = Counter(row["targetOffsetHex"] for row in rows)
    target0_opcode_counts = Counter(row["opcodeHex"] for row in target0)
    source_rows = source_consumers()
    summary = {
        "dynamicWriterCandidateCount": len(rows),
        "target0CandidateCount": len(target0),
        "target0InStatusMenuPayloadCount": len(target0_status_menu),
        "target0InStatusCommentPayloadCount": len(target0_status_comment),
        "sourceOffset0ConsumerCount": len(source_rows),
        "target0OpcodeCounts": dict(target0_opcode_counts),
        "topTargetOffsets": dict(target_counts.most_common(12)),
        "statusCommentSelectorIndirectWriterProven": bool(target0_status_comment),
        "statusMenuTarget0WriterProven": bool(target0_status_menu),
        "decision": (
            "Object-VM dynamic selector writers 0x8c/0x8d/0x8f do have target offset +0 candidates elsewhere, "
            "but none are inside the status/menu payload range and none are inside the status-comment payload. "
            "The status/comment +0 producer therefore remains unresolved; the known +0 records are consumers, not writers."
        ),
    }
    return {
        "version": 1,
        "kind": "hwanse-status-comment-indirect-selector-writer-review",
        "source": "tools/build_status_comment_indirect_selector_writer_review.py",
        "summary": summary,
        "sourceOffset0Consumers": source_rows,
        "target0WriterCandidates": target0,
        "target0StatusMenuCandidates": target0_status_menu,
        "target0StatusCommentCandidates": target0_status_comment,
        "dynamicWriterRows": rows,
    }


def render_html(data: dict[str, Any]) -> str:
    s = data["summary"]
    cards = "".join(
        f"<div class='card'><b>{h(k)}</b><span>{h(v)}</span></div>"
        for k, v in s.items()
        if k != "decision"
    )
    consumer_rows = "".join(
        "<tr>"
        f"<td>{h(row['table'])}</td>"
        f"<td><code>{h(row['recordVaHex'])}</code></td>"
        f"<td>{h(row['classification'])}</td>"
        f"<td>{h(row['sourceExpression'])}</td>"
        f"<td>{h(row['itemCount'])}</td>"
        f"<td><code>{h(row['entryTableVaHex'])}</code></td>"
        "</tr>"
        for row in data["sourceOffset0Consumers"]
    )
    target_rows = "".join(
        "<tr>"
        f"<td><code>{h(row['commandVaHex'])}</code></td>"
        f"<td><code>{h(row['rawHex'])}</code></td>"
        f"<td>{h(row['opcodeHex'])}</td>"
        f"<td>{h(row['role'])}</td>"
        f"<td>{h(row['targetOffsetHex'])}</td>"
        f"<td>{h(row['inStatusMenuPayloadRange'])}</td>"
        f"<td>{h(row['inStatusCommentPayloadRange'])}</td>"
        f"<td>{h(', '.join(row.get('rootSelectorKeys') or []))}</td>"
        f"<td>{h(', '.join(row.get('fieldMaps') or []))}</td>"
        "</tr>"
        for row in data["target0WriterCandidates"]
    )
    return f"""<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Status Comment Indirect Selector Writer Review</title>
<style>
  :root {{ --bg:#f6f7f9; --panel:#fff; --line:#d8dee8; --text:#20242b; --muted:#687080; }}
  * {{ box-sizing:border-box; }}
  body {{ margin:0; background:var(--bg); color:var(--text); font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; }}
  main {{ width:min(1180px, calc(100vw - 28px)); margin:0 auto; padding:24px 0 40px; }}
  h1 {{ margin:0 0 6px; font-size:28px; }}
  h2 {{ margin:22px 0 8px; font-size:18px; }}
  p {{ margin:0; line-height:1.45; color:var(--muted); }}
  .decision {{ margin:14px 0; padding:12px; border:1px solid #efc3bb; background:#fff5f3; border-radius:8px; color:var(--text); }}
  .cards {{ display:grid; grid-template-columns:repeat(auto-fit,minmax(180px,1fr)); gap:10px; margin:16px 0; }}
  .card,section {{ background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:12px; }}
  .card b {{ display:block; color:var(--muted); font-size:12px; }}
  .card span {{ display:block; margin-top:4px; font-weight:700; overflow-wrap:anywhere; }}
  table {{ width:100%; border-collapse:collapse; font-size:13px; }}
  th,td {{ border-top:1px solid var(--line); padding:7px; text-align:left; vertical-align:top; }}
  th {{ color:var(--muted); font-size:12px; }}
  code {{ font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; }}
  @media (max-width:760px) {{ main {{ width:min(100vw - 18px,1180px); padding-top:16px; }} table {{ display:block; overflow-x:auto; white-space:nowrap; }} }}
</style>
<main>
  <h1>Status Comment Indirect Selector Writer Review</h1>
  <p>object+0xa8[+0]을 읽는 status/menu selector record와, object VM dynamic writer opcode 0x8c/0x8d/0x8f의 +0 write 후보를 대조합니다.</p>
  <div class="decision"><b>결론:</b> {h(s['decision'])}</div>
  <div class="cards">{cards}</div>
  <section>
    <h2>Source Offset +0 Consumers</h2>
    <table><thead><tr><th>table</th><th>record</th><th>class</th><th>source</th><th>count</th><th>entry table</th></tr></thead><tbody>{consumer_rows}</tbody></table>
  </section>
  <section>
    <h2>Dynamic Writer Target +0 Candidates</h2>
    <table><thead><tr><th>command</th><th>raw</th><th>op</th><th>role</th><th>target</th><th>status/menu</th><th>status/comment</th><th>root selectors</th><th>field maps</th></tr></thead><tbody>{target_rows}</tbody></table>
  </section>
</main>
"""


def main() -> None:
    OUT.mkdir(exist_ok=True)
    WEB.mkdir(exist_ok=True)
    data = build()
    (OUT / "status_comment_indirect_selector_writer_review.json").write_text(
        json.dumps(data, ensure_ascii=False, separators=(",", ":")),
        encoding="utf-8",
    )
    (WEB / "status_comment_indirect_selector_writer_review.html").write_text(
        render_html(data),
        encoding="utf-8",
    )
    s = data["summary"]
    print(
        "wrote status comment indirect selector writer review "
        f"(target0={s['target0CandidateCount']}, statusMenu={s['target0InStatusMenuPayloadCount']})"
    )


if __name__ == "__main__":
    main()
