# @File: backend/services/uptimerobot/monitors.py
# @Desc: Client-facing monitor operations. Enforces package quotas, allowed
#        monitor types, and ownership. All upstream calls go through the
#        shared UptimeRobotClient so logs/retries are centralised.
from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

from models.uptimerobot import UptimeClientMonitor, UptimeClientSubscription, UptimePackage
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from services.uptimerobot import UptimeRobotError, get_client
from services.uptimerobot.packages import (
    assert_interval_ok,
    assert_quota_ok,
    get_package,
    package_allows_type,
)
from services.uptimerobot.subscriptions import refresh_usage_counters

logger = logging.getLogger(__name__)


def _ur_type_from(monitor_type: str) -> str:
    """Translate our internal monitor_type to the UptimeRobot v3 wire value."""
    mapping = {
        "http": "http",
        "ping": "ping",
        "port": "port",
        "keyword": "keyword",
        "heartbeat": "heartbeat",
    }
    return mapping.get(monitor_type, "http")


def _extract_id_from_response(resp: Any) -> Optional[str]:
    """Pull the newly-created object id from an UR v3 response envelope."""
    if isinstance(resp, dict):
        for key in ("id", "monitor_id", "contact_id", "window_id", "page_id"):
            val = resp.get(key)
            if val:
                return str(val)
        data = resp.get("data")
        if isinstance(data, dict):
            for key in ("id", "monitor_id", "contact_id", "window_id", "page_id"):
                val = data.get(key)
                if val:
                    return str(val)
        monitor = resp.get("monitor")
        if isinstance(monitor, dict) and monitor.get("id"):
            return str(monitor["id"])
    return None


def _build_ur_payload(
    *,
    friendly_name: str,
    monitor_type: str,
    url: Optional[str],
    keyword: Optional[str],
    port: Optional[int],
    interval_seconds: int,
) -> Dict[str, Any]:
    payload: Dict[str, Any] = {
        "friendly_name": friendly_name,
        "type": _ur_type_from(monitor_type),
        "interval": interval_seconds,
    }
    if monitor_type in ("http", "keyword"):
        if not url:
            raise ValueError("URL is required for http/keyword monitors.")
        payload["url"] = url
        if monitor_type == "keyword":
            if not keyword:
                raise ValueError("Keyword is required for keyword monitors.")
            payload["keyword_value"] = keyword
            payload["keyword_type"] = "exists"
    elif monitor_type == "ping":
        if not url:
            raise ValueError("Host is required for ping monitors.")
        payload["url"] = url
    elif monitor_type == "port":
        if not url or port is None:
            raise ValueError("Host and port are required for port monitors.")
        payload["url"] = url
        payload["port"] = int(port)
    elif monitor_type == "heartbeat":
        payload["type"] = "heartbeat"
    return payload


async def list_monitors_for_subscription(
    session: AsyncSession, subscription_id: int, *, include_archived: bool = False
) -> List[UptimeClientMonitor]:
    stmt = select(UptimeClientMonitor).where(
        UptimeClientMonitor.subscription_id == subscription_id
    )
    if not include_archived:
        stmt = stmt.where(UptimeClientMonitor.archived.is_(False))
    stmt = stmt.order_by(UptimeClientMonitor.created_at.desc())
    result = await session.execute(stmt)
    return list(result.scalars().all())


async def get_monitor(
    session: AsyncSession, monitor_id: int, *, user_id: Optional[str] = None
) -> Optional[UptimeClientMonitor]:
    mon = await session.get(UptimeClientMonitor, monitor_id)
    if mon is None:
        return None
    if user_id is not None and mon.user_id != user_id:
        return None
    return mon


async def _assert_can_add_monitor(
    session: AsyncSession,
    subscription: UptimeClientSubscription,
    package: UptimePackage,
    monitor_type: str,
    interval_seconds: int,
) -> None:
    if subscription.status != "active":
        raise ValueError("Subscription is not active.")
    if not package_allows_type(package, monitor_type):
        raise ValueError(
            f"Monitor type '{monitor_type}' is not allowed by package '{package.name}'."
        )
    assert_interval_ok(package, interval_seconds)
    await refresh_usage_counters(session, subscription)
    assert_quota_ok(package, subscription.monitors_used, limit_field="max_monitors")


async def create_monitor(
    session: AsyncSession,
    subscription: UptimeClientSubscription,
    *,
    friendly_name: str,
    monitor_type: str,
    url: Optional[str] = None,
    keyword: Optional[str] = None,
    port: Optional[int] = None,
    interval_seconds: int = 300,
) -> UptimeClientMonitor:
    package = await get_package(session, subscription.package_id)
    if package is None:
        raise ValueError("Subscription package not found.")
    await _assert_can_add_monitor(
        session, subscription, package, monitor_type, interval_seconds
    )

    payload = _build_ur_payload(
        friendly_name=friendly_name,
        monitor_type=monitor_type,
        url=url,
        keyword=keyword,
        port=port,
        interval_seconds=interval_seconds,
    )
    client = await get_client(user_email=subscription.user_email)
    ur_resp = await client.create_monitor(payload)
    ur_monitor_id = _extract_id_from_response(ur_resp)
    if not ur_monitor_id:
        raise UptimeRobotError(
            "validation_error", "UptimeRobot did not return a monitor id"
        )

    monitor = UptimeClientMonitor(
        subscription_id=subscription.id,
        user_id=subscription.user_id,
        ur_monitor_id=str(ur_monitor_id),
        friendly_name=friendly_name,
        monitor_type=monitor_type,
        url=url,
        keyword=keyword,
        port=port,
        interval_seconds=interval_seconds,
        status="unknown",
        paused=False,
        archived=False,
        last_synced_at=datetime.now(tz=timezone.utc),
    )
    session.add(monitor)
    await refresh_usage_counters(session, subscription)
    await session.commit()
    await session.refresh(monitor)
    return monitor


async def update_monitor(
    session: AsyncSession,
    monitor: UptimeClientMonitor,
    *,
    friendly_name: Optional[str] = None,
    url: Optional[str] = None,
    keyword: Optional[str] = None,
    port: Optional[int] = None,
    interval_seconds: Optional[int] = None,
) -> UptimeClientMonitor:
    sub = await session.get(UptimeClientSubscription, monitor.subscription_id)
    if sub is None:
        raise ValueError("Parent subscription missing.")
    package = await get_package(session, sub.package_id)
    if package is None:
        raise ValueError("Subscription package not found.")
    if interval_seconds is not None:
        assert_interval_ok(package, interval_seconds)

    payload: Dict[str, Any] = {}
    if friendly_name is not None:
        payload["friendly_name"] = friendly_name
    if url is not None:
        payload["url"] = url
    if keyword is not None and monitor.monitor_type == "keyword":
        payload["keyword_value"] = keyword
    if port is not None and monitor.monitor_type == "port":
        payload["port"] = int(port)
    if interval_seconds is not None:
        payload["interval"] = int(interval_seconds)

    if payload:
        client = await get_client(user_email=sub.user_email)
        await client.update_monitor(monitor.ur_monitor_id, payload)

    if friendly_name is not None:
        monitor.friendly_name = friendly_name
    if url is not None:
        monitor.url = url
    if keyword is not None:
        monitor.keyword = keyword
    if port is not None:
        monitor.port = int(port)
    if interval_seconds is not None:
        monitor.interval_seconds = int(interval_seconds)
    monitor.last_synced_at = datetime.now(tz=timezone.utc)
    await session.commit()
    await session.refresh(monitor)
    return monitor


async def set_monitor_paused(
    session: AsyncSession, monitor: UptimeClientMonitor, paused: bool
) -> UptimeClientMonitor:
    sub = await session.get(UptimeClientSubscription, monitor.subscription_id)
    client = await get_client(user_email=sub.user_email if sub else None)
    try:
        await client.update_monitor(
            monitor.ur_monitor_id, {"status": "paused" if paused else "active"}
        )
    except UptimeRobotError as exc:
        logger.warning(
            "Failed to toggle upstream monitor %s paused=%s: %s",
            monitor.ur_monitor_id,
            paused,
            exc.message,
        )
        raise
    monitor.paused = paused
    monitor.status = "paused" if paused else "unknown"
    monitor.last_synced_at = datetime.now(tz=timezone.utc)
    await session.commit()
    await session.refresh(monitor)
    return monitor


async def delete_monitor(
    session: AsyncSession, monitor: UptimeClientMonitor
) -> None:
    sub = await session.get(UptimeClientSubscription, monitor.subscription_id)
    client = await get_client(user_email=sub.user_email if sub else None)
    try:
        await client.delete_monitor(monitor.ur_monitor_id)
    except UptimeRobotError as exc:
        # Don't block the local delete if upstream is already gone.
        logger.warning(
            "Upstream delete failed for monitor %s (%s) — continuing with local cleanup: %s",
            monitor.id,
            monitor.ur_monitor_id,
            exc.message,
        )
    await session.delete(monitor)
    if sub is not None:
        await refresh_usage_counters(session, sub)
    await session.commit()


async def fetch_monitor_logs(monitor: UptimeClientMonitor, *, limit: int = 20) -> Any:
    """Return the raw upstream logs payload for a monitor."""
    client = await get_client()
    return await client.monitor_logs(monitor.ur_monitor_id, limit=limit)