# @File: backend/services/uptimerobot/client.py
# @Desc: Thin async wrapper around the UptimeRobot API v3
#        (https://uptimerobot.com/api/v3/). All outbound calls are funneled
#        through :meth:`UptimeRobotClient.request` so retry, rate-limit
#        handling, error classification, and logging are applied uniformly.
#
# Credential resolution precedence:
#   1. DB-stored value in ``api_hub_credentials`` (provider_id="uptimerobot",
#      name="UPTIMEROBOT_API_KEY")
#   2. Environment variable ``UPTIMEROBOT_API_KEY``
#   3. No key → ``UR_ERR_INVALID_KEY``
#
# The master API key is never returned to the frontend; admins see only a
# masked representation.
from __future__ import annotations

import asyncio
import logging
import os
import time
from typing import Any, Dict, Optional

import httpx
from core.database import db_manager
from core.mask_crypto import decrypt_text
from services.uptimerobot.logger import record_uptimerobot_call

logger = logging.getLogger(__name__)

UR_API_BASE = os.environ.get("UPTIMEROBOT_API_BASE", "https://api.uptimerobot.com/v3")
UR_PROVIDER_ID = "uptimerobot"
UR_API_KEY_NAME = "UPTIMEROBOT_API_KEY"

# Error classification tags (surfaced to admin UI + diagnostics).
UR_ERR_INVALID_KEY = "invalid_key"
UR_ERR_PERMISSION = "permission_denied"
UR_ERR_RATE_LIMIT = "rate_limited"
UR_ERR_VALIDATION = "validation_error"
UR_ERR_UPSTREAM = "upstream_unavailable"
UR_ERR_TRANSPORT = "transport_error"


class UptimeRobotError(Exception):
    """Typed error for UptimeRobot API failures."""

    def __init__(
        self,
        error_class: str,
        message: str,
        *,
        http_status: Optional[int] = None,
        payload: Optional[Any] = None,
    ) -> None:
        super().__init__(message)
        self.error_class = error_class
        self.message = message
        self.http_status = http_status
        self.payload = payload


async def _load_api_key_from_db() -> Optional[str]:
    """Try to read the encrypted UptimeRobot API key from api_hub_credentials."""
    if not db_manager.async_session_maker:
        try:
            await db_manager.ensure_initialized()
        except Exception:  # noqa: BLE001
            return None
    if not db_manager.async_session_maker:
        return None
    try:
        from models.api_hub_credentials import ApiHubCredential
        from sqlalchemy import select

        async with db_manager.async_session_maker() as session:
            result = await session.execute(
                select(ApiHubCredential).where(
                    ApiHubCredential.provider_id == UR_PROVIDER_ID,
                    ApiHubCredential.name == UR_API_KEY_NAME,
                )
            )
            row = result.scalar_one_or_none()
            if row is None or not row.encrypted_value:
                return None
            try:
                return decrypt_text(row.encrypted_value)
            except Exception:  # noqa: BLE001
                logger.exception("Failed to decrypt UPTIMEROBOT_API_KEY from DB")
                return None
    except Exception:  # noqa: BLE001
        logger.exception("Failed to read UPTIMEROBOT_API_KEY from DB")
        return None


async def get_master_api_key() -> tuple[Optional[str], str]:
    """Return (api_key, source) where source is db|env|none."""
    db_value = await _load_api_key_from_db()
    if db_value:
        return db_value, "db"
    env_value = os.environ.get(UR_API_KEY_NAME)
    if env_value:
        return env_value, "env"
    return None, "none"


def mask_api_key(key: Optional[str]) -> Optional[str]:
    """Return a ``****1234``-style masked representation of a key."""
    if not key:
        return None
    clean = key.strip()
    if len(clean) <= 6:
        return "*" * len(clean)
    return f"****{clean[-4:]}"


def _classify_http_error(status_code: int, body: Any) -> str:
    if status_code in (401,):
        return UR_ERR_INVALID_KEY
    if status_code in (403,):
        return UR_ERR_PERMISSION
    if status_code in (429,):
        return UR_ERR_RATE_LIMIT
    if status_code in (400, 422):
        return UR_ERR_VALIDATION
    if status_code >= 500:
        return UR_ERR_UPSTREAM
    return UR_ERR_UPSTREAM


def _extract_error_message(payload: Any) -> str:
    if isinstance(payload, dict):
        for key in ("error", "message", "detail", "error_description"):
            val = payload.get(key)
            if isinstance(val, str) and val.strip():
                return val.strip()
            if isinstance(val, dict):
                nested = val.get("message") or val.get("detail")
                if isinstance(nested, str) and nested.strip():
                    return nested.strip()
    if isinstance(payload, str):
        return payload.strip()[:500]
    return "UptimeRobot API error"


class UptimeRobotClient:
    """Async UptimeRobot v3 client with retry + classification + logging."""

    def __init__(
        self,
        api_key: Optional[str] = None,
        *,
        base_url: str = UR_API_BASE,
        timeout: float = 20.0,
        max_retries: int = 3,
        user_email: Optional[str] = None,
    ) -> None:
        self.api_key = api_key
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout
        self.max_retries = max(1, int(max_retries))
        self.user_email = user_email

    # ---------- low-level ----------
    async def request(
        self,
        method: str,
        path: str,
        *,
        json: Optional[Dict[str, Any]] = None,
        params: Optional[Dict[str, Any]] = None,
    ) -> Any:
        if not self.api_key:
            await record_uptimerobot_call(
                endpoint=path,
                method=method,
                status="error",
                error_class=UR_ERR_INVALID_KEY,
                error_message="Missing UPTIMEROBOT_API_KEY",
                user_email=self.user_email,
            )
            raise UptimeRobotError(UR_ERR_INVALID_KEY, "Missing UPTIMEROBOT_API_KEY")

        url = f"{self.base_url}{path}"
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

        last_exc: Optional[Exception] = None
        last_status: Optional[int] = None
        for attempt in range(1, self.max_retries + 1):
            start = time.monotonic()
            try:
                async with httpx.AsyncClient(timeout=self.timeout) as client:
                    resp = await client.request(
                        method.upper(), url, headers=headers, json=json, params=params
                    )
                duration_ms = int((time.monotonic() - start) * 1000)
                last_status = resp.status_code
                try:
                    payload = resp.json()
                except Exception:  # noqa: BLE001
                    payload = resp.text

                # Success path
                if 200 <= resp.status_code < 300:
                    await record_uptimerobot_call(
                        endpoint=path,
                        method=method,
                        status="ok",
                        http_status=resp.status_code,
                        duration_ms=duration_ms,
                        user_email=self.user_email,
                    )
                    return payload

                # Classify the error and decide if it's retryable.
                err_class = _classify_http_error(resp.status_code, payload)
                message = _extract_error_message(payload)
                if err_class in (UR_ERR_RATE_LIMIT, UR_ERR_UPSTREAM) and attempt < self.max_retries:
                    # Exponential backoff: 0.5s, 1s, 2s...
                    delay = 0.5 * (2 ** (attempt - 1))
                    logger.warning(
                        "UptimeRobot %s %s transient %s (attempt %s/%s) — retry in %.1fs",
                        method,
                        path,
                        err_class,
                        attempt,
                        self.max_retries,
                        delay,
                    )
                    await asyncio.sleep(delay)
                    continue

                await record_uptimerobot_call(
                    endpoint=path,
                    method=method,
                    status="error",
                    http_status=resp.status_code,
                    duration_ms=duration_ms,
                    error_class=err_class,
                    error_message=message,
                    user_email=self.user_email,
                )
                raise UptimeRobotError(
                    err_class, message, http_status=resp.status_code, payload=payload
                )
            except UptimeRobotError:
                raise
            except (httpx.TimeoutException, httpx.TransportError) as exc:
                duration_ms = int((time.monotonic() - start) * 1000)
                last_exc = exc
                if attempt < self.max_retries:
                    delay = 0.5 * (2 ** (attempt - 1))
                    logger.warning(
                        "UptimeRobot %s %s transport error (attempt %s/%s): %s — retry in %.1fs",
                        method,
                        path,
                        attempt,
                        self.max_retries,
                        exc,
                        delay,
                    )
                    await asyncio.sleep(delay)
                    continue
                await record_uptimerobot_call(
                    endpoint=path,
                    method=method,
                    status="error",
                    http_status=None,
                    duration_ms=duration_ms,
                    error_class=UR_ERR_TRANSPORT,
                    error_message=str(exc),
                    user_email=self.user_email,
                )
                raise UptimeRobotError(UR_ERR_TRANSPORT, str(exc)) from exc

        # Should be unreachable; defensive guard.
        raise UptimeRobotError(
            UR_ERR_UPSTREAM,
            f"UptimeRobot call exhausted retries: last_status={last_status}, last_exc={last_exc}",
        )

    # ---------- typed convenience methods ----------
    async def account_details(self) -> Any:
        return await self.request("GET", "/account-details")

    async def list_monitors(self, *, limit: int = 50, offset: int = 0) -> Any:
        return await self.request(
            "GET", "/monitors", params={"limit": limit, "offset": offset}
        )

    async def create_monitor(self, data: Dict[str, Any]) -> Any:
        return await self.request("POST", "/monitors", json=data)

    async def update_monitor(self, monitor_id: str, data: Dict[str, Any]) -> Any:
        return await self.request("PATCH", f"/monitors/{monitor_id}", json=data)

    async def delete_monitor(self, monitor_id: str) -> Any:
        return await self.request("DELETE", f"/monitors/{monitor_id}")

    async def monitor_logs(self, monitor_id: str, *, limit: int = 20) -> Any:
        return await self.request(
            "GET", f"/monitors/{monitor_id}/logs", params={"limit": limit}
        )

    async def list_alert_contacts(self) -> Any:
        return await self.request("GET", "/alert-contacts")

    async def create_alert_contact(self, data: Dict[str, Any]) -> Any:
        return await self.request("POST", "/alert-contacts", json=data)

    async def delete_alert_contact(self, contact_id: str) -> Any:
        return await self.request("DELETE", f"/alert-contacts/{contact_id}")

    async def list_maintenance_windows(self) -> Any:
        return await self.request("GET", "/maintenance-windows")

    async def create_maintenance_window(self, data: Dict[str, Any]) -> Any:
        return await self.request("POST", "/maintenance-windows", json=data)

    async def delete_maintenance_window(self, window_id: str) -> Any:
        return await self.request("DELETE", f"/maintenance-windows/{window_id}")

    async def list_status_pages(self) -> Any:
        return await self.request("GET", "/public-status-pages")

    async def create_status_page(self, data: Dict[str, Any]) -> Any:
        return await self.request("POST", "/public-status-pages", json=data)

    async def delete_status_page(self, page_id: str) -> Any:
        return await self.request("DELETE", f"/public-status-pages/{page_id}")


async def get_client(user_email: Optional[str] = None) -> UptimeRobotClient:
    """Factory that injects the currently-configured master API key."""
    api_key, _source = await get_master_api_key()
    return UptimeRobotClient(api_key=api_key, user_email=user_email)