"""
candy.core — Interface universelle v15
======================================
Toutes les recommandations ChatGPT intégrées :
- One-liner universel avec auto-détection
- Score de prompt corrigé (pénalités réelles)
- Multi-agents (run, debate, build_app)
- Mémoire persistante avec sessions nommées
- Validation format + JSON auto-correction
- Logger détaillé
- Pipeline YAML-like
- Dry-run mode
- Auto-versioning des sessions
- Scoring gamifié
"""

import json
import re
import time
import os
import copy
import hashlib
from pathlib import Path
from typing import Optional, Union

from .client import _client
from .config import get_profile


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

_AUTO_KEYWORDS = {
    "coding":        ["code","script","python","javascript","fonction","class","bug","erreur","algorithm","programme","def ","import "],
    "math":          ["calcul","équation","intégrale","dérivée","math","formule","résoudre","algèbre","probabilité","statistique"],
    "writing":       ["écris","rédige","article","texte","roman","poème","email","lettre","histoire","rédaction"],
    "cooking":       ["recette","cuisine","plat","ingrédient","cuisson","repas","menu","chef","gastronomie"],
    "translator":    ["traduis","translate","traduction","traduire","version anglaise","version française"],
    "summarizer":    ["résume","résumé","synthèse","synthétise","summary","raccourcis"],
    "medicine":      ["symptôme","médecin","maladie","santé","traitement","médicament","douleur","diagnostic"],
    "finance":       ["investissement","bourse","budget","argent","finance","économie","action","portefeuille"],
    "law":           ["loi","juridique","contrat","droit","légal","avocat","tribunal","réglementation"],
    "science":       ["physique","chimie","biologie","astronomie","science","expérience","atome","molécule"],
    "history":       ["histoire","historique","guerre","siècle","époque","civilisation","empire","révolution"],
    "marketing":     ["marketing","marque","campagne","publicité","audience","conversion","croissance"],
    "seo":           ["seo","référencement","google","mots-clés","backlink","classement","trafic"],
    "youtube":       ["youtube","vidéo","chaîne","miniature","abonnés","vues","creator"],
    "socialmedia":   ["instagram","tiktok","linkedin","twitter","réseaux sociaux","followers","post","reel"],
    "devops":        ["docker","kubernetes","ci/cd","déploiement","cloud","aws","pipeline","conteneur"],
    "mlops":         ["machine learning","modèle","ml","deep learning","neural","ia","entraînement","dataset"],
    "prompt":        ["prompt","llm","chatgpt","instruction","chain","few-shot","optimise ce prompt"],
    "nutrition":     ["nutrition","alimentation","régime","calories","vitamines","protéines","diète"],
    "yoga":          ["yoga","méditation","posture","pranayama","bien-être","relaxation"],
    "leadership":    ["leadership","management","équipe","manager","diriger","motivation","organisation"],
    "branding":      ["marque","branding","identité","logo","positionnement","naming","brand"],
    "debugging":     ["bug","erreur","traceback","fix","corrige","exception","crash"],
}

def _detect_personality(prompt: str) -> str:
    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 v15.
    
    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 sort function", 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:
        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 or _detect_personality(prompt)
        result = _client.ask(auto, prompt, p2, context=context, stream=False)
        return result.get("response", "")

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

    def score(self, prompt: str) -> dict:
        """
        Score qualité d'un prompt (0-100).
        Pénalités réelles et calibrées.
        """
        issues      = []
        suggestions = []
        score       = 100

        # Trop court
        if len(prompt) < 10:
            issues.append("Prompt trop court (<10 chars)")
            suggestions.append("Ajoute plus de contexte et de précision")
            score -= 40

        # Trop long
        elif len(prompt) > 500:
            issues.append("Prompt trop long (>500 chars)")
            suggestions.append("Raccourcis à l'essentiel, sois direct")
            score -= 20

        # Moins de 3 mots
        if len(prompt.split()) < 3:
            issues.append("Trop peu de mots (<3 mots)")
            suggestions.append("Décris plus précisément ta demande")
            score -= 25

        # Tout en majuscules
        if len(prompt) > 3 and prompt == prompt.upper():
            issues.append("Tout en majuscules")
            suggestions.append("Utilise une casse normale")
            score -= 10

        # Pas de ponctuation
        if not any(c in prompt for c in ["?", ".", "!", ":", ","]):
            suggestions.append("Ajoute une ponctuation pour clarifier l'intention")
            score -= 8

        # Termes vagues — pénalité forte
        vague = ["truc", "chose", "ça", "ca", "faire", "un peu",
                 "peut-être", "machin", "bidule", "genre", "kek"]
        found_vague = [w for w in vague if w in prompt.lower().split()]
        if found_vague:
            issues.append(f"Termes vagues : {', '.join(found_vague)}")
            suggestions.append("Remplace par des termes précis et mesurables")
            score -= len(found_vague) * 15

        # Pas d'action claire (pas de verbe d'action)
        actions = ["écris","génère","explique","analyse","crée","résume",
                   "traduis","code","write","generate","create","explain",
                   "list","compare","describe","fix","review","optimize"]
        has_action = any(a in prompt.lower() for a in actions)
        if not has_action and len(prompt.split()) > 2:
            suggestions.append("Commence par un verbe d'action clair (écris, génère, explique...)")
            score -= 5

        score     = max(0, min(100, score))
        detected  = _detect_personality(prompt)
        quality   = "excellent" if score >= 80 else "bon" if score >= 60 else "à améliorer" if score >= 40 else "insuffisant"

        return {
            "score":                score,
            "quality":              quality,
            "grade":                "A" if score >= 90 else "B" if score >= 75 else "C" if score >= 60 else "D" if score >= 40 else "F",
            "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."""
        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, avec verbe d'action. "
            f"Retourne UNIQUEMENT le prompt amélioré, rien d'autre : {prompt}",
            p2, stream=False)
        return result.get("response", prompt).strip()

    def dry_run(self, prompt: str, personality: Optional[str] = None) -> dict:
        """
        Simule l'envoi sans appeler l'API.
        Retourne ce qui SERAIT envoyé : personnalité détectée, score, config.
        """
        detected = personality or _detect_personality(prompt)
        score_data = self.score(prompt)
        return {
            "would_use_personality": detected,
            "prompt_score":          score_data["score"],
            "prompt_quality":        score_data["quality"],
            "prompt_grade":          score_data["grade"],
            "issues":                score_data["issues"],
            "suggestions":           score_data["suggestions"],
            "prompt_length":         len(prompt),
            "api_call":              "SKIPPED (dry-run mode)",
        }


Candy = _CandyUniversal()


# ── ask_validated ─────────────────────────────────────────────────────────────

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 les champs requis.
    Régénère automatiquement si le format n'est pas respecté.

    from candy import cfg, Cooking, ask_validated
    cfg.p.lang = "FR"
    page = ask_validated(
        Cooking, "p",
        prompt="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"TENTATIVE {attempt}/{max_attempts} — CHAMPS MANQUANTS : {', '.join(missing)}\n"
            f"Respecte STRICTEMENT le format. Ne rien ajouter hors format."
        )
    return response


# ── ask_json ──────────────────────────────────────────────────────────────────

def ask_json(module, profile: str, prompt: str,
             max_attempts: int = 3) -> dict:
    """
    Génère du JSON valide avec auto-correction.

    from candy import cfg, Analytic, ask_json
    cfg.a.lang = "FR"
    result = ask_json(Analytic, "a",
        'Analyse. JSON strict: {"ton": "", "score": 0}\\nTexte: ...'
    )
    """
    current_prompt = prompt
    for attempt in range(1, max_attempts + 1):
        response = module.use(profile).ask(current_prompt)
        clean = re.sub(r"```json\s*", "", response.strip())
        clean = re.sub(r"```\s*", "", clean).strip()
        m = re.search(r'\{.*\}|\[.*\]', clean, re.DOTALL)
        if m:
            clean = m.group()
        try:
            return json.loads(clean)
        except json.JSONDecodeError as e:
            current_prompt = (
                f"JSON invalide (erreur: {e}).\n"
                f"JSON reçu : {clean[:200]}\n\n"
                f"Retourne UNIQUEMENT un JSON valide, sans texte ni markdown.\n"
                f"Prompt original : {prompt}"
            )
    return {}


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

class _Agent:
    """
    Orchestrateur multi-agents v15.
    
    from candy import cfg, Agent, Coding, Debugger, Reviewer
    cfg.dev = cfg.preset("coder")
    result = Agent.run([Coding, Debugger, Reviewer], "Écris un client REST", profile="dev")
    print(result["final"])
    """

    def run(self, modules: list, task: str, profile: str = "default",
            instructions: Optional[list] = None, verbose: bool = False,
            dry_run: bool = False) -> dict:
        """
        Pipeline séquentiel. Chaque module reçoit l'output du précédent.
        dry_run=True : simule sans appeler l'API.
        """
        if dry_run:
            return {
                "dry_run":    True,
                "steps":      {f"step_{i+1}_{m.__class__.__name__ if not isinstance(m, type) else m.__name__}": "SKIPPED" for i, m in enumerate(modules)},
                "final":      "DRY RUN — aucun appel API effectué",
                "total_steps": len(modules),
            }

        steps   = {}
        current = task

        for i, module in enumerate(modules):
            name = module.__class__.__name__ if not isinstance(module, type) else module.__name__
            instr = instructions[i] if instructions and i < len(instructions) else None
            prompt = f"{instr}\n\n{current}" if instr else current
            if verbose:
                print(f"  [{i+1}/{len(modules)}] {name}...")
            response = module.use(profile).ask(prompt)
            steps[f"step_{i+1}_{name}"] = response
            current = response

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

    def debate(self, modules: list, question: str,
               profile: str = "default") -> dict:
        """
        Fait débattre plusieurs personnalités + synthèse par Full.
        
        from candy import Agent, Philosophy, Ethics, Law
        result = Agent.debate([Philosophy, Ethics, Law], "L'IA doit-elle être régulée ?")
        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 en 100 mots max sur : {question}. "
                f"Sois direct et argumente 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 différents points de vue en une conclusion équilibrée de 150 mots.",
            context=context
        )
        return {"opinions": opinions, "synthesis": synthesis, "question": question}

    def build_app(self, description: str, profile: str = "default",
                  lang: str = "FR", output_dir: str = ".",
                  dry_run: bool = False) -> dict:
        """
        Génère une app complète : code + tests + README.
        
        from candy import cfg, Agent
        cfg.dev = cfg.preset("coder")
        result = Agent.build_app("todo app avec login", profile="dev")
        """
        if dry_run:
            return {
                "dry_run": True,
                "would_generate": ["main.py", "tests.py", "README.md"],
                "description": description,
            }

        from .modules.coding import Coding
        from .modules.reviewer import Reviewer

        p = get_profile(profile)
        p2 = copy.copy(p)
        object.__setattr__(p2, "lang", lang)
        object.__setattr__(p2, "max_tokens", 4096)

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

        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),
        }

    def pipeline(self, text: str, steps: list, lang: str = "FR") -> dict:
        """
        Pipeline de transformations sur un texte.
        steps = [(module_name, instruction), ...]
        
        from candy import Agent
        result = Agent.pipeline(
            "Mon texte...",
            steps=[
                ("summarizer", "Résume en 2 phrases."),
                ("writing",    "Rends plus engageant."),
                ("reviewer",   "Corrige les erreurs."),
            ]
        )
        print(result["final"])
        """
        current = text
        results = {}
        for i, (module_name, instruction) in enumerate(steps):
            p = get_profile("default")
            p2 = copy.copy(p)
            object.__setattr__(p2, "lang", lang)
            object.__setattr__(p2, "max_tokens", 800)
            result = _client.ask(module_name,
                f"{instruction} Retourne UNIQUEMENT le résultat : {current}",
                p2, stream=False)
            current = result.get("response", current).strip()
            results[f"step_{i+1}_{module_name}"] = current
        return {"steps": results, "final": current}


Agent = _Agent()


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

class Memory:
    """
    Sessions persistantes avec auto-versioning.
    
    from candy import Memory
    mem = Memory("mon_projet")
    mem.add("user", "Bonjour")
    mem.save()
    
    # Reprendre plus tard
    mem2 = Memory("mon_projet")
    print(mem2.history)
    print(Memory.list_sessions())
    """

    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._version     = 1
        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", [])
                self._version = data.get("version", 1)
            except Exception:
                self.history = []

    def add(self, role: str, content: str):
        self.history.append({
            "role":      role,
            "content":   content,
            "timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
        })

    def save(self):
        data = {
            "session":  self.session_name,
            "version":  self._version,
            "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 save_version(self, version_name: str):
        """
        Sauvegarde une version nommée de la session.
        
        mem.save_version("v1-avant-refactor")
        """
        vpath = self.base_dir / f"{self.session_name}_{version_name}.json"
        data = {
            "session":      self.session_name,
            "version_name": version_name,
            "saved_at":     time.strftime("%Y-%m-%d %H:%M:%S"),
            "history":      self.history,
        }
        vpath.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
        return str(vpath)

    def clear(self):
        self.history = []
        if self.path.exists():
            self.path.unlink()

    def as_context(self, last_n: int = 10) -> str:
        recent = self.history[-last_n:]
        return "\n".join([f"{h['role'].upper()}: {h['content']}" for h in recent])

    def count_turns(self) -> int:
        return len([h for h in self.history if h["role"] == "user"])

    def export(self, path: str):
        """Exporte la session vers un fichier externe."""
        Path(path).write_text(
            json.dumps({"session": self.session_name, "history": self.history},
                       ensure_ascii=False, indent=2), encoding="utf-8")

    @classmethod
    def list_sessions(cls, base_dir: Optional[str] = None) -> list:
        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") if "_" not in f.stem or f.stem.count("_") == 0]

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


# ── Logger ────────────────────────────────────────────────────────────────────

class Logger:
    """
    Logs détaillés : prompts, réponses, erreurs, timing.
    
    from candy import Logger
    log = Logger("session")
    log.log_request("coding", "mon prompt", "réponse", 1230)
    log.save()
    print(log.summary())
    """

    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()),
            "prompt_score": Candy.score(prompt)["score"],
        })

    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
        avg_score = sum(e.get("prompt_score", 0) for e in self.entries) / len(self.entries)
        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"]),
            "avg_prompt_score":   round(avg_score, 1),
        }
