"""Read-only first-pass traceability scanner.

Does the tedious first pass of `project-traceability-sync`: collect every
canonical ID, collect every ID referenced by other records, and report
**dangling links** (a record references an ID that has no canonical record) and
**orphans** (a canonical artifact with no inbound or outbound links). Suggestion
only; never mutates maps or state.
"""

import argparse
import json
import re
from pathlib import Path


ID_RE = re.compile(r"\b([A-Z]{2,}(?:-[A-Z]{1,})*-\d{1,})\b")

STATE_FILES = {
    ".prd_plugin/state/requests.json": "requests",
    ".prd_plugin/state/tracking.json": "records",
    ".prd_plugin/state/decisions.json": "decisions",
    ".prd_plugin/state/health.json": "findings",
    ".prd_plugin/state/changelog.json": "changes",
    ".prd_plugin/state/evidence.json": "records",
    ".prd_plugin/state/memory.json": "records",
}
LINK_KEYS = ("linked_ids", "graduated_to", "affected_ids", "supports", "supported_ids",
             "supersedes", "superseded_by", "blocks", "routed_to", "linked_health_findings")


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


def _ids_in(value):
    """Every PRD-Plugin-style ID appearing anywhere in a value."""
    out = set()
    if isinstance(value, str):
        out.update(ID_RE.findall(value))
    elif isinstance(value, dict):
        for v in value.values():
            out |= _ids_in(v)
    elif isinstance(value, list):
        for v in value:
            out |= _ids_in(v)
    return out


def _canonical_ids_in(value):
    """Every value of an `id` field at any depth (captures nested artifact IDs)."""
    out = set()
    if isinstance(value, dict):
        rid = value.get("id")
        if isinstance(rid, str) and ID_RE.fullmatch(rid):
            out.add(rid)
        for v in value.values():
            out |= _canonical_ids_in(v)
    elif isinstance(value, list):
        for v in value:
            out |= _canonical_ids_in(v)
    return out


def scan(root="."):
    root = Path(root).resolve()
    canonical = set()
    referenced = {}  # id -> set(source record ids)
    outbound = {}     # record id -> bool has outbound link
    artifact_dir = root / ".prd_plugin" / "state" / "artifacts"

    records = []
    for rel, key in STATE_FILES.items():
        data = None
        path = root / rel
        if path.is_file():
            try:
                data = _read_json(path)
            except Exception:
                data = None
        if isinstance(data, dict):
            canonical |= _canonical_ids_in(data)
            for item in data.get(key, []):
                if isinstance(item, dict) and "id" in item:
                    records.append(item)
    if artifact_dir.is_dir():
        for p in artifact_dir.rglob("*.json"):
            try:
                item = _read_json(p)
            except Exception:
                continue
            canonical |= _canonical_ids_in(item)
            if isinstance(item, dict) and "id" in item:
                records.append(item)

    for item in records:
        rid = item["id"]
        linked = set()
        for k in LINK_KEYS:
            linked |= _ids_in(item.get(k))
        # also scan source_refs ids
        linked |= _ids_in(item.get("source_refs"))
        linked.discard(rid)
        outbound[rid] = bool(linked)
        for target in linked:
            referenced.setdefault(target, set()).add(rid)

    dangling = sorted(
        (
            {"id": tid, "referenced_by": sorted(srcs)}
            for tid, srcs in referenced.items()
            if tid not in canonical and ID_RE.fullmatch(tid)
        ),
        key=lambda d: d["id"],
    )

    inbound = set(referenced.keys())
    orphans = sorted(
        rid for rid in canonical
        if not outbound.get(rid) and rid not in inbound
    )

    return {
        "status": "attention" if (dangling or orphans) else "ok",
        "repo_root": str(root),
        "counts": {"canonical": len(canonical), "dangling": len(dangling), "orphans": len(orphans)},
        "dangling": dangling,
        "orphans": orphans,
    }


def format_markdown(report):
    lines = ["# PRD Plugin Traceability First-Pass", "",
             f"Status: `{report['status']}`", "",
             f"Canonical IDs: {report['counts']['canonical']} | "
             f"dangling links: {report['counts']['dangling']} | "
             f"orphans: {report['counts']['orphans']}", "",
             "## Dangling links (referenced but no canonical record)", ""]
    if not report["dangling"]:
        lines.append("- None")
    for d in report["dangling"]:
        lines.append(f"- `{d['id']}` referenced by {', '.join(d['referenced_by'])}")
    lines += ["", "## Orphans (no inbound or outbound links)", ""]
    if not report["orphans"]:
        lines.append("- None")
    for o in report["orphans"]:
        lines.append(f"- `{o}`")
    return "\n".join(lines) + "\n"


def main(argv=None):
    parser = argparse.ArgumentParser(description="First-pass traceability scan: dangling links and orphans (read-only).")
    parser.add_argument("--repo-root", default=".")
    parser.add_argument("--format", choices=("json", "markdown"), default="markdown")
    parser.add_argument("--output")
    args = parser.parse_args(argv)
    report = scan(args.repo_root)
    text = json.dumps(report, indent=2) if args.format == "json" else format_markdown(report)
    if args.output:
        Path(args.output).parent.mkdir(parents=True, exist_ok=True)
        Path(args.output).write_text(text, encoding="utf-8")
    print(text)
    return 0


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