#!/usr/bin/env python3
"""arq-nango-proxy — Nango proxy bridge for mesh-routed SaaS dispatches.

MVP — ONE provider (slack-oauth), ONE verb (GET /users.list). Every
additional SaaS platform is ONE whitelist row away once this proves.

Canonical policies this satisfies:
  arq://doc/policy/twin-dispatch-mesh-only-v1
  arq://doc/principle/substrate-is-the-exchange-v1
  arq://doc/principle/every-platform-is-a-peer-v1
  arq://doc/principle/one-primitive-speaks-every-protocol-v1

How it works:
  Nango ( https://api.nango.dev ) is ARQERA's managed-OAuth layer. It
  holds 30 live connection credentials against 750+ integration schemas
  (slack, stripe, sendgrid, xero, google-workspace, ...). Every proxy
  call is: `POST/GET https://api.nango.dev/proxy/<path>` with headers:
    Authorization: Bearer <NANGO_SECRET_KEY>
    Provider-Config-Key: <integration>
    Connection-Id: <tenant-scoped id>
  Nango forwards to the target platform using the stored OAuth/API-key
  credentials. This wrapper adds mesh discipline on top: whitelist gate
  + substrate acts before/after + evidence hash.

Every call emits:
  arq://act/nango_proxy_sent/<provider>-<path>-<ts>    (pre-dispatch)
  arq://act/nango_proxy_ack/<provider>-<path>-<ts>     (post-dispatch)
  OR
  arq://act/nango_proxy_rejected/<provider>-<path>-<ts> (whitelist miss)

Usage:
  arq-nango-proxy <provider-config-key> <METHOD> <path> [--body JSON] [--connection-id ID]

Example (MVP):
  arq-nango-proxy slack-oauth GET /users.list

Credential resolution (first non-empty wins):
  1. env NANGO_SECRET_KEY
  2. env ARQ_VAULT_NANGO_SECRET_KEY     (matches arq-call convention)
  3. SOPS ~/Desktop/Project/.secrets.yaml  path: ["nango"]["nango_secret_key"]

Connection-id convention:
  defaults to `tenant-<n>-<provider-config-key>` per
  backend/app/services/nango_tenant.py::build_connection_id_for_integration.
  Default tenant = 1 (ARQERA org). Override with --connection-id.
"""

from __future__ import annotations

import argparse
import hashlib
import json
import os
import subprocess
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
from pathlib import Path

# Shared primitive helper (daemon.act.emit path → falls back to twin subprocess)
sys.path.insert(0, str(Path(__file__).resolve().parent))
try:
    from _arq_primitive import primitive_invoke as _primitive_base  # noqa: E402
except ImportError:
    _primitive_base = None  # type: ignore

TWIN_CLI = Path.home() / ".local" / "bin" / "twin"
NANGO_BASE_URL = os.environ.get("NANGO_BASE_URL", "https://api.nango.dev")
DEFAULT_TENANT = int(os.environ.get("ARQ_NANGO_DEFAULT_TENANT", "1"))
ACTOR_PEER_ADDRESS = os.environ.get(
    "ARQ_ACTOR_PEER_ADDRESS",
    "arq://body/peer/578412e7b083b40e56e228779804582a",  # Twin's peer on this Mac
)

# ── v0 WHITELIST ─────────────────────────────────────────────────────────────
#
# MVP scope: ONE SaaS tenant + ONE verb. Each new row is one entity that
# becomes mesh-governable. Keep the set *small* and explicit; treat this
# file as a substrate-mirrored config — every row here is a governed
# claim. Future Epic H.5: move this to `arq://doc/nango-proxy-whitelist-v1`.
#
# Tuple key: (provider_config_key, HTTP method, path)
WHITELIST: set[tuple[str, str, str]] = {
    ("slack-oauth", "GET", "/users.list"),
}


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


def _normalise_ref(provider: str, path: str) -> str:
    """Build a stable act reference slug from provider + path.

    Slack's "/users.list" → "slack-users-list". Keeps the verifier's
    `twin context recall` grep predictable.
    """
    slug = path.strip("/").replace("/", "-").replace(".", "-")
    return f"{provider.replace('-', '')}-{slug}" if False else f"{provider}-{slug}"


# ── substrate-act emission (daemon.act.emit → twin subprocess fallback) ──

def _primitive_invoke(verb: str, payload: dict, timeout_s: float = 10.0):
    if _primitive_base is None:
        return ("unavailable", None)
    return _primitive_base(verb, payload, ACTOR_PEER_ADDRESS, timeout_s=timeout_s)


def _emit_via_daemon(class_: str, type_: str, reference: str, payload: dict) -> tuple[str, str | None]:
    try:
        outcome, result = _primitive_invoke(
            "daemon.act.emit",
            {"class": class_, "type": type_, "reference": reference, "payload": payload},
        )
    except Exception:
        return ("unavailable", None)
    if outcome == "ok" and isinstance(result, dict):
        return ("ok", result.get("address"))
    if outcome == "unknown_verb":
        return ("unknown_verb", None)
    if outcome == "error":
        return ("error", None)
    return ("unavailable", None)


def _emit_via_twin(class_: str, type_: str, reference: str, payload: dict) -> str | None:
    if not TWIN_CLI.exists():
        return None
    try:
        result = subprocess.run(
            [
                str(TWIN_CLI),
                "--use-keychain",
                "act",
                "emit",
                class_,
                type_,
                reference,
                "--payload",
                json.dumps(payload),  # noqa: ARQ-NO-JSON-HOT-PATH twin CLI input boundary — twin accepts --payload <json>
            ],
            capture_output=True,
            text=True,
            timeout=8,
            check=False,
        )
        if result.returncode != 0:
            print(f"[arq-nango-proxy] substrate emit failed: {result.stderr.strip()}", file=sys.stderr)
            return None
        for line in result.stdout.splitlines():
            line = line.strip()
            if line.startswith("arq://"):
                return line
    except (subprocess.TimeoutExpired, OSError) as exc:
        print(f"[arq-nango-proxy] substrate emit exception: {exc}", file=sys.stderr)
    return None


def _emit_act(class_: str, type_: str, reference: str, payload: dict) -> str | None:
    """daemon.act.emit → twin subprocess fallback. `error` verdict is
    authoritative: skip fallback so we don't mask addressing bugs."""
    outcome, address = _emit_via_daemon(class_, type_, reference, payload)
    if outcome == "ok":
        return address
    if outcome == "error":
        return None
    return _emit_via_twin(class_, type_, reference, payload)


# ── credential resolution ────────────────────────────────────────────────────

def _resolve_nango_secret() -> str | None:
    """Resolve NANGO_SECRET_KEY. Env first (cheapest), then SOPS.

    Returns None if no source is reachable; caller emits a rejected act.
    """
    for env_name in ("NANGO_SECRET_KEY", "ARQ_VAULT_NANGO_SECRET_KEY"):
        v = os.environ.get(env_name)
        if v:
            return v

    # SOPS fallback — ~/Desktop/Project/.secrets.yaml path ["nango"]["nango_secret_key"]
    sops_path = Path.home() / "Desktop" / "Project" / ".secrets.yaml"
    age_key = Path.home() / ".config" / "sops" / "age" / "keys.txt"
    if not sops_path.exists() or not age_key.exists():
        return None
    try:
        result = subprocess.run(
            [
                "sops",
                "-d",
                "--extract",
                '["nango"]["nango_secret_key"]',
                str(sops_path),
            ],
            capture_output=True,
            text=True,
            timeout=10,
            check=False,
            env={**os.environ, "SOPS_AGE_KEY_FILE": str(age_key)},
        )
        if result.returncode == 0:
            token = result.stdout.strip()
            return token or None
    except (FileNotFoundError, subprocess.TimeoutExpired, OSError):
        return None
    return None


# ── dispatch ─────────────────────────────────────────────────────────────────

def _dispatch(
    provider: str,
    method: str,
    path: str,
    body: dict | None,
    connection_id: str,
    nango_key: str,
) -> tuple[int, bytes, str]:
    """Execute the Nango proxy HTTPS call. Python urllib (not curl) — the
    mesh-enforce hook permits urllib since this wrapper IS the mesh
    primitive for this SaaS surface.

    Returns (status_code, raw_body, error_detail).
    """
    url = f"{NANGO_BASE_URL.rstrip('/')}/proxy{path if path.startswith('/') else '/' + path}"
    data = json.dumps(body).encode("utf-8") if (body and method.upper() != "GET") else None  # noqa: ARQ-NO-JSON-HOT-PATH Nango proxy vendor wire format (proxies downstream SaaS API)
    headers = {
        "Authorization": f"Bearer {nango_key}",
        "Provider-Config-Key": provider,
        "Connection-Id": connection_id,
        "Accept": "application/json",
        "User-Agent": "arq-nango-proxy/0.1",
    }
    if data is not None:
        headers["Content-Type"] = "application/json"

    req = urllib.request.Request(url, method=method.upper(), data=data, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=30) as resp:
            return resp.status, resp.read(), ""
    except urllib.error.HTTPError as exc:
        raw = exc.read() if exc.fp else b""
        return exc.code, raw, f"HTTPError {exc.code}"
    except (urllib.error.URLError, TimeoutError, OSError) as exc:
        return 0, b"", f"{type(exc).__name__}: {exc}"


# ── main ─────────────────────────────────────────────────────────────────────

def main() -> int:
    ap = argparse.ArgumentParser(
        prog="arq-nango-proxy",
        description=(
            "Dispatch whitelisted SaaS API calls through Nango's proxy surface "
            "with substrate envelopes + governance + evidence. MVP: one "
            "(provider, method, path) tuple per row — Slack /users.list first."
        ),
    )
    ap.add_argument("provider", help="Nango provider_config_key (e.g. slack-oauth)")
    ap.add_argument("method", help="HTTP method (GET, POST, PUT, DELETE, PATCH)")
    ap.add_argument("path", help="API path on the target platform (e.g. /users.list)")
    ap.add_argument("--body", default=None, help="JSON body for non-GET methods")
    ap.add_argument(
        "--connection-id",
        default=None,
        help=(
            "Override connection_id. Default: tenant-<ARQ_NANGO_DEFAULT_TENANT>-<provider>. "
            f"Current default: tenant-{DEFAULT_TENANT}-<provider>."
        ),
    )
    args = ap.parse_args()

    provider = args.provider
    method = args.method.upper()
    path = args.path if args.path.startswith("/") else f"/{args.path}"
    connection_id = args.connection_id or f"tenant-{DEFAULT_TENANT}-{provider}"
    ts = _utc_ts()
    ref = f"{_normalise_ref(provider, path)}-{ts}"

    # Parse body
    body: dict | None = None
    if args.body:
        try:
            body = json.loads(args.body)  # noqa: ARQ-NO-JSON-HOT-PATH operator CLI input boundary — --body arg is documented JSON
        except json.JSONDecodeError as exc:
            print(f"arq-nango-proxy: --body must be valid JSON: {exc}", file=sys.stderr)
            return 2

    # Whitelist gate
    tup = (provider, method, path)
    if tup not in WHITELIST:
        rejected_addr = _emit_act(
            "act",
            "nango_proxy_rejected",
            ref,
            {
                "provider": provider,
                "method": method,
                "path": path,
                "connection_id": connection_id,
                "reason": "not_whitelisted",
                "whitelist_size": len(WHITELIST),
                "actor_peer": ACTOR_PEER_ADDRESS,
                "ts": ts,
            },
        )
        print(
            f"arq-nango-proxy: ({provider}, {method}, {path}) not in whitelist "
            f"({len(WHITELIST)} rows). rejected_act={rejected_addr or 'emit-failed'}",
            file=sys.stderr,
        )
        return 1

    # Credential resolution
    nango_key = _resolve_nango_secret()
    if not nango_key:
        rejected_addr = _emit_act(
            "act",
            "nango_proxy_rejected",
            ref,
            {
                "provider": provider,
                "method": method,
                "path": path,
                "connection_id": connection_id,
                "reason": "nango_secret_unreachable",
                "attempted_sources": ["NANGO_SECRET_KEY", "ARQ_VAULT_NANGO_SECRET_KEY", "sops:.secrets.yaml"],
                "actor_peer": ACTOR_PEER_ADDRESS,
                "ts": ts,
            },
        )
        print(
            f"arq-nango-proxy: NANGO_SECRET_KEY not reachable (env + SOPS tried). "
            f"rejected_act={rejected_addr or 'emit-failed'}",
            file=sys.stderr,
        )
        return 1

    # Pre-dispatch envelope
    sent_addr = _emit_act(
        "act",
        "nango_proxy_sent",
        ref,
        {
            "provider": provider,
            "method": method,
            "path": path,
            "connection_id": connection_id,
            "actor_peer": ACTOR_PEER_ADDRESS,
            "has_body": body is not None,
            "ts": ts,
        },
    )

    # Dispatch
    t0 = time.monotonic()
    status, raw_body, err = _dispatch(provider, method, path, body, connection_id, nango_key)
    latency_ms = int((time.monotonic() - t0) * 1000)

    # Response hash — evidence-grade integrity marker
    response_hash = hashlib.sha256(raw_body or b"").hexdigest() if raw_body else None

    # Parse body for stdout (but we hash the raw bytes)
    parsed: dict | None = None
    if raw_body:
        try:
            parsed = json.loads(raw_body)
        except json.JSONDecodeError:
            parsed = None

    # Post-dispatch envelope
    _emit_act(
        "act",
        "nango_proxy_ack",
        ref,
        {
            "provider": provider,
            "method": method,
            "path": path,
            "connection_id": connection_id,
            "envelope_sent": sent_addr,
            "http_status": status,
            "response_hash": response_hash,
            "response_bytes": len(raw_body or b""),
            "latency_ms": latency_ms,
            "upstream_ok": parsed.get("ok") if isinstance(parsed, dict) and "ok" in parsed else None,
            "actor_peer": ACTOR_PEER_ADDRESS,
            "ts": ts,
        },
    )

    # Exit code & stdout
    if 200 <= status < 300:
        if parsed is not None:
            print(json.dumps(parsed, indent=2))
        else:
            sys.stdout.buffer.write(raw_body or b"")
        return 0

    print(
        f"arq-nango-proxy: {provider} {method} {path} → HTTP {status} ({err or 'no details'})",
        file=sys.stderr,
    )
    if raw_body:
        sys.stderr.buffer.write(b"response: ")
        sys.stderr.buffer.write(raw_body[:500])
        sys.stderr.write("\n")
    return 1


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