"""Adapter 1min.ai — implementation alternative des ports LLMProvider / LLMChatProvider.

API 1min.ai (cf. https://docs.1min.ai/docs/api/chat-with-ai-api) :
  - POST https://api.1min.ai/api/chat-with-ai            (one-shot)
  - POST https://api.1min.ai/api/chat-with-ai?isStreaming=true  (SSE)
  - Auth : header "API-KEY: <cle>"
  - Body : {"type": "UNIFY_CHAT_WITH_AI", "model": "...",
            "promptObject": {"prompt": "..."}}

Le port LoreMind expose une API "messages[]", mais 1min.ai attend un prompt
unique. On aplatit donc l'historique + system prompt en un seul bloc texte,
avec des marqueurs de role lisibles pour le modele.
"""
from __future__ import annotations

import json
import logging
from typing import AsyncIterator

import httpx

from app.core.config import Settings
from app.domain.models import ChatMessage
from app.domain.ports import LLMProviderError

logger = logging.getLogger(__name__)

_API_BASE = "https://api.1min.ai/api/chat-with-ai"
_PAYLOAD_TYPE = "UNIFY_CHAT_WITH_AI"


class OneMinAiLLMProvider:
    """Adapter 1min.ai — satisfait LLMProvider et LLMChatProvider par duck typing."""

    def __init__(self, settings: Settings) -> None:
        if not settings.onemin_api_key:
            raise LLMProviderError(
                "Cle API 1min.ai manquante. Configure-la depuis l'ecran Parametres."
            )
        self._api_key = settings.onemin_api_key
        self._model = settings.onemin_model
        self._timeout = settings.llm_timeout_seconds

    def _headers(self) -> dict[str, str]:
        return {"API-KEY": self._api_key, "Content-Type": "application/json"}

    def _payload(self, prompt: str) -> dict[str, object]:
        return {
            "type": _PAYLOAD_TYPE,
            "model": self._model,
            "promptObject": {"prompt": prompt},
        }

    def _format_http_error(self, exc: httpx.HTTPError) -> str:
        """Message d'erreur lisible. Un timeout httpx a un str() vide → on le nomme."""
        if isinstance(exc, httpx.TimeoutException):
            return (
                f"Erreur 1min.ai : délai dépassé (timeout {self._timeout}s). Le modèle a mis "
                "trop de temps à répondre — typique d'un morceau d'import trop gros. "
                "Réduisez « Taille des morceaux à l'import » (Paramètres → Import de PDF) "
                "ou augmentez le timeout LLM."
            )
        detail = str(exc) or exc.__class__.__name__
        return f"Erreur 1min.ai ({exc.__class__.__name__}) : {detail}"

    async def generate(
        self,
        prompt: str,
        *,
        output_format: str | None = None,  # 1min.ai ne supporte pas format=json
        temperature: float | None = None,  # idem, pas d'hyperparam expose ici
    ) -> str:
        """One-shot, mais via l'endpoint STREAMING (puis recollage).

        On NE passe PAS par l'endpoint non-streame `chat-with-ai` : sur les longues
        generations (gros imports), la passerelle Cloudflare de 1min.ai coupe la
        connexion au bout de ~100s et renvoie un HTTP 524. En streaming, des octets
        circulent en continu => pas de 524, quelle que soit la duree. On accumule
        tous les fragments pour reconstituer la reponse complete.
        """
        chunks: list[str] = []
        async for token in self._stream_prompt(prompt):
            chunks.append(token)
        return "".join(chunks)

    async def stream_chat(
        self,
        messages: list[ChatMessage],
        *,
        system_prompt: str | None = None,
        temperature: float | None = None,
    ) -> AsyncIterator[str]:
        """Streame une conversation : aplatit les messages puis delegue au coeur SSE."""
        prompt = self._flatten_messages(messages, system_prompt)
        async for token in self._stream_prompt(prompt):
            yield token

    async def _stream_prompt(self, prompt: str) -> AsyncIterator[str]:
        """Coeur du streaming SSE 1min.ai (`?isStreaming=true`) pour un prompt brut.

        1min.ai expose : `event: content` → `data: {"content": "..."}`, `event: done`,
        `event: error`. On yield le champ `content` au fil de l'arrivee.
        """
        url = f"{_API_BASE}?isStreaming=true"
        async with httpx.AsyncClient(timeout=self._timeout) as client:
            try:
                async with client.stream(
                    "POST", url, headers=self._headers(), json=self._payload(prompt)
                ) as response:
                    response.raise_for_status()
                    async for token in self._parse_sse(response):
                        yield token
            except httpx.HTTPError as exc:
                raise LLMProviderError(self._format_http_error(exc)) from exc

    # --- Helpers ------------------------------------------------------------

    @staticmethod
    async def _parse_sse(response: httpx.Response) -> AsyncIterator[str]:
        """Decoupe le flux SSE ligne par ligne et yield les chunks 'content'."""
        current_event: str | None = None
        current_data = ""
        async for line in response.aiter_lines():
            if line == "":
                # Fin d'un evenement SSE : dispatch
                if current_event == "done":
                    return
                if current_event == "error":
                    raise LLMProviderError(f"1min.ai a signale une erreur : {current_data}")
                if current_data and current_event in (None, "content", "message"):
                    token = OneMinAiLLMProvider._extract_content_chunk(current_data)
                    if token:
                        yield token
                current_event = None
                current_data = ""
                continue
            if line.startswith("event:"):
                current_event = line[6:].strip()
            elif line.startswith("data:"):
                chunk = line[5:].lstrip()
                current_data = f"{current_data}\n{chunk}" if current_data else chunk

    @staticmethod
    def _extract_content_chunk(data: str) -> str:
        """Extrait le champ `content` d'un data JSON, avec tolerance si format brut."""
        try:
            obj = json.loads(data)
        except json.JSONDecodeError:
            return data  # filet de securite si le serveur envoie du texte brut
        if isinstance(obj, dict):
            return obj.get("content") or obj.get("token") or ""
        return ""

    @staticmethod
    def _extract_result(payload: dict) -> str:
        """Extrait le texte final d'une reponse non-streamee.

        Schema attendu : `aiRecord.aiRecordDetail.resultObject` (list[str]).
        On concatene par securite (le serveur renvoie habituellement un seul element).
        """
        record = payload.get("aiRecord") or {}
        detail = record.get("aiRecordDetail") or {}
        result = detail.get("resultObject")
        if isinstance(result, list) and result:
            return "".join(str(x) for x in result)
        if isinstance(result, str) and result:
            return result

        # Schema inattendu : on remonte un EXTRAIT du vrai payload pour diagnostiquer.
        # Causes frequentes : credits/quota 1min.ai epuises, moderation, modele
        # indisponible, ou reponse asynchrone (record cree mais resultat pas encore
        # pret). Sans ce detail, l'erreur "resultObject absent" est aveugle.
        snippet = json.dumps(payload, ensure_ascii=False)
        if len(snippet) > 800:
            snippet = snippet[:800] + "…"
        logger.warning("Reponse 1min.ai inattendue (resultObject absent) : %s", snippet)
        raise LLMProviderError(f"Reponse 1min.ai inattendue (resultObject absent) : {snippet}")

    @staticmethod
    def _flatten_messages(
        messages: list[ChatMessage], system_prompt: str | None
    ) -> str:
        """Transforme [system_prompt, history] en un unique prompt textuel.

        1min.ai n'accepte qu'un champ `prompt` : on serialise la conversation
        avec des marqueurs explicites pour que le modele comprenne les tours.
        """
        parts: list[str] = []
        if system_prompt:
            parts.append(f"[SYSTEM]\n{system_prompt}")
        if messages:
            history = "\n\n".join(
                f"[{m.role.upper()}]\n{m.content}" for m in messages
            )
            parts.append(history)
        parts.append("[ASSISTANT]")  # invite le modele a continuer
        return "\n\n".join(parts)
