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

Per arq://doc/principle/arq-grafana-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: CLI surface (dashboard list/view, annotation push, alert
list, datasource list) + template-mediated credential gate +
operator_tier_surface fallback + substrate act emission.

Phase B (separate, blocked on operator):
  - Mint bridge keypair (Mac Keychain)
  - Swap signing identity Twin → bridge via `twin act emit --from-peer`
  - First runtime-pressure wiring: ARQERA deploy events → Grafana
    annotations on the staging-latency dashboard

Operator-tier prerequisites:
  - Create Grafana service-account / API key with Editor role
  - arq-connection put grafana.com arq-grafana-bridge-v1.api-key <key>
  - arq-connection put grafana.com arq-grafana-bridge-v1.base-url <url>
"""
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-grafana-bridge-v1",
    principle_ref="arq://doc/principle/arq-grafana-bridge-authority-bounds-v1",
    vendor_service="grafana.com",
    credential_slug="arq-grafana-bridge-v1",
)

REMEDIATION = (
    "Operator: create a Grafana service-account / API key with Editor role per "
    "arq-grafana-bridge-authority-bounds-v1, then `arq-connection put grafana.com "
    "arq-grafana-bridge-v1.api-key <key>` and `arq-connection put grafana.com "
    "arq-grafana-bridge-v1.base-url <url>`."
)


def _grafana_request(
    method: str, path: str, api_key: str, base_url: str,
    body: dict[str, Any] | None = None,
) -> tuple[int, Any, str]:
    """Defensive decode so non-UTF-8 error bodies don't crash dispatch
    (Sentry catch on #4017)."""
    url = base_url.rstrip("/") + path
    data: bytes | None = None
    headers = {"Authorization": f"Bearer {api_key}", "Accept": "application/json"}
    if body is not None:
        data = json.dumps(body).encode()  # noqa: ARQ-NO-JSON-HOT-PATH Grafana 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("message", "") if isinstance(parsed, dict) else text[:200]
    )
    return status, parsed, err


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


def cmd_dashboard_list(args: argparse.Namespace) -> int:
    creds = BRIDGE.require_credentials(
        verb="dashboard-list",
        resources=("api-key", "base-url"),
        remediation_text=REMEDIATION,
    )
    if creds is None:
        return 3
    api_key, base_url = creds
    qs = "?type=dash-db"
    if args.folder:
        qs += f"&folderIds={args.folder}"
    if args.tag:
        qs += f"&tag={args.tag}"
    status, parsed, err = _grafana_request("GET", f"/api/search{qs}", api_key, base_url)
    ref = f"arq-grafana-dashboard-list-{utc_ts()}"
    BRIDGE.emit_act(
        "grafana_dashboard_queried", ref,
        {
            "verb": "grafana.dashboard-list",
            "folder": args.folder or None,
            "tag": args.tag or None,
            "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 d in parsed:
            print(f"{d.get('uid', '?')}  {d.get('title', '')[:80]}")
        return 0
    print(f"arq-grafana dashboard-list failed: status={status} err={err}", file=sys.stderr)
    return 1


def cmd_dashboard_view(args: argparse.Namespace) -> int:
    creds = BRIDGE.require_credentials(
        verb="dashboard-view",
        resources=("api-key", "base-url"),
        remediation_text=REMEDIATION,
    )
    if creds is None:
        return 3
    api_key, base_url = creds
    status, parsed, err = _grafana_request(
        "GET", f"/api/dashboards/uid/{args.uid}", api_key, base_url,
    )
    ref = f"arq-grafana-dashboard-view-{args.uid}-{utc_ts()}"
    BRIDGE.emit_act(
        "grafana_dashboard_queried", ref,
        {
            "verb": "grafana.dashboard-view",
            "dashboard_uid": args.uid,
            "http_status": status,
            "error": err if err else None,
        },
    )
    if 200 <= status < 300:
        print(json.dumps(parsed, indent=2) if parsed else "")
        return 0
    print(f"arq-grafana dashboard-view failed: status={status} err={err}", file=sys.stderr)
    return 1


def cmd_annotation_push(args: argparse.Namespace) -> int:
    creds = BRIDGE.require_credentials(
        verb="annotation-push",
        resources=("api-key", "base-url"),
        remediation_text=REMEDIATION,
    )
    if creds is None:
        return 3
    api_key, base_url = creds
    body = {
        "text": args.text,
        "tags": ["arq-grafana-bridge", "principle:cryptographic-citizenship"]
                + ([args.tag] if args.tag else []),
        "dashboardUID": args.dashboard,
    }
    status, parsed, err = _grafana_request(
        "POST", "/api/annotations", api_key, base_url, body=body,
    )
    ref = f"arq-grafana-annotation-push-{args.dashboard}-{utc_ts()}"
    BRIDGE.emit_act(
        "grafana_annotation_pushed", ref,
        {
            "verb": "grafana.annotation-push",
            "dashboard_uid": args.dashboard,
            "text": args.text[:200],
            "tag": args.tag or None,
            "http_status": status,
            "error": err if err else None,
            "annotation_id": parsed.get("id") if isinstance(parsed, dict) else None,
        },
    )
    if 200 <= status < 300:
        aid = parsed.get("id") if isinstance(parsed, dict) else None
        print(f"grafana-annotation-pushed: dashboard={args.dashboard} id={aid}")
        return 0
    print(f"arq-grafana annotation-push failed: status={status} err={err}", file=sys.stderr)
    return 1


def cmd_alert_list(args: argparse.Namespace) -> int:
    creds = BRIDGE.require_credentials(
        verb="alert-list",
        resources=("api-key", "base-url"),
        remediation_text=REMEDIATION,
    )
    if creds is None:
        return 3
    api_key, base_url = creds
    status, parsed, err = _grafana_request(
        "GET", "/api/v1/provisioning/alert-rules", api_key, base_url,
    )
    ref = f"arq-grafana-alert-list-{utc_ts()}"
    BRIDGE.emit_act(
        "grafana_alert_queried", ref,
        {
            "verb": "grafana.alert-list",
            "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 a in parsed:
            print(f"{a.get('uid', '?')}  {a.get('title', '')[:80]}")
        return 0
    print(f"arq-grafana alert-list failed: status={status} err={err}", file=sys.stderr)
    return 1


def cmd_datasource_list(args: argparse.Namespace) -> int:
    creds = BRIDGE.require_credentials(
        verb="datasource-list",
        resources=("api-key", "base-url"),
        remediation_text=REMEDIATION,
    )
    if creds is None:
        return 3
    api_key, base_url = creds
    status, parsed, err = _grafana_request("GET", "/api/datasources", api_key, base_url)
    ref = f"arq-grafana-datasource-list-{utc_ts()}"
    BRIDGE.emit_act(
        "grafana_datasource_queried", ref,
        {
            "verb": "grafana.datasource-list",
            "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 d in parsed:
            print(f"{d.get('uid', '?')}  {d.get('type', '?'):16s}  {d.get('name', '')[:60]}")
        return 0
    print(f"arq-grafana datasource-list failed: status={status} err={err}", file=sys.stderr)
    return 1


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


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

    dash = sub.add_parser("dashboard").add_subparsers(dest="verb", required=True)
    dl = dash.add_parser("list", help="list dashboards (filter by folder/tag)")
    dl.add_argument("--folder", default=None, help="folder slug/id")
    dl.add_argument("--tag", default=None)
    dl.set_defaults(func=cmd_dashboard_list)
    dv = dash.add_parser("view", help="view dashboard by UID")
    dv.add_argument("uid", help="dashboard UID")
    dv.set_defaults(func=cmd_dashboard_view)

    ann = sub.add_parser("annotation").add_subparsers(dest="verb", required=True)
    ap = ann.add_parser("push", help="push an annotation to a dashboard")
    ap.add_argument("--dashboard", required=True, help="dashboard UID")
    ap.add_argument("--text", required=True, help="annotation text")
    ap.add_argument("--tag", default=None, help="additional tag")
    ap.set_defaults(func=cmd_annotation_push)

    al = sub.add_parser("alert").add_subparsers(dest="verb", required=True)
    all_ = al.add_parser("list", help="list configured alert rules")
    all_.set_defaults(func=cmd_alert_list)

    ds = sub.add_parser("datasource").add_subparsers(dest="verb", required=True)
    dsl = ds.add_parser("list", help="list configured datasources")
    dsl.set_defaults(func=cmd_datasource_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 = "grafana"
REQUIRED_SCOPES: dict[str, list[str]] = {
    # Grafana service-account-token RBAC actions (Grafana 9+ fine-grained perms).
    "dashboard list": ["dashboards:read"],
    "dashboard view": ["dashboards:read"],
    "annotation push": ["annotations:write"],
    "alert list": ["alert.rules:read"],
    "datasource list": ["datasources: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())
