#!/usr/bin/env python3
"""arq-ui-redesign-audit — substrate-attested redesign audit per
arq://doc/principle/ui-serves-customer-needs-v1.

Per operator directive 2026-05-28 ("complete the architecture, no audit
reports"), this CLI emits substrate ACTS per finding kind — NOT markdown
reports. The acts are the source of truth; stdout is a terse TUI rollup.

Findings emitted as acts:
  arq://act/ui_route_no_need_served/<ref>           orphan route (no need inferable)
  arq://act/customer_need_no_ui_surface/<ref>       workflow gap (need with 0 routes)
  arq://act/customer_need_oversupplied_uis/<ref>    consolidation candidate (>N routes)
  arq://act/ui_route_serves_need_inferred/<ref>     heuristic auto-tag (bipartite edge)

Operator workflow:
  arq-ui-redesign-audit                  # heuristic auto-tag + emit findings
  arq-ui-redesign-audit --dry-run        # compute + print TUI only, NO emit
  arq-ui-redesign-audit --max 6          # change consolidation threshold
  arq-ui-redesign-audit --manual <route> <need>   # operator-asserted edge

Source of truth = substrate acts. Stdout is a convenience summary.
"""
from __future__ import annotations

import argparse
import json
import re
import subprocess
import sys
import urllib.request
from collections import defaultdict
from datetime import UTC, datetime
from pathlib import Path

BRIDGE_DIR = Path(__file__).parent
sys.path.insert(0, str(BRIDGE_DIR))
# substrate-bypass: this is the live producer of the audit acts; emit_act is the canonical helper.
from _arq_provider_base import emit_act  # noqa: E402

ADDRESSING_BASE = "https://addressing.arqera.io"


# ── substrate fetch ───────────────────────────────────────────────────────


def _index(type_: str) -> list[dict]:
    r = subprocess.run(
        ["twin", "--use-keychain", "index", "--class", "body", "--type", type_,
         "--limit", "1000", "--json"],
        capture_output=True, text=True, check=False, timeout=20,
    )
    if r.returncode != 0:
        return []
    try:
        return json.loads(r.stdout) or []
    except (json.JSONDecodeError, ValueError):
        return []


def _fetch(type_: str, ref: str) -> dict | None:
    try:
        with urllib.request.urlopen(
            f"{ADDRESSING_BASE}/address/body/{type_}/{ref}?full=true", timeout=8
        ) as resp:
            d = json.load(resp)
    except Exception:  # noqa: PSWP-scaffold-narrow-in-followup
        return None
    p = d.get("payload")
    return p if isinstance(p, dict) else None


# ── path → need heuristic (auto-tag) ──────────────────────────────────────
# Maps a URL path pattern to a customer need slug. Multiple patterns may
# match a path — all matched needs become served-by edges for that route.
# Operator refines via --manual <route> <need> for any mis-tags.
ROUTE_NEED_HINTS: list[tuple[re.Pattern, str]] = [
    (re.compile(r"^/(\(marketing\))?/?$"),                       "learn-what-arqera-is"),
    (re.compile(r"/about"),                                       "learn-what-arqera-is"),
    (re.compile(r"/products?(/|$)|/features"),                    "see-if-arqera-fits-me"),
    (re.compile(r"/compare|/vs/"),                                "compare-to-alternatives"),
    (re.compile(r"/pricing|/plans"),                              "understand-pricing"),
    (re.compile(r"/sign(up|-up)|/register|/trial|/demo"),         "convert-to-paid"),
    (re.compile(r"/auth(/|$)|/login|/sign-?in"),                  "sign-in"),
    (re.compile(r"/chat|/ask|/command|/inbox/ask"),               "ask-arqera-a-question"),
    (re.compile(r"/agents?(/|$)|/delegate"),                      "delegate-work-to-agent"),
    (re.compile(r"/dashboard|/briefing|/analytics|/home"),        "see-my-status"),
    (re.compile(r"/build|/studio|/canvas|/compose|/create"),      "make-something-new"),
    (re.compile(r"/billing|/payment|/invoice|/ledger"),           "understand-my-billing"),
    (re.compile(r"/settings|/preferences|/profile"),              "configure-my-account"),
    (re.compile(r"/connect|/integrations"),                       "connect-an-integration"),
    (re.compile(r"/help|/docs?(/|$)|/support|/guides"),           "get-help"),
    (re.compile(r"/data(/|$)|/my-data"),                          "review-my-data"),
    (re.compile(r"/governance|/policy|/agent-policy|/approvals"), "govern-arqera"),
    (re.compile(r"/health|/status|/monitor"),                     "monitor-arqera-health"),
    (re.compile(r"/audit|/trail"),                                "audit-a-decision"),
    (re.compile(r"/substrate|/protocol/execution|/canonisation"), "shape-substrate"),
    (re.compile(r"/release|/deploy"),                             "approve-release"),
    (re.compile(r"/business|/revenue|/arr|/nps|/board"),          "see-business-state"),
    (re.compile(r"/inbox|/ticket|/conversations?"),               "respond-to-customer"),
    (re.compile(r"/blog|/changelog|/case-studies|/content"),      "publish-content"),
    (re.compile(r"/experiment|/lab|/flag"),                       "experiment-with-feature"),
    (re.compile(r"/team|/workforce|/people"),                     "manage-team"),
]


def infer_needs(path: str) -> list[str]:
    matched: list[str] = []
    for rx, need in ROUTE_NEED_HINTS:
        if rx.search(path) and need not in matched:
            matched.append(need)
    return matched


# ── audit core ────────────────────────────────────────────────────────────


def _emit_orphan(route_slug: str, route_payload: dict, ts: str) -> bool:
    return emit_act(
        "act", "ui_route_no_need_served", f"{route_slug}-{ts}",
        {
            "principle": "arq://doc/principle/ui-serves-customer-needs-v1",
            "route_slug": route_slug,
            "route_address": f"arq://body/wieldable_ui_route/{route_slug}",
            "path": route_payload.get("path", ""),
            "source_file": route_payload.get("source_file", ""),
            "verdict": "ORPHAN — no inferable customer need; either annotate or delete",
            "audit_ts": ts,
        },
    )


def _emit_gap(need_slug: str, need_payload: dict, ts: str) -> bool:
    return emit_act(
        "act", "customer_need_no_ui_surface", f"{need_slug}-{ts}",
        {
            "principle": "arq://doc/principle/ui-serves-customer-needs-v1",
            "need_slug": need_slug,
            "need_address": f"arq://body/wieldable_customer_need/{need_slug}",
            "need_name": need_payload.get("name", ""),
            "persona": need_payload.get("persona", []),
            "importance": need_payload.get("importance", ""),
            "verdict": "GAP — declared customer need with zero ui_route surfaces; workflow broken",
            "audit_ts": ts,
        },
    )


def _emit_oversupplied(need_slug: str, need_payload: dict, route_slugs: list[str],
                        threshold: int, ts: str) -> bool:
    return emit_act(
        "act", "customer_need_oversupplied_uis", f"{need_slug}-{ts}",
        {
            "principle": "arq://doc/principle/ui-serves-customer-needs-v1",
            "need_slug": need_slug,
            "need_address": f"arq://body/wieldable_customer_need/{need_slug}",
            "need_name": need_payload.get("name", ""),
            "route_count": len(route_slugs),
            "route_slugs": route_slugs[:20],  # cap to keep payload bounded
            "threshold": threshold,
            "verdict": f"OVER-SUPPLIED — {len(route_slugs)} routes for one need; consolidate or differentiate",
            "audit_ts": ts,
        },
    )


def _emit_inferred_edge(route_slug: str, need_slug: str, path: str, ts: str) -> bool:
    """Heuristic auto-tag — route serves need (inferred from path pattern).

    Operator can supersede with `--manual <route> <need>` to assert a
    different / additional edge.
    """
    return emit_act(
        "act", "ui_route_serves_need_inferred", f"{route_slug}--{need_slug}-{ts}",
        {
            "principle": "arq://doc/principle/ui-serves-customer-needs-v1",
            "route_slug": route_slug,
            "route_address": f"arq://body/wieldable_ui_route/{route_slug}",
            "need_slug": need_slug,
            "need_address": f"arq://body/wieldable_customer_need/{need_slug}",
            "path": path,
            "inference": "path-pattern heuristic (arq-ui-redesign-audit ROUTE_NEED_HINTS)",
            "confidence": "heuristic",
            "audit_ts": ts,
        },
    )


def _emit_manual_edge(route_slug: str, need_slug: str, ts: str) -> bool:
    return emit_act(
        "act", "ui_route_serves_need_manual", f"{route_slug}--{need_slug}-{ts}",
        {
            "principle": "arq://doc/principle/ui-serves-customer-needs-v1",
            "route_slug": route_slug,
            "route_address": f"arq://body/wieldable_ui_route/{route_slug}",
            "need_slug": need_slug,
            "need_address": f"arq://body/wieldable_customer_need/{need_slug}",
            "confidence": "operator-asserted",
            "audit_ts": ts,
        },
    )


# ── main ──────────────────────────────────────────────────────────────────


def main() -> int:
    p = argparse.ArgumentParser(prog="arq-ui-redesign-audit")
    p.add_argument("--max", type=int, default=6, help="threshold for over-supplied need (≥N routes)")
    p.add_argument("--dry-run", action="store_true", help="compute + print TUI only; do NOT emit acts")
    p.add_argument("--manual", nargs=2, metavar=("ROUTE_SLUG", "NEED_SLUG"),
                   help="operator-asserted serves_need edge; emits ui_route_serves_need_manual + exits")
    args = p.parse_args()

    ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")

    # ── manual mode: operator asserts edge ──────────────────────────────
    if args.manual:
        route_slug, need_slug = args.manual
        ok = _emit_manual_edge(route_slug, need_slug, ts)
        print(f"  {'emitted' if ok else 'FAILED'}: ui_route_serves_need_manual/{route_slug}--{need_slug}-{ts}")
        return 0 if ok else 1

    # ── load substrate state ────────────────────────────────────────────
    needs = {r["ref"]: _fetch("wieldable_customer_need", r["ref"])
             for r in _index("wieldable_customer_need")}
    needs = {k: v for k, v in needs.items() if v}

    routes = {r["ref"]: _fetch("wieldable_ui_route", r["ref"])
              for r in _index("wieldable_ui_route")}
    routes = {k: v for k, v in routes.items() if v}

    # ── compute bipartite graph ─────────────────────────────────────────
    served_by: dict[str, list[str]] = defaultdict(list)
    inferred_edges: list[tuple[str, str, str]] = []  # (route_slug, need_slug, path)
    orphan_routes: list[str] = []

    for slug, payload in routes.items():
        path = payload.get("path", "")
        inferred = infer_needs(path)
        if not inferred:
            orphan_routes.append(slug)
            continue
        for need in inferred:
            if need in needs:
                served_by[need].append(slug)
                inferred_edges.append((slug, need, path))

    gaps = sorted(n for n in needs if not served_by.get(n))
    crowded = sorted(
        ((n, len(rs)) for n, rs in served_by.items() if len(rs) >= args.max),
        key=lambda x: -x[1],
    )

    # ── stdout rollup (TUI; SOURCE OF TRUTH = acts) ─────────────────────
    print(f"  ── ui-serves-customer-needs audit @ {ts} ──")
    print(f"  catalogue: {len(needs)} customer needs · {len(routes)} ui routes")
    print(f"  orphan routes (no need): {len(orphan_routes)}")
    print(f"  gap needs (0 routes):    {len(gaps)}")
    print(f"  over-supplied (≥{args.max} routes): {len(crowded)}")
    print(f"  inferred edges:          {len(inferred_edges)}")

    if args.dry_run:
        print("\n  --dry-run: NOT emitting acts (run without --dry-run to land on substrate)")
        return 0

    # ── emit findings as substrate acts ─────────────────────────────────
    emitted = {
        "ui_route_no_need_served": 0,
        "customer_need_no_ui_surface": 0,
        "customer_need_oversupplied_uis": 0,
        "ui_route_serves_need_inferred": 0,
    }
    failed = 0

    for slug in orphan_routes:
        if _emit_orphan(slug, routes[slug], ts):
            emitted["ui_route_no_need_served"] += 1
        else:
            failed += 1

    for need_slug in gaps:
        if _emit_gap(need_slug, needs[need_slug], ts):
            emitted["customer_need_no_ui_surface"] += 1
        else:
            failed += 1

    for need_slug, _count in crowded:
        if _emit_oversupplied(need_slug, needs[need_slug], served_by[need_slug],
                              args.max, ts):
            emitted["customer_need_oversupplied_uis"] += 1
        else:
            failed += 1

    for route_slug, need_slug, path in inferred_edges:
        if _emit_inferred_edge(route_slug, need_slug, path, ts):
            emitted["ui_route_serves_need_inferred"] += 1
        else:
            failed += 1

    # rollup summary act
    rollup_ok = emit_act(
        "act", "ui_redesign_audit_completed", f"{ts}",
        {
            "principle": "arq://doc/principle/ui-serves-customer-needs-v1",
            "needs_total": len(needs),
            "routes_total": len(routes),
            "orphan_routes": len(orphan_routes),
            "gap_needs": len(gaps),
            "oversupplied_needs": len(crowded),
            "inferred_edges": len(inferred_edges),
            "threshold_max_routes_per_need": args.max,
            "emitted": emitted,
            "failed": failed,
            "audit_ts": ts,
        },
    )

    print()
    print("  substrate emissions:")
    for k, v in emitted.items():
        print(f"    {v:>4}  arq://act/{k}/...")
    print(f"    {'rollup ok' if rollup_ok else 'rollup FAILED'}: arq://act/ui_redesign_audit_completed/{ts}")
    if failed:
        print(f"  ⚠ {failed} emission failure(s) — check act-queue for retry")
    return 0 if failed == 0 else 1


if __name__ == "__main__":
    sys.exit(main())
