#!/usr/bin/env python3
"""Scan broad block write candidates that could cover opcode 0x24 runtime flag."""
from __future__ import annotations

import argparse
import html
import json
from pathlib import Path

import summarize_save_selector_opcode24_mode1_block_writes as block_scan


ROOT = Path(__file__).resolve().parents[1]
OUT = ROOT / "out"
RUNTIME_ENABLED_FLAG = 0x0059E34D
BLOCK_SCAN_START = 0x0059E300
BLOCK_SCAN_END = RUNTIME_ENABLED_FLAG
WINDOW_BYTES = block_scan.WINDOW_BYTES
FAILED_OPCODE24_RUNTIME_ENABLED_BLOCK_WRITE_GATE_IDS = [
    "runtime-enabled-flag-block-write",
    "covering-size-transfer-candidate",
    "runtime-watchpoint-trace",
]
OPCODE24_RUNTIME_ENABLED_BLOCK_WRITE_MISSING_EVIDENCE = [
    "broad block write candidate covering opcode 0x24 runtime-enabled flag 0x0059e34d",
    "nearby covering size and transfer instruction proving a static runtime-flag block producer",
    "runtime watchpoint trace proving the 0x0059e34d producer",
]
OPCODE24_RUNTIME_ENABLED_BLOCK_WRITE_EVIDENCE_REFS = [
    {"path": "Hwanse2.exe", "description": "dword-reference block-write scan over 0x0059e300..0x0059e34d"},
    {"path": "out/save_selector_opcode24_runtime_enabled_context.json", "description": "runtime-enabled flag proof gap"},
    {"path": "out/runtime_trace_feasibility.json", "description": "runtime watchpoint trace availability blocker"},
]


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


def scan_rows(exe: bytes) -> list[dict]:
    old_values = (
        block_scan.MODE1_SOURCE,
        block_scan.BLOCK_SCAN_START,
        block_scan.BLOCK_SCAN_END,
    )
    try:
        block_scan.MODE1_SOURCE = RUNTIME_ENABLED_FLAG
        block_scan.BLOCK_SCAN_START = BLOCK_SCAN_START
        block_scan.BLOCK_SCAN_END = BLOCK_SCAN_END
        rows = block_scan.build_rows(exe)
    finally:
        (
            block_scan.MODE1_SOURCE,
            block_scan.BLOCK_SCAN_START,
            block_scan.BLOCK_SCAN_END,
        ) = old_values
    for row in rows:
        row["requiredSizeToCoverRuntimeFlag"] = row.get("requiredSizeToCoverMode1")
        row["requiredSizeToCoverRuntimeFlagHex"] = row.get("requiredSizeToCoverMode1Hex")
    return rows


def build_summary(exe: bytes) -> dict:
    rows = scan_rows(exe)
    address_like = [row for row in rows if row["addressLikeCoveringBase"]]
    direct_covering = [row for row in rows if row["directCoveringWrite"]]
    block_candidates = [row for row in rows if row["blockWriteCandidate"]]
    if not direct_covering and not block_candidates:
        address_note = (
            f" It did find {len(address_like)} address-like covering base ref(s), but none has a nearby "
            "covering size immediate, so they are not block-write candidates."
            if address_like
            else " It found no address-like covering base candidate."
        )
        conclusion = (
            "No broad block write candidate was found for runtime flag 0x0059e34d. The scan covers dword "
            "immediates in 0x0059e300..0x0059e34d and looks for address-like uses with nearby covering size "
            "immediates and call/rep transfer instructions; it also checks direct wide stores that could cover "
            f"the flag.{address_note} This does not replace a runtime trace, but it removes the obvious static "
            "memset/memcpy-style runtime-flag producer shape from the current evidence."
        )
    else:
        conclusion = (
            "At least one broad block write candidate was found; inspect candidate rows before treating "
            "0x0059e34d as an unresolved runtime flag."
        )
    return {
        "scope": "dword references to 0x0059e300..0x0059e34d with nearby size/call/rep block-write scan",
        "runtimeEnabledFlagHex": hex32(RUNTIME_ENABLED_FLAG),
        "scanRangeHex": f"{hex32(BLOCK_SCAN_START)}..{hex32(BLOCK_SCAN_END)}",
        "windowBytes": WINDOW_BYTES,
        "rowCount": len(rows),
        "addressLikeCoveringBaseCount": len(address_like),
        "directCoveringWriteCount": len(direct_covering),
        "blockWriteCandidateCount": len(block_candidates),
        "addressLikeCoveringBases": address_like,
        "directCoveringWrites": direct_covering,
        "blockWriteCandidates": block_candidates,
        "rows": rows,
        "proofFound": False,
        "opcode24RuntimeEnabledBlockWriteProofFound": False,
        "failedOpcode24RuntimeEnabledBlockWriteGateIds": FAILED_OPCODE24_RUNTIME_ENABLED_BLOCK_WRITE_GATE_IDS,
        "missingEvidence": OPCODE24_RUNTIME_ENABLED_BLOCK_WRITE_MISSING_EVIDENCE,
        "evidenceRefs": OPCODE24_RUNTIME_ENABLED_BLOCK_WRITE_EVIDENCE_REFS,
        "evidenceRefCount": len(OPCODE24_RUNTIME_ENABLED_BLOCK_WRITE_EVIDENCE_REFS),
        "promotionStatus": "blocked",
        "conclusion": conclusion,
    }


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x24 Runtime Enabled Block Writes",
        "",
        f"Scope: {summary['scope']}.",
        "",
        f"- runtime enabled flag: `{summary['runtimeEnabledFlagHex']}`",
        f"- scan range: `{summary['scanRangeHex']}`",
        f"- scan window bytes: {summary['windowBytes']}",
        f"- rows: {summary['rowCount']}",
        f"- address-like covering bases: {summary['addressLikeCoveringBaseCount']}",
        f"- direct covering writes: {summary['directCoveringWriteCount']}",
        f"- block write candidates: {summary['blockWriteCandidateCount']}",
        f"- proofFound: `{summary['proofFound']}`",
        f"- opcode24RuntimeEnabledBlockWriteProofFound: `{summary['opcode24RuntimeEnabledBlockWriteProofFound']}`",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Failed Gates",
        "",
    ]
    lines.extend(f"- `{item}`" for item in summary["failedOpcode24RuntimeEnabledBlockWriteGateIds"])
    lines.extend([
        "",
        "## Missing Evidence",
        "",
    ])
    lines.extend(f"- {item}" for item in summary["missingEvidence"])
    lines.extend([
        "",
        "## Evidence Refs",
        "",
    ])
    lines.extend(
        f"- `{row['path']}`: {row['description']}"
        for row in summary["evidenceRefs"]
    )
    lines.extend([
        "",
        "## Candidate Rows",
        "",
        "| address | ref | kind | access | required size | sizes | transfers | block candidate | instruction |",
        "| --- | --- | --- | --- | ---: | --- | --- | --- | --- |",
    ])
    candidate_rows = [
        row for row in summary["rows"]
        if row["addressLikeCoveringBase"] or row["directCoveringWrite"] or row["blockWriteCandidate"]
    ]
    for row in candidate_rows:
        sizes = ", ".join(f"{item['valueHex']}@{item['vaHex']}" for item in row["coveringSizeCandidates"]) or "-"
        transfers = ", ".join(f"{item['kind']}@{item['vaHex']}" for item in row["transferCandidates"]) or "-"
        lines.append(
            f"| `{row['addressHex']}` | `{row['instructionVaHex']}` | {row['instructionKind']} | "
            f"{row['accessKind']} | `{row['requiredSizeToCoverRuntimeFlagHex']}` | {sizes} | {transfers} | "
            f"{row['blockWriteCandidate']} | `{row['instruction']}` |"
        )
    if not candidate_rows:
        lines.append("| - | - | - | - | - | - | - | - | - |")
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    candidate_rows = []
    for row in summary["rows"]:
        if not (row["addressLikeCoveringBase"] or row["directCoveringWrite"] or row["blockWriteCandidate"]):
            continue
        sizes = ", ".join(f"{item['valueHex']}@{item['vaHex']}" for item in row["coveringSizeCandidates"]) or "-"
        transfers = ", ".join(f"{item['kind']}@{item['vaHex']}" for item in row["transferCandidates"]) or "-"
        candidate_rows.append(
            "<tr>"
            f"<td><code>{html.escape(row['addressHex'])}</code></td>"
            f"<td><code>{html.escape(row['instructionVaHex'])}</code></td>"
            f"<td>{html.escape(row['instructionKind'])}</td>"
            f"<td>{html.escape(row['accessKind'])}</td>"
            f"<td><code>{html.escape(row['requiredSizeToCoverRuntimeFlagHex'])}</code></td>"
            f"<td>{html.escape(sizes)}</td>"
            f"<td>{html.escape(transfers)}</td>"
            f"<td>{row['blockWriteCandidate']}</td>"
            f"<td><code>{html.escape(row['instruction'])}</code></td>"
            "</tr>"
        )
    failed_gates = "".join(
        f"<li><code>{html.escape(item)}</code></li>"
        for item in summary["failedOpcode24RuntimeEnabledBlockWriteGateIds"]
    )
    missing_evidence = "".join(
        f"<li>{html.escape(item)}</li>"
        for item in summary["missingEvidence"]
    )
    evidence_refs = "".join(
        f"<li><code>{html.escape(row['path'])}</code>: {html.escape(row['description'])}</li>"
        for row in summary["evidenceRefs"]
    )
    return "\n".join([
        "<!doctype html><meta charset=\"utf-8\"><title>Save Selector Opcode 0x24 Runtime Enabled Block Writes</title>",
        "<style>body{font-family:system-ui,sans-serif;background:#111;color:#eee;max-width:1180px;margin:24px auto}table{border-collapse:collapse}td,th{border:1px solid #444;padding:6px 8px;vertical-align:top}code{color:#9bd4ff}</style>",
        "<h1>Save Selector Opcode 0x24 Runtime Enabled Block Writes</h1>",
        f"<p>Scope: {html.escape(summary['scope'])}.</p>",
        "<ul>",
        f"<li>runtime enabled flag: <code>{html.escape(summary['runtimeEnabledFlagHex'])}</code></li>",
        f"<li>scan range: <code>{html.escape(summary['scanRangeHex'])}</code></li>",
        f"<li>scan window bytes: {summary['windowBytes']}</li>",
        f"<li>rows: {summary['rowCount']}</li>",
        f"<li>address-like covering bases: {summary['addressLikeCoveringBaseCount']}</li>",
        f"<li>direct covering writes: {summary['directCoveringWriteCount']}</li>",
        f"<li>block write candidates: {summary['blockWriteCandidateCount']}</li>",
        f"<li>proofFound: <code>{html.escape(str(summary['proofFound']))}</code></li>",
        f"<li>promotion status: <code>{html.escape(summary['promotionStatus'])}</code></li>",
        "</ul>",
        f"<p>{html.escape(summary['conclusion'])}</p>",
        "<h2>Failed Gates</h2>",
        f"<ul>{failed_gates}</ul>",
        "<h2>Missing Evidence</h2>",
        f"<ul>{missing_evidence}</ul>",
        "<h2>Evidence Refs</h2>",
        f"<ul>{evidence_refs}</ul>",
        "<h2>Candidate Rows</h2>",
        "<table><thead><tr><th>address</th><th>ref</th><th>kind</th><th>access</th><th>required size</th><th>sizes</th><th>transfers</th><th>block candidate</th><th>instruction</th></tr></thead><tbody>",
        "\n".join(candidate_rows) or '<tr><td colspan="9">No block write candidates.</td></tr>',
        "</tbody></table>",
    ])


def write_outputs(summary: dict, out_dir: Path = OUT, html_out: Path | None = None) -> Path:
    out_dir.mkdir(parents=True, exist_ok=True)
    json_out = out_dir / "save_selector_opcode24_runtime_enabled_block_writes.json"
    json_out.write_text(
        json.dumps(summary, ensure_ascii=False, indent=2) + "\n",
        encoding="utf-8",
    )
    if html_out is not None:
        html_out.parent.mkdir(parents=True, exist_ok=True)
        html_out.write_text(html_page(summary), encoding="utf-8")
    return json_out


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path)
    args = parser.parse_args()
    summary = build_summary(args.exe.read_bytes())
    json_out = write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote opcode24 runtime enabled block writes -> {json_out}")


if __name__ == "__main__":
    main()
