#!/usr/bin/env python3
"""Validate capability inputs and generated outputs."""

from __future__ import annotations

import json
import re
import sys
from pathlib import Path
from typing import Any


ROOT = Path(__file__).resolve().parents[1]
CAP = ROOT / "capabilities"
REF = ROOT / "references" / "yandex-oauth-scopes.json"
TASK = ROOT / ".agent" / "tasks" / "GH38-api-method-scope-capability-matrix"
TOKEN_RE = re.compile(r"\by0_[A-Za-z0-9_-]{20,}\b")
YANDEX_DIAGNOSTIC_RE = re.compile(r"\bsc=[A-Za-z0-9_-]+")
FORM_ID_RE = re.compile(r'"survey_id"\s*:\s*"[0-9a-f]{24}"')
NUMERIC_ID_FIELD_RE = re.compile(r'"id"\s*:\s*[0-9]{6,}')
CLIENT_ID_RE = re.compile(r"^[0-9a-f]{32}$")
COMPOSITE_SCOPE_RE = re.compile(r"^(.+)\.\{([^{}]+)\}$")


def load(path: Path) -> Any:
    return json.loads(path.read_text(encoding="utf-8"))


def err(errors: list[str], msg: str) -> None:
    errors.append(msg)


def canonical_scopes() -> set[str]:
    return {item["id"] for item in load(REF)}


def expand_composite_scope(scope: str) -> list[str] | None:
    match = COMPOSITE_SCOPE_RE.fullmatch(scope)
    if not match:
        return None
    prefix, names = match.groups()
    return [f"{prefix}.{name.strip()}" for name in names.split(",") if name.strip()]


def validate_atomic(errors: list[str], scopes: set[str]) -> set[str]:
    client_ids = load(CAP / "atomic.client_id.json")
    if not isinstance(client_ids, dict):
        err(errors, "atomic.client_id.json must be an object")
        return set()
    for scope, client_id in client_ids.items():
        expanded = expand_composite_scope(scope)
        if expanded:
            for item in expanded:
                if item not in scopes:
                    err(errors, f"atomic.client_id.json composite scope {scope} references unknown scope {item}")
        elif scope not in scopes:
            err(errors, f"atomic.client_id.json has unknown scope {scope}")
        if not isinstance(client_id, str) or not client_id.strip():
            err(errors, f"atomic.client_id.json has empty client_id for {scope}")
    token_path = CAP / "atomic.token"
    if token_path.exists():
        tokens = load(token_path)
        missing = sorted(set(client_ids) - set(tokens))
        if missing:
            err(errors, f"atomic.token missing keys: {', '.join(missing)}")
        for scope, token in tokens.items():
            value = str(token).strip()
            if value == str(client_ids.get(scope, "")).strip():
                err(errors, f"atomic.token value for {scope} is the client_id, not an OAuth token")
            elif CLIENT_ID_RE.fullmatch(value):
                err(errors, f"atomic.token value for {scope} looks like a client_id, not an OAuth token")
    return set(client_ids) | {"public"}


def validate_methods(errors: list[str], scopes: set[str]) -> set[str]:
    data = load(CAP / "methods.json")
    seen: set[str] = set()
    by_service: dict[str, set[str]] = {}
    for method in data.get("methods", []):
        method_id = method.get("id")
        if not method_id:
            err(errors, "method missing id")
            continue
        if method_id in seen:
            err(errors, f"duplicate method id {method_id}")
        seen.add(method_id)
        for field in ("sub_skill", "protocol", "operation", "path", "classification"):
            if not method.get(field):
                err(errors, f"{method_id} missing {field}")
        by_service.setdefault(method.get("sub_skill"), set()).add(method_id)
        if method.get("sub_skill") in data.get("probe_plans", {}) and not method.get("probe_requests"):
            err(errors, f"{method_id} missing probe_requests")
        if "expected_scopes" in method:
            err(errors, f"{method_id} must not define expected_scopes")
    for service, plan in data.get("probe_plans", {}).items():
        planned = set(plan.get("methods", []))
        actual = by_service.get(service, set())
        missing = sorted(actual - planned)
        extra = sorted(planned - actual)
        if missing:
            err(errors, f"probe plan {service} missing methods: {', '.join(missing)}")
        if extra:
            err(errors, f"probe plan {service} references unknown methods: {', '.join(extra)}")
    return seen


def validate_matrix(errors: list[str], method_ids: set[str], scopes: set[str]) -> None:
    matrix = load(CAP / "matrix.json")
    if matrix.get("generated_by") != "capabilities/probe.py":
        err(errors, "matrix.json must be generated by capabilities/probe.py")
    for i, cell in enumerate(matrix.get("cells", [])):
        if cell.get("method_id") not in method_ids:
            err(errors, f"matrix cell {i} references unknown method {cell.get('method_id')}")
        if cell.get("scope_id") not in scopes:
            err(errors, f"matrix cell {i} references unknown scope/context {cell.get('scope_id')}")
        if cell.get("status") not in {"works", "does_not_work", "not_applicable", "not_tested", "unclear_needs_retest"}:
            err(errors, f"matrix cell {i} has invalid status {cell.get('status')}")
    covered = {cell.get("method_id") for cell in matrix.get("cells", [])}
    disk_methods = {
        method["id"]
        for method in load(CAP / "methods.json").get("methods", [])
        if method.get("sub_skill") == "disk"
    }
    missing_disk = sorted(disk_methods - covered)
    if missing_disk:
        err(errors, "matrix missing Disk methods: " + ", ".join(missing_disk))


def validate_requirements(errors: list[str], method_ids: set[str], scopes: set[str]) -> None:
    req_path = CAP / "method-scope-map.json"
    if not req_path.exists():
        err(errors, "method-scope-map.json missing")
        return
    req = load(req_path)
    if req.get("generated_by") != "capabilities/probe.py":
        err(errors, "method-scope-map.json must be generated by capabilities/probe.py")
    for method_id, value in req.get("methods", {}).items():
        if method_id not in method_ids:
            err(errors, f"method-scope-map references unknown method {method_id}")
        if value.get("public") is True:
            continue
        one_of = value.get("one_of")
        all_of = value.get("all_of")
        if not one_of and not all_of:
            err(errors, f"method-scope-map {method_id} must have public=true, non-empty one_of, or non-empty all_of")
            continue
        for scope in one_of or []:
            if scope not in scopes or scope == "public":
                err(errors, f"method-scope-map {method_id} references invalid scope {scope}")
        if all_of is not None and not isinstance(all_of, list):
            err(errors, f"method-scope-map {method_id} has invalid all_of list")
            continue
        for scope in all_of or []:
            if not isinstance(scope, str) or scope not in scopes or scope == "public":
                err(errors, f"method-scope-map {method_id} references invalid all_of scope {scope}")


def validate_no_secret_leaks(errors: list[str]) -> None:
    for path in [
        CAP / "README.md",
        CAP / "methods.json",
        CAP / "matrix.json",
        CAP / "method-scope-map.json",
        CAP / "atomic.client_id.json",
        TASK / "spec.md",
        TASK / "evidence.md",
        TASK / "evidence.json",
        TASK / "verdict.json",
    ]:
        if path.exists() and TOKEN_RE.search(path.read_text(encoding="utf-8")):
            err(errors, f"possible raw OAuth token in {path.relative_to(ROOT)}")
    for path in (CAP / "raw").glob("*.json"):
        text = path.read_text(encoding="utf-8")
        if TOKEN_RE.search(text):
            err(errors, f"possible raw OAuth token in {path.relative_to(ROOT)}")
        if YANDEX_DIAGNOSTIC_RE.search(text):
            err(errors, f"unredacted Yandex diagnostic id in {path.relative_to(ROOT)}")
        if FORM_ID_RE.search(text) or NUMERIC_ID_FIELD_RE.search(text):
            err(errors, f"possible unredacted live object id in {path.relative_to(ROOT)}")


def main() -> int:
    errors: list[str] = []
    try:
        scopes = canonical_scopes()
        contexts = validate_atomic(errors, scopes)
        method_ids = validate_methods(errors, scopes)
        validate_matrix(errors, method_ids, contexts)
        validate_requirements(errors, method_ids, contexts)
        validate_no_secret_leaks(errors)
    except Exception as exc:
        errors.append(str(exc))
    if errors:
        for item in errors:
            print(f"ERROR: {item}", file=sys.stderr)
        return 1
    print("Capability validation PASS")
    print(f"oauth_scopes={len(scopes)}")
    print(f"auth_contexts={len(contexts)}")
    print(f"methods={len(method_ids)}")
    return 0


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