#!/usr/bin/env python3
"""Review player support/status battle actions.

The canonical battle skill timeline covers visual attack actions.  The
remaining rows in the implementation-gap report are intentionally different:
run, defend, recovery, drink, taunt, eye-candy and Sukyeong are result/status
policies rather than missing attack animation timelines.
"""

from __future__ import annotations

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


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
WEB = ROOT / "web"
OUT_JSON = OUT / "battle_support_status_policy_review.json"
OUT_MD = OUT / "battle_support_status_policy_review.md"


TARGET_SCOPE_LABELS = {
    0x01: "자기 자신/setup",
    0x05: "아군 전체",
    0x06: "적 전체",
    0x09: "아군 1명",
    0x0A: "적 1명",
}


FAMILY_POLICY = {
    0x01: {
        "label": "도주 flow request",
        "policy": "전투 흐름",
        "effect": "+0x62 bit 0x04를 세워 도주 fan-out phase를 요청한다.",
        "apply": "0x0043485e",
    },
    0x02: {
        "label": "방어 command",
        "policy": "자세/명령",
        "effect": "result-family apply는 no-op이다. 방어 보정은 command/action layer에서 처리된다.",
        "apply": "0x004357d0",
    },
    0x03: {
        "label": "인법·몸감추기 command",
        "policy": "자세/명령",
        "effect": "result-family apply는 no-op이다. 회피/표시 상태는 별도 command/action layer로 분리된다.",
        "apply": "0x004357d0",
    },
    0x20: {
        "label": "HP 회복",
        "policy": "회복",
        "effect": "payload byte0 값을 HP 회복량으로 사용하고 max HP에서 clamp한다.",
        "apply": "0x0043487a -> 0x00435295",
    },
    0x21: {
        "label": "MP 회복",
        "policy": "회복",
        "effect": "payload byte0 값을 MP 회복량으로 사용하고 max MP에서 clamp한다.",
        "apply": "0x0043487a -> 0x00435356",
    },
    0x22: {
        "label": "독/마비 치료",
        "policy": "상태 해제",
        "effect": "독 또는 마비 상태면 +0x66의 prior mode를 복원하고 timer/lock flag를 해제한다.",
        "apply": "0x0043497d",
    },
    0x23: {
        "label": "HP 회복",
        "policy": "회복",
        "effect": "주 백약지장용 HP 회복 family. payload byte0 값을 HP 회복량으로 사용한다.",
        "apply": "0x0043487a -> 0x00435295",
    },
    0x24: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(8)+1을 2회 합산해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x25: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(32)+1을 1회 적용해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x26: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(8)+1을 4회 합산해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x27: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(12)+1을 4회 합산해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x28: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(32)+1을 2회 합산해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x29: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(20)+1을 4회 합산해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x2A: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(12)+1을 8회 합산해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x2C: {
        "label": "취기 누적",
        "policy": "아타호 술 상태",
        "effect": "random(20)+1을 8회 합산해 취기/주량 경험치에 더한다.",
        "apply": "0x00434a4a",
    },
    0x2D: {
        "label": "눈요기 progression",
        "policy": "스마슈 고유 상태",
        "effect": "흥분 -> 분노 -> 폭발직전 -> 대폭발 -> 푸쉬 상태를 진행한다.",
        "apply": "0x00434cbb",
    },
    0x2E: {
        "label": "도발 random mode",
        "policy": "린샹 고유 상태",
        "effect": "random(4)+0x10으로 16..19 모드를 고르고, 같은 상태면 피곤함으로 전환한다.",
        "apply": "0x00434d8d",
    },
    0x2F: {
        "label": "수경",
        "policy": "원소 방어",
        "effect": "+0x2f/+0x30/+0x31의 화염/수빙/풍뢰 guard bytes를 reset한다.",
        "apply": "0x00434e29",
    },
}


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


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


def as_int_family(value: Any) -> int | None:
    if isinstance(value, int):
        return value
    if isinstance(value, str):
        try:
            return int(value, 16) if value.startswith("0x") else int(value)
        except ValueError:
            return None
    return None


def row_key(row: dict[str, Any]) -> tuple[str, str]:
    return str(row.get("ownerKey")), str(row.get("skillIdHex"))


def state_detail_map(dispatch: dict[str, Any]) -> dict[int, dict[str, Any]]:
    out: dict[int, dict[str, Any]] = {}
    for row in dispatch.get("stateFamilyDetails") or []:
        fam = as_int_family(row.get("family"))
        if fam is not None:
            out[fam] = row
    return out


def dispatch_map(dispatch: dict[str, Any]) -> dict[int, dict[str, Any]]:
    out: dict[int, dict[str, Any]] = {}
    for row in dispatch.get("dispatchRows") or []:
        fam = as_int_family(row.get("index"))
        if fam is not None:
            out[fam] = row
    return out


def unit_rows(action: dict[str, Any]) -> list[dict[str, Any]]:
    units = action.get("unitsHex") or []
    scopes = action.get("targetScopes") or []
    families = action.get("families") or []
    statuses = action.get("statuses") or []
    out = []
    for index, unit_hex in enumerate(units):
        fam = as_int_family(families[index]) if index < len(families) else None
        scope = as_int_family(scopes[index]) if index < len(scopes) else None
        status = statuses[index] if index < len(statuses) else None
        unit_bytes = []
        if isinstance(unit_hex, str):
            try:
                unit_bytes = [int(part, 16) for part in unit_hex.split()]
            except ValueError:
                unit_bytes = []
        amount = unit_bytes[0] if unit_bytes else None
        out.append(
            {
                "unitIndex": index,
                "unitHex": unit_hex,
                "amountByte0": amount,
                "targetScope": scope,
                "targetScopeHex": f"0x{scope:02x}" if scope is not None else None,
                "targetScopeLabel": TARGET_SCOPE_LABELS.get(scope, "미분류") if scope is not None else None,
                "family": fam,
                "familyHex": f"0x{fam:02x}" if fam is not None else None,
                "statusEffect": status,
            }
        )
    return out


def build() -> dict[str, Any]:
    gaps = load_json(OUT / "battle_skill_implementation_gap_review.json")
    mapping = load_json(OUT / "battle_action_mapping.json")
    dispatch = load_json(OUT / "battle_result_family_dispatch_review.json")
    status = load_json(OUT / "battle_status_transition_review.json")
    flags = load_json(OUT / "battle_result_flag_lifecycle_review.json")

    player_by_key = {row_key(row): row for row in mapping.get("playerRows") or []}
    dispatch_by_family = dispatch_map(dispatch)
    state_by_family = state_detail_map(dispatch)

    rows: list[dict[str, Any]] = []
    for gap in gaps.get("rows") or []:
        if gap.get("gapClass") != "presentation-policy":
            continue
        key = row_key(gap)
        action = player_by_key.get(key, {})
        units = []
        for unit in unit_rows(action):
            fam = unit.get("family")
            policy = FAMILY_POLICY.get(fam, {})
            dispatch_row = dispatch_by_family.get(fam, {})
            state_row = state_by_family.get(fam, {})
            merged_effect = policy.get("effect") or state_row.get("effect") or dispatch_row.get("role")
            merged_detail = state_row.get("detail") or dispatch_row.get("note") or ""
            units.append(
                {
                    **unit,
                    "policy": policy.get("policy", "미분류"),
                    "label": policy.get("label") or state_row.get("label") or dispatch_row.get("role"),
                    "effect": merged_effect,
                    "detail": merged_detail,
                    "applyRoutine": policy.get("apply") or state_row.get("routineVaHex") or dispatch_row.get("functionVaHex"),
                    "dispatchRole": dispatch_row.get("role"),
                    "dispatchNote": dispatch_row.get("note"),
                }
            )
        policies = sorted({unit.get("policy") for unit in units if unit.get("policy")})
        rows.append(
            {
                "ownerKey": gap.get("ownerKey"),
                "ownerName": gap.get("ownerName"),
                "skillIdHex": gap.get("skillIdHex"),
                "skillName": gap.get("skillName"),
                "payloadVaHex": action.get("payloadVaHex"),
                "mpCost": action.get("mpCost"),
                "effectCount": action.get("effectCount"),
                "targetScopes": action.get("targetScopes") or [],
                "families": action.get("families") or [],
                "policyClasses": policies,
                "analysisStatus": "confirmed-support/status-policy",
                "runnerTreatment": "공격 타임라인 누락이 아니라 battle formula/status UI 경로에서 처리해야 한다.",
                "units": units,
            }
        )

    family_counts = Counter()
    policy_counts = Counter()
    owner_counts = Counter()
    for row in rows:
        owner_counts[row["ownerName"]] += 1
        for policy in row["policyClasses"]:
            policy_counts[policy] += 1
        for unit in row["units"]:
            fam = unit.get("familyHex")
            if fam:
                family_counts[fam] += 1

    report = {
        "version": 1,
        "kind": "hwanse-battle-support-status-policy-review",
        "source": [
            "out/battle_skill_implementation_gap_review.json",
            "out/battle_action_mapping.json",
            "out/battle_result_family_dispatch_review.json",
            "out/battle_status_transition_review.json",
            "out/battle_result_flag_lifecycle_review.json",
            "out/battle_recovery_item_effect_review.json",
        ],
        "status": "confirmed",
        "summary": {
            "rows": len(rows),
            "owners": dict(owner_counts),
            "policyClasses": dict(policy_counts),
            "families": dict(family_counts),
            "staticAnalysisGapRows": 0,
            "runnerAnimationGapRows": 0,
        },
        "interpretationNotes": [
            "이 25개 행은 공격 프레임 누락이 아니라 support/status action이다.",
            "도주/방어/몸감추기는 result-family apply가 no-op이거나 flow flag만 세우며, 실제 처리는 command/action layer에서 분리된다.",
            "회복 계열은 family 0x20/0x23=HP, 0x21=MP, 0x22=독/마비 해제 경로로 분류한다.",
            "술/도발/눈요기/수경은 actor status bytes(+0x2a, +0x65, +0x18, +0x1a, +0x2f..+0x31)를 갱신하는 정책 행이다.",
        ],
        "statusProducerGroups": status.get("statusProducerGroups") or [],
        "resultFlags": flags.get("flags") or [],
        "rows": rows,
    }
    return report


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


def markdown(report: dict[str, Any]) -> str:
    lines = [
        "# Battle Support/Status Policy Review",
        "",
        f"- status: `{report['status']}`",
        f"- rows: `{report['summary']['rows']}`",
        f"- static analysis gaps: `{report['summary']['staticAnalysisGapRows']}`",
        f"- runner animation gaps: `{report['summary']['runnerAnimationGapRows']}`",
        "",
        "## Policy Counts",
        "",
    ]
    for key, value in sorted(report["summary"]["policyClasses"].items()):
        lines.append(f"- {key}: {value}")
    lines += ["", "## Rows", ""]
    for row in report["rows"]:
        lines.append(f"### {row['ownerName']} {row['skillName']} `{row['skillIdHex']}`")
        lines.append("")
        lines.append(f"- payload: `{row.get('payloadVaHex')}`")
        lines.append(f"- MP: `{row.get('mpCost')}`")
        lines.append(f"- treatment: {row['runnerTreatment']}")
        for unit in row["units"]:
            lines.append(
                f"- unit#{unit['unitIndex']}: `{unit['unitHex']}` / "
                f"{unit.get('familyHex')} {unit.get('label')} / {unit.get('targetScopeLabel')} / "
                f"amount={unit.get('amountByte0')} / apply `{unit.get('applyRoutine')}`"
            )
            if unit.get("effect"):
                lines.append(f"  - effect: {unit['effect']}")
        lines.append("")
    return "\n".join(lines).rstrip() + "\n"


def html_doc(report: dict[str, Any]) -> str:
    policy_items = "".join(
        f"<li><strong>{esc(key)}</strong>: {value}</li>"
        for key, value in sorted(report["summary"]["policyClasses"].items())
    )
    rows_html = []
    for row in report["rows"]:
        unit_rows_html = []
        for unit in row["units"]:
            unit_rows_html.append(
                "<tr>"
                f"<td>{unit['unitIndex']}</td>"
                f"<td><code>{esc(unit.get('unitHex'))}</code></td>"
                f"<td><code>{esc(unit.get('familyHex'))}</code><br>{esc(unit.get('label'))}</td>"
                f"<td>{esc(unit.get('policy'))}</td>"
                f"<td>{esc(unit.get('targetScopeLabel'))}</td>"
                f"<td>{esc(unit.get('amountByte0'))}</td>"
                f"<td><code>{esc(unit.get('applyRoutine'))}</code></td>"
                f"<td>{esc(unit.get('effect'))}<br><small>{esc(unit.get('detail'))}</small></td>"
                "</tr>"
            )
        rows_html.append(
            "<section class='card'>"
            f"<h2>{esc(row['ownerName'])} · {esc(row['skillName'])} <code>{esc(row['skillIdHex'])}</code></h2>"
            f"<p>payload <code>{esc(row.get('payloadVaHex'))}</code> · MP {esc(row.get('mpCost'))} · {esc(row['runnerTreatment'])}</p>"
            "<table><thead><tr><th>#</th><th>unit</th><th>family</th><th>policy</th><th>target</th><th>amount</th><th>apply</th><th>effect</th></tr></thead>"
            f"<tbody>{''.join(unit_rows_html)}</tbody></table>"
            "</section>"
        )
    return f"""<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Battle Support/Status Policy Review</title>
<style>
body {{ margin: 0; font-family: system-ui, sans-serif; background: #f7f7f4; color: #202020; }}
main {{ max-width: 1180px; margin: 0 auto; padding: 24px; }}
h1 {{ margin: 0 0 8px; font-size: 26px; }}
h2 {{ font-size: 18px; margin: 0 0 8px; }}
.lede {{ color: #555; margin: 0 0 18px; }}
.card {{ background: #fff; border: 1px solid #ddd7c9; border-radius: 8px; padding: 14px; margin: 14px 0; box-shadow: 0 1px 2px rgba(0,0,0,.04); }}
table {{ width: 100%; border-collapse: collapse; font-size: 13px; }}
th, td {{ border-top: 1px solid #e7e1d3; padding: 7px 8px; vertical-align: top; text-align: left; }}
th {{ background: #f1ecdf; }}
code {{ background: #f2f2ee; padding: 1px 4px; border-radius: 4px; }}
small {{ color: #666; }}
ul {{ margin: 8px 0 0; }}
</style>
</head>
<body>
<main>
<h1>Battle Support/Status Policy Review</h1>
<p class="lede">남은 25개 전투 행은 공격 애니메이션 누락이 아니라 도주/방어/회복/술/도발/눈요기/수경 같은 support/status 정책으로 분류된다.</p>
<section class="card">
<h2>Summary</h2>
<ul>
<li>rows: <code>{report['summary']['rows']}</code></li>
<li>static analysis gaps: <code>{report['summary']['staticAnalysisGapRows']}</code></li>
<li>runner animation gaps: <code>{report['summary']['runnerAnimationGapRows']}</code></li>
{policy_items}
</ul>
</section>
{''.join(rows_html)}
</main>
</body>
</html>
"""


def main() -> None:
    report = build()
    write_json(report)
    OUT_MD.write_text(markdown(report), encoding="utf-8")
    html_text = html_doc(report)
    print(f"wrote {OUT_JSON.relative_to(ROOT)}")
    print(f"rows={report['summary']['rows']} policies={report['summary']['policyClasses']}")


if __name__ == "__main__":
    main()
