"""
Las tareas del módulo: analizar la foto y producir la imagen.

Reintentan sólo lo que el servicio marcó como transitorio —el proveedor pidió
esperar, o no contestó— y pocas veces: cada intento que sí llega a contestar
cuesta dinero, y un pedido que falla varias veces seguidas no se va a arreglar
insistiendo. Lo demás ya quedó cerrado en `FAILED` por el servicio.
"""

import logging
import random

from celery import shared_task

from apps.ai import analysis, services
from apps.ai.models import ImageGeneration, PhotoAnalysis

logger = logging.getLogger(__name__)

MAX_RETRIES = 2
#: El análisis reintenta una sola vez (R46). Sale del tope del servicio, que
#: es quien cierra el análisis en el último intento.
MAX_ANALYSIS_RETRIES = analysis.MAX_ATTEMPTS - 1


def backoff(attempt: int) -> int:
    """Segundos de espera antes del intento `attempt`, con azar para no coincidir."""
    return 30 * attempt + random.randint(0, 15)  # noqa: S311 — no es criptográfico


@shared_task(name='apps.ai.tasks.generate_image', bind=True)
def generate_image(self, generation_id: str) -> dict:
    generation = ImageGeneration.objects.filter(id=generation_id).first()
    if generation is None:
        return {'skipped': 'unknown-generation'}

    try:
        services.execute(generation)
    except services.TransientProviderError as error:
        if self.request.retries >= MAX_RETRIES:
            logger.warning('IA: pedido %s abandonado con %s', generation.id, error.code)
            services.fail(generation, error.code)
            return {'failed': error.code}
        raise self.retry(exc=error, countdown=backoff(self.request.retries + 1)) from error

    return {'generation': str(generation.id), 'state': generation.state}


@shared_task(name='apps.ai.tasks.analyze_photo', bind=True)
def analyze_photo(self, analysis_id: str) -> dict:
    """
    Misma forma que `generate_image`: el servicio ejecuta y clasifica, y acá
    sólo se decide si vale la pena volver a llamar. El servicio ya cierra el
    análisis en su último intento, así que la guarda de abajo es para el caso
    en que la cola entregue la tarea más veces de las que el servicio contó.
    """
    photo_analysis = PhotoAnalysis.objects.filter(id=analysis_id).first()
    if photo_analysis is None:
        return {'skipped': 'unknown-analysis'}

    try:
        analysis.execute(photo_analysis)
    except services.TransientProviderError as error:
        if self.request.retries >= MAX_ANALYSIS_RETRIES:
            logger.warning('IA: análisis %s abandonado con %s', photo_analysis.id, error.code)
            analysis.fail(photo_analysis, error.code)
            return {'failed': error.code}
        raise self.retry(exc=error, countdown=backoff(self.request.retries + 1)) from error

    return {'analysis': str(photo_analysis.id), 'state': photo_analysis.state}
