#!/usr/bin/env python3
"""Explain unreadable nearest producers before source/predecessor opcode 0x08 activators."""
from __future__ import annotations

import argparse
import html
import json
import sys
from pathlib import Path
from typing import Any

sys.path.insert(0, str(Path(__file__).resolve().parent))

from probe_exe_scene_tables import read_sections
from summarize_script_handler_table import section_name_for_va


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

SOURCE = "map1_01a"
TARGET = "map2_02d"
SOURCE_SELECTOR = "0:0"
PREDECESSOR_SELECTOR = "1:0"
CURRENT_SELECTOR = "2:0"


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


def parse_hex(value: str | None) -> int | None:
    if not value:
        return None
    return int(value, 16)


def hex_section(sections: list[dict], value_hex: str | None) -> str | None:
    value = parse_hex(value_hex)
    if value is None:
        return None
    return section_name_for_va(sections, value)


def row_index(selected_pointer_opcode_paths: dict) -> dict[tuple[str, str], dict]:
    rows = {}
    for scan in selected_pointer_opcode_paths.get("contextScans") or []:
        selector = scan.get("selector")
        for row in scan.get("rows") or []:
            if selector and row.get("vaHex"):
                rows[(selector, row["vaHex"])] = row
    return rows


def explain_producer(sections: list[dict], selector: str, producer_row: dict) -> dict:
    opcode = producer_row.get("opcodeHex")
    value_hex = producer_row.get("valueHex")
    if opcode == "0x07":
        selected_slot = producer_row.get("selectedSlotHex")
        selected_value = producer_row.get("selectedValueHex")
        selected_slot_section = hex_section(sections, selected_slot)
        return {
            "selector": selector,
            "producerVaHex": producer_row.get("vaHex"),
            "producerOpcodeHex": opcode,
            "producerValueHex": value_hex,
            "producerValueSection": hex_section(sections, value_hex),
            "index": producer_row.get("index"),
            "operandTableHex": producer_row.get("operandTableHex"),
            "operandTableSection": hex_section(sections, producer_row.get("operandTableHex")),
            "selectedSlotHex": selected_slot,
            "selectedSlotSection": selected_slot_section,
            "selectedValueHex": selected_value,
            "selectedValueSection": hex_section(sections, selected_value),
            "selectedValueClass": producer_row.get("selectedValueClass") or {},
            "unreadableReason": (
                "opcode 0x07 selected slot is outside mapped executable sections"
                if selected_slot_section is None else
                "opcode 0x07 selected value is unreadable in the static executable image"
            ),
            "staticCurrentPointerEvidence": False,
        }
    if opcode == "0x09":
        return {
            "selector": selector,
            "producerVaHex": producer_row.get("vaHex"),
            "producerOpcodeHex": opcode,
            "producerValueHex": value_hex,
            "producerValueSection": hex_section(sections, value_hex),
            "mode": producer_row.get("mode"),
            "modeHex": producer_row.get("modeHex"),
            "storedPointerSource": producer_row.get("storedPointerSource"),
            "storedPointerHex": producer_row.get("storedPointerHex"),
            "storedPointerClass": producer_row.get("storedPointerClass") or {},
            "unreadableReason": "opcode 0x09 row uses an unsupported mode, so no static stored pointer is decoded",
            "staticCurrentPointerEvidence": False,
        }
    return {
        "selector": selector,
        "producerVaHex": producer_row.get("vaHex"),
        "producerOpcodeHex": opcode,
        "producerValueHex": value_hex,
        "producerValueSection": hex_section(sections, value_hex),
        "unreadableReason": "unsupported producer opcode",
        "staticCurrentPointerEvidence": False,
    }


def build_summary(
    exe: bytes,
    activation_windows: dict | None = None,
    selected_pointer_opcode_paths: dict | None = None,
) -> dict:
    activation_windows = activation_windows or load_json(OUT / "save_selector_opcode08_activation_windows.json", {})
    selected_pointer_opcode_paths = selected_pointer_opcode_paths or load_json(
        OUT / "save_selector_selected_pointer_opcode_paths.json",
        {},
    )
    sections = read_sections(exe)
    indexed_rows = row_index(selected_pointer_opcode_paths)
    activations = []
    producer_groups: dict[tuple[str, str], dict] = {}
    for context in activation_windows.get("contextWindows") or []:
        selector = context.get("selector")
        if selector not in {SOURCE_SELECTOR, PREDECESSOR_SELECTOR}:
            continue
        for activation in context.get("activations") or []:
            if activation.get("nearestProducerBucket") != "unreadable":
                continue
            producer_va = activation.get("nearestProducerVaHex")
            producer_row = indexed_rows.get((selector, producer_va), {})
            explanation = explain_producer(sections, selector, producer_row)
            row = {
                "selector": selector,
                "activationVaHex": activation.get("activationVaHex"),
                "activationValueHex": activation.get("activationValueHex"),
                "distanceBytes": activation.get("distanceBytes"),
                "producer": explanation,
            }
            activations.append(row)
            key = (selector, producer_va or "")
            group = producer_groups.setdefault(key, {
                **explanation,
                "activationCount": 0,
                "firstActivationVaHex": activation.get("activationVaHex"),
                "lastActivationVaHex": activation.get("activationVaHex"),
            })
            group["activationCount"] += 1
            group["lastActivationVaHex"] = activation.get("activationVaHex")
    producer_rows = sorted(
        producer_groups.values(),
        key=lambda row: (row.get("selector") or "", parse_hex(row.get("producerVaHex")) or 0),
    )
    opcode07_activations = [
        row for row in activations
        if (row.get("producer") or {}).get("producerOpcodeHex") == "0x07"
    ]
    opcode09_activations = [
        row for row in activations
        if (row.get("producer") or {}).get("producerOpcodeHex") == "0x09"
    ]
    opcode07_unmapped_slot_activations = [
        row for row in opcode07_activations
        if (row.get("producer") or {}).get("selectedSlotSection") is None
    ]
    opcode09_unsupported_activations = [
        row for row in opcode09_activations
        if (row.get("producer") or {}).get("storedPointerSource") == "unsupported-mode"
    ]
    current_static_hits = [
        row for row in activations
        if (row.get("producer") or {}).get("staticCurrentPointerEvidence")
    ]
    conclusion = (
        "The unreadable source/predecessor opcode 0x08 producer windows do not hide a static current-selector "
        "producer. Opcode 0x07 cases point their selected slots at unmapped scalar/table-like addresses, and "
        "opcode 0x09 cases use unsupported modes instead of a decoded next-stream/current pointer store. This "
        "keeps opcode 0x08 promotion blocked until a runtime selected-pointer trace or captured selector 2:0 "
        "save appears."
    )
    return {
        "source": SOURCE,
        "target": TARGET,
        "sourceSelector": SOURCE_SELECTOR,
        "predecessorSelector": PREDECESSOR_SELECTOR,
        "currentSelector": CURRENT_SELECTOR,
        "sourceOrPredecessorUnreadableActivationCount": len(activations),
        "uniqueUnreadableProducerCount": len(producer_rows),
        "opcode07UnreadableActivationCount": len(opcode07_activations),
        "opcode09UnreadableActivationCount": len(opcode09_activations),
        "opcode07UnmappedSelectedSlotActivationCount": len(opcode07_unmapped_slot_activations),
        "opcode09UnsupportedModeActivationCount": len(opcode09_unsupported_activations),
        "staticCurrentPointerEvidenceCount": len(current_static_hits),
        "opcode08UnreadableProducerPromotesRoute": False,
        "promotionStatus": "blocked",
        "producerRows": producer_rows,
        "activations": activations,
        "remainingProofs": [
            "capture opcode 0x08 selected-pointer runtime state",
            "capture a real selector 2:0 gameplay save",
            "find a strict map1_01a source coordinate or hotspot",
        ],
        "conclusion": conclusion,
    }


def producer_brief(row: dict) -> str:
    opcode = row.get("producerOpcodeHex")
    if opcode == "0x07":
        return (
            f"op7 slot={row.get('selectedSlotHex')} "
            f"slotSection={row.get('selectedSlotSection') or '-'}"
        )
    if opcode == "0x09":
        return (
            f"op9 mode={row.get('modeHex')} "
            f"valueSection={row.get('producerValueSection') or '-'}"
        )
    return row.get("unreadableReason") or "-"


def markdown(summary: dict) -> str:
    lines = [
        "# Save Selector Opcode 0x08 Unreadable Producers",
        "",
        f"- route: {summary['source']} -> {summary['target']}",
        f"- source/predecessor unreadable activations: {summary['sourceOrPredecessorUnreadableActivationCount']}",
        f"- unique unreadable producers: {summary['uniqueUnreadableProducerCount']}",
        f"- opcode 0x07 unreadable activations: {summary['opcode07UnreadableActivationCount']}",
        f"- opcode 0x07 unmapped selected-slot activations: {summary['opcode07UnmappedSelectedSlotActivationCount']}",
        f"- opcode 0x09 unreadable activations: {summary['opcode09UnreadableActivationCount']}",
        f"- opcode 0x09 unsupported-mode activations: {summary['opcode09UnsupportedModeActivationCount']}",
        f"- static current-pointer evidence: {summary['staticCurrentPointerEvidenceCount']}",
        f"- opcode08 unreadable producer promotes route: {summary['opcode08UnreadableProducerPromotesRoute']}",
        f"- promotion status: `{summary['promotionStatus']}`",
        "",
        summary["conclusion"],
        "",
        "## Unique Producers",
        "",
        "| selector | producer | activations | opcode | value | explanation | first | last |",
        "| --- | --- | ---: | --- | --- | --- | --- | --- |",
    ]
    for row in summary["producerRows"]:
        lines.append(
            f"| `{row.get('selector')}` | `{row.get('producerVaHex')}` | {row.get('activationCount')} | "
            f"`{row.get('producerOpcodeHex')}` | `{row.get('producerValueHex')}` | "
            f"{producer_brief(row)} | `{row.get('firstActivationVaHex')}` | `{row.get('lastActivationVaHex')}` |"
        )
    lines.extend([
        "",
        "## Activation Rows",
        "",
        "| selector | activation | producer | opcode | distance | reason |",
        "| --- | --- | --- | --- | ---: | --- |",
    ])
    for row in summary["activations"]:
        producer = row.get("producer") or {}
        lines.append(
            f"| `{row.get('selector')}` | `{row.get('activationVaHex')}` | `{producer.get('producerVaHex')}` | "
            f"`{producer.get('producerOpcodeHex')}` | {row.get('distanceBytes')} | {producer.get('unreadableReason')} |"
        )
    lines.extend(["", "## Remaining Proofs", ""])
    lines.extend(f"- {item}" for item in summary["remainingProofs"])
    lines.append("")
    return "\n".join(lines)


def html_page(summary: dict) -> str:
    producer_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('selector')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('producerVaHex')))}</code></td>"
        f"<td>{row.get('activationCount')}</td>"
        f"<td><code>{html.escape(str(row.get('producerOpcodeHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('producerValueHex')))}</code></td>"
        f"<td>{html.escape(producer_brief(row))}</td>"
        f"<td><code>{html.escape(str(row.get('firstActivationVaHex')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('lastActivationVaHex')))}</code></td>"
        "</tr>"
        for row in summary["producerRows"]
    )
    activation_rows = "\n".join(
        "<tr>"
        f"<td><code>{html.escape(str(row.get('selector')))}</code></td>"
        f"<td><code>{html.escape(str(row.get('activationVaHex')))}</code></td>"
        f"<td><code>{html.escape(str((row.get('producer') or {}).get('producerVaHex')))}</code></td>"
        f"<td><code>{html.escape(str((row.get('producer') or {}).get('producerOpcodeHex')))}</code></td>"
        f"<td>{html.escape(str(row.get('distanceBytes')))}</td>"
        f"<td>{html.escape(str((row.get('producer') or {}).get('unreadableReason')))}</td>"
        "</tr>"
        for row in summary["activations"]
    )
    proofs = "".join(f"<li>{html.escape(item)}</li>" for item in summary["remainingProofs"])
    return "\n".join([
        "<!doctype html>",
        '<html lang="en">',
        "<head>",
        '  <meta charset="utf-8">',
        '  <meta name="viewport" content="width=device-width, initial-scale=1">',
        "  <title>Save Selector Opcode 0x08 Unreadable Producers</title>",
        "  <style>",
        "    :root { color-scheme: dark; font-family: system-ui, sans-serif; background: #101010; color: #eee; }",
        "    body { margin: 0; padding: 24px; }",
        "    table { width: 100%; border-collapse: collapse; margin: 12px 0 20px; font-size: 13px; }",
        "    th, td { border-bottom: 1px solid #303030; padding: 7px 8px; text-align: left; vertical-align: top; }",
        "    th { background: #181818; color: #ddd; }",
        "    code { color: #d7f0ff; }",
        "  </style>",
        "</head>",
        "<body>",
        "  <h1>Save Selector Opcode 0x08 Unreadable Producers</h1>",
        f"  <p>route {html.escape(summary['source'])} -&gt; {html.escape(summary['target'])}; source/predecessor unreadable activations {summary['sourceOrPredecessorUnreadableActivationCount']}; unique unreadable producers {summary['uniqueUnreadableProducerCount']}; static current-pointer evidence {summary['staticCurrentPointerEvidenceCount']}; opcode08 unreadable producer promotes route {summary['opcode08UnreadableProducerPromotesRoute']}.</p>",
        f"  <p>opcode 0x07 unreadable activations {summary['opcode07UnreadableActivationCount']}; opcode 0x07 unmapped selected-slot activations {summary['opcode07UnmappedSelectedSlotActivationCount']}; opcode 0x09 unsupported-mode activations {summary['opcode09UnsupportedModeActivationCount']}.</p>",
        f"  <p>{html.escape(summary['conclusion'])}</p>",
        "  <h2>Unique Producers</h2>",
        "  <table><thead><tr><th>selector</th><th>producer</th><th>activations</th><th>opcode</th><th>value</th><th>explanation</th><th>first</th><th>last</th></tr></thead><tbody>",
        producer_rows,
        "  </tbody></table>",
        "  <h2>Activation Rows</h2>",
        "  <table><thead><tr><th>selector</th><th>activation</th><th>producer</th><th>opcode</th><th>distance</th><th>reason</th></tr></thead><tbody>",
        activation_rows,
        "  </tbody></table>",
        f"  <h2>Remaining Proofs</h2><ul>{proofs}</ul>",
        "</body></html>",
    ])


def write_outputs(summary: dict, out_dir: Path, html_out: Path | None = None) -> None:
    out_dir.mkdir(parents=True, exist_ok=True)
    (out_dir / "save_selector_opcode08_unreadable_producers.json").write_text(
        json.dumps(summary, ensure_ascii=False, separators=(",", ":")),
        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")


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--exe", type=Path, default=ROOT / "Hwanse2.exe")
    parser.add_argument("--activation-windows", type=Path, default=OUT / "save_selector_opcode08_activation_windows.json")
    parser.add_argument("--selected-pointer", type=Path, default=OUT / "save_selector_selected_pointer_opcode_paths.json")
    parser.add_argument("--out-dir", type=Path, default=OUT)
    parser.add_argument("--html-out", type=Path, default=None, help="Optional HTML report output path.")
    args = parser.parse_args()
    summary = build_summary(
        args.exe.read_bytes(),
        load_json(args.activation_windows, {}),
        load_json(args.selected_pointer, {}),
    )
    write_outputs(summary, args.out_dir, args.html_out)
    print(f"wrote save selector opcode 0x08 unreadable producers -> {args.out_dir / 'save_selector_opcode08_unreadable_producers.json'}")


if __name__ == "__main__":
    main()
