#!/usr/bin/env python3
"""Classify active-object x/y/w/h as bounds/hotspot vs visible source evidence.

The active object initializer writes tile x/y/w/h and optionally object+0x28.
The latter is grounded as draw-source selector (surface slot + frame index).
This report intentionally keeps those two facts separate: tile bounds are not
promoted to final draw placement unless a consumer proves that interpretation.
"""
from __future__ import annotations

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


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


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


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


def primary_asset(row: dict[str, Any]) -> str:
    assets = row.get("boundAssets") or []
    return str(assets[0]) if len(assets) == 1 else ""


def asset_family(asset: str) -> str:
    if asset == "cara_etc":
        return "cara_etc"
    if asset.startswith("map_") and asset.endswith("3"):
        return "map_*3"
    if asset.startswith("map_"):
        return "map_tile"
    if asset.startswith("cara_"):
        return "cara_*"
    return asset or "(none)"


def has_prompt(row: dict[str, Any]) -> bool:
    return row.get("scriptClassification") == "text/prompt-interaction" or int(row.get("textPayloadRefCount") or 0) > 0


def classify_row(row: dict[str, Any], blank_classes: dict[str, str]) -> dict[str, Any]:
    status = row.get("bindingStatus")
    assets = row.get("boundAssets") or []
    asset = primary_asset(row)
    prompt = has_prompt(row)
    init = row.get("initializerVaHex") or ""
    blank_class = blank_classes.get(init, "")

    if status == "no-draw-source-selector":
        source_class = "no-draw-source-selector"
        consumer_class = blank_class or ("text-hotspot" if prompt else "invisible-controller")
        placement_class = "not-visible-placement"
        reason = "object+0x28 is blank, so no visible draw source is grounded for this initializer."
    elif status == "root-slot-unmatched":
        source_class = "draw-selector-unmatched"
        consumer_class = "text-hotspot" if prompt else "selector-controller"
        placement_class = "not-promoted"
        reason = "object+0x28 exists, but no same-root resource slot resolves the source asset."
    elif len(assets) > 1:
        source_class = "ambiguous-draw-source"
        consumer_class = "text-hotspot" if prompt else "ambiguous-visible-controller"
        placement_class = "not-promoted"
        reason = "The same root+slot resolves to multiple assets, so source asset and placement cannot be promoted."
    elif prompt:
        source_class = f"single-{asset_family(asset)}-source"
        consumer_class = "visible-source-with-text-hotspot"
        placement_class = "bounds-hotspot-not-placement"
        reason = "A visible source is identified, but the script consumes the object as a prompt/interaction hotspot."
    else:
        source_class = f"single-{asset_family(asset)}-source"
        consumer_class = "visible-source-no-prompt"
        placement_class = "visible-source-bounds-candidate"
        reason = "A single visible source is identified and no prompt payload is attached; still no final draw-placement consumer was proven."

    tile = {
        "x": row.get("xCandidate"),
        "y": row.get("yCandidate"),
        "w": row.get("wCandidate"),
        "h": row.get("hCandidate"),
    }
    return {
        "initializerVaHex": init,
        "scriptVaHex": row.get("scriptVaHex"),
        "field0x28Hex": row.get("field0x28Hex") or "",
        "bindingStatus": status,
        "boundAssets": assets,
        "primaryAsset": asset,
        "assetFamily": asset_family(asset),
        "scriptClassification": row.get("scriptClassification"),
        "textPayloadRefCount": row.get("textPayloadRefCount") or 0,
        "tileBounds": tile,
        "shape": f"{tile['w']}x{tile['h']}",
        "sourceClass": source_class,
        "consumerClass": consumer_class,
        "placementClass": placement_class,
        "reason": reason,
        "routeProofFound": bool(row.get("routeProofFound")),
        "mapCnsOperandRefCount": row.get("mapCnsOperandRefCount") or 0,
        "mapCnsNearbyRefCount": row.get("mapCnsNearbyRefCount") or 0,
        "resourceMatchCount": row.get("resourceMatchCount") or 0,
        "drawSlotHex": row.get("drawSlotHex") or "",
        "drawFrameIndexHex": row.get("drawFrameIndexHex") or "",
    }


def build() -> dict[str, Any]:
    slot_review = load_json(OUT / "active_object_resource_slot_review.json", {})
    blank_review = load_json(OUT / "active_object_blank_draw_source_review.json", {})
    blank_classes = {
        row.get("initializerVaHex"): row.get("blankReviewClass")
        for row in blank_review.get("rows") or []
        if row.get("initializerVaHex")
    }
    rows = [classify_row(row, blank_classes) for row in slot_review.get("activeObjectBindings") or []]

    source_counts = Counter(row["sourceClass"] for row in rows)
    consumer_counts = Counter(row["consumerClass"] for row in rows)
    placement_counts = Counter(row["placementClass"] for row in rows)
    shape_by_placement: dict[str, Counter[str]] = defaultdict(Counter)
    asset_by_placement: dict[str, Counter[str]] = defaultdict(Counter)
    for row in rows:
        shape_by_placement[row["placementClass"]][row["shape"]] += 1
        asset_by_placement[row["placementClass"]][row["assetFamily"]] += 1

    single_source_rows = [row for row in rows if row["sourceClass"].startswith("single-")]
    visible_no_prompt = [row for row in rows if row["placementClass"] == "visible-source-bounds-candidate"]
    hotspot_rows = [row for row in rows if "hotspot" in row["consumerClass"]]

    summary = {
        "activeInitializerCount": len(rows),
        "drawSourceSelectorRows": sum(1 for row in rows if row["field0x28Hex"]),
        "blankDrawSourceRows": sum(1 for row in rows if not row["field0x28Hex"]),
        "singleSourceRows": len(single_source_rows),
        "visibleSourceNoPromptCandidateRows": len(visible_no_prompt),
        "hotspotOrPromptRows": len(hotspot_rows),
        "finalDrawPlacementProofRows": 0,
        "sourceClassCounts": dict(source_counts.most_common()),
        "consumerClassCounts": dict(consumer_counts.most_common()),
        "placementClassCounts": dict(placement_counts.most_common()),
        "shapeByPlacement": {key: dict(counter.most_common()) for key, counter in shape_by_placement.items()},
        "assetFamilyByPlacement": {key: dict(counter.most_common()) for key, counter in asset_by_placement.items()},
        "conclusion": (
            "object+0x28 is a draw-source selector, but active x/y/w/h remains tile bounds/hotspot/object bounds. "
            "No consumer proof currently promotes these bounds to final pixel draw placement. "
            "The strongest visible-object candidates are the single-source/no-prompt rows."
        ),
    }
    return {
        "kind": "hwanse-active-object-bounds-semantics-review",
        "summary": summary,
        "rows": rows,
        "visibleSourceNoPromptCandidates": visible_no_prompt,
        "hotspotOrPromptSamples": hotspot_rows[:120],
        "nonClaims": [
            "x/y/w/h is not treated as source rect.",
            "x/y/w/h is not promoted to final pixel draw placement.",
            "Single source + no prompt is only a visible-object/bounds candidate until the draw consumer is proven.",
            "Prompt rows prove interaction/hotspot behavior, not route destination or final placement.",
        ],
    }


def render_html(payload: dict[str, Any]) -> str:
    s = payload["summary"]
    data = json.dumps(payload, ensure_ascii=False)
    metrics = [
        ("active initializers", s["activeInitializerCount"]),
        ("draw-source selector rows", s["drawSourceSelectorRows"]),
        ("blank draw-source rows", s["blankDrawSourceRows"]),
        ("single-source rows", s["singleSourceRows"]),
        ("visible source/no-prompt", s["visibleSourceNoPromptCandidateRows"]),
        ("hotspot or prompt rows", s["hotspotOrPromptRows"]),
        ("final draw placement proof", s["finalDrawPlacementProofRows"]),
    ]
    metric_html = "".join(
        f"<div class='metric'><span>{esc(label)}</span><strong>{esc(value)}</strong></div>"
        for label, value in metrics
    )
    placement_rows = []
    for cls, count in s["placementClassCounts"].items():
        shapes = ", ".join(f"{esc(k)}:{esc(v)}" for k, v in s["shapeByPlacement"].get(cls, {}).items())
        assets = ", ".join(f"{esc(k)}:{esc(v)}" for k, v in s["assetFamilyByPlacement"].get(cls, {}).items())
        placement_rows.append(f"<tr><td><code>{esc(cls)}</code></td><td>{count}</td><td>{shapes}</td><td>{assets}</td></tr>")
    non_claims = "".join(f"<li>{esc(item)}</li>" for item in payload["nonClaims"])
    return f"""<!doctype html>
<meta charset="utf-8">
<title>Active Object Bounds Semantics Review</title>
<style>
:root{{color-scheme:dark;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;background:#101214;color:#e5e7eb}}
body{{margin:24px;background:#101214;color:#e5e7eb;line-height:1.45}}
a{{color:#93c5fd}} code{{color:#bfdbfe}}
.metrics{{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:10px;margin:14px 0}}
.metric{{border:1px solid #2f3640;background:#181c20;border-radius:6px;padding:10px}}
.metric span{{display:block;color:#9ca3af;font-size:12px}} .metric strong{{font-size:24px}}
table{{border-collapse:collapse;width:100%;margin:14px 0}} td,th{{border:1px solid #374151;padding:7px 9px;vertical-align:top}} th{{background:#1f2937}}
.note{{border:1px solid #4b5563;background:#15191e;border-radius:8px;padding:12px;margin:12px 0}}
.warn{{color:#fbbf24}} .muted{{color:#9ca3af}}
.filters{{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}}
button{{border:1px solid #4b5563;background:#1f2937;color:#e5e7eb;border-radius:4px;min-height:30px;padding:0 10px;cursor:pointer}}
button.active{{border-color:#22c55e;background:#16351f}}
</style>
<h1>Active Object Bounds Semantics Review</h1>
<p class="note">{esc(s["conclusion"])}</p>
<div class="metrics">{metric_html}</div>
<section>
  <h2>Non-claims</h2>
  <ul>{non_claims}</ul>
</section>
<section>
  <h2>Placement Classes</h2>
  <table><thead><tr><th>class</th><th>count</th><th>top shapes</th><th>top assets</th></tr></thead><tbody>{''.join(placement_rows)}</tbody></table>
</section>
<section>
  <h2>Rows</h2>
  <div class="filters" id="filters"></div>
  <table>
    <thead><tr><th>init</th><th>source</th><th>consumer</th><th>placement</th><th>tile bounds</th><th>reason</th></tr></thead>
    <tbody id="rows"></tbody>
  </table>
</section>
<script>
window.HWANSE_ACTIVE_OBJECT_BOUNDS_SEMANTICS_REVIEW = {data};
const payload = window.HWANSE_ACTIVE_OBJECT_BOUNDS_SEMANTICS_REVIEW;
const rowsEl = document.getElementById('rows');
const filtersEl = document.getElementById('filters');
let activeFilter = 'all';
const classes = ['all', ...new Set(payload.rows.map(row => row.placementClass))];
function renderFilters() {{
  filtersEl.innerHTML = '';
  classes.forEach(cls => {{
    const button = document.createElement('button');
    button.type = 'button';
    button.textContent = cls;
    button.className = cls === activeFilter ? 'active' : '';
    button.addEventListener('click', () => {{ activeFilter = cls; render(); }});
    filtersEl.append(button);
  }});
}}
function render() {{
  renderFilters();
  const rows = payload.rows.filter(row => activeFilter === 'all' || row.placementClass === activeFilter);
  rowsEl.innerHTML = rows.map(row => {{
    const tile = row.tileBounds || {{}};
    const source = row.primaryAsset || row.sourceClass;
    return `<tr>
      <td><code>${{row.initializerVaHex}}</code><br><span class="muted">${{row.scriptVaHex || ''}}</span></td>
      <td><code>${{source}}</code><br><span class="muted">${{row.field0x28Hex || '(blank)'}} ${{row.drawFrameIndexHex || ''}}</span></td>
      <td><code>${{row.consumerClass}}</code><br><span class="muted">${{row.scriptClassification || ''}} text=${{row.textPayloadRefCount}}</span></td>
      <td><code>${{row.placementClass}}</code></td>
      <td>${{tile.x}},${{tile.y}} ${{tile.w}}x${{tile.h}}<br><span class="muted">${{row.shape}}</span></td>
      <td>${{row.reason}}</td>
    </tr>`;
  }}).join('');
}}
render();
</script>
"""


def main() -> None:
    OUT.mkdir(parents=True, exist_ok=True)
    payload = build()
    (OUT / "active_object_bounds_semantics_review.json").write_text(
        json.dumps(payload, ensure_ascii=False, separators=(",", ":")) + "\n",
        encoding="utf-8",
    )
    (OUT / "active_object_bounds_semantics_review.html").write_text(render_html(payload), encoding="utf-8")
    print(json.dumps(payload["summary"], ensure_ascii=False, indent=2))


if __name__ == "__main__":
    main()
