#!/usr/bin/env python3
"""arq-sentry — mesh-routed Sentry bridge citizen (v0 Phase A, template-based).

Per arq://doc/principle/arq-sentry-bridge-authority-bounds-v1 and
arq://doc/principle/cryptographic-citizenship-hack-impossible-reconstruct-possible-v1
(parent). Refactored 2026-05-22 to use the shared CitizenBridge template —
~150 LOC of duplicated helpers removed; logic now lives once in
_citizen_bridge.py.

Phase A scope (this file):
  - CLI surface: event list / issue list (2 read verbs; event-capture
    removed after Sentry CRITICAL catch on #4016 — needs DSN-based
    credential model, deferred to Phase A.1)
  - All credential gate + act emission + operator_tier_surface logic
    delegated to CitizenBridge template

Phase B (separate PR, blocked until operator provisions Sentry token):
  - Wire backend SDK call-sites through this bridge incrementally
  - Mint the bridge's own Ed25519 keypair at Mac Keychain
  - Swap signing identity from Twin → bridge citizen

Operator-tier prerequisites:
  - Create a Sentry internal-integration on the org with scopes per
    arq://doc/principle/arq-sentry-bridge-authority-bounds-v1
  - arq-connection put sentry.com arq-sentry-bridge-v1.auth-token <token>
  - arq-connection put sentry.com arq-sentry-bridge-v1.org-slug <slug>
"""
from __future__ import annotations

import argparse
import json
import sys
import urllib.error
import urllib.request
from pathlib import Path
from typing import Any

# Sibling-import the citizen-bridge template (lives next to this file).
sys.path.insert(0, str(Path(__file__).resolve().parent))
from _citizen_bridge import CitizenBridge, utc_ts  # noqa: E402


BRIDGE = CitizenBridge(
    citizen_address="arq://body/peer/arq-sentry-bridge-v1",
    principle_ref="arq://doc/principle/arq-sentry-bridge-authority-bounds-v1",
    vendor_service="sentry.com",
    credential_slug="arq-sentry-bridge-v1",
)

SENTRY_API_BASE = "https://sentry.io/api/0"

REMEDIATION = (
    "Operator: register a Sentry internal-integration with bounded scopes per "
    "arq-sentry-bridge-authority-bounds-v1, then provision the token via "
    "`arq-connection put sentry.com arq-sentry-bridge-v1.auth-token` and the "
    "org slug via `arq-connection put sentry.com arq-sentry-bridge-v1.org-slug`."
)


def _sentry_request(
    method: str, path: str, token: str, body: dict[str, Any] | None = None,
) -> tuple[int, Any, str]:
    """Tiny HTTP wrapper for Sentry's REST API. Defensive decode so
    non-UTF-8 error bodies don't crash the dispatch (Sentry catch on
    #4017 grafana, applied here symmetrically)."""
    url = SENTRY_API_BASE + path
    data: bytes | None = None
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
    if body is not None:
        data = json.dumps(body).encode()  # noqa: ARQ-NO-JSON-HOT-PATH Sentry REST API vendor wire format
        headers["Content-Type"] = "application/json"
    req = urllib.request.Request(url, method=method, data=data, headers=headers)
    try:
        with urllib.request.urlopen(req, timeout=10) as resp:
            text = resp.read().decode("utf-8", errors="replace")
            status = resp.getcode()
    except urllib.error.HTTPError as e:
        text = e.read().decode("utf-8", errors="replace") if e.fp else ""
        status = e.code
    except Exception as e:
        return 0, None, f"transport: {e}"
    try:
        parsed = json.loads(text) if text else None
    except json.JSONDecodeError:
        parsed = None
    err = "" if 200 <= status < 300 else (
        parsed.get("detail", "") if isinstance(parsed, dict) else text[:200]
    )
    return status, parsed, err


# ─── verbs ─────────────────────────────────────────────────────────────────


# cmd_event_capture removed in Phase A.0 (Sentry CRITICAL catch on #4016):
# Sentry's event submission API requires the DSN-based `/api/<project_id>/store/`
# endpoint with `X-Sentry-Auth` header (NOT the REST `/projects/.../events/`
# path which is GET-only). Implementing it correctly needs:
#   - A separate credential surface (DSN public_key alongside auth-token)
#   - Project-id resolution (REST GET /api/0/projects/{org}/{slug}/)
#   - X-Sentry-Auth header construction with sentry_version, sentry_client, etc.
# That's a credential-model expansion deferred to Phase A.1. See sibling
# script `scripts/peers/arq-sentry-sink.py` for the correct DSN-based shape.


def cmd_event_list(args: argparse.Namespace) -> int:
    creds = BRIDGE.require_credentials(
        verb="event-list",
        resources=("auth-token", "org-slug"),
        remediation_text=REMEDIATION,
    )
    if creds is None:
        return 3
    token, org = creds
    qs = f"?limit={args.limit}"
    status, parsed, err = _sentry_request(
        "GET", f"/projects/{org}/{args.project}/events/{qs}", token,
    )
    ref = f"arq-sentry-event-list-{args.project}-{utc_ts()}"
    BRIDGE.emit_act(
        "sentry_event_queried", ref,
        {
            "verb": "sentry.event-list",
            "project": args.project,
            "limit": args.limit,
            "http_status": status,
            "error": err if err else None,
            "result_count": len(parsed) if isinstance(parsed, list) else None,
        },
    )
    if 200 <= status < 300 and isinstance(parsed, list):
        for e in parsed[: args.limit]:
            print(
                f"{e.get('dateCreated', '?')}  {e.get('eventID', '?')[:8]}  "
                f"{e.get('title', e.get('message', ''))[:80]}"
            )
        return 0
    print(f"arq-sentry event-list failed: status={status} err={err}", file=sys.stderr)
    return 1


def cmd_issue_list(args: argparse.Namespace) -> int:
    creds = BRIDGE.require_credentials(
        verb="issue-list",
        resources=("auth-token", "org-slug"),
        remediation_text=REMEDIATION,
    )
    if creds is None:
        return 3
    token, org = creds
    qs = f"?limit={args.limit}"
    if args.query:
        from urllib.parse import quote
        qs += f"&query={quote(args.query)}"
    status, parsed, err = _sentry_request(
        "GET", f"/projects/{org}/{args.project}/issues/{qs}", token,
    )
    ref = f"arq-sentry-issue-list-{args.project}-{utc_ts()}"
    BRIDGE.emit_act(
        "sentry_issue_queried", ref,
        {
            "verb": "sentry.issue-list",
            "project": args.project,
            "query": args.query or None,
            "limit": args.limit,
            "http_status": status,
            "error": err if err else None,
            "result_count": len(parsed) if isinstance(parsed, list) else None,
        },
    )
    if 200 <= status < 300 and isinstance(parsed, list):
        for i in parsed[: args.limit]:
            print(
                f"{i.get('shortId', '?')}  {i.get('status', '?')}  "
                f"{i.get('title', '')[:80]}"
            )
        return 0
    print(f"arq-sentry issue-list failed: status={status} err={err}", file=sys.stderr)
    return 1


# ─── arg parser ────────────────────────────────────────────────────────────


def build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        prog="arq-sentry",
        description=(
            "Mesh-routed Sentry bridge citizen (v0 Phase A). Every verb "
            "wraps a Sentry API call in a substrate envelope signed by the "
            f"arq-sentry-bridge citizen. Per {BRIDGE.principle_ref}."
        ),
    )
    sub = p.add_subparsers(dest="verb_class", required=True)

    # event verbs (capture deferred to Phase A.1 — see comment above)
    ev = sub.add_parser("event").add_subparsers(dest="verb", required=True)
    lst = ev.add_parser("list", help="list recent events in a project")
    lst.add_argument("--project", required=True, help="Sentry project slug")
    lst.add_argument("--limit", type=int, default=20)
    lst.set_defaults(func=cmd_event_list)

    # issue verbs
    iss = sub.add_parser("issue").add_subparsers(dest="verb", required=True)
    il = iss.add_parser("list", help="list issues in a project (filtered by --query)")
    il.add_argument("--project", required=True, help="Sentry project slug")
    il.add_argument("--query", default=None, help="Sentry search query")
    il.add_argument("--limit", type=int, default=20)
    il.set_defaults(func=cmd_issue_list)

    return p


from pathlib import Path as _ScopesPath
import sys as _scopes_sys
_scopes_sys.path.insert(0, str(_ScopesPath(__file__).parent))
from _arq_provider_base import handle_meta_flags  # noqa: E402

PROVIDER = "sentry"
REQUIRED_SCOPES: dict[str, list[str]] = {
    # Sentry Auth Token scopes (organization-level tokens).
    "event list": ["event:read"],
    "issue list": ["event:read", "project:read"],
}

def main() -> int:
    handle_meta_flags(PROVIDER, REQUIRED_SCOPES)
    args = build_parser().parse_args()
    return args.func(args)


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