#!/usr/bin/env python3
"""Build a focused review for actor +0x62 bit 0x02 timed locks."""
from __future__ import annotations

import html
import json
import re
import subprocess
from pathlib import Path
from typing import Any


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


def esc(value: Any) -> str:
    return html.escape(str(value if value is not None else ""))


def hex32(value: int) -> str:
    return f"0x{value:08x}"


def disassemble(start_va: int, stop_va: int) -> str:
    try:
        result = subprocess.run(
            [
                "objdump",
                "-Mintel",
                "-D",
                "-b",
                "pei-i386",
                f"--start-address=0x{start_va:08x}",
                f"--stop-address=0x{stop_va:08x}",
                str(EXE),
            ],
            check=True,
            stdout=subprocess.PIPE,
            stderr=subprocess.PIPE,
            text=True,
        )
    except (OSError, subprocess.CalledProcessError) as exc:
        return f"; disassembly unavailable: {exc}"
    return "\n".join(line for line in result.stdout.splitlines() if re.match(r"\s*[0-9a-f]{6,8}:", line))


def compact_lines(disasm: str, needles: list[str], *, context: int = 0) -> list[str]:
    lines = disasm.splitlines()
    picked: list[tuple[int, str]] = []
    for index, line in enumerate(lines):
        if any(needle in line for needle in needles):
            for i in range(max(0, index - context), min(len(lines), index + context + 1)):
                picked.append((i, lines[i]))
    seen: set[int] = set()
    result: list[str] = []
    for index, line in picked:
        if index not in seen:
            seen.add(index)
            result.append(line)
    return result


PRODUCER_ROWS = [
    {
        "va": 0x00434D1A,
        "source": "Smash eye-candy terminal state",
        "status": "+0x2a=0x0f",
        "timer": "+0x64=random(2)+2",
        "flag": "+0x62|=0x02",
        "meaning": "스마슈 눈요기 고유 모드가 끝 상태에 닿으면 timed command lock을 건다.",
        "confidence": "확정: write path",
    },
    {
        "va": 0x00434DEF,
        "source": "Linshang provoke duplicate roll",
        "status": "+0x2a=0x14",
        "timer": "+0x64=random(2)+2",
        "flag": "+0x62|=0x02",
        "meaning": "도발이 현재 모드와 같은 값을 다시 뽑으면 피곤함 계열 timed lock으로 전환한다.",
        "confidence": "확정: write path",
    },
    {
        "va": 0x00434F87,
        "source": "status result 5",
        "status": "+0x2a=0x1a",
        "timer": "+0x64=random(3)+4",
        "flag": "+0x62|=0x02",
        "meaning": "전투 상태 result 5: 마비. 해독초/기공독치료에서 이 bit가 해제된다.",
        "confidence": "확정: status mapping + bit write",
    },
    {
        "va": 0x00434FC1,
        "source": "status result 6",
        "status": "+0x2a=0x15",
        "timer": "+0x64 += random(3)+4",
        "flag": "+0x62|=0x02",
        "meaning": "전투 상태 result 6: 졸림. 장기 command lock 계열이다.",
        "confidence": "확정: status mapping + bit write",
    },
    {
        "va": 0x00435263,
        "source": "Ataho drunk-mode failure check",
        "status": "+0x2a=0x0a",
        "timer": "+0x64=0",
        "flag": "+0x62|=0x02",
        "meaning": "0x0a는 timer 0인 중간 상태다. 같은 routine 후반에서 0x15 + timer로 넘어가는 경로가 있다.",
        "confidence": "고신뢰",
    },
]


CONSUMER_ROWS = [
    {
        "va": 0x0043512E,
        "consumer": "timed status recovery",
        "condition": "if +0x64>0: decrement; recover when timer reaches 0 or random gate passes",
        "effect": "restore +0x2a from +0x66 when present; clear +0x66/+0x64; +0x62&=0xfd",
        "meaning": "일반 timed lock 해제 경로. bit 0x02만 지운다.",
    },
    {
        "va": 0x004351DA,
        "consumer": "0x0a secondary transition",
        "condition": "if +0x2a==0x0a and random(2)!=0",
        "effect": "+0x2a=0x15; +0x64=random(3)+4; +0x65=0; +0x68=1",
        "meaning": "timer 0인 0x0a lock은 바로 clear되지 않고 0x15 timed lock으로 승격될 수 있다.",
    },
    {
        "va": 0x00434996,
        "consumer": "antidote/paralysis cure",
        "condition": "if +0x2a==0x1a",
        "effect": "+0x62|=0x01, restore +0x2a from +0x66, clear +0x66/+0x64, +0x62&=0xfd",
        "meaning": "마비 해제는 0x02를 지우고 0x01 one-turn latch를 세운다.",
    },
    {
        "va": 0x0040DB36,
        "consumer": "action-script current-order gate",
        "condition": "test +0x62, 0x83",
        "effect": "0x02가 세워진 actor는 normal action stream 대신 branch target으로 간다.",
        "meaning": "timed lock actor는 명령/action script 진행에서 제외된다.",
    },
]


RELATED_FIELDS = [
    ("actor+0x2a", "현재 자세/상태 id."),
    ("actor+0x62 bit 0x02", "timed command lock flag."),
    ("actor+0x64", "timed lock timer. 일반 회복 루틴은 이 값을 감소시키고 random gate를 통과하면 0x02를 지운다."),
    ("actor+0x65", "아타호 술취함 내부 누적치. 0x0a->0x15 전환에서 0으로 정리된다."),
    ("actor+0x66", "상태 진입 전 +0x2a 백업 슬롯."),
    ("actor+0x68", "표시/상태 갱신 dirty flag."),
    ("actor+0x69", "아타호 곤드레만드레 hazard check pending flag. 처리 뒤 0으로 정리된다."),
    ("actor+0x26", "timed recovery random gate에서 /10 기준값으로 쓰이는 actor stat word."),
]


EVIDENCE_SPECS = [
    {
        "label": "Smash eye-candy can produce timed lock 0x0f",
        "range": (0x00434CBB, 0x00434D8D),
        "needles": ["[eax+0x66]", "[eax+0x2a],0xf", "[ecx+0x64]", "or     cl,0x2", "[eax+0x68],0x1"],
        "context": 2,
    },
    {
        "label": "Linshang provoke duplicate roll can produce timed lock 0x14",
        "range": (0x00434D8D, 0x00434E29),
        "needles": ["[eax+0x2a],0x14", "[ecx+0x64]", "or     cl,0x2", "[eax+0x68],0x1"],
        "context": 2,
    },
    {
        "label": "battle status effect values 5/6 produce 0x02 locks",
        "range": (0x00434F77, 0x00435039),
        "needles": ["[eax+0x2a],0x1a", "[eax+0x2a],0x15", "or     cl,0x2", "[eax+0x64]"],
        "context": 2,
    },
    {
        "label": "timed recovery clears bit 0x02",
        "range": (0x0043512E, 0x004351DA),
        "needles": ["[eax+0x64]", "dec    BYTE PTR [eax+0x64]", "[eax+0x26]", "[eax+0x66]", "[ecx+0x2a]", "and    cl,0xfd"],
        "context": 2,
    },
    {
        "label": "0x0a can turn into 0x15 timed lock",
        "range": (0x004351DA, 0x00435290),
        "needles": ["cmp    ecx,0xa", "[eax+0x2a],0x15", "[ecx+0x64]", "[eax+0x65],0x0", "[eax+0x62]", "or     cl,0x2", "[eax+0x69],0x0"],
        "context": 2,
    },
    {
        "label": "action script gate includes bit 0x02 in mask 0x83",
        "range": (0x0040DB36, 0x0040DB8C),
        "needles": ["[eax+0x62]", "test   cl,0x83", "[eax+0x4]", "add    DWORD PTR [eax+0x40],0x8"],
        "context": 2,
    },
]


def build_data() -> dict[str, Any]:
    evidence = []
    for spec in EVIDENCE_SPECS:
        start_va, stop_va = spec["range"]
        disasm = disassemble(start_va, stop_va)
        evidence.append(
            {
                "label": spec["label"],
                "range": f"{hex32(start_va)}..{hex32(stop_va)}",
                "lines": compact_lines(disasm, spec["needles"], context=spec["context"]),
            }
        )
    return {
        "title": "Battle Timed Lock Actor Flag Review",
        "subject": "actor +0x62 bit 0x02",
        "summary": [
            "actor +0x62 bit 0x02 is a timed command/action lock flag.",
            "Most 0x02 producers pair it with +0x64 timer and +0x66 prior-status backup.",
            "The common recovery path at 0x43512e decrements +0x64 and clears only bit 0x02 with &=0xfd after timer/random recovery.",
            "The action-script gate at 0x40db36 tests mask 0x83, so 0x02 actors branch away from the normal action stream like 0x01 and 0x80 actors.",
            "A special 0x0a state can carry bit 0x02 with timer 0, then transition into +0x2a=0x15 with a real timer before normal recovery.",
        ],
        "producerRows": PRODUCER_ROWS,
        "consumerRows": CONSUMER_ROWS,
        "relatedFields": RELATED_FIELDS,
        "evidence": evidence,
    }


def write_json(data: dict[str, Any]) -> None:
    (OUT / "battle_timed_lock_flag_review.json").write_text(
        json.dumps(data, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )


def write_md(data: dict[str, Any]) -> None:
    lines = ["# Battle Timed Lock Actor Flag Review", "", "`actor +0x62 bit 0x02`는 timed command/action lock 플래그다.", "", "## 결론", ""]
    for item in data["summary"]:
        lines.append(f"- {item}")
    lines.extend(["", "## Producers", ""])
    lines.append("| VA | Source | Status | Timer | Flag | Meaning | Confidence |")
    lines.append("|---|---|---|---|---|---|---|")
    for row in data["producerRows"]:
        lines.append(
            f"| `{hex32(row['va'])}` | {row['source']} | `{row['status']}` | `{row['timer']}` | `{row['flag']}` | {row['meaning']} | {row['confidence']} |"
        )
    lines.extend(["", "## Consumers", ""])
    lines.append("| VA | Consumer | Condition | Effect | Meaning |")
    lines.append("|---|---|---|---|---|")
    for row in data["consumerRows"]:
        lines.append(
            f"| `{hex32(row['va'])}` | {row['consumer']} | `{row['condition']}` | `{row['effect']}` | {row['meaning']} |"
        )
    lines.extend(["", "## Related Fields", ""])
    for field, meaning in data["relatedFields"]:
        lines.append(f"- `{field}`: {meaning}")
    lines.extend(["", "## Evidence", ""])
    for ev in data["evidence"]:
        lines.append(f"### {ev['label']} ({ev['range']})")
        lines.append("")
        lines.append("```asm")
        lines.extend(ev["lines"] or ["; no compact evidence lines matched"])
        lines.append("```")
        lines.append("")
    (OUT / "battle_timed_lock_flag_review.md").write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")


def table(headers: list[str], rows: list[list[Any]]) -> str:
    chunks = ["<table><thead><tr>"]
    chunks.extend(f"<th>{esc(header)}</th>" for header in headers)
    chunks.append("</tr></thead><tbody>")
    for row in rows:
        chunks.append("<tr>")
        chunks.extend(f"<td>{cell}</td>" for cell in row)
        chunks.append("</tr>")
    chunks.append("</tbody></table>")
    return "".join(chunks)


def write_html(data: dict[str, Any]) -> None:
    producer_rows = [
        [
            f"<code>{hex32(row['va'])}</code>",
            esc(row["source"]),
            f"<code>{esc(row['status'])}</code>",
            f"<code>{esc(row['timer'])}</code>",
            f"<code>{esc(row['flag'])}</code>",
            esc(row["meaning"]),
            f"<span class=\"tag good\">{esc(row['confidence'])}</span>",
        ]
        for row in data["producerRows"]
    ]
    consumer_rows = [
        [
            f"<code>{hex32(row['va'])}</code>",
            esc(row["consumer"]),
            f"<code>{esc(row['condition'])}</code>",
            f"<code>{esc(row['effect'])}</code>",
            esc(row["meaning"]),
        ]
        for row in data["consumerRows"]
    ]
    related_rows = [[f"<code>{esc(field)}</code>", esc(meaning)] for field, meaning in data["relatedFields"]]
    evidence_blocks = []
    for ev in data["evidence"]:
        evidence_blocks.append(
            "<section class=\"card\"><h2>"
            + esc(ev["label"])
            + f" <small>{esc(ev['range'])}</small></h2><pre><code>"
            + esc("\n".join(ev["lines"] or ["; no compact evidence lines matched"]))
            + "</code></pre></section>"
        )

    html_text = f"""<!doctype html>
<html lang="ko">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>{esc(data['title'])}</title>
  <style>
    :root {{ color-scheme: light; --bg:#f5f7fb; --panel:#fff; --line:#d8dee9; --ink:#18202b; --muted:#5b6472; --good:#127a43; }}
    * {{ box-sizing:border-box; }}
    body {{ margin:0; font-family:Arial, "Malgun Gothic", sans-serif; background:var(--bg); color:var(--ink); }}
    main {{ max-width:1180px; margin:0 auto; padding:24px; }}
    h1 {{ margin:0 0 8px; font-size:26px; }}
    h2 {{ margin:0 0 12px; font-size:18px; }}
    small {{ color:var(--muted); font-weight:400; }}
    a {{ color:#1f5fbf; text-decoration:none; }}
    a:hover {{ text-decoration:underline; }}
    .nav {{ display:flex; gap:12px; flex-wrap:wrap; margin:0 0 18px; }}
    .card {{ background:var(--panel); border:1px solid var(--line); border-radius:8px; padding:16px; margin:14px 0; box-shadow:0 1px 2px rgba(20,30,50,.05); }}
    .summary {{ margin:8px 0 0; padding-left:20px; }}
    .summary li {{ margin:6px 0; }}
    table {{ width:100%; border-collapse:collapse; font-size:13px; }}
    th, td {{ border:1px solid var(--line); padding:8px; vertical-align:top; }}
    th {{ background:#eef2f8; text-align:left; }}
    code, pre {{ font-family:Consolas, "Liberation Mono", monospace; }}
    code {{ background:#f0f3f8; padding:1px 4px; border-radius:4px; }}
    pre {{ overflow:auto; padding:12px; background:#111827; color:#d9e3f0; border-radius:6px; font-size:12px; line-height:1.45; }}
    pre code {{ background:transparent; padding:0; }}
    .tag {{ display:inline-block; border:1px solid #bfe3cc; border-radius:999px; padding:2px 7px; background:#eefaf2; color:var(--good); white-space:nowrap; }}
  </style>
</head>
<body>
<main>
  <h1>{esc(data['title'])}</h1>
  <div class="nav"><a href="../web/index.html">index</a><a href="battle_result_flag_lifecycle_review.html">result flag lifecycle</a><a href="battle_one_turn_flag_review.html">one-turn flag</a><a href="battle_status_transition_review.html">status transition</a></div>
  <section class="card">
    <h2>결론</h2>
    <ul class="summary">{''.join(f'<li>{esc(item)}</li>' for item in data['summary'])}</ul>
  </section>
  <section class="card"><h2>Producers</h2>{table(["VA", "source", "status", "timer", "flag", "meaning", "confidence"], producer_rows)}</section>
  <section class="card"><h2>Consumers</h2>{table(["VA", "consumer", "condition", "effect", "meaning"], consumer_rows)}</section>
  <section class="card"><h2>Related Fields</h2>{table(["field", "meaning"], related_rows)}</section>
  {''.join(evidence_blocks)}
</main>
</body>
</html>
"""
    (OUT / "battle_timed_lock_flag_review.html").write_text(html_text, encoding="utf-8")


def main() -> None:
    OUT.mkdir(exist_ok=True)
    data = build_data()
    write_json(data)
    write_md(data)
    write_html(data)
    print("wrote battle_timed_lock_flag_review html/json/md")


if __name__ == "__main__":
    main()
