#!/usr/bin/env python3
"""link_check.py — cross-link graph and broken-link report for an OKF bundle.

Builds the directed graph of internal markdown links between concepts, reports
broken links, orphan concepts (no inbound or outbound internal links), and
basic graph statistics. OKF tolerates broken links (SPEC §5.3), so by default a
broken link is a warning; use --strict to fail on them.

Usage:
    python link_check.py <bundle_dir> [--strict] [--json]
"""

from __future__ import annotations

import argparse
import json
import os
import sys

from okf_common import (
    extract_links,
    is_agent_doc,
    is_internal_link,
    is_reserved,
    iter_markdown,
    read_document,
)


def _resolve(source_rel: str, target: str) -> str | None:
    target = target.split("#", 1)[0]
    if not target:
        return None
    if target.endswith("/"):
        target += "index.md"
    if target.startswith("/"):
        return target.lstrip("/")
    base_dir = os.path.dirname(source_rel)
    return os.path.normpath(os.path.join(base_dir, target)).replace(os.sep, "/")


def analyze(bundle_root: str) -> dict:
    nodes: set[str] = set()
    edges: list[tuple[str, str]] = []
    broken: list[tuple[str, str]] = []
    existing: set[str] = set()

    md = list(iter_markdown(bundle_root))
    for _abs, rel in md:
        # Any markdown file that exists on disk is a valid link target — including
        # reserved index.md / log.md files (progressive-disclosure navigation).
        existing.add(rel)

    for abs_path, rel in md:
        if is_reserved(rel) or is_agent_doc(rel):
            continue
        nodes.add(rel)
        try:
            _fm, body, _raw = read_document(abs_path)
        except (OSError, UnicodeDecodeError):
            continue
        for _text, target in extract_links(body):
            if not is_internal_link(target):
                continue
            resolved = _resolve(rel, target)
            if resolved is None:
                continue
            if resolved in existing:
                edges.append((rel, resolved))
            else:
                broken.append((rel, target))

    outbound = {n: 0 for n in nodes}
    inbound = {n: 0 for n in nodes}
    for src, dst in edges:
        outbound[src] = outbound.get(src, 0) + 1
        inbound[dst] = inbound.get(dst, 0) + 1
    orphans = sorted(n for n in nodes if outbound.get(n, 0) == 0 and inbound.get(n, 0) == 0)

    return {
        "nodes": sorted(nodes),
        "edges": edges,
        "broken": broken,
        "orphans": orphans,
        "stats": {
            "concepts": len(nodes),
            "links": len(edges),
            "broken": len(broken),
            "orphans": len(orphans),
        },
    }


def main() -> int:
    parser = argparse.ArgumentParser(description="Check cross-links in an OKF bundle.")
    parser.add_argument("bundle", help="Path to the OKF bundle directory.")
    parser.add_argument("--strict", action="store_true", help="Fail on broken links.")
    parser.add_argument("--json", action="store_true", help="Emit a JSON report.")
    args = parser.parse_args()

    if not os.path.isdir(args.bundle):
        print(f"Bundle directory not found: {args.bundle}", file=sys.stderr)
        return 1

    result = analyze(args.bundle)

    if args.json:
        print(json.dumps(result, indent=2))
    else:
        s = result["stats"]
        print(f"Link report for: {args.bundle}")
        print(f"  concepts={s['concepts']}  links={s['links']}  broken={s['broken']}  orphans={s['orphans']}")
        if result["broken"]:
            print("\n  BROKEN LINKS:")
            for src, target in result["broken"]:
                print(f"    ✗ {src} → {target}")
        if result["orphans"]:
            print("\n  ORPHAN CONCEPTS (no inbound/outbound links):")
            for o in result["orphans"]:
                print(f"    · {o}")
        if not result["broken"] and not result["orphans"]:
            print("  ✓ No broken links, no orphans.")

    if args.strict and result["broken"]:
        return 1
    return 0


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