# @File: backend/routers/uptimerobot_admin.py
# @Desc: Admin-facing UptimeRobot Reseller endpoints. All routes require an
#        admin JWT (via ``get_admin_user``); WHMCS hooks additionally accept
#        a shared-secret header for automated invocation from WHMCS.
from __future__ import annotations

import logging
import os
from datetime import datetime
from typing import List, Optional

from core.database import get_db
from core.mask_crypto import encrypt_text
from dependencies.auth import get_admin_user
from fastapi import APIRouter, Depends, Header, HTTPException, Query, status
from models.uptimerobot import (
    UptimeApiLog,
    UptimeClientMonitor,
    UptimeClientSubscription,
    UptimePackage,
)
from schemas.auth import UserResponse
from schemas.uptimerobot import (
    UptimeApiLogItem,
    UptimeApiLogListResponse,
    UptimeApiSettingsOut,
    UptimeApiTestResult,
    UptimeMonitorOut,
    UptimePackageCreate,
    UptimePackageOut,
    UptimePackageUpdate,
    UptimeSubscriptionCreate,
    UptimeSubscriptionOut,
    UptimeSubscriptionUpdate,
    UptimeSyncResult,
    UptimeTeamUsageOut,
)
from services.audit_log import record_admin_event
from services.uptimerobot import (
    UR_ERR_INVALID_KEY,
    UptimeRobotError,
    get_client,
    get_master_api_key,
)
from services.uptimerobot.client import UR_API_KEY_NAME, UR_PROVIDER_ID, mask_api_key
from services.uptimerobot.packages import (
    get_package,
    list_packages,
    normalize_allowed_types,
)
from services.uptimerobot.provisioning import (
    on_product_activated,
    on_product_changed,
    on_product_suspended,
    on_product_terminated,
    on_product_unsuspended,
)
from services.uptimerobot.subscriptions import (
    change_package,
    create_subscription,
    list_subscriptions,
    refresh_usage_counters,
    resume_subscription,
    suspend_subscription,
    terminate_subscription,
)
from services.uptimerobot.sync import run_sync
from services.uptimerobot.usage import (
    WARNING_LABELS,
    compute_team_usage,
    latest_snapshot,
)
from sqlalchemy import func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession

logger = logging.getLogger(__name__)

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


# ---------- Packages ----------
@router.get("/packages", response_model=List[UptimePackageOut])
async def admin_list_packages(
    include_inactive: bool = Query(True),
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> List[UptimePackageOut]:
    packages = await list_packages(db, include_inactive=include_inactive)
    return [UptimePackageOut.model_validate(p) for p in packages]


@router.post("/packages", response_model=UptimePackageOut, status_code=status.HTTP_201_CREATED)
async def admin_create_package(
    payload: UptimePackageCreate,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimePackageOut:
    existing = await db.execute(select(UptimePackage).where(UptimePackage.slug == payload.slug))
    if existing.scalar_one_or_none() is not None:
        raise HTTPException(status_code=409, detail="Slug already in use.")
    data = payload.model_dump()
    data["allowed_monitor_types"] = normalize_allowed_types(data.get("allowed_monitor_types", ""))
    pkg = UptimePackage(**data)
    db.add(pkg)
    await db.commit()
    await db.refresh(pkg)
    await record_admin_event(
        db,
        action="admin.uptimerobot.package.created",
        admin_user_id=current_user.id,
        admin_email=current_user.email,
        details={"package_id": pkg.id, "slug": pkg.slug, "name": pkg.name},
    )
    return UptimePackageOut.model_validate(pkg)


@router.patch("/packages/{package_id}", response_model=UptimePackageOut)
async def admin_update_package(
    package_id: int,
    payload: UptimePackageUpdate,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimePackageOut:
    pkg = await get_package(db, package_id)
    if pkg is None:
        raise HTTPException(status_code=404, detail="Package not found")
    changes = payload.model_dump(exclude_unset=True)
    if "allowed_monitor_types" in changes and changes["allowed_monitor_types"] is not None:
        changes["allowed_monitor_types"] = normalize_allowed_types(
            changes["allowed_monitor_types"]
        )
    for key, value in changes.items():
        setattr(pkg, key, value)
    await db.commit()
    await db.refresh(pkg)
    await record_admin_event(
        db,
        action="admin.uptimerobot.package.updated",
        admin_user_id=current_user.id,
        admin_email=current_user.email,
        details={"package_id": pkg.id, "changes": list(changes.keys())},
    )
    return UptimePackageOut.model_validate(pkg)


@router.delete("/packages/{package_id}", status_code=status.HTTP_204_NO_CONTENT)
async def admin_delete_package(
    package_id: int,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> None:
    pkg = await get_package(db, package_id)
    if pkg is None:
        return
    # Refuse to delete a package that still has subscriptions.
    count = await db.scalar(
        select(func.count(UptimeClientSubscription.id)).where(
            UptimeClientSubscription.package_id == package_id
        )
    )
    if int(count or 0) > 0:
        raise HTTPException(
            status_code=409,
            detail="Package still has active subscriptions; deactivate instead.",
        )
    await db.delete(pkg)
    await db.commit()
    await record_admin_event(
        db,
        action="admin.uptimerobot.package.deleted",
        admin_user_id=current_user.id,
        admin_email=current_user.email,
        details={"package_id": package_id},
    )


# ---------- Subscriptions ----------
def _sub_to_out(sub: UptimeClientSubscription, pkg_name: Optional[str]) -> UptimeSubscriptionOut:
    data = UptimeSubscriptionOut.model_validate(sub)
    data.package_name = pkg_name
    return data


@router.get("/subscriptions", response_model=List[UptimeSubscriptionOut])
async def admin_list_subscriptions(
    status_filter: Optional[str] = Query(None, alias="status"),
    user_id: Optional[str] = Query(None),
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> List[UptimeSubscriptionOut]:
    subs = await list_subscriptions(db, user_id=user_id, status=status_filter)
    # Preload packages for nice display.
    package_ids = {s.package_id for s in subs}
    packages_map = {}
    if package_ids:
        pkgs = await db.execute(
            select(UptimePackage).where(UptimePackage.id.in_(package_ids))
        )
        packages_map = {p.id: p.name for p in pkgs.scalars().all()}
    return [_sub_to_out(s, packages_map.get(s.package_id)) for s in subs]


@router.post(
    "/subscriptions",
    response_model=UptimeSubscriptionOut,
    status_code=status.HTTP_201_CREATED,
)
async def admin_create_subscription(
    payload: UptimeSubscriptionCreate,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeSubscriptionOut:
    try:
        sub = await create_subscription(
            db,
            user_id=payload.user_id,
            user_email=payload.user_email,
            package_id=payload.package_id,
            whmcs_service_id=payload.whmcs_service_id,
            whmcs_product_id=payload.whmcs_product_id,
            notes=payload.notes,
        )
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc))
    pkg = await get_package(db, sub.package_id)
    await record_admin_event(
        db,
        action="admin.uptimerobot.subscription.created",
        admin_user_id=current_user.id,
        admin_email=current_user.email,
        target_user_id=sub.user_id,
        target_email=sub.user_email,
        details={"subscription_id": sub.id, "package_id": sub.package_id},
    )
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.patch("/subscriptions/{subscription_id}", response_model=UptimeSubscriptionOut)
async def admin_update_subscription(
    subscription_id: int,
    payload: UptimeSubscriptionUpdate,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeSubscriptionOut:
    sub = await db.get(UptimeClientSubscription, subscription_id)
    if sub is None:
        raise HTTPException(status_code=404, detail="Subscription not found")
    if payload.package_id is not None and payload.package_id != sub.package_id:
        try:
            sub = await change_package(db, sub, payload.package_id)
        except ValueError as exc:
            raise HTTPException(status_code=400, detail=str(exc))
    if payload.notes is not None:
        sub.notes = payload.notes
    if payload.status is not None and payload.status != sub.status:
        if payload.status == "suspended":
            await suspend_subscription(db, sub)
        elif payload.status == "active":
            await resume_subscription(db, sub)
        elif payload.status == "terminated":
            await terminate_subscription(db, sub)
    await db.commit()
    await db.refresh(sub)
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.post("/subscriptions/{subscription_id}/suspend", response_model=UptimeSubscriptionOut)
async def admin_suspend_subscription(
    subscription_id: int,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeSubscriptionOut:
    sub = await db.get(UptimeClientSubscription, subscription_id)
    if sub is None:
        raise HTTPException(status_code=404, detail="Subscription not found")
    await suspend_subscription(db, sub)
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.post("/subscriptions/{subscription_id}/resume", response_model=UptimeSubscriptionOut)
async def admin_resume_subscription(
    subscription_id: int,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeSubscriptionOut:
    sub = await db.get(UptimeClientSubscription, subscription_id)
    if sub is None:
        raise HTTPException(status_code=404, detail="Subscription not found")
    await resume_subscription(db, sub)
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.post("/subscriptions/{subscription_id}/terminate", response_model=UptimeSubscriptionOut)
async def admin_terminate_subscription(
    subscription_id: int,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeSubscriptionOut:
    sub = await db.get(UptimeClientSubscription, subscription_id)
    if sub is None:
        raise HTTPException(status_code=404, detail="Subscription not found")
    await terminate_subscription(db, sub)
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.get(
    "/subscriptions/{subscription_id}/monitors", response_model=List[UptimeMonitorOut]
)
async def admin_list_subscription_monitors(
    subscription_id: int,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> List[UptimeMonitorOut]:
    sub = await db.get(UptimeClientSubscription, subscription_id)
    if sub is None:
        raise HTTPException(status_code=404, detail="Subscription not found")
    result = await db.execute(
        select(UptimeClientMonitor)
        .where(UptimeClientMonitor.subscription_id == subscription_id)
        .order_by(UptimeClientMonitor.created_at.desc())
    )
    return [UptimeMonitorOut.model_validate(m) for m in result.scalars().all()]


# ---------- Team usage / API settings / sync / diagnostics ----------
@router.get("/usage", response_model=UptimeTeamUsageOut)
async def admin_team_usage(
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeTeamUsageOut:
    usage, _err = await compute_team_usage(db)
    # Override snapshot_at with the latest persisted snapshot, if any, so the
    # UI shows when the last *durable* record happened.
    snap = await latest_snapshot(db)
    if snap is not None:
        usage["snapshot_at"] = snap.created_at
    return UptimeTeamUsageOut(**usage)


@router.get("/api-settings", response_model=UptimeApiSettingsOut)
async def admin_get_api_settings(
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeApiSettingsOut:
    api_key, source = await get_master_api_key()
    # Pull the most recent error from uptime_api_logs, if any.
    last_ok_result = await db.execute(
        select(UptimeApiLog)
        .where(UptimeApiLog.status == "ok")
        .order_by(UptimeApiLog.created_at.desc())
        .limit(1)
    )
    last_ok = last_ok_result.scalar_one_or_none()
    last_err_result = await db.execute(
        select(UptimeApiLog)
        .where(UptimeApiLog.status == "error")
        .order_by(UptimeApiLog.created_at.desc())
        .limit(1)
    )
    last_err = last_err_result.scalar_one_or_none()
    return UptimeApiSettingsOut(
        api_key_present=bool(api_key),
        api_key_masked=mask_api_key(api_key) if api_key else None,
        api_key_source=source,  # type: ignore[arg-type]
        last_sync_at=last_ok.created_at if last_ok else None,
        last_error_at=last_err.created_at if last_err else None,
        last_error_class=last_err.error_class if last_err else None,
        last_error_message=last_err.error_message if last_err else None,
    )


class _ApiKeyPayload:
    """Simple container; we do not use Pydantic here to keep the router lean
    because the request body is tiny (one field)."""


@router.post("/api-settings/key", response_model=UptimeApiSettingsOut)
async def admin_save_api_key(
    body: dict,
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeApiSettingsOut:
    """Store/rotate the master UptimeRobot API key. Encrypted at rest."""
    key_value = (body or {}).get("api_key") or ""
    if not isinstance(key_value, str) or not key_value.strip():
        raise HTTPException(status_code=400, detail="api_key is required")
    try:
        from models.api_hub_credentials import ApiHubCredential
    except ImportError as exc:  # pragma: no cover
        raise HTTPException(
            status_code=500, detail="api_hub_credentials model not available"
        ) from exc

    encrypted = encrypt_text(key_value.strip())
    existing = await db.execute(
        select(ApiHubCredential).where(
            ApiHubCredential.provider_id == UR_PROVIDER_ID,
            ApiHubCredential.name == UR_API_KEY_NAME,
        )
    )
    row = existing.scalar_one_or_none()
    if row is None:
        row = ApiHubCredential(
            provider_id=UR_PROVIDER_ID,
            name=UR_API_KEY_NAME,
            encrypted_value=encrypted,
            updated_by=current_user.email,
        )
        db.add(row)
    else:
        row.encrypted_value = encrypted
        row.updated_by = current_user.email
    await db.commit()
    await record_admin_event(
        db,
        action="admin.uptimerobot.api_key.rotated",
        admin_user_id=current_user.id,
        admin_email=current_user.email,
    )
    return await admin_get_api_settings(current_user=current_user, db=db)


@router.post("/api-settings/test", response_model=UptimeApiTestResult)
async def admin_test_api_key(
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeApiTestResult:
    client = await get_client(user_email=current_user.email)
    try:
        payload = await client.account_details()
    except UptimeRobotError as exc:
        return UptimeApiTestResult(
            ok=False,
            http_status=exc.http_status,
            error_class=exc.error_class,
            error_message=exc.message,
        )
    plan_name = None
    if isinstance(payload, dict):
        src = payload.get("data") or payload
        if isinstance(src, dict):
            plan = src.get("plan")
            if isinstance(plan, dict):
                plan_name = plan.get("name") or plan.get("title")
            elif isinstance(plan, str):
                plan_name = plan
    return UptimeApiTestResult(ok=True, plan_name=plan_name)


@router.post("/sync-now", response_model=UptimeSyncResult)
async def admin_sync_now(
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeSyncResult:
    result = await run_sync(db)
    await record_admin_event(
        db,
        action="admin.uptimerobot.sync",
        admin_user_id=current_user.id,
        admin_email=current_user.email,
        details={k: v for k, v in result.items() if k != "error"},
    )
    return UptimeSyncResult(**result)


@router.get("/api-logs", response_model=UptimeApiLogListResponse)
async def admin_api_logs(
    limit: int = Query(50, ge=1, le=500),
    offset: int = Query(0, ge=0),
    status_filter: Optional[str] = Query(None, alias="status"),
    q: Optional[str] = Query(None),
    current_user: UserResponse = Depends(get_admin_user),
    db: AsyncSession = Depends(get_db),
) -> UptimeApiLogListResponse:
    stmt = select(UptimeApiLog).order_by(UptimeApiLog.created_at.desc())
    count_stmt = select(func.count(UptimeApiLog.id))
    if status_filter:
        stmt = stmt.where(UptimeApiLog.status == status_filter)
        count_stmt = count_stmt.where(UptimeApiLog.status == status_filter)
    if q:
        like = f"%{q}%"
        clause = or_(
            UptimeApiLog.endpoint.ilike(like),
            UptimeApiLog.error_class.ilike(like),
            UptimeApiLog.error_message.ilike(like),
            UptimeApiLog.user_email.ilike(like),
        )
        stmt = stmt.where(clause)
        count_stmt = count_stmt.where(clause)
    total = int((await db.scalar(count_stmt)) or 0)
    result = await db.execute(stmt.offset(offset).limit(limit))
    items = [UptimeApiLogItem.model_validate(r) for r in result.scalars().all()]
    return UptimeApiLogListResponse(items=items, total=total)


# ---------- WHMCS provisioning hooks ----------
def _assert_whmcs_secret(secret: Optional[str]) -> None:
    expected = os.environ.get("UPTIMEROBOT_WHMCS_HOOK_SECRET")
    if not expected:
        # If no secret configured, fall through — the route still requires
        # admin JWT because it lives under /admin/. This keeps it usable
        # from the admin dashboard for manual testing.
        return
    if not secret or secret != expected:
        raise HTTPException(status_code=401, detail="Invalid WHMCS hook secret.")


@router.post("/whmcs-hooks/activated", response_model=UptimeSubscriptionOut)
async def whmcs_hook_activated(
    body: dict,
    current_user: UserResponse = Depends(get_admin_user),
    x_uptimerobot_secret: Optional[str] = Header(default=None),
    db: AsyncSession = Depends(get_db),
) -> UptimeSubscriptionOut:
    _assert_whmcs_secret(x_uptimerobot_secret)
    try:
        sub = await on_product_activated(
            db,
            user_id=str(body["user_id"]),
            user_email=body.get("user_email"),
            whmcs_service_id=int(body["whmcs_service_id"]),
            whmcs_product_id=int(body["whmcs_product_id"]),
            package_id=body.get("package_id"),
        )
    except KeyError as exc:
        raise HTTPException(status_code=400, detail=f"Missing field: {exc.args[0]}")
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc))
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.post("/whmcs-hooks/changed", response_model=Optional[UptimeSubscriptionOut])
async def whmcs_hook_changed(
    body: dict,
    current_user: UserResponse = Depends(get_admin_user),
    x_uptimerobot_secret: Optional[str] = Header(default=None),
    db: AsyncSession = Depends(get_db),
):
    _assert_whmcs_secret(x_uptimerobot_secret)
    try:
        sub = await on_product_changed(
            db,
            whmcs_service_id=int(body["whmcs_service_id"]),
            new_whmcs_product_id=int(body["new_whmcs_product_id"]),
            new_package_id=body.get("new_package_id"),
        )
    except KeyError as exc:
        raise HTTPException(status_code=400, detail=f"Missing field: {exc.args[0]}")
    except ValueError as exc:
        raise HTTPException(status_code=400, detail=str(exc))
    if sub is None:
        return None
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.post("/whmcs-hooks/suspended", response_model=Optional[UptimeSubscriptionOut])
async def whmcs_hook_suspended(
    body: dict,
    current_user: UserResponse = Depends(get_admin_user),
    x_uptimerobot_secret: Optional[str] = Header(default=None),
    db: AsyncSession = Depends(get_db),
):
    _assert_whmcs_secret(x_uptimerobot_secret)
    try:
        sub = await on_product_suspended(db, whmcs_service_id=int(body["whmcs_service_id"]))
    except KeyError as exc:
        raise HTTPException(status_code=400, detail=f"Missing field: {exc.args[0]}")
    if sub is None:
        return None
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.post("/whmcs-hooks/unsuspended", response_model=Optional[UptimeSubscriptionOut])
async def whmcs_hook_unsuspended(
    body: dict,
    current_user: UserResponse = Depends(get_admin_user),
    x_uptimerobot_secret: Optional[str] = Header(default=None),
    db: AsyncSession = Depends(get_db),
):
    _assert_whmcs_secret(x_uptimerobot_secret)
    try:
        sub = await on_product_unsuspended(db, whmcs_service_id=int(body["whmcs_service_id"]))
    except KeyError as exc:
        raise HTTPException(status_code=400, detail=f"Missing field: {exc.args[0]}")
    if sub is None:
        return None
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)


@router.post("/whmcs-hooks/terminated", response_model=Optional[UptimeSubscriptionOut])
async def whmcs_hook_terminated(
    body: dict,
    current_user: UserResponse = Depends(get_admin_user),
    x_uptimerobot_secret: Optional[str] = Header(default=None),
    db: AsyncSession = Depends(get_db),
):
    _assert_whmcs_secret(x_uptimerobot_secret)
    try:
        sub = await on_product_terminated(db, whmcs_service_id=int(body["whmcs_service_id"]))
    except KeyError as exc:
        raise HTTPException(status_code=400, detail=f"Missing field: {exc.args[0]}")
    if sub is None:
        return None
    pkg = await get_package(db, sub.package_id)
    return _sub_to_out(sub, pkg.name if pkg else None)