"""
El tope de generaciones por visitante de la landing.

La landing es pública y cada generación es una llamada paga. El visitante no
tiene cuenta, así que se lo reconoce por una huella: IP, user agent y día.
Dos ventanas, las dos configurables en «Config» (`AiSettings`): la última hora
y el día. Los análisis no cuentan; sólo las generaciones (decisión de la owner,
2026-10-08).

El día entra en la huella, así que la ventana diaria es «todo lo que tiene esta
huella» y se reinicia sola al cambiar la fecha. El día es el de la zona de
presentación (`core.dates.today`), el mismo «hoy» que el resto del producto.

Lo cuenta Tools y no la landing porque el número se configura acá y porque la
cola de Tools es la que sabe cuántos pedidos entraron de verdad. La IP y el
user agent los manda el proxy de la landing: a Tools le llega la IP de ese
servidor, no la del visitante.
"""

import hashlib
from datetime import datetime, time, timedelta

from django.utils import timezone

from apps.ai.models import AiSettings, GenerationOrigin, ImageGeneration
from apps.core import dates

HOUR = timedelta(hours=1)

WINDOW_HOUR = 'hour'
WINDOW_DAY = 'day'

VISITOR_LIMIT_REACHED = 'VISITOR_LIMIT_REACHED'


class VisitorLimitReached(Exception):
    """El visitante agotó una de las dos ventanas. Dice cuál y cuándo se libera."""

    code = VISITOR_LIMIT_REACHED

    def __init__(self, window: str, limit: int, retry_after: int):
        super().__init__(f'{VISITOR_LIMIT_REACHED}: {window}')
        self.window = window
        self.limit = limit
        #: Segundos hasta que la ventana vuelve a admitir un pedido.
        self.retry_after = retry_after


def visitor_fingerprint(ip: str, user_agent: str) -> str:
    """
    La huella del visitante en el día de hoy.

    Se guarda el hash y nunca la IP: alcanza para contar, y la tabla no se
    queda con un dato personal.
    """
    raw = f'{ip}|{user_agent}|{dates.today().isoformat()}'
    return hashlib.sha256(raw.encode()).hexdigest()


def visitor_generations(fingerprint: str):
    """
    Lo que cuenta para el tope: los pedidos de la landing con esa huella.

    Cuenta todo lo que se registró, en cualquier estado: el tope mide pedidos,
    no imágenes que salieron bien.
    """
    return ImageGeneration.objects.filter(
        origin=GenerationOrigin.WEBSITE, visitor_fingerprint=fingerprint
    )


def ensure_visitor_quota(fingerprint: str) -> None:
    """Deja pasar el pedido o levanta `VisitorLimitReached` con la ventana agotada."""
    current = AiSettings.current()
    now = timezone.now()
    generations = visitor_generations(fingerprint)

    if generations.count() >= current.website_daily_limit:
        tomorrow = datetime.combine(
            dates.today() + timedelta(days=1), time.min, tzinfo=dates.display_timezone()
        )
        raise VisitorLimitReached(
            WINDOW_DAY, current.website_daily_limit, _seconds_until(tomorrow, now)
        )

    last_hour = generations.filter(created_at__gt=now - HOUR)
    if last_hour.count() >= current.website_hourly_limit:
        oldest = last_hour.order_by('created_at').values_list('created_at', flat=True).first()
        # Sin pedidos en la hora sólo se llega acá con el tope en cero: se libera
        # cuando alguien lo suba, y una hora es lo honesto de decir mientras tanto.
        frees_at = (oldest or now) + HOUR
        raise VisitorLimitReached(
            WINDOW_HOUR, current.website_hourly_limit, _seconds_until(frees_at, now)
        )


def _seconds_until(moment: datetime, now: datetime) -> int:
    """Redondeado para arriba y nunca menos de uno: «reintentá en 0 segundos» no se cumple."""
    return max(1, int((moment - now).total_seconds() + 0.999))
