"""
candy.core — Interface universelle v14
======================================

One-liner universel :
    from candy import Candy
    Candy.ask("génère un tri de liste")

Multi-agents :
    from candy import Agent
    Agent.run([Coding, Reviewer, Debugger], "mon code")

Validation & auto-correction :
    from candy import ask_validated, ask_json
"""

import json
import re
import time
import os
import hashlib
from pathlib import Path
from typing import Optional

from .client import _client
from .config import get_profile, _Config


# ── Détection de personnalité auto ───────────────────────────────────────────

_AUTO_KEYWORDS = {
    "coding":       ["code","script","python","javascript","fonction","class","bug","debug","erreur","algorithm"],
    "math":         ["calcul","équation","intégrale","dérivée","math","formule","résoudre","algèbre"],
    "writing":      ["écris","rédige","article","texte","roman","poème","email","lettre","histoire"],
    "cooking":      ["recette","cuisine","plat","ingrédient","cuisson","repas","menu","chef"],
    "translator":   ["traduis","translate","traduction","traduction"],
    "summarizer":   ["résume","résumé","synthèse","synthétise","summary"],
    "medicine":     ["symptôme","médecin","maladie","santé","traitement","médicament","santé"],
    "finance":      ["investissement","bourse","budget","argent","finance","économie","action"],
    "law":          ["loi","juridique","contrat","droit","légal","avocat","tribunal"],
    "science":      ["physique","chimie","biologie","astronomie","science","expérience"],
    "history":      ["histoire","historique","guerre","siècle","époque","civilisation"],
    "marketing":    ["marketing","marque","campagne","publicité","audience","conversion"],
    "seo":          ["seo","référencement","google","mots-clés","backlink"],
    "youtube":      ["youtube","vidéo","chaîne","miniature","abonnés","vues"],
    "socialmedia":  ["instagram","tiktok","linkedin","twitter","réseaux sociaux","followers"],
    "devops":       ["docker","kubernetes","ci/cd","déploiement","cloud","aws","pipeline"],
    "mlops":        ["machine learning","modèle","ml","deep learning","neural","ia","entraînement"],
    "prompt":       ["prompt","llm","chatgpt","instruction","chain","few-shot"],
}

def _detect_personality(prompt: str) -> str:
    """Détecte automatiquement la meilleure personnalité pour un prompt."""
    p = prompt.lower()
    scores = {}
    for personality, keywords in _AUTO_KEYWORDS.items():
        scores[personality] = sum(1 for kw in keywords if kw in p)
    best = max(scores, key=scores.get)
    return best if scores[best] > 0 else "full"


# ── One-liner universel ───────────────────────────────────────────────────────

class _CandyUniversal:
    """
    Interface one-liner universelle.
    Détecte automatiquement la personnalité, applique le profil default.

    Usage :
        from candy import Candy
        print(Candy.ask("Explique les décorateurs Python"))
        print(Candy.ask("Recette de tiramisu", lang="FR"))
        print(Candy.ask("Write a binary search", lang="EN", max_tokens=500))
    """

    def ask(self, prompt: str, lang: str = "FR", max_tokens: int = 1024,
            style: str = "default", temperature: float = 0.7,
            personality: Optional[str] = None, context: Optional[str] = None) -> str:
        """
        One-liner universel.
        Détecte automatiquement la personnalité si non spécifiée.
        """
        import copy
        p = get_profile("default")
        p2 = copy.copy(p)
        object.__setattr__(p2, "lang", lang)
        object.__setattr__(p2, "max_tokens", max_tokens)
        object.__setattr__(p2, "style", style)
        object.__setattr__(p2, "temperature", temperature)

        auto_personality = personality or _detect_personality(prompt)
        result = _client.ask(auto_personality, prompt, p2, context=context, stream=False)
        return result.get("response", "")

    def stream(self, prompt: str, lang: str = "FR", personality: Optional[str] = None):
        """Streaming one-liner."""
        import copy
        p = get_profile("default")
        p2 = copy.copy(p)
        object.__setattr__(p2, "lang", lang)
        auto_personality = personality or _detect_personality(prompt)
        yield from _client.stream(auto_personality, prompt, p2)

    def score(self, prompt: str) -> dict:
        """
        Score qualité d'un prompt (0-100).
        Retourne {"score": int, "issues": [], "suggestions": []}
        """
        issues = []
        suggestions = []
        score = 100

        if len(prompt) > 500:
            issues.append("Prompt trop long (>500 chars)")
            suggestions.append("Raccourcis à l'essentiel")
            score -= 20
        if len(prompt) < 10:
            issues.append("Prompt trop court (<10 chars)")
            suggestions.append("Ajoute plus de contexte")
            score -= 30
        if prompt == prompt.upper():
            issues.append("Tout en majuscules")
            score -= 10
        if not any(c in prompt for c in ["?", ".", "!", ":"]):
            suggestions.append("Ajoute une ponctuation pour clarifier l'intention")
            score -= 5
        vague = ["truc", "chose", "ça", "ca", "faire", "un peu", "peut-être"]
        found_vague = [w for w in vague if w in prompt.lower()]
        if found_vague:
            issues.append(f"Termes vagues : {', '.join(found_vague)}")
            suggestions.append("Remplace par des termes précis")
            score -= len(found_vague) * 5

        detected = _detect_personality(prompt)
        return {
            "score":       max(0, score),
            "quality":     "excellent" if score >= 80 else "bon" if score >= 60 else "à améliorer",
            "issues":      issues,
            "suggestions": suggestions,
            "detected_personality": detected,
            "chars":       len(prompt),
            "words":       len(prompt.split()),
        }

    def fix(self, prompt: str, lang: str = "FR") -> str:
        """Auto-optimise un prompt et retourne la version améliorée."""
        import copy
        p = get_profile("default")
        p2 = copy.copy(p)
        object.__setattr__(p2, "lang", lang)
        object.__setattr__(p2, "max_tokens", 300)
        result = _client.ask("prompt",
            f"Optimise ce prompt pour un LLM. Court, structuré, précis. "
            f"Retourne UNIQUEMENT le prompt amélioré, rien d'autre : {prompt}",
            p2, stream=False)
        return result.get("response", prompt).strip()


Candy = _CandyUniversal()


# ── ask_validated — retry si format non respecté ────────────────────────────

def ask_validated(module, profile: str, prompt: str,
                  required_fields: list, max_attempts: int = 3) -> str:
    """
    Génère une réponse et vérifie que les champs requis sont présents.
    Régénère automatiquement si le format n'est pas respecté.

    Args:
        module:          Module candy (ex: Cooking, Coding...)
        profile:         Nom du profil cfg
        prompt:          Le prompt initial
        required_fields: Liste de strings qui doivent apparaître dans la réponse
        max_attempts:    Nombre max de tentatives (défaut 3)

    Exemple::
        from candy import cfg, Cooking, ask_validated
        cfg.p.lang = "FR"
        page = ask_validated(
            Cooking, "p",
            prompt="Écris une recette. Format: - Plat:\\n- Ingrédients:\\n- Étapes:",
            required_fields=["- Plat:", "- Ingrédients:", "- Étapes:"]
        )
    """
    current_prompt = prompt
    for attempt in range(1, max_attempts + 1):
        response = module.use(profile).ask(current_prompt)
        if all(field in response for field in required_fields):
            return response
        missing = [f for f in required_fields if f not in response]
        current_prompt = (
            f"{prompt}\n\n"
            f"ATTENTION tentative {attempt}/{max_attempts} : "
            f"Champs manquants : {', '.join(missing)}. "
            f"Respecte STRICTEMENT le format demandé. Ne rien ajouter hors format."
        )
    return response  # retourne la dernière tentative


# ── ask_json — génère et valide du JSON ──────────────────────────────────────

def ask_json(module, profile: str, prompt: str, max_attempts: int = 3) -> dict:
    """
    Génère une réponse JSON valide.
    Auto-corrige si le JSON est invalide.

    Args:
        module:       Module candy (ex: Analytic, Coding...)
        profile:      Nom du profil cfg
        prompt:       Le prompt (doit demander du JSON)
        max_attempts: Nombre max de tentatives (défaut 3)

    Exemple::
        from candy import cfg, Analytic, ask_json
        cfg.a.lang = "FR"
        result = ask_json(Analytic, "a",
            'Analyse ce texte. JSON strict: {"ton": "", "mots_cles": []}\\nTexte: ...'
        )
        print(result["ton"])
    """
    current_prompt = prompt
    for attempt in range(1, max_attempts + 1):
        response = module.use(profile).ask(current_prompt)
        clean = response.strip()
        # Nettoie les backticks markdown
        clean = re.sub(r"```json\s*", "", clean)
        clean = re.sub(r"```\s*", "", clean)
        clean = clean.strip()
        # Extrait le JSON si entouré de texte
        match = re.search(r'\{.*\}|\[.*\]', clean, re.DOTALL)
        if match:
            clean = match.group()
        try:
            return json.loads(clean)
        except json.JSONDecodeError as e:
            current_prompt = (
                f"Le JSON précédent était invalide.\n"
                f"Erreur : {e}\n"
                f"JSON incorrect : {clean[:200]}\n\n"
                f"Retourne UNIQUEMENT un JSON valide, sans markdown, sans texte avant ou après.\n"
                f"Prompt original : {prompt}"
            )
    return {}


# ── Multi-agents ──────────────────────────────────────────────────────────────

class _Agent:
    """
    Orchestrateur multi-agents.
    Fait passer un contenu à travers plusieurs personnalités en séquence.

    Usage :
        from candy import cfg, Agent, Coding, Debugger, Reviewer
        cfg.dev = cfg.preset("coder")
        cfg.dev.lang = "FR"

        result = Agent.run(
            modules=[Coding, Debugger, Reviewer],
            task="Écris un client REST Python",
            profile="dev"
        )
        print(result["final"])
        print(result["steps"])   # dict avec output de chaque étape
    """

    def run(self, modules: list, task: str, profile: str = "default",
            instructions: Optional[list] = None, verbose: bool = False) -> dict:
        """
        Pipeline multi-agents.

        Args:
            modules:      Liste de modules candy [Coding, Debugger, Reviewer]
            task:         Tâche initiale
            profile:      Profil cfg à utiliser
            instructions: Instructions spécifiques par étape (optionnel)
                          Ex: ["Écris le code", "Trouve les bugs", "Améliore la qualité"]
            verbose:      Affiche la progression si True
        """
        steps = {}
        current_content = task

        for i, module in enumerate(modules):
            module_name = module.__class__.__name__ if not isinstance(module, type) else module.__name__
            instruction = instructions[i] if instructions and i < len(instructions) else None

            if instruction:
                prompt = f"{instruction}\n\n{current_content}"
            else:
                prompt = current_content

            if verbose:
                print(f"[{i+1}/{len(modules)}] {module_name}...")

            response = module.use(profile).ask(prompt)
            steps[f"step_{i+1}_{module_name}"] = response
            current_content = response

        return {"steps": steps, "final": current_content, "total_steps": len(modules)}

    def debate(self, modules: list, question: str, profile: str = "default") -> dict:
        """
        Fait débattre plusieurs personnalités sur une question.
        Chaque module donne son point de vue, puis Full synthétise.

        Exemple::
            from candy import Agent, Philosophy, Ethics, Law
            result = Agent.debate(
                [Philosophy, Ethics, Law],
                "Le droit à la déconnexion doit-il être obligatoire ?"
            )
            print(result["synthesis"])
        """
        from .modules.full import Full
        opinions = {}
        for module in modules:
            name = module.__class__.__name__ if not isinstance(module, type) else module.__name__
            response = module.use(profile).ask(
                f"Donne ton point de vue d'expert sur : {question}. "
                f"Sois concis (150 mots max) et argue clairement."
            )
            opinions[name] = response

        context = "\n\n".join([f"{k}:\n{v}" for k, v in opinions.items()])
        synthesis = Full.use(profile).ask(
            "Synthétise ces points de vue en une conclusion équilibrée.",
            context=context
        )
        return {"opinions": opinions, "synthesis": synthesis}

    def build_app(self, description: str, profile: str = "default",
                  lang: str = "FR", output_dir: str = ".") -> dict:
        """
        Génère une application complète (code + README + tests).

        Exemple::
            from candy import cfg, Agent
            cfg.dev = cfg.preset("coder")
            result = Agent.build_app("todo app avec login", profile="dev")
            # Sauvegarde main.py, README.md, tests.py
        """
        from .modules.coding import Coding
        from .modules.reviewer import Reviewer
        from .modules.debugger import Debugger
        import copy
        p = get_profile(profile)
        p2 = copy.copy(p)
        object.__setattr__(p2, "lang", lang)
        object.__setattr__(p2, "max_tokens", 4096)

        # 1. Génère le code
        code = Coding.use(profile).ask(
            f"Génère le code Python complet pour : {description}. "
            f"Code seul, commenté, fonctionnel, avec if __name__ == '__main__'."
        )
        # 2. Génère les tests
        tests = Coding.use(profile).ask(
            "Génère les tests unitaires pytest pour ce code.",
            context=code
        )
        # 3. Génère le README
        readme = _client.ask("writing",
            f"Génère un README.md complet pour : {description}. "
            f"Sections : Description, Installation, Usage, Exemples.",
            p2, stream=False
        ).get("response", "")
        # 4. Review finale
        review = Reviewer.use(profile).ask(
            "Évalue la qualité du code et liste les améliorations prioritaires.",
            context=code
        )

        # Sauvegarde
        output = Path(output_dir)
        output.mkdir(exist_ok=True)
        files = {"main.py": code, "tests.py": tests, "README.md": readme}
        for fname, content in files.items():
            (output / fname).write_text(content, encoding="utf-8")

        return {
            "files":   list(files.keys()),
            "code":    code,
            "tests":   tests,
            "readme":  readme,
            "review":  review,
            "output_dir": str(output),
        }


Agent = _Agent()


# ── Mémoire persistante ───────────────────────────────────────────────────────

class Memory:
    """
    Mémoire persistante pour les sessions.
    Sauvegarde dans .candy_sessions/ (local) ou chemin configurable.

    Usage :
        from candy import Memory
        mem = Memory("ma_session")
        mem.add("user", "Bonjour")
        mem.add("assistant", "Bonjour ! Comment puis-je vous aider ?")
        mem.save()

        # Reprendre plus tard
        mem2 = Memory("ma_session")
        print(mem2.history)
    """

    DEFAULT_DIR = Path.home() / ".candy" / "sessions"

    def __init__(self, session_name: str, base_dir: Optional[str] = None):
        self.session_name = session_name
        self.base_dir = Path(base_dir) if base_dir else self.DEFAULT_DIR
        self.base_dir.mkdir(parents=True, exist_ok=True)
        self.path = self.base_dir / f"{session_name}.json"
        self.history = []
        self._load()

    def _load(self):
        if self.path.exists():
            try:
                data = json.loads(self.path.read_text(encoding="utf-8"))
                self.history = data.get("history", [])
            except Exception:
                self.history = []

    def add(self, role: str, content: str):
        """Ajoute un message à l'historique."""
        self.history.append({
            "role":      role,
            "content":   content,
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        })

    def save(self):
        """Sauvegarde la session sur disque."""
        data = {
            "session":  self.session_name,
            "created":  self.history[0]["timestamp"] if self.history else "",
            "updated":  time.strftime("%Y-%m-%d %H:%M:%S"),
            "turns":    len([h for h in self.history if h["role"] == "user"]),
            "history":  self.history,
        }
        self.path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

    def clear(self):
        """Efface l'historique en mémoire et sur disque."""
        self.history = []
        if self.path.exists():
            self.path.unlink()

    def as_context(self, last_n: int = 10) -> str:
        """Retourne les N derniers échanges formatés comme contexte."""
        recent = self.history[-last_n:]
        return "\n".join([f"{h['role'].upper()}: {h['content']}" for h in recent])

    @classmethod
    def list_sessions(cls, base_dir: Optional[str] = None) -> list:
        """Liste toutes les sessions sauvegardées."""
        d = Path(base_dir) if base_dir else cls.DEFAULT_DIR
        if not d.exists():
            return []
        return [f.stem for f in d.glob("*.json")]

    @classmethod
    def delete_session(cls, session_name: str, base_dir: Optional[str] = None):
        """Supprime une session."""
        d = Path(base_dir) if base_dir else cls.DEFAULT_DIR
        path = d / f"{session_name}.json"
        if path.exists():
            path.unlink()


# ── Logging ───────────────────────────────────────────────────────────────────

class Logger:
    """
    Système de logs détaillés (prompts, outputs, erreurs, timing).

    Usage :
        from candy import Logger
        log = Logger("session_log")
        log.log_request("coding", "mon prompt", "la réponse", 1.23)
        log.save()
    """

    def __init__(self, name: str = "candy_log", base_dir: Optional[str] = None):
        self.name = name
        self.base_dir = Path(base_dir) if base_dir else Path.home() / ".candy" / "logs"
        self.base_dir.mkdir(parents=True, exist_ok=True)
        self.path = self.base_dir / f"{name}.json"
        self.entries = []

    def log_request(self, personality: str, prompt: str, response: str,
                    time_ms: float = 0, error: Optional[str] = None):
        self.entries.append({
            "timestamp":   time.strftime("%Y-%m-%d %H:%M:%S"),
            "personality": personality,
            "prompt":      prompt[:300],
            "response":    response[:500],
            "time_ms":     round(time_ms, 1),
            "error":       error,
            "words_out":   len(response.split()),
        })

    def save(self):
        data = {"name": self.name, "entries": self.entries, "total": len(self.entries)}
        self.path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")

    def summary(self) -> dict:
        if not self.entries:
            return {"total": 0}
        personalities = {}
        for e in self.entries:
            personalities[e["personality"]] = personalities.get(e["personality"], 0) + 1
        return {
            "total":               len(self.entries),
            "top_personality":     max(personalities, key=personalities.get),
            "personalities_used":  personalities,
            "avg_time_ms":         round(sum(e["time_ms"] for e in self.entries) / len(self.entries), 1),
            "errors":              sum(1 for e in self.entries if e["error"]),
        }
