# @File: backend/services/uptimerobot/subscriptions.py
# @Desc: Subscription lifecycle helpers — create / upgrade / downgrade /
#        suspend / resume / terminate. Downgrades are blocked when the
#        client's current usage exceeds the target package's quota.
from __future__ import annotations

import logging
from datetime import datetime, timezone
from typing import 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 get_package

logger = logging.getLogger(__name__)


async def list_subscriptions(
    session: AsyncSession,
    *,
    user_id: Optional[str] = None,
    status: Optional[str] = None,
) -> List[UptimeClientSubscription]:
    stmt = select(UptimeClientSubscription).order_by(
        UptimeClientSubscription.created_at.desc()
    )
    if user_id:
        stmt = stmt.where(UptimeClientSubscription.user_id == user_id)
    if status:
        stmt = stmt.where(UptimeClientSubscription.status == status)
    result = await session.execute(stmt)
    return list(result.scalars().all())


async def get_active_subscription_for_user(
    session: AsyncSession, user_id: str
) -> Optional[UptimeClientSubscription]:
    """Return the user's active subscription, if any. If multiple exist
    (rare, edge case from manual admin actions), the most recent one wins."""
    result = await session.execute(
        select(UptimeClientSubscription)
        .where(UptimeClientSubscription.user_id == user_id)
        .where(UptimeClientSubscription.status == "active")
        .order_by(UptimeClientSubscription.created_at.desc())
    )
    return result.scalars().first()


async def get_subscription_by_whmcs_service(
    session: AsyncSession, whmcs_service_id: int
) -> Optional[UptimeClientSubscription]:
    result = await session.execute(
        select(UptimeClientSubscription).where(
            UptimeClientSubscription.whmcs_service_id == whmcs_service_id
        )
    )
    return result.scalars().first()


async def refresh_usage_counters(
    session: AsyncSession, subscription: UptimeClientSubscription
) -> None:
    """Recompute cached `*_used` counters from the monitor/contact/window/page
    tables. Cheap because everything is indexed by subscription_id."""
    from sqlalchemy import func

    from models.uptimerobot import (
        UptimeAlertContact,
        UptimeMaintenanceWindow,
        UptimeStatusPage,
    )

    monitors_count = await session.scalar(
        select(func.count(UptimeClientMonitor.id))
        .where(UptimeClientMonitor.subscription_id == subscription.id)
        .where(UptimeClientMonitor.archived.is_(False))
    )
    contacts_count = await session.scalar(
        select(func.count(UptimeAlertContact.id)).where(
            UptimeAlertContact.subscription_id == subscription.id
        )
    )
    windows_count = await session.scalar(
        select(func.count(UptimeMaintenanceWindow.id)).where(
            UptimeMaintenanceWindow.subscription_id == subscription.id
        )
    )
    pages_count = await session.scalar(
        select(func.count(UptimeStatusPage.id)).where(
            UptimeStatusPage.subscription_id == subscription.id
        )
    )
    subscription.monitors_used = int(monitors_count or 0)
    subscription.alert_contacts_used = int(contacts_count or 0)
    subscription.maintenance_windows_used = int(windows_count or 0)
    subscription.status_pages_used = int(pages_count or 0)


def _ensure_downgrade_fits(
    subscription: UptimeClientSubscription, target: UptimePackage
) -> None:
    """Raise ValueError if the current usage would exceed the target package."""
    checks = [
        ("monitors", subscription.monitors_used, target.max_monitors, target.monitors_unlimited),
        (
            "alert contacts",
            subscription.alert_contacts_used,
            target.max_alert_contacts,
            target.alert_contacts_unlimited,
        ),
        (
            "maintenance windows",
            subscription.maintenance_windows_used,
            target.max_maintenance_windows,
            target.maintenance_windows_unlimited,
        ),
        (
            "status pages",
            subscription.status_pages_used,
            target.max_status_pages,
            target.status_pages_unlimited,
        ),
    ]
    for label, used, cap, unlimited in checks:
        if unlimited:
            continue
        if int(used) > int(cap):
            raise ValueError(
                f"Cannot downgrade: current {label} usage ({used}) exceeds target "
                f"package limit ({cap})."
            )


async def create_subscription(
    session: AsyncSession,
    *,
    user_id: str,
    package_id: int,
    user_email: Optional[str] = None,
    whmcs_service_id: Optional[int] = None,
    whmcs_product_id: Optional[int] = None,
    notes: Optional[str] = None,
) -> UptimeClientSubscription:
    package = await get_package(session, package_id)
    if package is None:
        raise ValueError("Package not found")
    if not package.is_active:
        raise ValueError("Cannot assign an inactive package")

    sub = UptimeClientSubscription(
        user_id=user_id,
        user_email=(user_email or "").strip().lower() or None,
        package_id=package_id,
        whmcs_service_id=whmcs_service_id,
        whmcs_product_id=whmcs_product_id,
        status="active",
        activated_at=datetime.now(tz=timezone.utc),
        notes=notes,
    )
    session.add(sub)
    await session.commit()
    await session.refresh(sub)
    return sub


async def change_package(
    session: AsyncSession,
    subscription: UptimeClientSubscription,
    new_package_id: int,
) -> UptimeClientSubscription:
    target = await get_package(session, new_package_id)
    if target is None:
        raise ValueError("Target package not found")
    await refresh_usage_counters(session, subscription)
    # If the target is strictly smaller, ensure the current usage fits.
    _ensure_downgrade_fits(subscription, target)
    subscription.package_id = target.id
    await session.commit()
    await session.refresh(subscription)
    return subscription


async def suspend_subscription(
    session: AsyncSession,
    subscription: UptimeClientSubscription,
    *,
    pause_monitors: bool = True,
) -> UptimeClientSubscription:
    subscription.status = "suspended"
    subscription.suspended_at = datetime.now(tz=timezone.utc)
    if pause_monitors:
        # Pause upstream monitors; local row marked as paused.
        monitors_result = await session.execute(
            select(UptimeClientMonitor)
            .where(UptimeClientMonitor.subscription_id == subscription.id)
            .where(UptimeClientMonitor.archived.is_(False))
        )
        monitors = list(monitors_result.scalars().all())
        client = await get_client(user_email=subscription.user_email)
        for mon in monitors:
            try:
                await client.update_monitor(mon.ur_monitor_id, {"status": "paused"})
            except UptimeRobotError as exc:
                logger.warning(
                    "Failed to pause upstream monitor %s during suspend: %s",
                    mon.ur_monitor_id,
                    exc.message,
                )
            mon.paused = True
    await session.commit()
    await session.refresh(subscription)
    return subscription


async def resume_subscription(
    session: AsyncSession, subscription: UptimeClientSubscription
) -> UptimeClientSubscription:
    subscription.status = "active"
    subscription.suspended_at = None
    monitors_result = await session.execute(
        select(UptimeClientMonitor)
        .where(UptimeClientMonitor.subscription_id == subscription.id)
        .where(UptimeClientMonitor.archived.is_(False))
    )
    monitors = list(monitors_result.scalars().all())
    client = await get_client(user_email=subscription.user_email)
    for mon in monitors:
        if mon.paused:
            try:
                await client.update_monitor(mon.ur_monitor_id, {"status": "active"})
            except UptimeRobotError as exc:
                logger.warning(
                    "Failed to resume upstream monitor %s during resume: %s",
                    mon.ur_monitor_id,
                    exc.message,
                )
            mon.paused = False
    await session.commit()
    await session.refresh(subscription)
    return subscription


async def terminate_subscription(
    session: AsyncSession,
    subscription: UptimeClientSubscription,
    *,
    action: Optional[str] = None,
) -> UptimeClientSubscription:
    """Terminate a subscription, deleting or archiving upstream monitors
    according to the package's `terminate_action` (or explicit override)."""
    package = await get_package(session, subscription.package_id)
    effective_action = action or (package.terminate_action if package else "archive")
    subscription.status = "terminated"
    subscription.terminated_at = datetime.now(tz=timezone.utc)

    monitors_result = await session.execute(
        select(UptimeClientMonitor).where(
            UptimeClientMonitor.subscription_id == subscription.id
        )
    )
    monitors = list(monitors_result.scalars().all())
    client = await get_client(user_email=subscription.user_email)
    for mon in monitors:
        if effective_action == "delete":
            try:
                await client.delete_monitor(mon.ur_monitor_id)
            except UptimeRobotError as exc:
                logger.warning(
                    "Failed to delete upstream monitor %s on terminate: %s",
                    mon.ur_monitor_id,
                    exc.message,
                )
            await session.delete(mon)
        else:
            try:
                await client.update_monitor(mon.ur_monitor_id, {"status": "paused"})
            except UptimeRobotError as exc:
                logger.warning(
                    "Failed to pause upstream monitor %s on terminate: %s",
                    mon.ur_monitor_id,
                    exc.message,
                )
            mon.paused = True
            mon.archived = True
    await session.commit()
    await session.refresh(subscription)
    return subscription