#!/usr/bin/env python3
"""validate_okf.py — OKF v0.1 conformance checker.

Checks a bundle directory against the Open Knowledge Format v0.1 conformance
rules (SPEC §9):

  1. Every non-reserved `.md` file has a parseable YAML frontmatter block.
  2. Every frontmatter block has a non-empty `type` field.
  3. Reserved files (`index.md`, `log.md`) follow their structural shape.

Errors are hard conformance failures. Warnings are soft guidance (missing
recommended fields, broken links, etc.) and never fail the bundle on their own.

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

Exit code: 0 when conformant (no errors), 1 otherwise. With --strict, warnings
also cause a non-zero exit.
"""

from __future__ import annotations

import argparse
import json
import os
import sys

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

RECOMMENDED_FIELDS = ("title", "description")


def validate(bundle_root: str) -> tuple[list[str], list[str], dict]:
    errors: list[str] = []
    warnings: list[str] = []
    concept_ids: set[str] = set()
    types: dict[str, int] = {}
    concept_count = 0

    if not os.path.isdir(bundle_root):
        return [f"Bundle directory not found: {bundle_root}"], [], {}

    md_files = list(iter_markdown(bundle_root))
    if not md_files:
        warnings.append("Bundle contains no markdown files.")

    for abs_path, rel_path in md_files:
        try:
            fm, body, _raw = read_document(abs_path)
        except (OSError, UnicodeDecodeError) as exc:
            errors.append(f"{rel_path}: cannot read file ({exc}).")
            continue

        basename = os.path.basename(rel_path).lower()
        is_root_index = rel_path == "index.md"

        if is_reserved(rel_path):
            # Reserved files must NOT carry frontmatter, except the bundle-root
            # index.md which MAY declare okf_version.
            if fm is not None:
                if is_root_index:
                    extraneous = [k for k in fm if k != "okf_version"]
                    if extraneous:
                        warnings.append(
                            f"{rel_path}: root index.md frontmatter should only "
                            f"contain 'okf_version'; found {extraneous}."
                        )
                else:
                    warnings.append(
                        f"{rel_path}: reserved file '{basename}' should not have "
                        "frontmatter."
                    )
            continue

        if is_agent_doc(rel_path):
            # Agent-convention file (AGENTS.md, CLAUDE.md, README.md, …): not an
            # OKF concept — skip silently so the bundle can be self-describing.
            continue

        # --- Concept document ---
        concept_count += 1
        concept_ids.add(rel_path[:-3])  # drop .md → concept id

        if fm is None:
            errors.append(
                f"{rel_path}: missing or unparseable YAML frontmatter block "
                "(must start with '---' on line 1 and close with '---')."
            )
            continue

        ctype = str(fm.get("type", "")).strip()
        if not ctype:
            errors.append(f"{rel_path}: frontmatter is missing a non-empty 'type' field.")
        else:
            types[ctype] = types.get(ctype, 0) + 1

        for field in RECOMMENDED_FIELDS:
            if not str(fm.get(field, "")).strip():
                warnings.append(f"{rel_path}: recommended field '{field}' is empty or absent.")

        if not body.strip():
            warnings.append(f"{rel_path}: concept body is empty.")

    # --- Cross-link integrity (warnings only; OKF tolerates broken links) ---
    for abs_path, rel_path in md_files:
        if is_reserved(rel_path) or is_agent_doc(rel_path):
            continue
        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_link(rel_path, target)
            if resolved is None:
                continue
            target_path = os.path.join(bundle_root, resolved)
            if not os.path.exists(target_path):
                warnings.append(f"{rel_path}: broken internal link → {target}")

    stats = {
        "concepts": concept_count,
        "types": types,
        "errors": len(errors),
        "warnings": len(warnings),
    }
    return errors, warnings, stats


def _resolve_link(source_rel: str, target: str) -> str | None:
    """Resolve an internal link to a bundle-relative path, or None if external."""
    target = target.split("#", 1)[0]
    if not target:
        return None
    if target.endswith("/"):
        target = 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 main() -> int:
    parser = argparse.ArgumentParser(description="Validate an OKF v0.1 bundle.")
    parser.add_argument("bundle", help="Path to the OKF bundle directory.")
    parser.add_argument("--strict", action="store_true", help="Treat warnings as failures.")
    parser.add_argument("--json", action="store_true", help="Emit a JSON report.")
    args = parser.parse_args()

    errors, warnings, stats = validate(args.bundle)

    if args.json:
        print(json.dumps({"errors": errors, "warnings": warnings, "stats": stats}, indent=2))
    else:
        print(f"OKF v0.1 conformance report for: {args.bundle}")
        print(f"  concepts: {stats.get('concepts', 0)}  types: {len(stats.get('types', {}))}")
        if errors:
            print(f"\n  ERRORS ({len(errors)}):")
            for e in errors:
                print(f"    ✗ {e}")
        if warnings:
            print(f"\n  WARNINGS ({len(warnings)}):")
            for w in warnings:
                print(f"    ! {w}")
        if not errors and not warnings:
            print("  ✓ Fully conformant, no warnings.")
        elif not errors:
            print("\n  ✓ Conformant (warnings are soft guidance).")
        else:
            print("\n  ✗ Not conformant — fix the errors above.")

    if errors:
        return 1
    if args.strict and warnings:
        return 1
    return 0


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