#!/usr/bin/env python3
"""arq-wieldability-audit — 5-dimensional credential grading per principle
arq://doc/principle/reachable-is-not-wieldable-v1.

For every REACHABLE provider, score R-K-B-E-N:
  R — Reachable      API responds 2xx
  K — Known scope    enumerate via /me, /oauth/token-info, /scopes, /v1/me, etc.
  B — RBAC bound     1 key = 1 workforce = 1 use-case (NOT shared)
  E — Equipped       has EXACT scopes for declared use-case
  N — eNforced       structural prevention layer exists

Verdict grades per principle:
  5/5 WIELDABLE
  4/5 EQUIPPED-ONLY / REACHABLE-OVERBROAD / REACHABLE-OPAQUE
  1-3 DEGRADED
  0   UNREACHABLE

Today (2026-05-23 baseline): most providers score 1/5 because per-workforce
credential issuance + enforcement layer haven't shipped. The audit exposes
the gap honestly so operator can prioritise the structural build.

Usage:
  arq-wieldability-audit                        # all REACHABLE providers
  arq-wieldability-audit --provider stripe      # subset
  arq-wieldability-audit --json                 # JSON output
  arq-wieldability-audit --emit                 # substrate wieldability_assessment act
"""
from __future__ import annotations
import argparse
import json
import os
import subprocess
import sys
import urllib.error
import urllib.request
from datetime import UTC, datetime
from pathlib import Path

BRIDGE_DIR = Path(__file__).parent
sys.path.insert(0, str(BRIDGE_DIR))
from _arq_provider_base import emit_act, sops_extract

# Per-provider scope-introspection: (endpoint, key-resolver, scope-extractor)
SCOPE_PROBES: dict[str, dict] = {
    "github": {
        "url": "https://api.github.com/user",
        "header": "Authorization",
        "fmt": "token {}",
        "key_fn": lambda: sops_extract('["github_twin"]["pat"]') or sops_extract('["github_twin"]["token"]'),
        "scope_header": "X-OAuth-Scopes",
    },
    "cloudflare": {
        "url": "https://api.cloudflare.com/client/v4/user/tokens/verify",
        "header": "Authorization",
        "fmt": "Bearer {}",
        "key_fn": lambda: sops_extract('["cloudflare"]["cloudflare_api_token"]'),
        "scope_path": "result.policies",
    },
    "stripe": {
        "url": "https://api.stripe.com/v1/account",
        "header": "Authorization",
        "fmt": "Bearer {}",
        "key_fn": lambda: sops_extract('["stripe"]["stripe_secret_key"]') or sops_extract('["stripe"]["stripe_api_key"]'),
        "scope_path": "capabilities",
    },
    "slack": {
        "url": "https://slack.com/api/auth.test",
        "header": "Authorization",
        "fmt": "Bearer {}",
        "key_fn": lambda: sops_extract('["slack"]["slack_bot_token"]'),
        "scope_header": "X-OAuth-Scopes",
    },
    "anthropic": {
        "url": "https://api.anthropic.com/v1/models",
        "header": "x-api-key",
        "fmt": "{}",
        "key_fn": lambda: sops_extract('["anthropic"]["anthropic_api_key"]'),
        "scope_path": None,  # Anthropic doesn't expose scopes — assume inference-only
    },
}


def _http_introspect(url: str, header: str, value: str) -> tuple[int, dict, dict]:
    """Returns (status, body_dict, headers_dict)."""
    req = urllib.request.Request(url, headers={header: value})
    try:
        with urllib.request.urlopen(req, timeout=10) as r:
            body = json.loads(r.read()) if r.read else {}  # noqa: ARQ-NO-JSON-HOT-PATH vendor introspection response (provider returns JSON)
            req.add_header(header, value)
            with urllib.request.urlopen(req, timeout=10) as r2:
                return r2.status, json.loads(r2.read()) if (b := r2.read()) else {}, dict(r2.headers)
    except urllib.error.HTTPError as e:
        try:
            body = json.loads(e.read())  # noqa: ARQ-NO-JSON-HOT-PATH vendor error response (provider returns JSON)
        except Exception:
            body = {}
        return e.code, body, dict(e.headers) if e.headers else {}
    except Exception as e:
        return 500, {"err": str(e)[:120]}, {}


def _grade_provider(provider: str) -> dict:
    """Return per-provider R-K-B-E-N matrix + verdict."""
    grade = {"provider": provider, "R": False, "K": False, "B": False, "E": False, "N": False}

    # R — call the primitive's own probe verb
    script = BRIDGE_DIR / f"arq-{provider}"
    if script.exists():
        try:
            r = subprocess.run([str(script), "--help"], capture_output=True, text=True, timeout=5, check=False)
            grade["R"] = r.returncode == 0  # placeholder; real R lives in gravity-audit
        except Exception:
            grade["R"] = False
    grade["R"] = True  # assume caller already filtered for R via gravity-audit

    # K — scope introspection
    probe = SCOPE_PROBES.get(provider)
    if probe and probe["key_fn"]:
        key = probe["key_fn"]()
        if key:
            url = probe["url"]
            hdr = probe["header"]
            val = probe["fmt"].format(key)
            try:
                req = urllib.request.Request(url, headers={hdr: val})
                with urllib.request.urlopen(req, timeout=10) as resp:
                    body = json.loads(resp.read())  # noqa: ARQ-NO-JSON-HOT-PATH vendor scope-probe response (provider returns JSON)
                    headers = dict(resp.headers)
                # extract scope
                scopes: list[str] = []
                if probe.get("scope_header") and probe["scope_header"] in headers:
                    scopes = [s.strip() for s in headers[probe["scope_header"]].split(",") if s.strip()]
                elif probe.get("scope_path"):
                    cursor = body
                    for part in probe["scope_path"].split("."):
                        cursor = cursor.get(part, {}) if isinstance(cursor, dict) else {}
                    if isinstance(cursor, list):
                        scopes = [str(c) for c in cursor]
                    elif isinstance(cursor, dict):
                        scopes = list(cursor.keys())
                grade["K"] = bool(scopes) or probe.get("scope_path") is None
                grade["scopes_observed"] = scopes or ["(introspection-disabled-for-this-provider)"]
            except Exception as e:
                grade["K"] = False
                grade["scopes_observed"] = f"introspection-failed: {str(e)[:60]}"

    # B — RBAC bound to a single workforce
    # Today: no per-workforce credential issuance exists. Every key is shared.
    # B = True iff substrate has arq://body/credential/<workforce>-<provider> record AND
    # exactly one workforce references the same SOPS path.
    # Phase 1 stub: always False (honest baseline)
    grade["B"] = False
    grade["B_reason"] = "no per-workforce credential issuance — all keys shared across workforces (baseline)"

    # E — Equipped (exact scope for declared use)
    # Phase 2: read the primitive's --required-scopes registry and intersect
    # with K (observed scopes). E = True iff every declared scope is observed
    # AND no verb declares "unknown" (an honest "we don't know what this verb
    # needs" — must be populated to flip E).
    declared_scopes: set[str] = set()
    declared_unknown_verbs: list[str] = []
    if script.exists():
        try:
            rs = subprocess.run(
                [str(script), "--required-scopes"],
                capture_output=True, text=True, timeout=5, check=False,
            )
            if rs.returncode == 0 and rs.stdout.strip().startswith("{"):
                rs_doc = json.loads(rs.stdout)
                for verb, scopes in (rs_doc.get("required_scopes_by_verb") or {}).items():
                    for s in scopes:
                        if s == "unknown":
                            declared_unknown_verbs.append(verb)
                        else:
                            declared_scopes.add(s)
        except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError):
            pass
    grade["scopes_declared"] = sorted(declared_scopes)
    grade["verbs_with_unknown_scope"] = declared_unknown_verbs

    observed = set(grade.get("scopes_observed") or [])
    if declared_unknown_verbs:
        grade["E"] = False
        grade["E_reason"] = (
            f"{len(declared_unknown_verbs)} verb(s) declare 'unknown' scope — "
            "populate REQUIRED_SCOPES in the primitive to flip E"
        )
    elif not declared_scopes:
        grade["E"] = False
        grade["E_reason"] = "no REQUIRED_SCOPES declared by primitive"
    elif not grade["K"]:
        grade["E"] = False
        grade["E_reason"] = "scopes declared but K (observed) failed — cannot verify intersection"
    else:
        missing = declared_scopes - observed
        if missing:
            grade["E"] = False
            grade["E_reason"] = f"declared scopes not observed on token: {sorted(missing)}"
        else:
            grade["E"] = True
            grade["E_reason"] = f"all {len(declared_scopes)} declared scopes present in observed"

    # N — eNforced (structural prevention layer + credential hygiene trail)
    # Phase 2: read substrate for credential_rotated and credential_audit_finding
    # acts. N=True iff: recent (<90d) credential_rotated act exists AND no
    # outstanding credential_audit_finding acts for this provider.
    # Honest baseline preserved: structural enforcement (arq-verify hook) is
    # still pending — N only flips True when the substrate trail is clean.
    grade["N"] = False
    grade["N_reason"] = "no credential_rotated act on substrate (≥90d or never)"
    try:
        from datetime import timedelta
        ninety_days_ago = (datetime.now(UTC) - timedelta(days=90)).strftime("%Y-%m-%dT%H:%M:%SZ")
        rot = subprocess.run(
            ["twin", "--use-keychain", "index",
             "--type", "credential_rotated",
             "--ref-prefix", f"{provider}-",
             "--since", ninety_days_ago,
             "--limit", "1", "--json"],
            capture_output=True, text=True, timeout=10, check=False,
        )
        recent_rotation = False
        if rot.returncode == 0 and rot.stdout.strip().startswith("["):
            recent_rotation = bool(json.loads(rot.stdout))
        fnd = subprocess.run(
            ["twin", "--use-keychain", "index",
             "--type", "credential_audit_finding",
             "--limit", "20", "--json"],
            capture_output=True, text=True, timeout=10, check=False,
        )
        outstanding_findings = []
        if fnd.returncode == 0 and fnd.stdout.strip().startswith("["):
            for r in json.loads(fnd.stdout):
                ref = r.get("ref", "")
                if ref.lower().startswith(f"{provider.lower()}-") or f"-{provider.lower()}-" in ref.lower() or f"cf-" == ref[:3].lower() and provider == "cloudflare":
                    outstanding_findings.append(ref)
        grade["credential_rotation_recent"] = recent_rotation
        grade["outstanding_audit_findings"] = outstanding_findings
        if recent_rotation and not outstanding_findings:
            grade["N"] = True
            grade["N_reason"] = "recent credential_rotated act + no outstanding credential_audit_finding"
        elif recent_rotation and outstanding_findings:
            grade["N_reason"] = f"recent rotation but {len(outstanding_findings)} outstanding finding(s): {outstanding_findings[:2]}"
        elif not recent_rotation and outstanding_findings:
            grade["N_reason"] = f"no recent rotation AND {len(outstanding_findings)} outstanding finding(s)"
    except (subprocess.TimeoutExpired, json.JSONDecodeError, OSError) as e:
        grade["N_reason"] = f"substrate query failed ({type(e).__name__}); defaulting N=False"

    # Verdict
    score = sum([grade["R"], grade["K"], grade["B"], grade["E"], grade["N"]])
    grade["score"] = f"{score}/5"
    if score == 5:
        grade["verdict"] = "WIELDABLE"
    elif score == 4 and not grade["N"]:
        grade["verdict"] = "EQUIPPED-ONLY"
    elif score == 4 and not grade["B"]:
        grade["verdict"] = "REACHABLE-OVERBROAD"
    elif score == 4 and not grade["K"]:
        grade["verdict"] = "REACHABLE-OPAQUE"
    elif score == 0:
        grade["verdict"] = "UNREACHABLE"
    else:
        grade["verdict"] = "DEGRADED"
    return grade


def _print_table(grades: list[dict]) -> None:
    print(f"{'provider':<14} {'R':>2} {'K':>2} {'B':>2} {'E':>2} {'N':>2}  {'score':<5}  verdict")
    print("-" * 75)
    for g in grades:
        cells = ["✓" if g[k] else "·" for k in ("R", "K", "B", "E", "N")]
        print(f"{g['provider']:<14} {cells[0]:>2} {cells[1]:>2} {cells[2]:>2} {cells[3]:>2} {cells[4]:>2}  {g['score']:<5}  {g['verdict']}")
    print()
    counts: dict[str, int] = {}
    for g in grades:
        counts[g['verdict']] = counts.get(g['verdict'], 0) + 1
    print("  verdict rollup:", ", ".join(f"{k}={v}" for k, v in sorted(counts.items())))


def main() -> int:
    p = argparse.ArgumentParser(prog="arq-wieldability-audit")
    p.add_argument("--provider", nargs="*", help="subset")
    p.add_argument("--json", action="store_true")
    p.add_argument("--emit", action="store_true")
    args = p.parse_args()

    # Default: providers where we can introspect (SCOPE_PROBES) + known REACHABLE
    targets = args.provider or sorted(set(SCOPE_PROBES.keys()))
    grades = [_grade_provider(p) for p in targets]

    if args.json:
        print(json.dumps({"grades": grades}, indent=2))
    else:
        _print_table(grades)

    if args.emit:
        ts = datetime.now(UTC).strftime("%Y%m%dT%H%M%SZ")
        for g in grades:
            ref = f"{g['provider']}-baseline-{ts}"
            emit_act("act", "wieldability_assessment", ref, g)
        print(f"\n  substrate-emitted: {len(grades)} wieldability_assessment acts")

    return 0


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