# @File: backend/routers/api_hub.py
# @Desc: Admin-only "API Hub" — a unified control surface for every
#        outbound API integration the portal uses. Returns structured
#        provider metadata + live status + a test-connection endpoint,
#        and lets admins edit provider credentials directly from the UI.
#
# The hub groups providers into three categories:
#   1. integrations       — WHMCS, future CRM / analytics slots
#   2. notifications      — SMS (Twilio / Vonage), WhatsApp Business,
#                           Email (SMTP / SendGrid), Push (Firebase / OneSignal)
#   3. payments           — PayPal, Payoneer, Stripe, Bank transfer
#
# Status values per provider:
#   - "connected"     : credentials present AND last test succeeded
#   - "configured"    : credentials present, not recently tested
#   - "error"         : last test failed
#   - "not_configured": credentials missing
#
# Credentials are resolved using precedence: DB (encrypted) > env var.
# Admins can write/overwrite/clear credentials through this router; the
# DB overrides are encrypted at rest via core.mask_crypto and take
# effect without a server restart.
import logging
import time
from typing import Any, Awaitable, Callable, Dict, List, Optional

import httpx
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field

from dependencies.auth import get_current_user
from schemas.auth import UserResponse
from services.api_hub_credentials import (
    delete_credential,
    resolve_credential,
    resolve_many,
    set_credential,
    stored_names_for,
)
from services.bizmail.client import probe_connection as _probe_bizmail360
from services.whmcs import detect_outbound_ip

logger = logging.getLogger(__name__)

router = APIRouter(prefix="/api/v1/admin/api-hub", tags=["api-hub"])


# ---------- Admin guard ----------
def _require_admin(user: UserResponse) -> None:
    """Require an admin-flagged user. Mirrors the check in whmcs_admin."""
    is_admin = bool(
        getattr(user, "is_admin", False)
        or getattr(user, "is_superuser", False)
        or getattr(user, "role", "") in ("admin", "superadmin")
    )
    if not is_admin:
        raise HTTPException(status_code=403, detail="Admin access required.")


# ---------- Schemas ----------
class ProviderCredentialHint(BaseModel):
    """Metadata about a single env var / secret that configures a provider."""

    name: str
    present: bool
    masked_value: Optional[str] = None
    required: bool = True
    source: str = "none"  # "db" | "env" | "none"
    editable: bool = True
    secret: bool = True  # whether the UI should render a password input
    label: Optional[str] = None
    hint: Optional[str] = None


class ProviderStatus(BaseModel):
    """Unified shape for any integration/notification/payment provider."""

    id: str
    name: str
    category: str  # integration | notification | payment
    description: str
    docs_url: Optional[str] = None
    # Status + diagnostics
    status: str  # connected | configured | error | not_configured
    status_message: str
    last_checked_at: Optional[float] = None
    latency_ms: Optional[int] = None
    # What env vars this provider reads
    credentials: List[ProviderCredentialHint]
    # Feature flags shown in the UI
    sandbox_mode: Optional[bool] = None
    enabled: Optional[bool] = None
    # Optional rendering hints
    logo_hint: Optional[str] = None
    test_supported: bool = True


class ApiHubAggregate(BaseModel):
    """Top-level dashboard payload."""

    integrations: List[ProviderStatus]
    notifications: List[ProviderStatus]
    payments: List[ProviderStatus]
    summary: Dict[str, int]
    generated_at: float


# ---------- Provider definitions ----------
#
# Each definition is a pure-data description of an outbound API the
# portal can integrate with. Keeping them in a single dict makes it
# trivial to add new providers later without touching the handlers.
#
# Each credential tuple is (name, required, secret, label, hint).
#   - secret: True means UI renders a password input + never echoes the value.
#   - label / hint drive the edit form in the UI.
#
# The probe is a small async callable that performs a real connectivity
# test and returns a dict with ok/message/latency_ms. Probes MUST NEVER
# raise — failure is part of the return payload.
_PROBE_CACHE: Dict[str, Dict[str, Any]] = {}


def _mask(value: Optional[str]) -> Optional[str]:
    """Return a short masked representation of a credential for display."""
    if not value:
        return None
    if len(value) <= 4:
        return "…"
    return f"{value[:3]}…{value[-2:]}"


# ---- Individual provider probes ----
#
# Probes use `resolve_credential` so admin-UI overrides apply without a
# restart. Each probe function is intentionally self-contained and never
# raises so the caller can uniformly map failures to ProviderStatus.


async def _probe_whmcs() -> Dict[str, Any]:
    """Lightweight WHMCS probe using the standard WhmcsDetails action."""
    url = await resolve_credential("whmcs", "WHMCS_API_URL")
    identifier = await resolve_credential("whmcs", "WHMCS_API_IDENTIFIER")
    secret = await resolve_credential("whmcs", "WHMCS_API_SECRET")
    if not (url and identifier and secret):
        return {"ok": False, "message": "WHMCS credentials missing.", "latency_ms": None}

    start = time.time()
    data = {
        "identifier": identifier,
        "secret": secret,
        "action": "WhmcsDetails",
        "responsetype": "json",
    }
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.post(url, data=data)
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}

    latency_ms = int((time.time() - start) * 1000)
    try:
        body = resp.json()
    except ValueError:
        return {"ok": False, "message": f"HTTP {resp.status_code}, non-JSON response.", "latency_ms": latency_ms}

    if resp.status_code == 200 and isinstance(body, dict) and str(body.get("result")).lower() == "success":
        version = body.get("whmcsVersion") or body.get("version")
        return {
            "ok": True,
            "message": f"WHMCS {version} responding." if version else "WHMCS responding.",
            "latency_ms": latency_ms,
        }
    return {
        "ok": False,
        "message": f"WHMCS rejected: {body.get('message') if isinstance(body, dict) else resp.status_code}",
        "latency_ms": latency_ms,
    }


async def _probe_twilio() -> Dict[str, Any]:
    """Twilio account fetch — only succeeds with valid SID + auth token."""
    sid = await resolve_credential("twilio", "TWILIO_ACCOUNT_SID")
    token = await resolve_credential("twilio", "TWILIO_AUTH_TOKEN")
    if not (sid and token):
        return {"ok": False, "message": "Twilio credentials missing.", "latency_ms": None}
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0, auth=(sid, token)) as client:
            resp = await client.get(f"https://api.twilio.com/2010-04-01/Accounts/{sid}.json")
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}
    latency_ms = int((time.time() - start) * 1000)
    if resp.status_code == 200:
        return {"ok": True, "message": "Twilio account reachable.", "latency_ms": latency_ms}
    return {"ok": False, "message": f"Twilio HTTP {resp.status_code}.", "latency_ms": latency_ms}


async def _probe_vonage() -> Dict[str, Any]:
    """Vonage (formerly Nexmo) balance check."""
    key = await resolve_credential("vonage", "VONAGE_API_KEY")
    secret = await resolve_credential("vonage", "VONAGE_API_SECRET")
    if not (key and secret):
        return {"ok": False, "message": "Vonage credentials missing.", "latency_ms": None}
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(
                "https://rest.nexmo.com/account/get-balance",
                params={"api_key": key, "api_secret": secret},
            )
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}
    latency_ms = int((time.time() - start) * 1000)
    if resp.status_code == 200:
        return {"ok": True, "message": "Vonage account reachable.", "latency_ms": latency_ms}
    return {"ok": False, "message": f"Vonage HTTP {resp.status_code}.", "latency_ms": latency_ms}


async def _probe_whatsapp() -> Dict[str, Any]:
    """WhatsApp Business Cloud API — phone number info endpoint."""
    token = await resolve_credential("whatsapp", "WHATSAPP_ACCESS_TOKEN")
    phone_id = await resolve_credential("whatsapp", "WHATSAPP_PHONE_NUMBER_ID")
    if not (token and phone_id):
        return {"ok": False, "message": "WhatsApp Business credentials missing.", "latency_ms": None}
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(
                f"https://graph.facebook.com/v18.0/{phone_id}",
                headers={"Authorization": f"Bearer {token}"},
            )
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}
    latency_ms = int((time.time() - start) * 1000)
    if resp.status_code == 200:
        return {"ok": True, "message": "WhatsApp phone number reachable.", "latency_ms": latency_ms}
    return {"ok": False, "message": f"WhatsApp HTTP {resp.status_code}.", "latency_ms": latency_ms}


async def _probe_sendgrid() -> Dict[str, Any]:
    """SendGrid account verification."""
    api_key = await resolve_credential("sendgrid", "SENDGRID_API_KEY")
    if not api_key:
        return {"ok": False, "message": "SendGrid API key missing.", "latency_ms": None}
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(
                "https://api.sendgrid.com/v3/user/account",
                headers={"Authorization": f"Bearer {api_key}"},
            )
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}
    latency_ms = int((time.time() - start) * 1000)
    if resp.status_code == 200:
        return {"ok": True, "message": "SendGrid authenticated.", "latency_ms": latency_ms}
    return {"ok": False, "message": f"SendGrid HTTP {resp.status_code}.", "latency_ms": latency_ms}


async def _probe_smtp() -> Dict[str, Any]:
    """SMTP: verify var presence without opening a real connection.

    Many deployment environments block outbound SMTP for security. Doing
    the full connect / STARTTLS / AUTH dance here would produce false
    negatives, so we only verify that the required vars are present.
    """
    host = await resolve_credential("smtp", "SMTP_HOST")
    port = await resolve_credential("smtp", "SMTP_PORT")
    user = await resolve_credential("smtp", "SMTP_USER")
    if not (host and port and user):
        return {"ok": False, "message": "SMTP credentials missing.", "latency_ms": None}
    return {"ok": True, "message": f"SMTP configured for {host}:{port}.", "latency_ms": None}


async def _probe_firebase() -> Dict[str, Any]:
    """Firebase service-account key presence check."""
    sa = await resolve_credential("firebase", "FIREBASE_SERVICE_ACCOUNT_JSON")
    if not sa:
        sa = await resolve_credential("firebase", "FIREBASE_SERVER_KEY")
    if not sa:
        return {"ok": False, "message": "Firebase credentials missing.", "latency_ms": None}
    return {"ok": True, "message": "Firebase credentials detected.", "latency_ms": None}


async def _probe_onesignal() -> Dict[str, Any]:
    """OneSignal app info."""
    app_id = await resolve_credential("onesignal", "ONESIGNAL_APP_ID")
    api_key = await resolve_credential("onesignal", "ONESIGNAL_REST_API_KEY")
    if not (app_id and api_key):
        return {"ok": False, "message": "OneSignal credentials missing.", "latency_ms": None}
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            resp = await client.get(
                f"https://onesignal.com/api/v1/apps/{app_id}",
                headers={"Authorization": f"Basic {api_key}"},
            )
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}
    latency_ms = int((time.time() - start) * 1000)
    if resp.status_code == 200:
        return {"ok": True, "message": "OneSignal app reachable.", "latency_ms": latency_ms}
    return {"ok": False, "message": f"OneSignal HTTP {resp.status_code}.", "latency_ms": latency_ms}


async def _probe_paypal() -> Dict[str, Any]:
    """PayPal OAuth token exchange — the cheapest real auth test."""
    client_id = await resolve_credential("paypal", "PAYPAL_CLIENT_ID")
    secret = await resolve_credential("paypal", "PAYPAL_CLIENT_SECRET")
    mode = (await resolve_credential("paypal", "PAYPAL_MODE")) or "live"
    mode = mode.lower()
    if not (client_id and secret):
        return {"ok": False, "message": "PayPal credentials missing.", "latency_ms": None}
    base = "https://api-m.sandbox.paypal.com" if mode == "sandbox" else "https://api-m.paypal.com"
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0, auth=(client_id, secret)) as client:
            resp = await client.post(
                f"{base}/v1/oauth2/token",
                data={"grant_type": "client_credentials"},
                headers={"Accept": "application/json"},
            )
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}
    latency_ms = int((time.time() - start) * 1000)
    if resp.status_code == 200:
        return {"ok": True, "message": f"PayPal OAuth OK ({mode}).", "latency_ms": latency_ms}
    return {"ok": False, "message": f"PayPal HTTP {resp.status_code}.", "latency_ms": latency_ms}


async def _probe_payoneer() -> Dict[str, Any]:
    """Payoneer: no simple public endpoint — only verify env var presence."""
    prog_id = await resolve_credential("payoneer", "PAYONEER_PROGRAM_ID")
    user_name = await resolve_credential("payoneer", "PAYONEER_USERNAME")
    api_pwd = await resolve_credential("payoneer", "PAYONEER_API_PASSWORD")
    if not (prog_id and user_name and api_pwd):
        return {"ok": False, "message": "Payoneer credentials missing.", "latency_ms": None}
    return {"ok": True, "message": "Payoneer credentials detected.", "latency_ms": None}


async def _probe_stripe() -> Dict[str, Any]:
    """Stripe account balance — the canonical 'is this key live?' check."""
    key = await resolve_credential("stripe", "STRIPE_SECRET_KEY")
    if not key:
        return {"ok": False, "message": "Stripe secret key missing.", "latency_ms": None}
    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0, auth=(key, "")) as client:
            resp = await client.get("https://api.stripe.com/v1/balance")
    except httpx.RequestError as exc:
        return {"ok": False, "message": f"Transport error: {exc}", "latency_ms": int((time.time() - start) * 1000)}
    latency_ms = int((time.time() - start) * 1000)
    if resp.status_code == 200:
        return {"ok": True, "message": "Stripe balance reachable.", "latency_ms": latency_ms}
    return {"ok": False, "message": f"Stripe HTTP {resp.status_code}.", "latency_ms": latency_ms}


async def _probe_bank_transfer() -> Dict[str, Any]:
    """Bank transfer is a 'manual gateway' — just check the display vars."""
    bank = await resolve_credential("bank_transfer", "BANK_TRANSFER_BANK_NAME")
    iban = await resolve_credential("bank_transfer", "BANK_TRANSFER_IBAN")
    if not (bank or iban):
        return {"ok": False, "message": "Bank transfer details missing.", "latency_ms": None}
    return {"ok": True, "message": f"Bank transfer configured ({bank or 'account'}).", "latency_ms": None}


# ---- Registry ----
#
# Keep the registry as a single source of truth for the whole hub.
# Each credential tuple: (name, required, secret, label, hint)
_REGISTRY: Dict[str, Dict[str, Any]] = {
    # integrations
    "whmcs": {
        "name": "WHMCS",
        "category": "integration",
        "description": "Billing, provisioning and client-portal backend.",
        "docs_url": "https://developers.whmcs.com/api/",
        "credentials": [
            ("WHMCS_API_URL", True, False, "API URL", "e.g. https://billing.example.com/includes/api.php"),
            ("WHMCS_API_IDENTIFIER", True, True, "API Identifier", "WHMCS API credential identifier."),
            ("WHMCS_API_SECRET", True, True, "API Secret", "WHMCS API credential secret."),
        ],
        "probe": _probe_whmcs,
        "logo_hint": "whmcs",
    },
    "bizmail360_smartermail": {
        "name": "Bizmail360 (SmarterMail)",
        "category": "integration",
        "description": "SmarterMail-powered email hosting for the Bizmail360 reseller module.",
        "docs_url": "https://help.smartermail.com/",
        "credentials": [
            (
                "BIZMAIL360_API_BASE_URL",
                False,
                False,
                "Base URL",
                "SmarterMail API base URL. Defaults to https://webmail.bizmail360.com.",
            ),
            (
                "BIZMAIL360_ADMIN_USERNAME",
                True,
                True,
                "System Admin Username",
                "SmarterMail system-admin login (e.g. 'admin').",
            ),
            (
                "BIZMAIL360_ADMIN_PASSWORD",
                True,
                True,
                "System Admin Password",
                "SmarterMail system-admin password.",
            ),
        ],
        "probe": _probe_bizmail360,
        "logo_hint": "mail",
    },
    "crm": {
        "name": "CRM / HubSpot",
        "category": "integration",
        "description": "Customer-relationship platform (placeholder slot).",
        "docs_url": "https://developers.hubspot.com/docs/api/overview",
        "credentials": [
            ("HUBSPOT_API_KEY", True, True, "HubSpot API Key", "Private app access token."),
        ],
        "probe": None,
        "logo_hint": "hubspot",
    },
    "analytics": {
        "name": "Analytics / GA4",
        "category": "integration",
        "description": "Site analytics and conversion tracking.",
        "docs_url": "https://developers.google.com/analytics",
        "credentials": [
            ("GA4_MEASUREMENT_ID", False, False, "Measurement ID", "e.g. G-XXXXXXX"),
            ("GA4_API_SECRET", False, True, "API Secret", "Measurement Protocol API secret."),
        ],
        "probe": None,
        "logo_hint": "ga",
    },
    # notifications
    "twilio": {
        "name": "Twilio SMS",
        "category": "notification",
        "description": "Programmable SMS & voice.",
        "docs_url": "https://www.twilio.com/docs/usage/api",
        "credentials": [
            ("TWILIO_ACCOUNT_SID", True, True, "Account SID", "Starts with 'AC…'"),
            ("TWILIO_AUTH_TOKEN", True, True, "Auth Token", "Primary Twilio auth token."),
            ("TWILIO_FROM_NUMBER", False, False, "From Number", "E.164, e.g. +15551234567"),
        ],
        "probe": _probe_twilio,
        "logo_hint": "twilio",
    },
    "vonage": {
        "name": "Vonage SMS",
        "category": "notification",
        "description": "Alternative SMS gateway (Nexmo).",
        "docs_url": "https://developer.vonage.com/messaging/sms/overview",
        "credentials": [
            ("VONAGE_API_KEY", True, True, "API Key", "Vonage API key."),
            ("VONAGE_API_SECRET", True, True, "API Secret", "Vonage API secret."),
        ],
        "probe": _probe_vonage,
        "logo_hint": "vonage",
    },
    "whatsapp": {
        "name": "WhatsApp Business",
        "category": "notification",
        "description": "Send WhatsApp templates and messages via Meta Cloud API.",
        "docs_url": "https://developers.facebook.com/docs/whatsapp",
        "credentials": [
            ("WHATSAPP_ACCESS_TOKEN", True, True, "Access Token", "Meta Cloud API access token."),
            ("WHATSAPP_PHONE_NUMBER_ID", True, False, "Phone Number ID", "Numeric ID from the Meta console."),
        ],
        "probe": _probe_whatsapp,
        "logo_hint": "whatsapp",
    },
    "sendgrid": {
        "name": "SendGrid Email",
        "category": "notification",
        "description": "Transactional email via SendGrid.",
        "docs_url": "https://docs.sendgrid.com/api-reference",
        "credentials": [
            ("SENDGRID_API_KEY", True, True, "API Key", "Starts with 'SG.'"),
            ("SENDGRID_FROM_EMAIL", False, False, "From Email", "Verified sender email address."),
        ],
        "probe": _probe_sendgrid,
        "logo_hint": "sendgrid",
    },
    "smtp": {
        "name": "SMTP Email",
        "category": "notification",
        "description": "Generic SMTP relay (fallback for transactional email).",
        "docs_url": None,
        "credentials": [
            ("SMTP_HOST", True, False, "Host", "e.g. smtp.example.com"),
            ("SMTP_PORT", True, False, "Port", "Commonly 587 (STARTTLS) or 465 (SSL)."),
            ("SMTP_USER", True, False, "Username", "SMTP auth username."),
            ("SMTP_PASSWORD", True, True, "Password", "SMTP auth password."),
        ],
        "probe": _probe_smtp,
        "logo_hint": "mail",
    },
    "firebase": {
        "name": "Firebase Push",
        "category": "notification",
        "description": "Mobile / web push notifications via FCM.",
        "docs_url": "https://firebase.google.com/docs/cloud-messaging",
        "credentials": [
            (
                "FIREBASE_SERVICE_ACCOUNT_JSON",
                True,
                True,
                "Service Account JSON",
                "Full service-account JSON (paste the file contents).",
            ),
        ],
        "probe": _probe_firebase,
        "logo_hint": "firebase",
    },
    "onesignal": {
        "name": "OneSignal",
        "category": "notification",
        "description": "Cross-platform push notifications.",
        "docs_url": "https://documentation.onesignal.com/reference",
        "credentials": [
            ("ONESIGNAL_APP_ID", True, False, "App ID", "OneSignal app identifier."),
            ("ONESIGNAL_REST_API_KEY", True, True, "REST API Key", "Server-side REST API key."),
        ],
        "probe": _probe_onesignal,
        "logo_hint": "onesignal",
    },
    # payments
    "paypal": {
        "name": "PayPal",
        "category": "payment",
        "description": "Accept payments via PayPal REST API.",
        "docs_url": "https://developer.paypal.com/api/rest/",
        "credentials": [
            ("PAYPAL_CLIENT_ID", True, True, "Client ID", "PayPal REST app client ID."),
            ("PAYPAL_CLIENT_SECRET", True, True, "Client Secret", "PayPal REST app client secret."),
            ("PAYPAL_MODE", False, False, "Mode", "'live' or 'sandbox'. Defaults to live."),
        ],
        "probe": _probe_paypal,
        "logo_hint": "paypal",
    },
    "payoneer": {
        "name": "Payoneer",
        "category": "payment",
        "description": "Mass payouts and cross-border transfers.",
        "docs_url": "https://developer.payoneer.com/",
        "credentials": [
            ("PAYONEER_PROGRAM_ID", True, False, "Program ID", "Assigned by Payoneer."),
            ("PAYONEER_USERNAME", True, True, "Username", "API user name."),
            ("PAYONEER_API_PASSWORD", True, True, "API Password", "API password / shared key."),
        ],
        "probe": _probe_payoneer,
        "logo_hint": "payoneer",
    },
    "stripe": {
        "name": "Stripe",
        "category": "payment",
        "description": "Cards, wallets, subscriptions.",
        "docs_url": "https://stripe.com/docs/api",
        "credentials": [
            ("STRIPE_SECRET_KEY", True, True, "Secret Key", "Use sk_test_… for sandbox, sk_live_… for production."),
            ("STRIPE_WEBHOOK_SECRET", False, True, "Webhook Secret", "Starts with 'whsec_'."),
        ],
        "probe": _probe_stripe,
        "logo_hint": "stripe",
    },
    "bank_transfer": {
        "name": "Bank Transfer",
        "category": "payment",
        "description": "Manual bank transfer / wire payment option.",
        "docs_url": None,
        "credentials": [
            ("BANK_TRANSFER_BANK_NAME", False, False, "Bank Name", "Shown to customers on the invoice."),
            ("BANK_TRANSFER_IBAN", False, False, "IBAN / Account", "Account / IBAN number."),
            ("BANK_TRANSFER_SWIFT", False, False, "SWIFT / BIC", "Bank SWIFT code."),
        ],
        "probe": _probe_bank_transfer,
        "logo_hint": "bank",
    },
}


def _status_from_creds(
    creds: List[ProviderCredentialHint], cached_ok: Optional[bool]
) -> str:
    """Derive status from credential presence + last probe result."""
    required_missing = [c for c in creds if c.required and not c.present]
    if required_missing:
        return "not_configured"
    if cached_ok is True:
        return "connected"
    if cached_ok is False:
        return "error"
    return "configured"


def _status_message(status: str, detail: Optional[str]) -> str:
    base = {
        "connected": "Connected. Last test successful.",
        "configured": "Credentials present. Run a test to verify connectivity.",
        "error": "Last test failed.",
        "not_configured": "Credentials are not configured.",
    }[status]
    if detail:
        return f"{base} {detail}"
    return base


async def _build_status(provider_id: str) -> ProviderStatus:
    """Assemble a ProviderStatus object for a given provider id.

    Reads credentials via the credential service (DB > env precedence) and
    annotates each hint with `source` so the UI can tell admins whether a
    value is backed by a DB override or a deploy-baked env var.
    """
    defn = _REGISTRY[provider_id]
    cred_specs: List[tuple] = defn["credentials"]

    names = [c[0] for c in cred_specs]
    resolved = await resolve_many(provider_id, names)
    db_names = await stored_names_for(provider_id)

    creds: List[ProviderCredentialHint] = []
    import os as _os  # local alias so the helper stays compact

    for spec in cred_specs:
        # Support both 3-tuple (legacy) and 5-tuple (with label+hint)
        if len(spec) == 5:
            name, required, secret, label, hint = spec
        elif len(spec) == 3:
            name, required, secret = spec
            label, hint = None, None
        else:
            name, required = spec[0], spec[1]
            secret, label, hint = True, None, None
        value = resolved.get(name)
        if name in db_names:
            source = "db"
        elif _os.getenv(name):
            source = "env"
        else:
            source = "none"
        creds.append(
            ProviderCredentialHint(
                name=name,
                present=bool(value),
                masked_value=_mask(value) if secret else (value or None),
                required=required,
                source=source,
                editable=True,
                secret=secret,
                label=label,
                hint=hint,
            )
        )

    cached = _PROBE_CACHE.get(provider_id) or {}
    last_ok = cached.get("ok")
    last_msg = cached.get("message")
    status = _status_from_creds(creds, last_ok)

    # Provider-specific flags for the UI
    sandbox: Optional[bool] = None
    if provider_id == "paypal":
        mode = await resolve_credential("paypal", "PAYPAL_MODE") or "live"
        sandbox = mode.lower() == "sandbox"
    elif provider_id == "stripe":
        key = await resolve_credential("stripe", "STRIPE_SECRET_KEY") or ""
        sandbox = key.startswith("sk_test_") if key else None

    return ProviderStatus(
        id=provider_id,
        name=defn["name"],
        category=defn["category"],
        description=defn["description"],
        docs_url=defn.get("docs_url"),
        status=status,
        status_message=_status_message(status, last_msg),
        last_checked_at=cached.get("at"),
        latency_ms=cached.get("latency_ms"),
        credentials=creds,
        sandbox_mode=sandbox,
        enabled=all(c.present for c in creds if c.required) if creds else None,
        logo_hint=defn.get("logo_hint"),
        test_supported=defn.get("probe") is not None,
    )


async def _filter(category: str) -> List[ProviderStatus]:
    out: List[ProviderStatus] = []
    for pid, defn in _REGISTRY.items():
        if defn["category"] == category:
            out.append(await _build_status(pid))
    return out


# ---------- Endpoints ----------
@router.get("/status", response_model=ApiHubAggregate)
async def api_hub_status(current_user: UserResponse = Depends(get_current_user)):
    """Top-level API Hub payload: every provider grouped by category."""
    _require_admin(current_user)
    integrations = await _filter("integration")
    notifications = await _filter("notification")
    payments = await _filter("payment")
    all_providers = integrations + notifications + payments
    summary = {
        "total": len(all_providers),
        "connected": sum(1 for p in all_providers if p.status == "connected"),
        "configured": sum(1 for p in all_providers if p.status == "configured"),
        "error": sum(1 for p in all_providers if p.status == "error"),
        "not_configured": sum(1 for p in all_providers if p.status == "not_configured"),
    }
    return ApiHubAggregate(
        integrations=integrations,
        notifications=notifications,
        payments=payments,
        summary=summary,
        generated_at=time.time(),
    )


@router.get("/integrations", response_model=List[ProviderStatus])
async def api_hub_integrations(current_user: UserResponse = Depends(get_current_user)):
    _require_admin(current_user)
    return await _filter("integration")


@router.get("/notifications", response_model=List[ProviderStatus])
async def api_hub_notifications(current_user: UserResponse = Depends(get_current_user)):
    _require_admin(current_user)
    return await _filter("notification")


@router.get("/payments", response_model=List[ProviderStatus])
async def api_hub_payments(current_user: UserResponse = Depends(get_current_user)):
    _require_admin(current_user)
    return await _filter("payment")


@router.get("/providers/{provider_id}", response_model=ProviderStatus)
async def api_hub_get_provider(
    provider_id: str,
    current_user: UserResponse = Depends(get_current_user),
):
    _require_admin(current_user)
    if provider_id not in _REGISTRY:
        raise HTTPException(status_code=404, detail=f"Unknown provider '{provider_id}'.")
    return await _build_status(provider_id)


class ProviderTestResponse(BaseModel):
    provider: str
    ok: bool
    message: str
    latency_ms: Optional[int] = None
    tested_at: float
    outbound_ip: Optional[str] = None


@router.post("/providers/{provider_id}/test", response_model=ProviderTestResponse)
async def api_hub_test_provider(
    provider_id: str,
    current_user: UserResponse = Depends(get_current_user),
):
    """Run a live connectivity probe for a single provider and cache the result.

    The result is cached in-process so the dashboard can render the last-known
    status without hammering every provider on every page load.
    """
    _require_admin(current_user)
    if provider_id not in _REGISTRY:
        raise HTTPException(status_code=404, detail=f"Unknown provider '{provider_id}'.")
    defn = _REGISTRY[provider_id]
    probe: Optional[Callable[[], Awaitable[Dict[str, Any]]]] = defn.get("probe")
    if probe is None:
        raise HTTPException(
            status_code=400,
            detail=f"Provider '{provider_id}' does not support live connectivity testing yet.",
        )

    result = await probe()
    tested_at = time.time()
    _PROBE_CACHE[provider_id] = {
        "ok": bool(result.get("ok")),
        "message": result.get("message"),
        "latency_ms": result.get("latency_ms"),
        "at": tested_at,
    }
    outbound_ip: Optional[str] = None
    # Only compute outbound IP when the probe failed — that's when admins
    # usually need it for IP-whitelisting.
    if not result.get("ok"):
        try:
            probe_ip = await detect_outbound_ip()
            outbound_ip = probe_ip.get("ip")
        except Exception:  # noqa: BLE001
            outbound_ip = None

    return ProviderTestResponse(
        provider=provider_id,
        ok=bool(result.get("ok")),
        message=str(result.get("message") or ""),
        latency_ms=result.get("latency_ms"),
        tested_at=tested_at,
        outbound_ip=outbound_ip,
    )


# ---------- Credential editing ----------
class CredentialUpdate(BaseModel):
    """Single credential change — empty value clears/deletes the override."""

    name: str = Field(..., min_length=1, max_length=128)
    value: str = ""


class CredentialUpdateBatch(BaseModel):
    """Batch payload for updating multiple credentials of a provider."""

    credentials: List[CredentialUpdate]


def _valid_names_for(provider_id: str) -> set[str]:
    return {c[0] for c in _REGISTRY[provider_id]["credentials"]}


@router.put("/providers/{provider_id}/credentials", response_model=ProviderStatus)
async def api_hub_update_credentials(
    provider_id: str,
    payload: CredentialUpdateBatch,
    current_user: UserResponse = Depends(get_current_user),
):
    """Update one or more credentials for a provider.

    Accepts only credential names declared in the provider registry so a
    malicious caller cannot stuff arbitrary rows into the table. Empty
    values are treated as a "clear" so admins can revert to env-var
    fallback values through the same endpoint.
    """
    _require_admin(current_user)
    if provider_id not in _REGISTRY:
        raise HTTPException(status_code=404, detail=f"Unknown provider '{provider_id}'.")
    if not payload.credentials:
        raise HTTPException(status_code=400, detail="No credentials provided.")

    valid_names = _valid_names_for(provider_id)
    unknown = [c.name for c in payload.credentials if c.name not in valid_names]
    if unknown:
        raise HTTPException(
            status_code=400,
            detail=f"Unknown credential field(s) for '{provider_id}': {', '.join(unknown)}",
        )

    updated_by = getattr(current_user, "email", None) or getattr(current_user, "username", None) or str(
        getattr(current_user, "id", "admin")
    )
    for cred in payload.credentials:
        await set_credential(provider_id, cred.name, cred.value, updated_by=updated_by)

    # Invalidate any cached probe result — previous verdict no longer
    # reflects the current credential set.
    _PROBE_CACHE.pop(provider_id, None)

    return await _build_status(provider_id)


@router.delete("/providers/{provider_id}/credentials/{name}", response_model=ProviderStatus)
async def api_hub_delete_credential(
    provider_id: str,
    name: str,
    current_user: UserResponse = Depends(get_current_user),
):
    """Remove a single stored credential override.

    After deletion the provider falls back to the deploy-baked env var
    (if any). Returns the refreshed provider status so the UI can update
    without a second round-trip.
    """
    _require_admin(current_user)
    if provider_id not in _REGISTRY:
        raise HTTPException(status_code=404, detail=f"Unknown provider '{provider_id}'.")
    if name not in _valid_names_for(provider_id):
        raise HTTPException(
            status_code=400,
            detail=f"Unknown credential '{name}' for provider '{provider_id}'.",
        )
    await delete_credential(provider_id, name)
    _PROBE_CACHE.pop(provider_id, None)
    return await _build_status(provider_id)