#!/usr/bin/env python3
"""Regenerate dual-lock / Hyperpart coverage inventory (Phase A).

Reads HM package layout + monorepo ``tests/unit/hm_contract_registry.py``
and writes a markdown table. Run from monorepo root or package root:

    python packages/hatchi-maxchi/tools/dual_lock_coverage.py
    python packages/hatchi-maxchi/tools/dual_lock_coverage.py --write

Default prints to stdout; ``--write`` updates
``packages/hatchi-maxchi/DUAL_LOCK_COVERAGE.md``.
"""

from __future__ import annotations

import argparse
import re
import sys
from pathlib import Path

PKG = Path(__file__).resolve().parents[1]
REPO = PKG.parent.parent  # packages/hatchi-maxchi -> packages -> repo
REGISTRY = REPO / "tests" / "unit" / "hm_contract_registry.py"
OUT = PKG / "DUAL_LOCK_COVERAGE.md"


def _extract_contract_stems(list_name: str, text: str) -> list[str]:
    m = re.search(rf"{list_name}: list\[.*?\] = \[(.*?)\]\n\n", text, re.S)
    if not m:
        m = re.search(rf"{list_name}: list\[.*?\] = \[(.*?)\]\n", text, re.S)
    if not m:
        return []
    return re.findall(r"contracts/([\w]+)\.py", m.group(1))


def _controller_map() -> dict[str, str]:
    """Map contract-ish stems (underscore or hyphen) -> controller filename."""
    out: dict[str, str] = {}
    for p in sorted((PKG / "controllers").glob("dz-*.js")):
        stem = p.stem[3:]  # drop dz-
        out[stem] = p.name
        out[stem.replace("-", "_")] = p.name
    return out


def inventory() -> list[dict[str, str]]:
    reg_text = REGISTRY.read_text(encoding="utf-8") if REGISTRY.is_file() else ""
    schema = set(_extract_contract_stems("CONTRACT_MODELS", reg_text))
    dom_only = set(_extract_contract_stems("DOM_ONLY_CONTRACTS", reg_text))
    deferred = set(_extract_contract_stems("DOM_ONLY_DEFERRED", reg_text))
    ctrls = _controller_map()

    rows: list[dict[str, str]] = []
    for p in sorted((PKG / "contracts").glob("*.py")):
        if p.stem in ("__init__", "_kit"):
            continue
        stem = p.stem
        text = p.read_text(encoding="utf-8")
        has_dom = "DOM_CONTRACT" in text
        has_model = "BaseModel" in text and re.search(r"^class \w+\(BaseModel\)", text, re.M)
        has_render = bool(re.search(r"^def render\(", text, re.M))
        ctrl = ctrls.get(stem) or ctrls.get(stem.replace("_", "-")) or "—"
        if stem in schema:
            dual = "schema+DOM"
        elif stem in dom_only:
            dual = "DOM-only"
        elif stem in deferred:
            dual = "deferred"
        else:
            dual = "none"
        rows.append(
            {
                "part": stem,
                "contract": p.name,
                "controller": ctrl,
                "dom_contract": "yes" if has_dom else "no",
                "model": "yes" if has_model else "no",
                "render": "yes" if has_render else "no",
                "dual_lock": dual,
            }
        )
    return rows


def render_md(rows: list[dict[str, str]]) -> str:
    schema_n = sum(1 for r in rows if r["dual_lock"] == "schema+DOM")
    dom_n = sum(1 for r in rows if r["dual_lock"] == "DOM-only")
    def_n = sum(1 for r in rows if r["dual_lock"] == "deferred")
    none_n = sum(1 for r in rows if r["dual_lock"] == "none")
    css_n = len(list((PKG / "components").glob("*.css")))
    ctrl_n = len(list((PKG / "controllers").glob("dz-*.js")))

    lines = [
        "# Dual-lock / Hyperpart coverage",
        "",
        "Auto-generated by `tools/dual_lock_coverage.py`. Do not hand-edit the table.",
        "",
        "Regenerate: `python packages/hatchi-maxchi/tools/dual_lock_coverage.py --write`",
        "",
        "## Summary",
        "",
        "| Metric | Count |",
        "|--------|------:|",
        f"| Component CSS modules | {css_n} |",
        f"| Controllers | {ctrl_n} |",
        f"| Contract modules | {len(rows)} |",
        f"| Dual-lock schema+DOM | {schema_n} |",
        f"| Dual-lock DOM-only | {dom_n} |",
        f"| Dual-lock deferred | {def_n} |",
        f"| Contract with no dual-lock row | {none_n} |",
        "",
        "Source of dual-lock columns: monorepo `tests/unit/hm_contract_registry.py`.",
        "",
        "## Contract modules",
        "",
        "| Part | Contract | Controller | DOM_CONTRACT | Model | render() | Dual-lock |",
        "|------|----------|------------|--------------|-------|----------|-----------|",
    ]
    for r in rows:
        lines.append(
            f"| `{r['part']}` | `{r['contract']}` | `{r['controller']}` | "
            f"{r['dom_contract']} | {r['model']} | {r['render']} | **{r['dual_lock']}** |"
        )
    lines.extend(
        [
            "",
            "## Priority notes (Phase B)",
            "",
            "1. **deferred → DOM-only** — promotable queue drained (confirm, pdf, wizard, "
            "master_detail via dual_pane_flow). Empty deferred is success.",
            "2. **DOM-only → schema+DOM** when an ingest model is shared with Dazzle "
            "(pattern: money / combobox / tags / grid_edit / search_select / action_grid). Root-only "
            "parts stay DOM-only until a real ingest model lands.",
            "3. **CSS-only Hyperparts** (no contract file) are out of this table — "
            "gallery/registry expansion is a separate queue from dual-locks.",
            "",
            "## Plan",
            "",
            "See `docs/superpowers/plans/2026-07-11-hm-sophistication-plan.md`.",
            "",
            "## Improve loop",
            "",
            "Ranked next promotions (machine queue for `/improve hm-convergence`):",
            "",
            "```bash",
            "python packages/hatchi-maxchi/tools/dual_lock_queue.py --top 5",
            "python packages/hatchi-maxchi/tools/dual_lock_queue.py --write   # → DUAL_LOCK_QUEUE.md",
            "```",
            "",
            "Playbook: `.claude/commands/improve/strategies/dual_lock_expand.md`.",
            "Force path: `/improve hm-convergence dual_lock_expand`.",
            "",
        ]
    )
    return "\n".join(lines)


def main(argv: list[str] | None = None) -> int:
    ap = argparse.ArgumentParser(description=__doc__)
    ap.add_argument(
        "--write",
        action="store_true",
        help=f"write {OUT.relative_to(REPO) if REPO in OUT.parents else OUT}",
    )
    args = ap.parse_args(argv)
    if not (PKG / "contracts").is_dir():
        print("error: contracts/ not found under package", file=sys.stderr)
        return 2
    md = render_md(inventory())
    if args.write:
        OUT.write_text(md, encoding="utf-8")
        print(f"wrote {OUT}")
    else:
        print(md)
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
