"""
El cliente del proveedor de imágenes, construido en un solo lugar.

Se usa el SDK oficial y no `httpx` a mano porque el módulo va a crecer más allá
de las imágenes —se espera Sora para video— y el SDK ya lo cubre. Dos cosas se
le apagan a propósito:

- **Los reintentos automáticos.** Una generación repetida por un timeout cuesta
  el doble sin que nadie lo note. Si vale la pena reintentar lo decide la tarea,
  que sabe qué falló y cuántas veces.
- **El timeout por omisión**, que es corto para esto: una imagen en calidad
  alta tarda entre 30 y 90 segundos, y la llamada bloquea hasta que la imagen
  está lista. La cola es nuestra, no del proveedor.

Acá viven también las **listas cerradas** de lo que la pantalla «Config» deja
elegir (P16, R52). Son listas y no texto libre porque un nombre de modelo mal
escrito recién falla cuando el trabajador llama al proveedor, con la foto ya
subida y el pedido ya en cola. Agregar un modelo es tocar la lista, no la
pantalla. Los nombres salen de los literales del SDK instalado (`ImageModel`,
`ChatModel`), no de memoria.
"""

from dataclasses import dataclass, field

from django.conf import settings
from openai import OpenAI

from apps.ai import fake

#: Sin foto: la variante rápida de la generación 2.5. Crea desde el texto, y ahí
#: la precisión de edición no compra nada. **No es configurable**: lo usa el
#: script de las imágenes de muestra del catálogo, que no pasa por la pantalla.
GENERATE_MODEL = 'gpt-image-2.5-flare'
#: Segundos. Con margen sobre lo que tarda una imagen en calidad alta.
TIMEOUT = 180
#: Segundos. Una respuesta de texto corta sobre una foto tarda segundos, no
#: minutos: si pasa de esto, el proveedor está trabado y conviene reintentar.
ANALYSIS_TIMEOUT = 60

#: Todas las calidades que el proveedor admite para los modelos GPT Image, de
#: menor a mayor. Las bajas se ofrecen a propósito: sirven para probar barato
#: mientras se afina el prompt (P16).
IMAGE_QUALITIES: tuple[str, ...] = ('low', 'medium', 'high', 'xhigh', 'max')
#: Las tres que admite **cualquier** modelo GPT Image. `xhigh` y `max` existen
#: sólo en la generación 2.5, según el docstring de `images.edit` del SDK.
BASE_IMAGE_QUALITIES: tuple[str, ...] = ('low', 'medium', 'high')


@dataclass(frozen=True)
class ImageModelOption:
    """
    Un modelo apto para editar una foto, con las calidades que admite.

    Las calidades van por modelo y no en una lista global porque no son las
    mismas: pedirle `max` a `gpt-image-1` es un 400 del proveedor, y ese error
    llegaría con la foto ya subida. La pantalla recorta el select de calidad
    según el modelo elegido y el servidor rechaza la combinación que no existe.
    """

    value: str
    qualities: tuple[str, ...] = field(default=IMAGE_QUALITIES)

    def admits(self, quality: str) -> bool:
        return quality in self.qualities


#: Los modelos que la pantalla ofrece para editar la foto, en el orden en que se
#: muestran. Los tres figuran en `ImageModel` del SDK y en la lista de modelos
#: que `images.edit` acepta. Se ofrecen los alias y no las instantáneas con
#: fecha (`-2026-09-08`): el alias sigue al modelo vigente, que es lo que se
#: quiere de una configuración que nadie va a revisar cada mes.
#:
#: Quedan afuera a propósito `gpt-image-2`, `gpt-image-1.5` y `gpt-image-1-mini`,
#: que el SDK también admite en edición: no se pidieron, y sumarlos es una línea
#: acá cuando haga falta.
IMAGE_MODELS: tuple[ImageModelOption, ...] = (
    # La variante que prioriza no deformar lo que ya está. Es el patio real de
    # un cliente; una casa torcida cuesta más que la diferencia de precio.
    ImageModelOption('gpt-image-2.5-sunburst'),
    # La variante rápida de la misma generación: más barata, menos cuidadosa.
    ImageModelOption('gpt-image-2.5-flare'),
    # La primera generación, sin `xhigh` ni `max`. Está por si hace falta
    # comparar o abaratar una prueba.
    ImageModelOption('gpt-image-1', BASE_IMAGE_QUALITIES),
)

#: Los modelos con visión y salida estructurada JSON en la Responses API que la
#: pantalla ofrece para el análisis de la foto, de menor a mayor. Los cuatro
#: figuran en `ChatModel` del SDK. Se deja afuera la familia `gpt-5.6-*` y las
#: variantes `pro`, `codex` y `chat-latest`: no está verificado que acepten una
#: imagen con `json_schema` estricto, y una opción que falla en el select es
#: peor que una que no está.
VISION_MODELS: tuple[str, ...] = ('gpt-5.4-nano', 'gpt-5.4-mini', 'gpt-5.4', 'gpt-5.5')

#: Con qué arranca la configuración cuando todavía nadie la tocó. Son los
#: valores que usaba la v1 como constantes: el registro nace con ellos (R52) y
#: la pantalla nunca muestra un select vacío. **No son la fuente**: la fuente es
#: `AiSettings.current()`; quien llame al proveedor lee de ahí.
#:
#: `gpt-image-2.5-sunburst` en `high`: lo que usaba la landing con el modelo
#: anterior, hasta comparar costo y resultado con el `usage` guardado en cada
#: pedido. `gpt-5.4-mini`: el escalón chico de la generación vigente; `nano` es
#: más barato todavía, pero acá el trabajo es distinguir un walkway de un
#: entryway en una foto de teléfono, y el ahorro no compra una detección que
#: después la persona tiene que corregir a mano (R28).
DEFAULT_IMAGE_MODEL = 'gpt-image-2.5-sunburst'
DEFAULT_IMAGE_QUALITY = 'high'
DEFAULT_VISION_MODEL = 'gpt-5.4-mini'


def image_model(value: str) -> ImageModelOption | None:
    """La opción de un modelo de imagen por su código, o nada si no está en la lista."""
    return next((option for option in IMAGE_MODELS if option.value == value), None)


def is_configured() -> bool:
    """Con el proveedor de prueba no hace falta clave: la generación no llama a nadie."""
    return fake.is_active() or bool(settings.OPENAI_API_KEY)


def client() -> OpenAI | fake.Client:
    """
    Un cliente nuevo por llamada.

    No se memoriza: el trabajador vive días y la clave puede cambiar entre un
    despliegue y otro. Construirlo cuesta nada al lado de lo que tarda la
    imagen.

    Con `AI_FAKE_PROVIDER` prendido es el de prueba (`fake.Client`), que
    contesta con la misma forma: todo lo que viene después no se entera.
    """
    if fake.is_active():
        return fake.Client(_openai)
    return _openai()


def _openai() -> OpenAI:
    return OpenAI(api_key=settings.OPENAI_API_KEY, max_retries=0, timeout=TIMEOUT)
