#!/usr/bin/env python3
"""Build a focused review for per-hit result bits in actor +0x62.

The broader result-flag document already lists these bits, but the battle
runner needs a narrower contract: which bit is produced by hit/avoid, which
one selects alternate damage/sound, and which one suppresses HP apply.
"""
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


FLAG_ROWS = [
    {
        "bit": "0x08",
        "owner": "target actor",
        "role": "guard/glancing chip-damage hit result",
        "producer": "0x434218 hit/avoid branch sets target +0x62|=0x08 when avoidance wins, but only by a small gap.",
        "consumer": "0x43414d sets attacker +0x6e=random(3)+1, clears target +0x6c, skips normal/alternate damage formula.",
        "sound": "0x40ef1b includes target bits 0x08/0x20 in mask 0x28, suppressing the normal hit-sound branch.",
        "confidence": "정적 확정 / 수동 관찰 정합",
        "remaining": "표시/수치 경로 확정. target result script 0x004545b4가 WLK id 11/helper 0x16과 btl_efc F56..F59를 선택하고, +0x6e random(1..3)을 숫자로 표시한다. 수동 관찰상 MISS 라벨은 뜨지 않고, 몬스터는 피격 프레임이 아니라 normal frame에 머물며, 약한 '팅' 계열 효과음과 1자리수 데미지만 표시된다.",
    },
    {
        "bit": "0x10",
        "owner": "attacker actor",
        "role": "critical presentation / alternate damage result",
        "producer": "0x4343db stat/random branch compares attacker +0x28/+0x72 against target +0x26/+0x7e, then passes a second >150 random gate before setting attacker +0x62|=0x10.",
        "consumer": "0x43414d selects alternate damage formula 0x434737; 0x40ef1b uses alternate WLK operand and effect script, then clears 0x10.",
        "sound": "0x40ef1b reads stream operand +3 for the alternate sound unless stream type is 2, spawns helper script 0x402321, and clears the bit.",
        "confidence": "정적 역할 확정 / 수동 관찰 정합",
        "remaining": "critical은 MISS처럼 텍스트가 뜨지 않고 섬광·전용 사운드·높은 데미지로 인지된다는 수동 관찰과 정합한다. dedicated WLK/helper 경로는 정적으로 확정됐고, controlled runtime은 실제 WLK id 14 재생과 타이밍 고정용으로만 남았다.",
    },
    {
        "bit": "0x20",
        "owner": "target actor",
        "role": "full miss / HP-apply skip",
        "producer": "0x434218 hit/avoid branch sets target +0x62|=0x20 when target score beats attacker score by more than 3.",
        "consumer": "0x43414d skips formula; 0x4340ae returns before HP/recovery apply; 0x40ef1b mask 0x28 suppresses normal hit sound.",
        "sound": "Normal result sound is skipped by the 0x28 mask. battle_result_helper_label_review grounds helper 0x05 / selector 0x00100051 as the MISS display branch.",
        "confidence": "확정",
        "remaining": "HP apply skip and MISS display branch are grounded. Exact surrounding battle text timing is outside this narrow flag.",
    },
]


PIPELINE_ROWS = [
    {
        "step": "reset hit-unit flags",
        "va": 0x00433F0E,
        "operation": "attacker/target +0x62 &= 0xc3",
        "meaning": "한 타격 시작 시 0x08/0x10/0x20 결과 비트를 지우고 0x01/0x02/0x40/0x80 류 상태성 비트만 보존한다.",
    },
    {
        "step": "common physical-family entry",
        "va": 0x0043414D,
        "operation": "attacker +0x6e=0; call 0x434218",
        "meaning": "결과값을 0으로 초기화하고 hit/avoid 판정으로 들어간다.",
    },
    {
        "step": "hit/avoid result",
        "va": 0x00434218,
        "operation": "compare attacker hit score vs target avoid score",
        "meaning": "공격자가 이기면 정상 진행, 방어자가 크게 이기면 0x20, 작게 이기면 0x08을 target에 세운다.",
    },
    {
        "step": "0x20 branch",
        "va": 0x00434174,
        "operation": "if target +0x62&0x20, jump to copy result",
        "meaning": "데미지 공식과 상태 후보 처리를 건너뛴다. 결과값은 0으로 남는다.",
    },
    {
        "step": "0x08 branch",
        "va": 0x00434185,
        "operation": "attacker +0x6e=random(3)+1; target +0x6c=0",
        "meaning": "정상 공식 대신 1..3의 가드/스침성 chip damage 결과를 만든다.",
    },
    {
        "step": "alternate/critical flag gate",
        "va": 0x004343DB,
        "operation": "luck-side random comparison, then attacker +0x62|=0x10",
        "meaning": "정상 명중 후 alternate/critical result flag를 세울 수 있다.",
    },
    {
        "step": "formula select",
        "va": 0x004341C7,
        "operation": "if attacker +0x62&0x10 call 0x434737 else call 0x43461f",
        "meaning": "0x10은 데미지 공식 선택에도 사용된다.",
    },
    {
        "step": "low alternate downgrade",
        "va": 0x00434737,
        "operation": "if alternate result < 3, set random(5)+1 and attacker +0x62&=0xef",
        "meaning": "alternate branch가 항상 최종 critical 표시로 유지되지는 않는다.",
    },
    {
        "step": "HP/recovery apply",
        "va": 0x004340AE,
        "operation": "if target +0x62&0x20 return; else dispatch by +0x6d",
        "meaning": "0x20은 최종 HP 적용을 막는 확정 skip 비트다.",
    },
    {
        "step": "sound/effect consume",
        "va": 0x0040EF16,
        "operation": "if actor +0x62&0x10 use alternate WLK/effect and clear 0x10; else check target result-bit mask 0x28 before normal sound",
        "meaning": "0x10은 결과음/이펙트 레이어에서 소비되고, 0x08/0x20은 일반 타격음을 억제하는 쪽으로 연결된다.",
    },
]


RELATED_FIELDS = [
    ("actor+0x04", "actor kind/type. hit/avoid table lookup branches by 1/2."),
    ("actor+0x06", "base level-ish term used in hit/avoid score."),
    ("actor+0x22", "attacker hit/tech contribution."),
    ("actor+0x24", "target avoid/agility contribution."),
    ("actor+0x26", "target side alternate/critical resistance-ish term."),
    ("actor+0x28", "attacker-side chance coefficient used by status and alternate/critical gates; UI name unknown."),
    ("actor+0x62 bit 0x08", "per-hit guard/glancing chip-damage target result."),
    ("actor+0x62 bit 0x10", "per-hit alternate/critical-like attacker result."),
    ("actor+0x62 bit 0x20", "per-hit full miss / HP apply skip target result."),
    ("actor+0x6c", "target result/status side byte cleared by the 0x08 branch."),
    ("actor+0x6d", "result family: 0 damage, 1 no-op, 2 recovery."),
    ("actor+0x6e", "numeric battle result value copied from attacker to target."),
    ("actor+0x72/+0x74", "attacker coefficient terms for alternate/hit formulas."),
    ("actor+0x7c/+0x7e", "target coefficient terms for avoid/alternate formulas."),
]


EVIDENCE_SPECS = [
    {
        "label": "hit-unit reset preserves long-lived actor bits and clears 0x08/0x10/0x20",
        "range": (0x00433F0E, 0x00433F50),
        "needles": ["[eax+0x62]", "and    cl,0xc3", "WORD PTR [eax+0x6e],0x0"],
        "context": 1,
    },
    {
        "label": "common physical-family coordinator routes 0x20, 0x08, and 0x10",
        "range": (0x0043414D, 0x00434218),
        "needles": ["call   0x434218", "test   cl,0x20", "test   cl,0x8", "0x427730", "WORD PTR [ecx+0x6e]", "call   0x4343db", "test   cl,0x10", "call   0x434737", "call   0x43461f", "call   0x4344ab"],
        "context": 1,
    },
    {
        "label": "hit/avoid comparison produces target 0x20 or 0x08",
        "range": (0x00434218, 0x004343DB),
        "needles": ["[eax+0x6]", "[eax+0x22]", "[eax+0x74]", "[eax+0x24]", "[eax+0x7c]", "call   0x427730", "sub    eax,0x3", "or     cl,0x20", "or     cl,0x8"],
        "context": 1,
    },
    {
        "label": "alternate/critical flag gate sets attacker 0x10",
        "range": (0x004343DB, 0x004344AB),
        "needles": ["[eax+0x28]", "[eax+0x72]", "[eax+0x26]", "[eax+0x7e]", "call   0x427730", "cmp    eax,0x96", "or     cl,0x10"],
        "context": 1,
    },
    {
        "label": "alternate damage formula can clear 0x10 when result is too low",
        "range": (0x00434737, 0x0043485E),
        "needles": ["[eax+0x70]", "[eax+0x1c]", "[eax+0x7a]", "[eax+0x20]", "cmp    eax,0x3", "WORD PTR [ecx+0x6e]", "and    cl,0xef"],
        "context": 1,
    },
    {
        "label": "result dispatcher skips HP/recovery apply when target has 0x20",
        "range": (0x004340AE, 0x0043414D),
        "needles": ["test   cl,0x20", "call   0x4353f8", "call   0x434e84", "call   0x435295"],
        "context": 1,
    },
    {
        "label": "sound/effect layer consumes 0x10 and checks target 0x28 before normal hit sound",
        "range": (0x0040EF16, 0x0040F01E),
        "needles": ["test   cl,0x10", "[eax+0x3]", "call   0x42af73", "call   0x435b5b", "0x402321", "and    cl,0xef", "test   cl,0x28", "[eax+0x2]"],
        "context": 1,
    },
]


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 Per-Hit Result Flag Review",
        "subject": "actor +0x62 bits 0x08, 0x10, and 0x20",
        "status": "per-hit-result-flags-grounded-critical-presentation-supported",
        "summary": [
            "Bits 0x08/0x10/0x20 are per-hit result bits cleared at hit-unit reset by +0x62 &= 0xc3.",
            "0x20 is the strongest conclusion: target full miss / HP-apply skip. 0x4340ae returns before HP/recovery apply.",
            "0x08 is a guard/glancing chip-damage target result: the normal formula is skipped and attacker +0x6e becomes random(3)+1.",
            "0x10 belongs to the attacker and selects the critical/alternate damage path and alternate result sound/effect; the result layer clears it after consumption.",
            "The 0x10 consumer path is statically grounded: stream operand +3 selects alternate WLK, helper runner 0x402321 is spawned, child script table [0x442da1+0x150] is attached, and +0x10 is cleared.",
            "0x10 can be downgraded by the alternate formula when the computed result is below 3, so the bit is not simply a permanent critical label.",
            "0x08/0x20 display bindings are grounded. 0x08 is not a MISS label path; it displays low damage over the normal target frame with a weak hit sound. Critical is not expected to have a text label; it is a flash/sound/high-damage presentation path.",
        ],
        "flagRows": FLAG_ROWS,
        "pipelineRows": PIPELINE_ROWS,
        "relatedFields": RELATED_FIELDS,
        "evidence": evidence,
    }


def write_json(data: dict[str, Any]) -> None:
    (OUT / "battle_per_hit_result_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 Per-Hit Result Flag Review",
        "",
        "`actor +0x62 bit 0x08/0x10/0x20`은 한 타격 안에서 만들어지고 소비되는 결과 플래그다.",
        "",
        "## 결론",
        "",
    ]
    for item in data["summary"]:
        lines.append(f"- {item}")
    lines.extend(["", "## Flag Roles", ""])
    lines.append("| Bit | Owner | Role | Producer | Consumer | Sound | Confidence | Remaining |")
    lines.append("|---|---|---|---|---|---|---|---|")
    for row in data["flagRows"]:
        lines.append(
            f"| `{row['bit']}` | {row['owner']} | {row['role']} | {row['producer']} | {row['consumer']} | {row['sound']} | {row['confidence']} | {row['remaining']} |"
        )
    lines.extend(["", "## Pipeline", ""])
    lines.append("| Step | VA | Operation | Meaning |")
    lines.append("|---|---|---|---|")
    for row in data["pipelineRows"]:
        lines.append(f"| {row['step']} | `{hex32(row['va'])}` | `{row['operation']}` | {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_per_hit_result_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:
    flag_rows = [
        [
            f"<code>{esc(row['bit'])}</code>",
            esc(row["owner"]),
            esc(row["role"]),
            esc(row["producer"]),
            esc(row["consumer"]),
            esc(row["sound"]),
            f"<span class=\"tag good\">{esc(row['confidence'])}</span>",
            esc(row["remaining"]),
        ]
        for row in data["flagRows"]
    ]
    pipeline_rows = [
        [
            esc(row["step"]),
            f"<code>{hex32(row['va'])}</code>",
            f"<code>{esc(row['operation'])}</code>",
            esc(row["meaning"]),
        ]
        for row in data["pipelineRows"]
    ]
    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:1240px; 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_damage_formula_trace_review.html">damage formula</a><a href="battle_sound_role_review.html">sound role</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>Flag Roles</h2>{table(["bit", "owner", "role", "producer", "consumer", "sound", "confidence", "remaining"], flag_rows)}</section>
  <section class="card"><h2>Pipeline</h2>{table(["step", "VA", "operation", "meaning"], pipeline_rows)}</section>
  <section class="card"><h2>Related Fields</h2>{table(["field", "meaning"], related_rows)}</section>
  {''.join(evidence_blocks)}
</main>
</body>
</html>
"""
    (OUT / "battle_per_hit_result_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_per_hit_result_flag_review html/json/md")


if __name__ == "__main__":
    main()
