#!/usr/bin/env python3
"""arq-capability-binding-verify v0 · capability-binding freshness primitive.

Per operator directive 2026-05-20 (corrected architecture):
  Substrate worker identity → capability binding → platform-native app/
  integration → live provider action → evidence back to substrate.

ARQERA must not assume a worker can act unless its live external binding
is fresh enough. This primitive is the minimum freshness check for ONE
pressure: the policy-approver worker's GitHub App binding (the recurring
trust-graded-merge-v1 cross-peer-approval pressure that surfaced 4x in
the 2026-05-20 session).

Scope (bounded · per operator directive):
  - github-app verb only
  - one binding (the policy-approver/GitHub App pressure)
  - no broad cloud sync engine
  - no all-provider inventory
  - no speculative connector rebuild
  - use-or-update existing GitHub App / arq-github / arq-config surfaces

Usage:
  arq-capability-binding-verify github-app \
    --required-permission pull_requests:write \
    --required-permission contents:read \
    --org Arqera-IO

Emits:
  - arq://act/capability_binding_verified/<binding-id> on success
  - arq://act/capability_binding_drift/<binding-id> on any drift
  - arq://act/operator_tier_surface/<binding-id> when credentials are
    locally unavailable (operator must provide GITHUB_APP_ID +
    GITHUB_APP_PRIVATE_KEY or surface operator-tier instructions)
"""
from __future__ import annotations

import argparse
import base64
import json
import os
import shutil
import subprocess
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone

POLICY_VERSION = "arq-capability-binding-verify-v0-2026-05-20"
# Portable binary resolution: env override → PATH lookup → None.
# When None, emit_act emits a loud stderr WARN and skips · never silent.
TWIN_BIN = os.environ.get("TWIN_BIN") or shutil.which("twin")
# arq-connection broker for credential reads (cascade step 4 · 2026-05-21).
# When None, falls back to direct env reads as in v0 (legacy path).
ARQ_CONNECTION_BIN = os.environ.get("ARQ_CONNECTION_BIN") or shutil.which("arq-connection")
GITHUB_API = "https://api.github.com"


def _read_via_broker(service: str, resource: str) -> str | None:
    """Read a credential through the arq-connection broker.

    Returns the value on success · None if the broker binary is missing or
    the resource is not in the vault. Audit emission (credential_accessed /
    credential_access_denied) is handled inside arq-connection itself ·
    consumer does not need to emit its own.
    """
    if not ARQ_CONNECTION_BIN:
        return None
    try:
        r = subprocess.run(
            [ARQ_CONNECTION_BIN, "access", service, resource,
             "--requesting-worker", "arq://body/worker/arq-capability-binding-verify"],
            check=False, timeout=10, capture_output=True, text=True,
        )
        if r.returncode == 0 and r.stdout:
            return r.stdout.rstrip("\n")
    except Exception:
        pass
    return None


def now_iso() -> str:
    return datetime.now(timezone.utc).isoformat()


def now_compact() -> str:
    return datetime.now(timezone.utc).strftime("%Y-%m-%dT%H%M%SZ")


_EMIT_WARN_LOGGED = False


def emit_act(act_type: str, ref: str, payload: dict) -> str | None:
    global _EMIT_WARN_LOGGED
    if not TWIN_BIN or not os.path.exists(TWIN_BIN):
        if not _EMIT_WARN_LOGGED:
            print(
                "arq-capability-binding-verify: WARN twin binary not found "
                "(set TWIN_BIN or install `twin` on PATH) · audit acts will be skipped",
                file=sys.stderr,
            )
            _EMIT_WARN_LOGGED = True
        return None
    try:
        r = subprocess.run(
            [TWIN_BIN, "--use-keychain", "act", "emit", "act", act_type,
             f"{ref}-{now_compact()}",
             "--payload", json.dumps({**payload, "policy": POLICY_VERSION, "issued_at": now_iso()})],
            check=False, timeout=10, capture_output=True, text=True,
        )
        out = (r.stdout or "").splitlines()
        for line in out:
            if line.startswith("arq://act/"):
                return line.strip()
        return None
    except Exception as e:
        print(f"arq-capability-binding-verify: emit_act warning: {e}", file=sys.stderr)
        return None


def _b64url(b: bytes) -> str:
    return base64.urlsafe_b64encode(b).rstrip(b"=").decode("ascii")


def _create_app_jwt(app_id: str, private_key_pem: bytes) -> str:
    """Create a 10-minute JWT for GitHub App auth. RS256 / PKCS1v15."""
    from cryptography.hazmat.primitives import hashes, serialization
    from cryptography.hazmat.primitives.asymmetric import padding

    header = {"alg": "RS256", "typ": "JWT"}
    now = int(time.time())
    payload = {"iat": now - 60, "exp": now + 540, "iss": str(app_id)}
    header_b64 = _b64url(json.dumps(header, separators=(",", ":")).encode())
    payload_b64 = _b64url(json.dumps(payload, separators=(",", ":")).encode())
    signing_input = f"{header_b64}.{payload_b64}".encode()

    key = serialization.load_pem_private_key(private_key_pem, password=None)
    sig = key.sign(signing_input, padding.PKCS1v15(), hashes.SHA256())
    return f"{header_b64}.{payload_b64}.{_b64url(sig)}"


def _gh_api(jwt: str, path: str) -> tuple[int, dict | list | None, str | None]:
    """Authenticated GET to GitHub REST API."""
    req = urllib.request.Request(
        f"{GITHUB_API}{path}",
        headers={
            "Authorization": f"Bearer {jwt}",
            "Accept": "application/vnd.github+json",
            "X-GitHub-Api-Version": "2022-11-28",
            "User-Agent": POLICY_VERSION,
        },
        method="GET",
    )
    try:
        with urllib.request.urlopen(req, timeout=12) as r:
            body = r.read()
            return r.status, json.loads(body), None
    except urllib.error.HTTPError as e:
        return e.code, None, f"HTTP {e.code} {e.reason}: {(e.read() or b'')[:200]!r}"
    except Exception as e:
        return 0, None, f"{type(e).__name__}: {e}"


def _parse_required_perm(spec: str) -> tuple[str, str]:
    if ":" not in spec:
        raise ValueError(f"invalid --required-permission {spec!r}; expected <permission>:<level>")
    k, _, v = spec.partition(":")
    return k.strip(), v.strip()


def _level_satisfies(observed: str | None, required: str) -> bool:
    # GitHub installation permission ladder: read < write < admin.
    rank = {None: 0, "none": 0, "read": 1, "write": 2, "admin": 3}
    return rank.get(observed, 0) >= rank.get(required, 0)


def cmd_github_app(args: argparse.Namespace) -> int:
    binding_ref = f"github-app-{args.org.replace('/', '_')}"
    # Credential resolution order:
    #   1. arq-connection broker (canonical · emits credential_accessed audit)
    #   2. env var (dev/legacy fallback · v0 transitional)
    #   3. CLI arg (operator override for one-shot verification)
    app_id = (
        _read_via_broker("github-app", "id")
        or os.environ.get("GITHUB_APP_ID")
        or args.app_id
    )
    private_key_arg = (
        _read_via_broker("github-app", "pem")
        or os.environ.get("GITHUB_APP_PRIVATE_KEY")
        or args.app_private_key
    )
    private_key_path = args.app_private_key_path

    private_key_pem: bytes | None = None
    if private_key_path and os.path.exists(private_key_path):
        with open(private_key_path, "rb") as f:
            private_key_pem = f.read()
    elif private_key_arg:
        # Allow base64-encoded or raw PEM in env
        if "BEGIN" in private_key_arg:
            private_key_pem = private_key_arg.encode()
        else:
            try:
                private_key_pem = base64.b64decode(private_key_arg)
            except Exception:
                private_key_pem = private_key_arg.encode()

    if not app_id or not private_key_pem:
        addr = emit_act("operator_tier_surface", binding_ref, {
            "binding": "github-app",
            "org": args.org,
            "reason": "github_app_credentials_not_locally_available",
            "details": "github-app id/pem not reachable via arq-connection broker AND not present in env (GITHUB_APP_ID / GITHUB_APP_PRIVATE_KEY) AND --app-private-key-path not supplied. Operator must provision credentials before binding freshness can be verified.",
            "operator_action_required": "provision via `arq-connection connect github-app` (canonical) · or set GITHUB_APP_ID + GITHUB_APP_PRIVATE_KEY env vars (dev/legacy fallback) · or pass --app-private-key-path",
            "broker_attempted": ARQ_CONNECTION_BIN is not None,
        })
        print(f"arq-capability-binding-verify: ✗ credentials unavailable · operator-tier surface emitted at {addr or 'n/a'}", file=sys.stderr)
        return 99

    try:
        jwt = _create_app_jwt(app_id, private_key_pem)
    except Exception as e:
        emit_act("capability_binding_drift", binding_ref, {
            "binding": "github-app", "org": args.org,
            "reason": "jwt_signing_failed", "error": f"{type(e).__name__}: {e}",
        })
        print(f"arq-capability-binding-verify: ✗ JWT signing failed: {e}", file=sys.stderr)
        return 1

    status, installations, err = _gh_api(jwt, "/app/installations")
    if status != 200 or not isinstance(installations, list):
        emit_act("capability_binding_drift", binding_ref, {
            "binding": "github-app", "org": args.org,
            "reason": "list_installations_failed", "http_status": status, "error": err,
        })
        print(f"arq-capability-binding-verify: ✗ /app/installations failed status={status} err={err}", file=sys.stderr)
        return 1

    target = next(
        (i for i in installations
         if isinstance(i, dict) and (i.get("account") or {}).get("login", "").lower() == args.org.lower()),
        None,
    )
    if not target:
        emit_act("capability_binding_drift", binding_ref, {
            "binding": "github-app", "org": args.org,
            "reason": "installation_not_found_for_org",
            "installations_seen": [
                (i.get("account") or {}).get("login")
                for i in installations
                if isinstance(i, dict)
            ],
        })
        print(f"arq-capability-binding-verify: ✗ no GitHub App installation for org {args.org}", file=sys.stderr)
        return 1

    permissions = target.get("permissions") or {}
    required: list[tuple[str, str]] = [_parse_required_perm(p) for p in args.required_permission]
    missing: list[dict] = []
    for k, v in required:
        observed = permissions.get(k)
        if not _level_satisfies(observed, v):
            missing.append({"permission": k, "required": v, "observed": observed})

    binding_summary = {
        "binding": "github-app",
        "org": args.org,
        "app_id": str(app_id),
        "installation_id": target.get("id"),
        "installation_url": f"https://github.com/organizations/{args.org}/settings/installations/{target.get('id')}",
        "app_slug": target.get("app_slug"),
        "observed_permissions": permissions,
        "required_permissions": [{"permission": k, "level": v} for k, v in required],
        "verified_at": now_iso(),
    }

    if missing:
        emit_act("capability_binding_drift", binding_ref, {
            **binding_summary,
            "missing_permissions": missing,
            "operator_action_required": "grant missing permissions to the GitHub App installation or re-install with broader scope",
        })
        print(f"arq-capability-binding-verify: ✗ DRIFT · {len(missing)} required permissions missing/insufficient", file=sys.stderr)
        for m in missing:
            print(f"  required {m['permission']}:{m['required']} · observed {m['observed']}", file=sys.stderr)
        return 1

    addr = emit_act("capability_binding_verified", binding_ref, binding_summary)
    print(f"arq-capability-binding-verify: ✓ github-app binding fresh for org={args.org} · installation_id={target.get('id')}")
    print(f"  required perms all satisfied: {', '.join(f'{k}:{v}' for k,v in required)}")
    if addr:
        print(f"  evidence: {addr}")
    return 0


def main() -> int:
    parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
    sub = parser.add_subparsers(dest="verb", required=True)

    gh = sub.add_parser("github-app", help="Verify GitHub App installation binding freshness")
    gh.add_argument("--org", required=True, help="GitHub org / login that owns the installation (e.g. Arqera-IO)")
    gh.add_argument("--required-permission", action="append", default=[],
                    help="<permission>:<level> · may be passed multiple times (e.g. pull_requests:write)")
    gh.add_argument("--app-id", default=None, help="GitHub App ID (overrides $GITHUB_APP_ID)")
    gh.add_argument("--app-private-key", default=None,
                    help="GitHub App private key as PEM string or base64 (overrides $GITHUB_APP_PRIVATE_KEY)")
    gh.add_argument("--app-private-key-path", default=None,
                    help="Path to PEM file containing the GitHub App private key")
    gh.set_defaults(func=cmd_github_app)

    args = parser.parse_args()
    return args.func(args)


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