"""
Punto único donde se pregunta si una acción está permitida.

Está cableado y apagado (principio VI). Con `BILLING_ENABLED` en falso autoriza
siempre, **pero registra el consumo igual**: las dos mitades son independientes
a propósito. Empezar a medir el día que se decida cobrar dejaría sin datos justo
la etapa que hay que dimensionar, y encender el cobro sobre un código que nunca
llamó a esta función obligaría a buscar los puntos de control uno por uno.

Que la llamada exista desde el primer día es lo que hace que encender la
facturación sea configuración y no refactor.
"""

from dataclasses import dataclass

from django.conf import settings
from django.db.models import F

from apps.billing.models import ConsumptionMetric, ConsumptionRecord
from apps.core.dates import current_period


class Action:
    """Acciones que consumen algo medible."""

    ADD_DOMAIN = 'ADD_DOMAIN'
    MONITOR_URLS = 'MONITOR_URLS'
    INSPECT_URLS = 'INSPECT_URLS'
    SUBMIT_SITEMAPS = 'SUBMIT_SITEMAPS'


# Qué métrica acumula cada acción. El alta de dominio no acumula ninguna: se
# controla contra el conteo de filas, que ya es el dato exacto.
METRIC_BY_ACTION = {
    Action.MONITOR_URLS: ConsumptionMetric.MONITORED_URLS,
    Action.INSPECT_URLS: ConsumptionMetric.INSPECTIONS,
    Action.SUBMIT_SITEMAPS: ConsumptionMetric.SITEMAP_SUBMITS,
}


@dataclass(frozen=True)
class LimitDecision:
    """
    Resultado de la consulta.

    Lleva el límite y el valor actual además del veredicto para que quien
    rechaza pueda decir cuánto falta, en vez de un «no se puede» sin número.
    """

    allowed: bool
    reason: str = ''
    limit: int | None = None
    current: int | None = None

    def __bool__(self) -> bool:
        return self.allowed


def record_consumption(account, action: str, amount: int, *, domain=None) -> None:
    """
    Acumula el consumo del período en curso. Se llama se cobre o no.

    Usa una actualización relativa en la base y no una lectura seguida de una
    escritura: dos trabajadores procesando lotes del mismo dominio a la vez
    perderían cuenta de uno de los dos.
    """
    metric = METRIC_BY_ACTION.get(action)
    if metric is None or amount <= 0:
        return

    record, created = ConsumptionRecord.objects.get_or_create(
        account=account,
        domain=domain,
        period=current_period(),
        metric=metric,
        defaults={'amount': amount},
    )
    if not created:
        ConsumptionRecord.objects.filter(pk=record.pk).update(amount=F('amount') + amount)


def check(account, action: str, amount: int = 1, *, domain=None) -> LimitDecision:
    """
    Autoriza o rechaza una acción, y deja registrado lo que se consumió.

    El orden importa: primero se registra y después se decide. Un rechazo
    también es información sobre la demanda real, y perderla haría que los
    planes se dimensionen sólo con lo que ya entró.
    """
    record_consumption(account, action, amount, domain=domain)

    if not settings.BILLING_ENABLED:
        return LimitDecision(allowed=True)

    plan = getattr(account, 'plan', None)
    if plan is None:
        return LimitDecision(allowed=True)

    if action == Action.ADD_DOMAIN and plan.max_domains is not None:
        # Sólo los activos: `max_domains` es cuántos sitios se pueden tener **a
        # la vez**, no cuántos se dieron de alta en total. Es lo que la interfaz
        # muestra, así que es lo que el plan tiene que contar; si la pantalla no
        # lo cuenta, el plan tampoco. Dar de baja libera el cupo.
        #
        # Contando las filas sin más, una cuenta con `max_domains = 1` —el plan
        # que el producto de un solo sitio vuelve normal— no podría cambiar de
        # sitio nunca: el dado de baja seguiría ocupando el único lugar.
        #
        # El filtro va en línea y no llamando a `domains.services.active_domains`
        # a propósito: este módulo evita depender de otras aplicaciones —resuelve
        # el modelo de URLs con `apps.get_model` por el orden de migración— y
        # `account.domains` es un `related_name`, así que no hace falta importar
        # nada.
        current = account.domains.filter(deactivated_at__isnull=True).count()
        if current + amount > plan.max_domains:
            return LimitDecision(
                allowed=False,
                reason='El plan no admite más dominios.',
                limit=plan.max_domains,
                current=current,
            )

    if action == Action.MONITOR_URLS and plan.max_monitored_urls is not None:
        current = _monitored_urls(account)
        if current + amount > plan.max_monitored_urls:
            return LimitDecision(
                allowed=False,
                reason='El plan no admite monitorear más URLs.',
                limit=plan.max_monitored_urls,
                current=current,
            )

    return LimitDecision(allowed=True)


def _monitored_urls(account) -> int:
    """
    Cuenta las URLs monitoreadas de la cuenta.

    Vive aparte porque el modelo de URLs llega con la historia de cobertura: la
    consulta se resuelve por nombre para que este módulo no dependa de una
    aplicación que en este punto puede no estar migrada todavía.
    """
    from django.apps import apps

    try:
        url_model = apps.get_model('sitemaps', 'Url')
    except LookupError:
        return 0
    return url_model.objects.filter(domain__account=account, in_sitemap=True).count()
