# @File: backend/routers/whmcs_admin.py
# @Desc: Admin-only WHMCS endpoints:
#   - WHMCS API activity logs (whmcs_api_logs table, paginated + filterable)
#   - System health surfacing (GetServers, GetHealthStatus, WhmcsDetails)
# All routes require an authenticated admin user.
import logging
import os
import time
from typing import Any, Dict, List, Optional

import httpx
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy import desc, func, select
from sqlalchemy.ext.asyncio import AsyncSession

from core.database import get_db
from dependencies.auth import get_current_user
from models.whmcs_api_logs import WhmcsApiLog
from schemas.auth import UserResponse
from services.whmcs import (
    call_whmcs,
    call_whmcs_safe,
    _get_credentials,
    classify_whmcs_error,
    detect_outbound_ip,
)

logger = logging.getLogger(__name__)

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


# ---------- Admin guard ----------
def _require_admin(user: UserResponse) -> None:
    """Require an admin-flagged user. We check a few common fields so this
    works regardless of how admin role is modelled in the auth schema."""
    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 WhmcsLogItem(BaseModel):
    id: int
    action: str
    user_email: Optional[str] = None
    status: str
    http_status: Optional[int] = None
    duration_ms: Optional[int] = None
    error: Optional[str] = None
    created_at: Optional[str] = None


class WhmcsLogList(BaseModel):
    items: List[WhmcsLogItem]
    total: int
    skip: int
    limit: int


class WhmcsLogStats(BaseModel):
    total: int
    success: int
    error: int
    top_actions: List[Dict[str, Any]]


# ---------- Logs ----------
@router.get("/logs", response_model=WhmcsLogList)
async def list_whmcs_logs(
    action: Optional[str] = Query(None, description="Filter by action (exact match)."),
    status: Optional[str] = Query(None, description="Filter by status: success | error."),
    user_email: Optional[str] = Query(None, description="Filter by user email (case-insensitive contains)."),
    skip: int = Query(0, ge=0),
    limit: int = Query(50, ge=1, le=500),
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """Paginated, filterable list of WHMCS API logs for the admin panel."""
    _require_admin(current_user)

    stmt = select(WhmcsApiLog)
    count_stmt = select(func.count(WhmcsApiLog.id))
    if action:
        stmt = stmt.where(WhmcsApiLog.action == action)
        count_stmt = count_stmt.where(WhmcsApiLog.action == action)
    if status:
        stmt = stmt.where(WhmcsApiLog.status == status)
        count_stmt = count_stmt.where(WhmcsApiLog.status == status)
    if user_email:
        needle = f"%{user_email.lower()}%"
        stmt = stmt.where(func.lower(WhmcsApiLog.user_email).like(needle))
        count_stmt = count_stmt.where(func.lower(WhmcsApiLog.user_email).like(needle))

    stmt = stmt.order_by(desc(WhmcsApiLog.created_at)).offset(skip).limit(limit)

    total_res = await db.execute(count_stmt)
    total = int(total_res.scalar() or 0)

    res = await db.execute(stmt)
    rows = res.scalars().all()
    items: List[WhmcsLogItem] = [
        WhmcsLogItem(
            id=row.id,
            action=row.action,
            user_email=row.user_email,
            status=row.status,
            http_status=row.http_status,
            duration_ms=row.duration_ms,
            error=row.error,
            created_at=row.created_at.isoformat() if row.created_at else None,
        )
        for row in rows
    ]
    return WhmcsLogList(items=items, total=total, skip=skip, limit=limit)


@router.get("/logs/stats", response_model=WhmcsLogStats)
async def whmcs_logs_stats(
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """Aggregate stats over the full log table for the admin dashboard card."""
    _require_admin(current_user)

    total_res = await db.execute(select(func.count(WhmcsApiLog.id)))
    total = int(total_res.scalar() or 0)

    success_res = await db.execute(
        select(func.count(WhmcsApiLog.id)).where(WhmcsApiLog.status == "success")
    )
    success = int(success_res.scalar() or 0)
    error = max(total - success, 0)

    top_stmt = (
        select(WhmcsApiLog.action, func.count(WhmcsApiLog.id).label("count"))
        .group_by(WhmcsApiLog.action)
        .order_by(desc("count"))
        .limit(10)
    )
    top_res = await db.execute(top_stmt)
    top_actions = [{"action": row[0], "count": int(row[1] or 0)} for row in top_res.all()]

    return WhmcsLogStats(total=total, success=success, error=error, top_actions=top_actions)


# ---------- System status (admin-only) ----------
@router.get("/system/servers")
async def whmcs_servers(current_user: UserResponse = Depends(get_current_user)):
    _require_admin(current_user)
    body = await call_whmcs("GetServers", {})
    return {"result": body.get("result"), "servers": body.get("servers", [])}


@router.get("/system/health")
async def whmcs_health(
    fetch_status: bool = Query(True),
    current_user: UserResponse = Depends(get_current_user),
):
    _require_admin(current_user)
    body = await call_whmcs("GetHealthStatus", {"fetchstatus": str(fetch_status).lower()})
    return body


@router.get("/system/details")
async def whmcs_details(current_user: UserResponse = Depends(get_current_user)):
    _require_admin(current_user)
    return await call_whmcs("WhmcsDetails", {})


# ---------- Outbound IP detection (admin-only) ----------
class OutboundIpResponse(BaseModel):
    """Public outbound IP of the backend — the IP that must be whitelisted
    in WHMCS → Setup → General Settings → Security → API IP Access Restriction.
    """

    ip: Optional[str] = None
    provider: Optional[str] = None
    error: Optional[str] = None
    instruction: str


@router.get("/system/outbound-ip", response_model=OutboundIpResponse)
async def whmcs_outbound_ip(current_user: UserResponse = Depends(get_current_user)):
    """Return the backend's public outbound IP.

    The administrator must add this exact IP to WHMCS → Setup → General
    Settings → Security → "API IP Access Restriction" for WHMCS to accept
    our API requests. When the backend runs behind a load balancer or CDN
    (Cloudflare, AWS ALB) this is NOT the load balancer IP — it's the
    origin backend's outbound NAT IP, which is the one WHMCS actually sees.
    """
    _require_admin(current_user)
    probe = await detect_outbound_ip()
    ip = probe.get("ip")
    if ip:
        instruction = (
            f"Add {ip} to the WHMCS API IP whitelist: WHMCS → Setup → "
            "General Settings → Security → API IP Access Restriction."
        )
    else:
        instruction = (
            "Could not detect outbound IP. Run the probe from the server "
            "directly or check outbound internet connectivity."
        )
    return OutboundIpResponse(
        ip=ip,
        provider=probe.get("provider"),
        error=probe.get("error"),
        instruction=instruction,
    )


# ---------- Connection test (admin-only) ----------
class WhmcsHealthCheckResponse(BaseModel):
    """Result of a live WHMCS connectivity test.

    Designed to be surfaced by a 'Test WHMCS Connection' button in Admin
    Settings so the administrator can verify the integration at a glance
    after credential / IP-whitelist / Cloudflare changes.
    """

    ok: bool
    http_status: Optional[int] = None
    latency_ms: Optional[int] = None
    cf_ray: Optional[str] = None
    server: Optional[str] = None
    message: str
    body_snippet: Optional[str] = None
    api_url: Optional[str] = None
    # Diagnostic enrichments:
    error_category: Optional[str] = None  # e.g. 'ip_not_whitelisted'
    whmcs_version: Optional[str] = None  # from WhmcsDetails
    whmcs_system_url: Optional[str] = None  # from WhmcsDetails
    authenticated_user: Optional[str] = None  # identifier prefix (credential hint)
    outbound_ip: Optional[str] = None  # backend public IP
    outbound_ip_provider: Optional[str] = None
    ip_whitelist_hint: Optional[str] = None  # pre-built user-facing instruction


@router.post("/system/health-check", response_model=WhmcsHealthCheckResponse)
@router.get("/system/health-check", response_model=WhmcsHealthCheckResponse)
async def whmcs_health_check(
    current_user: UserResponse = Depends(get_current_user),
):
    """Perform a lightweight live probe of the WHMCS API.

    Calls ``WhmcsDetails`` which is the cheapest safe action that requires
    real credentials + IP whitelist + reachable origin. Returns structured
    diagnostics (status, cf-ray, latency, snippet, error category, WHMCS
    version, outbound IP) so the UI can show actionable information instead
    of a generic error.

    This endpoint NEVER raises on upstream failure — failure is part of the
    return payload so the admin panel always renders a result.
    """
    _require_admin(current_user)

    # Always fetch outbound IP in parallel with the WHMCS probe so the admin
    # sees the IP to whitelist even when the probe itself fails due to a
    # whitelist block.
    try:
        creds = _get_credentials()
    except HTTPException as exc:
        outbound = await detect_outbound_ip()
        return WhmcsHealthCheckResponse(
            ok=False,
            message=str(exc.detail),
            outbound_ip=outbound.get("ip"),
            outbound_ip_provider=outbound.get("provider"),
            ip_whitelist_hint=(
                f"WHMCS credentials are not configured. Set WHMCS_API_URL, "
                f"WHMCS_API_IDENTIFIER, WHMCS_API_SECRET in backend env."
            ),
        )

    identifier_hint = (creds.get("identifier") or "")[:6] + "…" if creds.get("identifier") else None

    data = {
        "identifier": creds["identifier"],
        "secret": creds["secret"],
        "action": "WhmcsDetails",
        "responsetype": "json",
    }
    headers = {
        "User-Agent": "DoitEasy-Portal/1.0 (+https://doiteasy.net)",
        "Accept": "application/json, text/plain, */*",
    }

    start = time.time()
    # Kick off outbound-ip detection concurrently so we don't pay its latency
    # on top of the WHMCS probe.
    import asyncio as _asyncio

    outbound_task = _asyncio.create_task(detect_outbound_ip())

    try:
        async with httpx.AsyncClient(timeout=15.0) as client:
            response = await client.post(creds["url"], data=data, headers=headers)
    except httpx.RequestError as exc:
        duration_ms = int((time.time() - start) * 1000)
        outbound = await outbound_task
        ip = outbound.get("ip")
        return WhmcsHealthCheckResponse(
            ok=False,
            latency_ms=duration_ms,
            message=f"Transport error — could not reach {creds['url']}: {exc}",
            api_url=creds["url"],
            error_category="transport_error",
            authenticated_user=identifier_hint,
            outbound_ip=ip,
            outbound_ip_provider=outbound.get("provider"),
            ip_whitelist_hint=(
                f"Verify WHMCS API URL and that outbound traffic from this "
                f"backend (IP: {ip or 'unknown'}) can reach the WHMCS host."
            ),
        )

    duration_ms = int((time.time() - start) * 1000)
    cf_ray = response.headers.get("cf-ray") or response.headers.get("CF-RAY")
    server = response.headers.get("server")
    snippet = (response.text or "")[:300]

    parsed_ok = False
    upstream_message: Optional[str] = None
    body: Any = None
    try:
        body = response.json()
        parsed_ok = True
        if isinstance(body, dict):
            upstream_message = body.get("message")
    except ValueError:
        parsed_ok = False

    # Wait for outbound IP result now (it's nearly always done by this point).
    outbound = await outbound_task
    outbound_ip = outbound.get("ip")

    # SUCCESS path: HTTP 200 + parseable JSON + result=success
    if response.status_code == 200 and parsed_ok:
        result_flag = (
            str(body.get("result")).lower() if isinstance(body, dict) else ""
        )
        if result_flag == "success":
            whmcs_version = None
            whmcs_system_url = None
            if isinstance(body, dict):
                whmcs_version = body.get("whmcsVersion") or body.get("version")
                whmcs_system_url = body.get("systemURL") or body.get("systemUrl")
            return WhmcsHealthCheckResponse(
                ok=True,
                http_status=response.status_code,
                latency_ms=duration_ms,
                cf_ray=cf_ray,
                server=server,
                message="Connection healthy. WHMCS accepted the request.",
                api_url=creds["url"],
                whmcs_version=whmcs_version,
                whmcs_system_url=whmcs_system_url,
                authenticated_user=identifier_hint,
                outbound_ip=outbound_ip,
                outbound_ip_provider=outbound.get("provider"),
                ip_whitelist_hint=(
                    f"Backend outbound IP: {outbound_ip}. This IP is "
                    "already accepted by WHMCS (whitelist OK)."
                    if outbound_ip
                    else "Backend outbound IP could not be detected."
                ),
            )

        # HTTP 200 but WHMCS rejected — classify using the upstream message.
        category = classify_whmcs_error(
            response.status_code, upstream_message, snippet, parsed_ok=True
        )
        ip_hint = _build_ip_hint(category, outbound_ip)
        return WhmcsHealthCheckResponse(
            ok=False,
            http_status=response.status_code,
            latency_ms=duration_ms,
            cf_ray=cf_ray,
            server=server,
            message=f"WHMCS rejected the request: {upstream_message or 'unknown error'}",
            body_snippet=snippet,
            api_url=creds["url"],
            error_category=category,
            authenticated_user=identifier_hint,
            outbound_ip=outbound_ip,
            outbound_ip_provider=outbound.get("provider"),
            ip_whitelist_hint=ip_hint,
        )

    # FAILURE path: classify and build actionable message.
    category = classify_whmcs_error(
        response.status_code, upstream_message, snippet, parsed_ok
    )
    body_lower = snippet.lower()
    if 520 <= response.status_code <= 530 or (
        response.status_code != 200
        and ("cloudflare" in body_lower or "origin web server" in body_lower)
    ):
        msg = (
            f"Cloudflare reported an origin error (HTTP {response.status_code}). "
            "The WHMCS host is likely down, overloaded, or its WAF is blocking "
            "our request. Forward the cf-ray id to Cloudflare / hosting support."
        )
    elif response.status_code == 403:
        msg = (
            f"HTTP 403 — WHMCS rejected the request. "
            f"{upstream_message or 'Check API IP whitelist and credentials.'}"
        )
    elif response.status_code == 404:
        msg = (
            f"HTTP 404 — The API endpoint path is wrong. "
            f"Current URL: {creds['url']}"
        )
    elif not parsed_ok and response.status_code == 200:
        msg = (
            "HTTP 200 but response is not JSON (probably an HTML challenge page "
            "from Cloudflare WAF or a server error page)."
        )
    else:
        msg = (
            f"Unexpected response (HTTP {response.status_code}). "
            f"{upstream_message or 'See body snippet.'}"
        )

    ip_hint = _build_ip_hint(category, outbound_ip)
    return WhmcsHealthCheckResponse(
        ok=False,
        http_status=response.status_code,
        latency_ms=duration_ms,
        cf_ray=cf_ray,
        server=server,
        message=msg,
        body_snippet=snippet,
        api_url=creds["url"],
        error_category=category,
        authenticated_user=identifier_hint,
        outbound_ip=outbound_ip,
        outbound_ip_provider=outbound.get("provider"),
        ip_whitelist_hint=ip_hint,
    )


# ---------- 24h uptime history (admin-only) ----------
class WhmcsUptimeBucket(BaseModel):
    """One hourly bucket of WHMCS API call stats."""

    hour_start: str  # ISO8601 UTC timestamp, e.g. "2026-05-03T12:00:00Z"
    total: int
    success: int
    error: int
    uptime_pct: float  # 0..100, 100 if no calls in bucket
    avg_latency_ms: Optional[int] = None


class WhmcsUptimeResponse(BaseModel):
    """Aggregated 24h WHMCS uptime series for the dashboard chart."""

    buckets: List[WhmcsUptimeBucket]
    total_calls: int
    total_success: int
    total_errors: int
    uptime_pct_24h: float
    avg_latency_ms_24h: Optional[int] = None
    window_hours: int


@router.get("/system/uptime-24h", response_model=WhmcsUptimeResponse)
async def whmcs_uptime_24h(
    hours: int = Query(24, ge=1, le=168),
    current_user: UserResponse = Depends(get_current_user),
    db: AsyncSession = Depends(get_db),
):
    """Return hourly WHMCS API uptime stats for the last ``hours`` hours.

    Derived from ``whmcs_api_logs``. Each bucket is one hour aligned to the
    current UTC hour boundary. Buckets with no calls are returned with
    uptime 100% so the chart shows a continuous timeline.
    """
    _require_admin(current_user)

    from datetime import datetime, timedelta, timezone

    now_utc = datetime.now(tz=timezone.utc).replace(minute=0, second=0, microsecond=0)
    window_start = now_utc - timedelta(hours=hours - 1)
    # Use naive UTC for DB comparisons to match server_default=func.now().
    window_start_naive = window_start.replace(tzinfo=None)

    stmt = select(
        WhmcsApiLog.status,
        WhmcsApiLog.duration_ms,
        WhmcsApiLog.created_at,
    ).where(WhmcsApiLog.created_at >= window_start_naive)

    rows = (await db.execute(stmt)).all()

    # Initialize buckets keyed by hour_start ISO string.
    buckets: Dict[str, Dict[str, Any]] = {}
    for i in range(hours):
        h = window_start + timedelta(hours=i)
        key = h.strftime("%Y-%m-%dT%H:00:00Z")
        buckets[key] = {
            "hour_start": key,
            "total": 0,
            "success": 0,
            "error": 0,
            "latency_sum": 0,
            "latency_count": 0,
        }

    total_calls = 0
    total_success = 0
    total_errors = 0
    latency_sum_all = 0
    latency_count_all = 0

    for status_val, duration_ms, created_at in rows:
        if created_at is None:
            continue
        # created_at is a naive UTC datetime (DB default).
        bucket_dt = created_at.replace(minute=0, second=0, microsecond=0)
        key = bucket_dt.strftime("%Y-%m-%dT%H:00:00Z")
        b = buckets.get(key)
        if b is None:
            continue
        b["total"] += 1
        total_calls += 1
        if str(status_val).lower() == "success":
            b["success"] += 1
            total_success += 1
        else:
            b["error"] += 1
            total_errors += 1
        if duration_ms is not None:
            b["latency_sum"] += int(duration_ms)
            b["latency_count"] += 1
            latency_sum_all += int(duration_ms)
            latency_count_all += 1

    bucket_list: List[WhmcsUptimeBucket] = []
    for key in sorted(buckets.keys()):
        b = buckets[key]
        total = b["total"]
        uptime_pct = 100.0 if total == 0 else round((b["success"] / total) * 100, 2)
        avg_latency = (
            int(b["latency_sum"] / b["latency_count"]) if b["latency_count"] else None
        )
        bucket_list.append(
            WhmcsUptimeBucket(
                hour_start=b["hour_start"],
                total=total,
                success=b["success"],
                error=b["error"],
                uptime_pct=uptime_pct,
                avg_latency_ms=avg_latency,
            )
        )

    uptime_pct_24h = (
        100.0 if total_calls == 0 else round((total_success / total_calls) * 100, 2)
    )
    avg_latency_24h = (
        int(latency_sum_all / latency_count_all) if latency_count_all else None
    )

    return WhmcsUptimeResponse(
        buckets=bucket_list,
        total_calls=total_calls,
        total_success=total_success,
        total_errors=total_errors,
        uptime_pct_24h=uptime_pct_24h,
        avg_latency_ms_24h=avg_latency_24h,
        window_hours=hours,
    )


def _build_ip_hint(category: Optional[str], outbound_ip: Optional[str]) -> str:
    """Build a user-facing remediation hint for the admin UI.

    Called from every failure branch in the health check so the admin panel
    can show a consistent "add this IP to WHMCS whitelist" message when the
    failure is actually an IP-restriction issue.
    """
    if category == "ip_not_whitelisted":
        if outbound_ip:
            return (
                f"WHMCS API access is blocked by IP restriction. Please "
                f"whitelist this backend outbound IP: {outbound_ip}"
            )
        return (
            "WHMCS API access is blocked by IP restriction. Detect the "
            "backend outbound IP and add it to the WHMCS API whitelist."
        )
    if category == "auth_failed":
        return (
            "Authentication failed. Verify WHMCS_API_IDENTIFIER / "
            "WHMCS_API_SECRET and that the linked admin user has API Access."
        )
    if category == "wrong_endpoint":
        return (
            "The API endpoint URL looks wrong. Expected format: "
            "https://<your-whmcs-host>/includes/api.php"
        )
    if category == "invalid_action":
        return (
            "The WHMCS API Role is missing permissions. Enable the required "
            "action under WHMCS → Setup → Staff Management → API Roles."
        )
    if category == "empty_response":
        return (
            "Empty or invalid JSON response from WHMCS. Check origin server "
            "logs and Cloudflare WAF rules."
        )
    if outbound_ip:
        return (
            f"Backend outbound IP: {outbound_ip}. Add this to the WHMCS API "
            "whitelist if it isn't already."
        )
    return "Check WHMCS logs for details."


# ---------- Per-action diagnostic (admin-only) ----------
class WhmcsActionProbe(BaseModel):
    """Result of probing a single WHMCS API action."""

    action: str
    feature: str
    ok: bool
    http_status: Optional[int] = None
    whmcs_result: Optional[str] = None  # 'success' | 'error' | None
    message: Optional[str] = None
    latency_ms: Optional[int] = None
    required_permission: str


class WhmcsActionsHealthResponse(BaseModel):
    """Aggregated result of probing every action the client dashboard uses."""

    ok: bool
    probed_at: float
    client_id_resolved: Optional[int] = None
    test_email: Optional[str] = None
    probes: List[WhmcsActionProbe]
    summary: str


async def _probe_whmcs_action(
    action: str,
    feature: str,
    required_permission: str,
    payload: Dict[str, Any],
) -> WhmcsActionProbe:
    """Probe a single WHMCS action and categorize the outcome.

    Unlike call_whmcs which raises on any error, this helper captures the
    outcome (HTTP status, WHMCS result flag, upstream message) so the admin
    can see at a glance which actions are allowed and which are blocked.
    """
    try:
        creds = _get_credentials()
    except HTTPException as exc:
        return WhmcsActionProbe(
            action=action,
            feature=feature,
            ok=False,
            message=str(exc.detail),
            required_permission=required_permission,
        )

    data = {
        "identifier": creds["identifier"],
        "secret": creds["secret"],
        "action": action,
        "responsetype": "json",
        **{k: v for k, v in payload.items() if v is not None},
    }
    headers = {
        "User-Agent": "DoitEasy-Portal/1.0 (+https://doiteasy.net)",
        "Accept": "application/json, text/plain, */*",
    }

    start = time.time()
    try:
        async with httpx.AsyncClient(timeout=10.0) as client:
            response = await client.post(creds["url"], data=data, headers=headers)
    except httpx.RequestError as exc:
        return WhmcsActionProbe(
            action=action,
            feature=feature,
            ok=False,
            latency_ms=int((time.time() - start) * 1000),
            message=f"Transport error: {exc}",
            required_permission=required_permission,
        )

    latency_ms = int((time.time() - start) * 1000)

    try:
        body = response.json()
    except ValueError:
        return WhmcsActionProbe(
            action=action,
            feature=feature,
            ok=False,
            http_status=response.status_code,
            latency_ms=latency_ms,
            message="Non-JSON response (likely Cloudflare HTML page).",
            required_permission=required_permission,
        )

    whmcs_result = (
        str(body.get("result")).lower() if isinstance(body, dict) else None
    )
    message = body.get("message") if isinstance(body, dict) else None

    # Distinguish a genuine permission/config error from a benign "no data"
    # response. For actions like GetClientsProducts/GetInvoices, WHMCS will
    # return result=success with an empty list when the client has no items —
    # that's NOT an error.
    ok = response.status_code == 200 and whmcs_result == "success"

    return WhmcsActionProbe(
        action=action,
        feature=feature,
        ok=ok,
        http_status=response.status_code,
        whmcs_result=whmcs_result,
        message=message,
        latency_ms=latency_ms,
        required_permission=required_permission,
    )


@router.get("/system/actions-health", response_model=WhmcsActionsHealthResponse)
async def whmcs_actions_health(
    test_email: Optional[str] = Query(
        None,
        description=(
            "Email of a real WHMCS client to use for client-scoped probes "
            "(GetClientsProducts, GetInvoices, GetClientsDomains, GetTickets, "
            "GetEmails). Defaults to the admin's own email."
        ),
    ),
    current_user: UserResponse = Depends(get_current_user),
):
    """Probe every WHMCS API action the client dashboard depends on.

    This is the diagnostic endpoint we surface on the admin WHMCS Health page.
    It returns a per-action pass/fail row so the administrator can immediately
    see which API permissions are missing (e.g. "GetInvoices is not allowed"
    vs "GetClientsProducts returned success but empty list").

    Never raises on upstream failure — failure is part of the response body.
    """
    _require_admin(current_user)

    # 1) Resolve a test client id. For client-scoped actions we need a real
    #    clientid; we prefer a supplied test_email (admin may want to probe
    #    against a specific customer), else fall back to the admin's own email.
    target_email = (test_email or getattr(current_user, "email", "") or "").strip().lower()
    client_id_resolved: Optional[int] = None

    if target_email:
        try:
            # We call the raw client lookup helper to avoid raising.
            from services.whmcs import get_client_by_email

            client = await get_client_by_email(target_email, use_cache=False)
            if client:
                raw_id = client.get("userid") or client.get("id")
                try:
                    client_id_resolved = int(raw_id) if raw_id is not None else None
                except (TypeError, ValueError):
                    client_id_resolved = None
        except Exception:  # noqa: BLE001
            logger.exception("actions-health: failed to resolve client_id")

    # 2) Define the probe matrix. Each entry lists:
    #    - action: WHMCS API action name
    #    - feature: the dashboard section it powers (for the admin UI)
    #    - required_permission: exact name to enable in WHMCS API Role
    #    - payload: request params (client-scoped probes need client_id_resolved)
    probe_defs: List[Dict[str, Any]] = [
        {
            "action": "WhmcsDetails",
            "feature": "Connectivity",
            "required_permission": "WhmcsDetails",
            "payload": {},
        },
        {
            "action": "GetClientsDetails",
            "feature": "Profile / Auth",
            "required_permission": "GetClientsDetails",
            "payload": {"email": target_email, "stats": "true"} if target_email else {},
        },
        {
            "action": "GetClientsProducts",
            "feature": "Services",
            "required_permission": "GetClientsProducts",
            "payload": (
                {"clientid": client_id_resolved, "stats": "true"}
                if client_id_resolved
                else {"limitnum": 1}
            ),
        },
        {
            "action": "GetInvoices",
            "feature": "Invoices",
            "required_permission": "GetInvoices",
            "payload": (
                {"userid": client_id_resolved, "limitnum": 1}
                if client_id_resolved
                else {"limitnum": 1}
            ),
        },
        {
            "action": "GetClientsDomains",
            "feature": "Domains",
            "required_permission": "GetClientsDomains",
            "payload": (
                {"clientid": client_id_resolved, "limitnum": 1}
                if client_id_resolved
                else {"limitnum": 1}
            ),
        },
        {
            "action": "GetTickets",
            "feature": "Tickets",
            "required_permission": "GetTickets",
            "payload": (
                {"clientid": client_id_resolved, "limitnum": 1}
                if client_id_resolved
                else {"limitnum": 1}
            ),
        },
        {
            "action": "GetEmails",
            "feature": "Email History",
            "required_permission": "GetEmails",
            "payload": (
                {"clientid": client_id_resolved, "limitnum": 1}
                if client_id_resolved
                else {"limitnum": 1}
            ),
        },
        {
            "action": "GetSupportDepartments",
            "feature": "Support Departments",
            "required_permission": "GetSupportDepartments",
            "payload": {},
        },
        {
            "action": "DomainWhois",
            "feature": "Domain Search",
            "required_permission": "DomainWhois",
            "payload": {"domain": "example.com"},
        },
        {
            "action": "GetOrders",
            "feature": "Orders",
            "required_permission": "GetOrders",
            "payload": (
                {"userid": client_id_resolved, "limitnum": 1}
                if client_id_resolved
                else {"limitnum": 1}
            ),
        },
        {
            "action": "GetProducts",
            "feature": "Product Catalog",
            "required_permission": "GetProducts",
            "payload": {},
        },
        {
            "action": "GetServers",
            "feature": "Servers",
            "required_permission": "GetServers",
            "payload": {},
        },
        {
            "action": "GetCurrencies",
            "feature": "Currencies",
            "required_permission": "GetCurrencies",
            "payload": {},
        },
        {
            "action": "GetPaymentMethods",
            "feature": "Payment Methods",
            "required_permission": "GetPaymentMethods",
            "payload": {},
        },
        {
            "action": "GetActivityLog",
            "feature": "Activity Log",
            "required_permission": "GetActivityLog",
            "payload": {"limitnum": 1},
        },
    ]

    # 3) Run probes sequentially to avoid hammering the WHMCS origin.
    probes: List[WhmcsActionProbe] = []
    for defn in probe_defs:
        probe = await _probe_whmcs_action(
            action=defn["action"],
            feature=defn["feature"],
            required_permission=defn["required_permission"],
            payload=defn["payload"],
        )
        probes.append(probe)

    # 4) Build a summary.
    failed = [p for p in probes if not p.ok]
    if not failed:
        summary = "All probed WHMCS API actions are healthy."
    else:
        failed_names = ", ".join(f"{p.action} ({p.feature})" for p in failed)
        summary = (
            f"{len(failed)} of {len(probes)} actions failed: {failed_names}. "
            "Enable the listed permissions in WHMCS → Setup → Staff Management "
            "→ API Roles for the role assigned to this identifier."
        )

    return WhmcsActionsHealthResponse(
        ok=len(failed) == 0,
        probed_at=time.time(),
        client_id_resolved=client_id_resolved,
        test_email=target_email or None,
        probes=probes,
        summary=summary,
    )


# ---------- Admin catalog endpoints (admin-only) ----------
# These surface WHMCS-global data (not client-scoped) so the admin UI can
# build system-wide views: product catalog, currency list, payment gateway
# list, all clients, all invoices, all orders. They use call_whmcs_safe so
# a missing API permission returns a structured error payload instead of
# crashing the page.


def _pick_list(body: Dict[str, Any], *keys: str) -> List[Dict[str, Any]]:
    """Extract a list from a WHMCS response regardless of wrapper shape.

    WHMCS is inconsistent: sometimes it returns ``{"products": {"product": [...]}}``
    and sometimes ``{"products": [...]}``. This helper walks a list of
    candidate keys and always returns a plain ``list[dict]``.
    """
    for key in keys:
        val = body.get(key)
        if isinstance(val, list):
            return val
        if isinstance(val, dict):
            # Common WHMCS nested shape: {"products": {"product": [...]}}
            for inner_key in ("product", "item", "order", "invoice", "client", "gateway", "currency", "server"):
                inner = val.get(inner_key)
                if isinstance(inner, list):
                    return inner
            # Last-ditch: single-row dict wrapped in object
            if val:
                return [val]  # type: ignore[list-item]
    return []


@router.get("/catalog/products")
async def whmcs_catalog_products(
    gid: Optional[int] = Query(None, description="Restrict to a specific product group id."),
    current_user: UserResponse = Depends(get_current_user),
):
    """List the full WHMCS product catalog (all products, all groups).

    Powers the admin Product Catalog view. Returns a safe error payload
    instead of raising when the API Role lacks ``GetProducts``.
    """
    _require_admin(current_user)
    payload: Dict[str, Any] = {}
    if gid is not None:
        payload["gid"] = gid
    body = await call_whmcs_safe("GetProducts", payload)
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetProducts.",
            "required_permission": "GetProducts",
            "products": [],
        }
    return {
        "ok": True,
        "total": int(body.get("totalresults") or 0),
        "products": _pick_list(body, "products"),
    }


@router.get("/catalog/currencies")
async def whmcs_catalog_currencies(
    current_user: UserResponse = Depends(get_current_user),
):
    """List all currencies configured in WHMCS (for FX display in admin)."""
    _require_admin(current_user)
    body = await call_whmcs_safe("GetCurrencies", {})
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetCurrencies.",
            "required_permission": "GetCurrencies",
            "currencies": [],
        }
    return {
        "ok": True,
        "currencies": _pick_list(body, "currencies"),
    }


@router.get("/catalog/payment-methods")
async def whmcs_catalog_payment_methods(
    current_user: UserResponse = Depends(get_current_user),
):
    """List all enabled payment gateways (Stripe, PayPal, bank transfer, etc.)."""
    _require_admin(current_user)
    body = await call_whmcs_safe("GetPaymentMethods", {})
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetPaymentMethods.",
            "required_permission": "GetPaymentMethods",
            "payment_methods": [],
        }
    return {
        "ok": True,
        "payment_methods": _pick_list(body, "paymentmethods"),
    }


@router.get("/catalog/servers")
async def whmcs_catalog_servers(
    current_user: UserResponse = Depends(get_current_user),
):
    """List all provisioning servers (hosting nodes) registered in WHMCS."""
    _require_admin(current_user)
    body = await call_whmcs_safe("GetServers", {})
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetServers.",
            "required_permission": "GetServers",
            "servers": [],
        }
    return {
        "ok": True,
        "servers": _pick_list(body, "servers"),
    }


@router.get("/clients/all")
async def whmcs_all_clients(
    search: Optional[str] = Query(None, description="Filter by email / name (WHMCS search)."),
    status: Optional[str] = Query(None, description="Active | Inactive | Closed."),
    limit: int = Query(100, ge=1, le=500),
    offset: int = Query(0, ge=0),
    current_user: UserResponse = Depends(get_current_user),
):
    """List every WHMCS client (admin global view).

    Unlike /api/v1/whmcs/me which is scoped to the logged-in user, this
    endpoint returns the full client roster so the admin can search and
    drill into any account. Requires the ``GetClients`` API permission.
    """
    _require_admin(current_user)
    payload: Dict[str, Any] = {"limitnum": limit, "limitstart": offset}
    if search:
        payload["search"] = search
    if status:
        payload["status"] = status
    body = await call_whmcs_safe("GetClients", payload)
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetClients.",
            "required_permission": "GetClients",
            "clients": [],
            "total": 0,
        }
    return {
        "ok": True,
        "total": int(body.get("totalresults") or 0),
        "start": int(body.get("startnumber") or 0),
        "returned": int(body.get("numreturned") or 0),
        "clients": _pick_list(body, "clients"),
    }


@router.get("/orders/all")
async def whmcs_all_orders(
    status: Optional[str] = Query(None, description="Pending | Active | Fraud | Cancelled."),
    limit: int = Query(100, ge=1, le=500),
    offset: int = Query(0, ge=0),
    current_user: UserResponse = Depends(get_current_user),
):
    """List every order across the system (admin global view)."""
    _require_admin(current_user)
    payload: Dict[str, Any] = {"limitnum": limit, "limitstart": offset}
    if status:
        payload["status"] = status
    body = await call_whmcs_safe("GetOrders", payload)
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetOrders.",
            "required_permission": "GetOrders",
            "orders": [],
            "total": 0,
        }
    return {
        "ok": True,
        "total": int(body.get("totalresults") or 0),
        "orders": _pick_list(body, "orders"),
    }


@router.get("/invoices/all")
async def whmcs_all_invoices(
    status: Optional[str] = Query(None, description="Paid | Unpaid | Cancelled | Refunded | Collections."),
    limit: int = Query(100, ge=1, le=500),
    offset: int = Query(0, ge=0),
    current_user: UserResponse = Depends(get_current_user),
):
    """List every invoice across the system (admin global view)."""
    _require_admin(current_user)
    payload: Dict[str, Any] = {"limitnum": limit, "limitstart": offset}
    if status:
        payload["status"] = status
    body = await call_whmcs_safe("GetInvoices", payload)
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetInvoices.",
            "required_permission": "GetInvoices",
            "invoices": [],
            "total": 0,
        }
    return {
        "ok": True,
        "total": int(body.get("totalresults") or 0),
        "invoices": _pick_list(body, "invoices"),
    }


@router.get("/tickets/all")
async def whmcs_all_tickets(
    status: Optional[str] = Query(None, description="Open | Answered | Customer-Reply | On Hold | In Progress | Closed."),
    department: Optional[int] = Query(None, description="Filter by department id."),
    limit: int = Query(100, ge=1, le=500),
    offset: int = Query(0, ge=0),
    current_user: UserResponse = Depends(get_current_user),
):
    """List every ticket across the system (admin global view)."""
    _require_admin(current_user)
    payload: Dict[str, Any] = {"limitnum": limit, "limitstart": offset}
    if status:
        payload["status"] = status
    if department is not None:
        payload["deptid"] = department
    body = await call_whmcs_safe("GetTickets", payload)
    if body.get("result") != "success":
        return {
            "ok": False,
            "message": body.get("message") or "WHMCS rejected GetTickets.",
            "required_permission": "GetTickets",
            "tickets": [],
            "total": 0,
        }
    return {
        "ok": True,
        "total": int(body.get("totalresults") or 0),
        "tickets": _pick_list(body, "tickets"),
    }