"""
A tenant's license: how many accounts it may have active, and the dates it's
valid between. A tenant can't be used until it has one: the Super Admin
saves it (app/routers/platform_admin.py; blank terms = unlimited), or the
tenant applies a license file the Super Admin issued
(app/crud/license_files.py). Tenant.license_set_at records which tenants
are licensed.

- **Seats.** An *active* account takes a seat: internal accounts count
  against max_users, Public Portal accounts (vendor_id set) against
  max_portal_users. Inactive accounts don't, so a Portal registration
  (created inactive) is fine; approving it needs a free seat. Lowering a
  limit below what's in use leaves those accounts working — only adding or
  re-activating one is refused until the count is back under.
- **Dates.** Valid from the start of starts_on through the end of ends_on,
  in UTC. Outside them — or with no license at all — nobody in the tenant
  can sign in or use a session,
  Portal registration is closed, and its scheduled jobs don't run. The
  Super Admin's own access to the tenant is unaffected.
"""

import time
import uuid
from datetime import date, datetime, timezone

from fastapi import HTTPException, status
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.identity import Tenant, User
from app.schemas.identity import LicenseUsage, TenantLicense, TenantLicenseStatus

class LicenseError(Exception):
    def __init__(self, code: str, message: str) -> None:
        super().__init__(message)
        self.code = code
        self.message = message


def http_error(exc: LicenseError) -> HTTPException:
    return HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail={"code": exc.code, "message": exc.message})


def today() -> date:
    return datetime.now(timezone.utc).date()


def long_date(d: date) -> str:
    return f"{d.day} {d:%B %Y}"


def period_state(starts_on: date | None, ends_on: date | None, on: date | None = None, *, licensed: bool = True) -> str:
    on = on or today()
    if not licensed:
        return "unlicensed"
    if starts_on is None and ends_on is None:
        return "unlimited"
    if starts_on is not None and on < starts_on:
        return "not_started"
    if ends_on is not None and on > ends_on:
        return "expired"
    return "active"


def period_message(starts_on: date | None, ends_on: date | None, on: date | None = None, *, licensed: bool = True) -> str | None:
    """Why the tenant can't be used today, or None if it can."""
    state = period_state(starts_on, ends_on, on, licensed=licensed)
    if state == "unlicensed":
        return "This workspace doesn't have a license yet. Apply the license file from your provider, or contact your administrator."
    if state == "not_started":
        return f"This workspace's license starts on {long_date(starts_on)}. Contact your administrator."
    if state == "expired":
        return f"This workspace's license ended on {long_date(ends_on)}. Contact your administrator to renew it."
    return None


def tenant_message(tenant: Tenant) -> str | None:
    """Why `tenant` can't be used today (unlicensed, or outside its dates), or None."""
    return period_message(tenant.license_starts_on, tenant.license_ends_on, licensed=tenant.licensed)


def check_period(tenant: Tenant) -> None:
    message = tenant_message(tenant)
    if message:
        raise LicenseError("license_inactive", message)


# ---- the per-request check ----------------------------------------------------------------------------------

# tenant_id -> (fetched at, licensed, starts_on, ends_on). Each request checks its
# tenant's dates; caching them keeps that from being a query per request.
# The Super Admin's own change clears its entry here at once; other
# processes see it within _CACHE_SECONDS.
_CACHE_SECONDS = 60.0
_periods: dict[uuid.UUID, tuple[float, bool, date | None, date | None]] = {}


def forget(tenant_id: uuid.UUID) -> None:
    _periods.pop(tenant_id, None)


async def ensure_session_allowed(db: AsyncSession, tenant_id: uuid.UUID) -> None:
    """Raises a 403 (code license_inactive) when the tenant is unlicensed or
    outside its license dates — for every request a tenant session makes."""
    cached = _periods.get(tenant_id)
    if cached is None or time.monotonic() - cached[0] > _CACHE_SECONDS:
        row = (
            await db.execute(
                text("SELECT license_set_at IS NOT NULL, license_starts_on, license_ends_on FROM tenants WHERE id = :id"), {"id": str(tenant_id)}
            )
        ).one_or_none()
        licensed, starts_on, ends_on = (row[0], row[1], row[2]) if row else (False, None, None)
        cached = (time.monotonic(), licensed, starts_on, ends_on)
        _periods[tenant_id] = cached
    message = period_message(cached[2], cached[3], licensed=cached[1])
    if message:
        raise http_error(LicenseError("license_inactive", message))


# ---- seats ------------------------------------------------------------------------------------------------

async def usage(db: AsyncSession, tenant_id: uuid.UUID) -> LicenseUsage:
    """Active accounts, internal and Portal. `db` must be scoped to the tenant."""
    rows = (
        await db.execute(
            select(User.vendor_id.is_(None), func.count())
            .where(User.tenant_id == tenant_id, User.status == "active")
            .group_by(User.vendor_id.is_(None))
        )
    ).all()
    counts = {internal: n for internal, n in rows}
    return LicenseUsage(users=counts.get(True, 0), portal_users=counts.get(False, 0))


async def require_seat(db: AsyncSession, tenant_id: uuid.UUID, *, portal: bool) -> None:
    """Before an account becomes active: refuses when the tenant has no seat
    left of that kind. Locks the tenant row, so two activations at once
    can't both take the last seat."""
    tenant = (await db.execute(select(Tenant).where(Tenant.id == tenant_id).with_for_update())).scalar_one()
    limit = tenant.license_max_portal_users if portal else tenant.license_max_users
    if limit is None:
        return
    used = await usage(db, tenant_id)
    in_use = used.portal_users if portal else used.users
    if in_use >= limit:
        kind = "Portal user" if portal else "user"
        raise LicenseError(
            "license_seats",
            f"This workspace is licensed for {limit} active {kind}{'' if limit == 1 else 's'}, and all are in use. "
            "Deactivate an account or ask for more seats.",
        )


async def ensure_seat(db: AsyncSession, tenant_id: uuid.UUID, *, portal: bool) -> None:
    """require_seat, as the API's 403 (code license_seats)."""
    try:
        await require_seat(db, tenant_id, portal=portal)
    except LicenseError as exc:
        raise http_error(exc) from None


async def license_status(db: AsyncSession, tenant: Tenant) -> TenantLicenseStatus:
    """`db` must be scoped to the tenant."""
    license_ = TenantLicense.model_validate(tenant.license)
    days_left = None
    if license_.ends_on is not None:
        days_left = max((license_.ends_on - today()).days + 1, 0)
    return TenantLicenseStatus(
        license=license_,
        usage=await usage(db, tenant.id),
        state=period_state(license_.starts_on, license_.ends_on, licensed=tenant.licensed),
        days_left=days_left,
    )
