"""PRD Plugin status — one-screen 'where are we?' read-only summary.

Counts requests by status, lists active tracking, open health findings, stale
items (if staleness_audit is available), and a skill-usage summary (if the
telemetry log exists). Backs the /prd-status command. Never mutates state.
"""

import argparse
import json
import re
import sys
from datetime import datetime, timezone
from pathlib import Path


def _read_json(path):
    return json.loads(Path(path).read_text(encoding="utf-8-sig"))


# One status vocabulary (open/active/complete/resolved/deferred/parked/
# superseded); legacy values map transparently on read so older records are
# never silently hidden by a literal filter.
LEGACY_STATUS_MAP = {
    "in_progress": "active", "in-progress": "active", "wip": "active", "started": "active",
    "done": "complete", "completed": "complete", "closed": "complete", "finished": "complete",
    "cancelled": "superseded", "canceled": "superseded", "abandoned": "superseded",
}


def _normalize_status(status):
    s = str(status or "").lower()
    return LEGACY_STATUS_MAP.get(s, s)


def _load(root, rel, key):
    path = Path(root) / rel
    if not path.is_file():
        return []
    try:
        data = _read_json(path)
    except Exception:
        return []
    return data.get(key, []) if isinstance(data, dict) else []


def _load_tracking_branches(root):
    branch_dir = Path(root) / ".prd_plugin" / "state" / "tracking-branches"
    branches = []
    errors = []
    if not branch_dir.is_dir():
        return branches, errors
    for path in sorted(branch_dir.glob("DBR-*.json")):
        try:
            if not re.fullmatch(r"DBR-\d+", path.stem):
                raise ValueError("tracking branch filename must be DBR-<number>.json")
            record = _read_json(path)
            if (not isinstance(record, dict) or record.get("id") != path.stem
                    or record.get("kind") != "tracking"):
                raise ValueError("file is not a tracking branch matching its DBR filename")
            proposed = record.get("proposed") if isinstance(record.get("proposed"), dict) else {}
            row = {
                "id": record.get("id"),
                "state": str(record.get("state") or "unknown"),
                "summary": proposed.get("summary") or record.get("summary"),
                "owner": record.get("owner"),
                "owner_agent": record.get("owner_agent"),
                "target_tracking_id": record.get("target_tracking_id"),
                "updated_at": record.get("updated_at"),
            }
            promotion = record.get("promotion")
            if isinstance(promotion, dict):
                row["promotion"] = {
                    key: promotion.get(key) for key in ("id", "tracking_id", "promoted_at")
                    if promotion.get(key) is not None
                }
            branches.append(row)
        except Exception as exc:
            errors.append({"file": path.name, "error": str(exc)})
    return branches, errors


REASON_GUARD_HOST_REPORTS = {
    "claude": ("Claude", "claude_reasoning_guard.json"),
    "codex": ("Codex", "codex_reasoning_guard.json"),
    "opencode": ("OpenCode", "opencode_reasoning_guard.json"),
}
REASON_GUARD_SURFACE_NAMES = {
    "claude_desktop": "Claude Desktop",
    "claude_cli": "Claude CLI",
    "codex_desktop": "Codex Desktop",
    "codex_cli": "Codex CLI",
    "opencode": "OpenCode",
    "unknown": "Surface unknown",
}
REASON_GUARD_EFFORT_NAMES = {
    "minimal": "Minimal",
    "low": "Low",
    "medium": "Medium",
    "high": "High",
    "xhigh": "Extra high",
    "max": "Max",
    "ultra": "Ultra",
    "none": "None",
    "unknown": "Not reported",
}


def _format_guard_session_time(value):
    try:
        parsed = datetime.fromisoformat(str(value).replace("Z", "+00:00"))
        parsed = parsed.astimezone(timezone.utc)
        return parsed.strftime("%d %b %Y %H:%M UTC")
    except (TypeError, ValueError):
        return "time unavailable"


def _load_reason_guard_sessions(root):
    rows = []
    latest_state = {}
    latest_at = ""
    local = Path(root) / ".prd_plugin" / "local"
    for host, (host_name, filename) in REASON_GUARD_HOST_REPORTS.items():
        try:
            ledger = _read_json(local / filename)
        except Exception:
            continue
        if (
            not isinstance(ledger, dict)
            or ledger.get("schema_version") != "1.0"
            or ledger.get("kind") != "reason_guard_host_ledger"
            or ledger.get("host") != host
            or not isinstance(ledger.get("sessions"), list)
        ):
            continue
        active_session_id = str(ledger.get("active_session_id") or "")
        for state in ledger["sessions"]:
            if not isinstance(state, dict):
                continue
            session_id = str(state.get("session_id") or "")
            updated_at = str(state.get("updated_at") or "")
            started_at = str(state.get("started_at") or updated_at)
            coverage = state.get("coverage")
            clearance = state.get("clearance")
            summary = state.get("summary")
            ingestion = state.get("reasoning_summary_ingestion")
            metrics = state.get("metrics")
            surface = str(
                state.get("surface")
                or ("opencode" if host == "opencode" else "unknown")
            )
            if surface not in REASON_GUARD_SURFACE_NAMES:
                surface = "unknown"
            surface_name = REASON_GUARD_SURFACE_NAMES[surface]
            model = str(state.get("model") or "unknown")
            effort = str(state.get("effort") or "unknown").lower()
            effort_name = REASON_GUARD_EFFORT_NAMES.get(
                effort, "Not reported"
            )
            row = {
                "host": host,
                "host_name": host_name,
                "surface": surface,
                "surface_name": surface_name,
                "model": model,
                "effort": effort,
                "effort_name": effort_name,
                "session_label": (
                    f"{surface_name} session / "
                    f"{_format_guard_session_time(updated_at)}"
                ),
                "session_id": session_id,
                "active": session_id == active_session_id,
                "started_at": started_at,
                "updated_at": updated_at,
                "coverage": (
                    coverage.get("level", "reduced")
                    if isinstance(coverage, dict)
                    else "reduced"
                ),
                "clearance": (
                    clearance.get("status", "unavailable")
                    if isinstance(clearance, dict)
                    else "unavailable"
                ),
                "open_obligations": (
                    summary.get("open", 0)
                    if isinstance(summary, dict)
                    else 0
                ),
                "findings": (
                    summary.get("violations", 0)
                    if isinstance(summary, dict)
                    else 0
                ),
                "summaries_processed": (
                    ingestion.get("summaries_processed", 0)
                    if isinstance(ingestion, dict)
                    else 0
                ),
                "p95_ms": (
                    metrics.get("p95_ms", 0.0)
                    if isinstance(metrics, dict)
                    else 0.0
                ),
            }
            rows.append(row)
            if updated_at >= latest_at:
                latest_at = updated_at
                latest_state = state
    rows.sort(
        key=lambda item: (
            item.get("updated_at", ""),
            item.get("host_name", ""),
            item.get("session_id", ""),
        ),
        reverse=True,
    )
    return rows, latest_state


def build_status(root="."):
    root = Path(root).resolve()
    requests = _load(root, ".prd_plugin/state/requests.json", "requests")
    tracking = _load(root, ".prd_plugin/state/tracking.json", "records")
    health = _load(root, ".prd_plugin/state/health.json", "findings")
    tracking_branches, tracking_branch_errors = _load_tracking_branches(root)

    req_by_status = {}
    for r in requests:
        if isinstance(r, dict):
            key = str(r.get("status") or "?")  # explicit null must not crash sorting
            req_by_status[key] = req_by_status.get(key, 0) + 1
    active_trk = [t.get("id") for t in tracking
                  if isinstance(t, dict) and _normalize_status(t.get("status")) in ("open", "active")]
    open_hlt = [h.get("id") for h in health
                if isinstance(h, dict) and _normalize_status(h.get("status")) in ("open", "blocked")]
    branch_counts = {}
    for branch in tracking_branches:
        state = branch["state"]
        branch_counts[state] = branch_counts.get(state, 0) + 1

    # autonomy level
    autonomy = "key_decision"
    config = {}
    cfg_path = root / ".prd_plugin" / "config.json"
    if cfg_path.is_file():
        try:
            config = _read_json(cfg_path)
            autonomy = (config.get("automation") or {}).get("autonomy_level", autonomy)
        except Exception:
            config = {}

    guard_config = config.get("reasoning_guard") or {}
    guard_mode = guard_config.get("mode", "report")
    if guard_mode not in {"on", "report", "off"}:
        guard_mode = "report"
    guard_categories = guard_config.get("categories") or {}
    guard_backfill = guard_config.get("backfill") or {}
    backfill_mode = guard_backfill.get("mode", "recent")
    if backfill_mode not in {"live", "recent", "full"}:
        backfill_mode = "recent"
    backfill_limit = guard_backfill.get("limit", 20)
    if (
        not isinstance(backfill_limit, int)
        or isinstance(backfill_limit, bool)
        or not 1 <= backfill_limit <= 100000
    ):
        backfill_limit = 20
    category_names = (
        "evidence_follow_through",
        "causal_claims",
        "permanent_mutations",
        "completion_claims",
    )
    category_modes = {}
    effective_modes = {}
    for category in category_names:
        category_mode = guard_categories.get(category, "inherit")
        if category_mode not in {"inherit", "on", "report", "off"}:
            category_mode = "inherit"
        category_modes[category] = category_mode
        effective_modes[category] = (
            "off" if guard_mode == "off"
            else guard_mode if category_mode == "inherit"
            else category_mode
        )
    guard_report = {}
    try:
        value = _read_json(root / ".prd_plugin/local/reason_guard.json")
        if (
            isinstance(value, dict)
            and value.get("schema_version") in {"1.0", "2.0", "3.0"}
        ):
            guard_report = value
    except Exception:
        pass
    guard_sessions, latest_guard_state = _load_reason_guard_sessions(root)
    if not guard_report and latest_guard_state:
        guard_report = latest_guard_state
    guard_metrics = guard_report.get("metrics") or {}
    guard_hosts = {item["host"] for item in guard_sessions}
    reasoning_guard = {
        "mode": guard_mode,
        "backfill": {
            "mode": backfill_mode,
            "limit": backfill_limit,
        },
        "categories": category_modes,
        "effective_modes": effective_modes,
        "report_available": bool(guard_report or guard_sessions),
        "host_count": len(guard_hosts),
        "session_count": len(guard_sessions),
        "sessions": guard_sessions,
        "coverage": guard_report.get("coverage") or {},
        "summary": guard_report.get("summary") or {
            "open": 0, "deferred": 0, "violations": 0, "blocked": 0,
        },
        "latency": {
            "samples_ms": guard_metrics.get("samples_ms") or [],
            "p50_ms": guard_metrics.get("p50_ms", 0.0),
            "p95_ms": guard_metrics.get("p95_ms", 0.0),
        },
        "classification": guard_metrics.get("classification") or {
            "summaries_seen": 0,
            "candidates": 0,
            "created": 0,
            "reconciled": 0,
            "ignored": 0,
            "uncertain": 0,
        },
        "ingestion": guard_report.get("reasoning_summary_ingestion") or {},
        "clearance": guard_report.get("clearance") or {},
    }

    # optional: staleness
    stale = None
    try:
        import sys
        sys.path.insert(0, str(Path(__file__).resolve().parent))
        import staleness_audit
        stale = len(staleness_audit.audit(str(root)).get("stale", []))
    except Exception:
        stale = None

    # optional: skill usage
    skills = {}
    log = root / ".prd_plugin" / "local" / "skill-usage.jsonl"
    if log.is_file():
        for line in log.read_text(encoding="utf-8-sig").splitlines():
            try:
                e = json.loads(line)
                skills[e.get("skill", "?")] = skills.get(e.get("skill", "?"), 0) + 1
            except Exception:
                continue

    # optional: a pending deep LLM-wiki backfill (established repo gained the wiki)
    wiki_backfill_pending = (
        (root / ".prd_plugin" / "local" / "wiki-backfill-needed").is_file()
        and not (root / "wiki" / "index.md").is_file()
    )

    # optional: implemented work with no corresponding wiki ingest (REQ-145).
    # wiki_backfill_pending cannot cover this - it measures the one-time backfill
    # for a repo that just gained a wiki, so it reads false while ongoing ingest
    # rots. Reported only when there is drift, so a clean repo stays quiet.
    wiki_ingest_drift = None
    try:
        import wiki_ingest_drift as _wiki_drift
        _report = _wiki_drift.build(str(root))
        if _report.get("uningested"):
            wiki_ingest_drift = _report
    except Exception:
        wiki_ingest_drift = None

    # optional: a newer plugin version on npm (cache-only, no network here)
    version_update = None
    try:
        import sys as _s
        _s.path.insert(0, str(Path(__file__).resolve().parent))
        import prd_version_check
        vs = prd_version_check.status(str(root))
        if vs.get("update_available"):
            version_update = {"installed": vs.get("installed"), "latest": vs.get("latest")}
        # The host plugin is a SEPARATE install channel from the repo, and it
        # can be stale while the repo is current — that combination shipped
        # silently for four versions (REQ-140).
        host = vs.get("host_plugin") or {}
        if host.get("update_available"):
            version_update = dict(version_update or {})
            version_update["host_plugin"] = {
                "installed": host.get("version"), "latest": vs.get("latest"),
                "action": host.get("reason"),
            }
    except Exception:
        version_update = None

    return {
        "repo_root": str(root),
        "autonomy_level": autonomy,
        "reasoning_guard": reasoning_guard,
        "requests_by_status": dict(sorted(req_by_status.items())),
        "active_tracking": active_trk,
        "tracking_branches": tracking_branches,
        "tracking_branch_counts": dict(sorted(branch_counts.items())),
        "tracking_branch_errors": tracking_branch_errors,
        "open_health": open_hlt,
        "stale_count": stale,
        "skill_usage": dict(sorted(skills.items(), key=lambda x: -x[1])),
        "wiki_backfill_pending": wiki_backfill_pending,
        "wiki_ingest_drift": wiki_ingest_drift,
        "version_update": version_update,
    }


def format_markdown(s):
    guard = s.get("reasoning_guard") or {}
    guard_summary = guard.get("summary") or {}
    guard_clearance = guard.get("clearance") or {}
    guard_classification = guard.get("classification") or {}
    guard_latency = guard.get("latency") or {}
    guard_ingestion = guard.get("ingestion") or {}
    lines = ["# PRD Plugin Status", "", f"Autonomy: `{s['autonomy_level']}`",
             "",
             "## Reasoning Guard",
             f"- mode: {guard.get('mode', 'report')}",
             f"- summary backfill: {guard_ingestion.get('mode', (guard.get('backfill') or {}).get('mode', 'recent'))}; "
             f"processed: {guard_ingestion.get('summaries_processed', 0)}; "
             f"historically complete: {str(bool(guard_ingestion.get('historically_complete'))).lower()}",
             f"- reasoning clearance: {guard_clearance.get('status', 'unavailable')}",
             f"- open evidence promises: {guard_summary.get('open', 0)}",
             f"- findings: {guard_summary.get('violations', 0)}; "
             f"blocked boundaries: {guard_summary.get('blocked', 0)}",
             f"- summary classifications: {guard_classification.get('candidates', 0)} "
             f"candidates from {guard_classification.get('summaries_seen', 0)} observed",
             f"- uncertain active summaries: "
             f"{guard_classification.get('uncertain', 0)}",
             f"- observed event latency p50: {guard_latency.get('p50_ms', 0.0)} ms; "
             f"p95: {guard_latency.get('p95_ms', 0.0)} ms",
             "",
             "## Reasoning Guard sessions"]
    guard_sessions = guard.get("sessions") or []
    if guard_sessions:
        for session in guard_sessions:
            active = " / active" if session.get("active") else ""
            session_id = session.get("session_id") or "unavailable"
            lines.append(
                f"- {session.get('session_label', 'Unknown session')} — "
                f"{session.get('clearance', 'unavailable')} clearance; "
                f"{session.get('coverage', 'reduced')} coverage{active}; "
                f"model {session.get('model', 'unknown')}; "
                f"effort {session.get('effort_name', 'Not reported')}; "
                f"{session.get('summaries_processed', 0)} summaries; "
                f"p95 {session.get('p95_ms', 0.0)} ms; "
                f"session `{session_id}`"
            )
    else:
        lines.append("- none retained")
    lines += ["", "## Requests"]
    if s["requests_by_status"]:
        for st, n in s["requests_by_status"].items():
            lines.append(f"- {st}: {n}")
    else:
        lines.append("- none")
    lines += ["", f"## Active tracking ({len(s['active_tracking'])})",
              ("- " + ", ".join(s["active_tracking"])) if s["active_tracking"] else "- none",
              "", f"## Tracking branches ({len(s.get('tracking_branches', []))})"]
    if s.get("tracking_branches"):
        for branch in s["tracking_branches"]:
            target = branch.get("target_tracking_id") or (branch.get("promotion") or {}).get("tracking_id")
            target_text = f" -> {target}" if target else ""
            lines.append(
                f"- {branch['id']}: {branch.get('state', 'unknown')} "
                f"({branch.get('owner') or branch.get('owner_agent') or 'unassigned'}){target_text}"
            )
    else:
        lines.append("- none")
    if s.get("tracking_branch_errors"):
        for error in s["tracking_branch_errors"]:
            lines.append(f"- unreadable {error['file']}: {error['error']}")
    lines += [
              "", f"## Open health findings ({len(s['open_health'])})",
              ("- " + ", ".join(s["open_health"])) if s["open_health"] else "- none",
              "", "## Stale items",
              (f"- {s['stale_count']}" if s["stale_count"] is not None else "- (staleness_audit not installed)"),
              "", "## Skills used this repo"]
    if s["skill_usage"]:
        for sk, n in s["skill_usage"].items():
            lines.append(f"- {sk}: {n}")
    else:
        lines.append("- none logged")
    if s.get("version_update"):
        vu = s["version_update"]
        lines += ["", "## Plugin version",
                  f"- update available: {vu.get('latest')} (you're on {vu.get('installed')}) "
                  "- npm update prd-plugin && npx prd-install . --force"]
    if s.get("wiki_backfill_pending"):
        lines += ["", "## LLM wiki",
                  "- backfill pending - run `prd_wiki_backfill.py --plan`, then the "
                  "project-llm-wiki Backfill mode"]
    return "\n".join(lines) + "\n"


def _harden_stdout():
    """Never let a stray non-ASCII glyph crash the status tool on a Windows
    console (cp1252). Belt-and-suspenders: output is kept ASCII, and this makes
    any future stray character degrade to a replacement instead of raising."""
    for stream in ("stdout", "stderr"):
        s = getattr(sys, stream, None)
        try:
            s.reconfigure(encoding="utf-8", errors="backslashreplace")
        except Exception:
            pass


def main(argv=None):
    _harden_stdout()
    parser = argparse.ArgumentParser(description="One-screen PRD Plugin status (read-only).")
    parser.add_argument("--repo-root", default=".")
    parser.add_argument("--format", choices=("json", "markdown"), default="markdown")
    args = parser.parse_args(argv)
    s = build_status(args.repo_root)
    print(json.dumps(s, indent=2) if args.format == "json" else format_markdown(s))
    return 0


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