"""
CedricAI — Client Python pour l'IA Cédric
==========================================
pip install cedric-ai

Usage :
    from cedric_ai import CedricAI
    ai = CedricAI(host="ai.monsite.com", token="MON_TOKEN")
    print(ai.ask("Salut c'est quoi Python ?"))
"""

import requests
import json
import time
import uuid
import os
import threading
from datetime import datetime
from typing import Optional, Callable


class CedricAI:
    """
    Client pour interroger l'IA Cédric (Ollama) hébergée sur un serveur distant.
    Gère automatiquement la mise en file d'attente si le serveur est hors ligne.
    """

    def __init__(
        self,
        host: str = "localhost",
        port: int = 11434,
        model: str = "llama3",
        token: Optional[str] = None,
        queue_mode: bool = True,
        queue_file: str = "cedric_queue.json",
        retry_interval: int = 30,
        timeout: int = 120,
    ):
        """
        Args:
            host:           Adresse du serveur (ex: "ai.monsite.com")
            port:           Port Ollama (défaut 11434)
            model:          Modèle à utiliser (défaut "llama3")
            token:          Token d'authentification (optionnel mais recommandé)
            queue_mode:     Si True, les requêtes sont mises en file si serveur hors ligne
            queue_file:     Fichier JSON pour persister la file d'attente
            retry_interval: Secondes entre chaque tentative si hors ligne
            timeout:        Timeout par requête en secondes
        """
        self.host           = host.rstrip("/")
        self.port           = port
        self.model          = model
        self.token          = token
        self.queue_mode     = queue_mode
        self.queue_file     = queue_file
        self.retry_interval = retry_interval
        self.timeout        = timeout

        # Construire l'URL de base
        if host.startswith("http://") or host.startswith("https://"):
            self.base_url = host.rstrip("/")
        else:
            # Utilise https si c'est un domaine distant, http sinon
            proto = "http" if host in ("localhost", "127.0.0.1") else "https"
            self.base_url = f"{proto}://{host}"
            if port != 443 and port != 80:
                self.base_url += f":{port}"

        self._queue: dict = self._load_queue()
        self._callbacks: dict = {}
        self._worker_running = False

        if queue_mode:
            self._start_worker()

    # ─────────────────────────────────────────────
    # 📡 STATUT
    # ─────────────────────────────────────────────

    def is_online(self) -> bool:
        """Vérifie si le serveur Ollama est accessible."""
        try:
            r = requests.get(
                f"{self.base_url}/api/tags",
                headers=self._headers(),
                timeout=5
            )
            return r.status_code == 200
        except Exception:
            return False

    # ─────────────────────────────────────────────
    # 💬 REQUÊTE DIRECTE (bloquante)
    # ─────────────────────────────────────────────

    def ask(
        self,
        prompt: str,
        system: Optional[str] = None,
        wait_if_offline: bool = True,
        max_wait: int = 600,
    ) -> str:
        """
        Pose une question à l'IA et retourne la réponse.

        Args:
            prompt:           Ta question
            system:           Personnalité / contexte (optionnel)
            wait_if_offline:  Attend que le serveur soit en ligne si True
            max_wait:         Temps max d'attente en secondes

        Returns:
            str — La réponse de l'IA

        Raises:
            ConnectionError: Si serveur hors ligne et wait_if_offline=False
            TimeoutError:    Si max_wait dépassé
        """
        full_prompt = self._build_prompt(prompt, system)

        if not wait_if_offline:
            if not self.is_online():
                raise ConnectionError(f"Serveur inaccessible : {self.base_url}")
            return self._send(full_prompt)

        waited = 0
        while not self.is_online():
            if waited >= max_wait:
                raise TimeoutError(f"Serveur toujours hors ligne après {max_wait}s")
            print(f"⏳ Serveur hors ligne, nouvelle tentative dans {self.retry_interval}s...")
            time.sleep(self.retry_interval)
            waited += self.retry_interval

        return self._send(full_prompt)

    # ─────────────────────────────────────────────
    # 📬 REQUÊTE ASYNC (file d'attente)
    # ─────────────────────────────────────────────

    def ask_async(
        self,
        prompt: str,
        system: Optional[str] = None,
        callback: Optional[Callable] = None,
    ) -> str:
        """
        Ajoute une requête en file d'attente.
        Exécutée automatiquement dès que le serveur est en ligne.

        Args:
            prompt:    Ta question
            system:    Personnalité / contexte (optionnel)
            callback:  Appelée quand la réponse arrive : callback(job_id, response)

        Returns:
            job_id (str) — pour récupérer le résultat avec get_result() ou wait_for_result()
        """
        job_id = str(uuid.uuid4())[:8]
        job = {
            "id":         job_id,
            "prompt":     self._build_prompt(prompt, system),
            "status":     "pending",
            "result":     None,
            "error":      None,
            "created_at": datetime.now().isoformat(),
            "done_at":    None,
        }
        self._queue[job_id] = job
        self._save_queue()

        if callback:
            self._callbacks[job_id] = callback

        print(f"📬 Job {job_id} en file d'attente")
        return job_id

    def get_result(self, job_id: str) -> Optional[str]:
        """
        Récupère le résultat d'un job async.
        Retourne None si pas encore terminé.

        Raises:
            RuntimeError: Si le job a échoué
            KeyError:     Si job_id inconnu
        """
        job = self._queue.get(job_id)
        if not job:
            raise KeyError(f"Job {job_id} introuvable")
        if job["status"] == "error":
            raise RuntimeError(f"Job {job_id} échoué : {job['error']}")
        if job["status"] == "done":
            return job["result"]
        return None  # pending

    def wait_for_result(self, job_id: str, timeout: int = 600) -> str:
        """
        Attend le résultat d'un job async (bloquant).

        Args:
            job_id:  L'ID retourné par ask_async()
            timeout: Temps max en secondes (défaut 10min)

        Returns:
            str — La réponse de l'IA

        Raises:
            TimeoutError: Si timeout dépassé
        """
        start = time.time()
        while time.time() - start < timeout:
            result = self.get_result(job_id)
            if result is not None:
                return result
            time.sleep(2)
        raise TimeoutError(f"Job {job_id} pas terminé après {timeout}s")

    def queue_status(self) -> dict:
        """Retourne un résumé de la file d'attente."""
        return {
            "online":  self.is_online(),
            "pending": sum(1 for j in self._queue.values() if j["status"] == "pending"),
            "done":    sum(1 for j in self._queue.values() if j["status"] == "done"),
            "errors":  sum(1 for j in self._queue.values() if j["status"] == "error"),
            "total":   len(self._queue),
        }

    def clear_done(self):
        """Supprime les jobs terminés de la file."""
        self._queue = {k: v for k, v in self._queue.items() if v["status"] != "done"}
        self._save_queue()

    # ─────────────────────────────────────────────
    # 🔧 INTERNE
    # ─────────────────────────────────────────────

    def _headers(self) -> dict:
        h = {"Content-Type": "application/json"}
        if self.token:
            h["Authorization"] = f"Bearer {self.token}"
        return h

    def _build_prompt(self, prompt: str, system: Optional[str]) -> str:
        if system:
            return f"{system}\n\nUser : {prompt}\nAssistant :"
        return prompt

    def _send(self, prompt: str) -> str:
        payload = {"model": self.model, "prompt": prompt, "stream": False}
        r = requests.post(
            f"{self.base_url}/api/generate",
            json=payload,
            headers=self._headers(),
            timeout=self.timeout,
        )
        if r.status_code == 401:
            raise PermissionError("Token invalide ou manquant")
        r.raise_for_status()
        return r.json()["response"].strip()

    def _load_queue(self) -> dict:
        try:
            if os.path.exists(self.queue_file):
                with open(self.queue_file, "r", encoding="utf-8") as f:
                    return json.load(f)
        except Exception:
            pass
        return {}

    def _save_queue(self):
        try:
            with open(self.queue_file, "w", encoding="utf-8") as f:
                json.dump(self._queue, f, indent=2, ensure_ascii=False)
        except Exception as e:
            print(f"⚠️ Erreur sauvegarde queue : {e}")

    def _start_worker(self):
        if self._worker_running:
            return
        self._worker_running = True
        t = threading.Thread(target=self._worker_loop, daemon=True)
        t.start()

    def _worker_loop(self):
        while True:
            pending = [j for j in self._queue.values() if j["status"] == "pending"]
            if pending and self.is_online():
                for job in pending:
                    try:
                        print(f"⚙️  Traitement job {job['id']}...")
                        result = self._send(job["prompt"])
                        job["status"]  = "done"
                        job["result"]  = result
                        job["done_at"] = datetime.now().isoformat()
                        self._save_queue()
                        print(f"✅ Job {job['id']} terminé")
                        cb = self._callbacks.get(job["id"])
                        if cb:
                            try:
                                cb(job["id"], result)
                            except Exception:
                                pass
                    except Exception as e:
                        job["status"] = "error"
                        job["error"]  = str(e)
                        self._save_queue()
                        print(f"❌ Job {job['id']} échoué : {e}")
            time.sleep(self.retry_interval)
