"""
candy.client — Client HTTP Cedric7-Thinking v6
Fix v6 : la langue est maintenant injectée directement dans le prompt
pour garantir que le modèle réponde dans la bonne langue.
"""

import httpx
import json
import time
from typing import Optional, Generator

CANDY_JSON_URL = "https://client.hubworld.net/candy.json"

LANG_INSTRUCTIONS = {
    "FR": "Tu dois OBLIGATOIREMENT répondre en français. Chaque mot de ta réponse doit être en français.",
    "EN": "You MUST respond in English. Every single word of your response must be in English.",
    "ES": "DEBES responder en español. Cada palabra de tu respuesta debe estar en español.",
    "DE": "Du MUSST auf Deutsch antworten. Jedes Wort deiner Antwort muss auf Deutsch sein.",
    "IT": "DEVI rispondere in italiano. Ogni parola della tua risposta deve essere in italiano.",
    "PT": "DEVE responder em português. Cada palavra da sua resposta deve estar em português.",
    "ZH": "你必须用中文回答。你回答中的每个字都必须是中文。",
    "JA": "必ず日本語で回答してください。回答のすべての言葉は日本語でなければなりません。",
    "AR": "يجب عليك الإجابة باللغة العربية. كل كلمة في إجابتك يجب أن تكون بالعربية.",
    "RU": "Ты ОБЯЗАН отвечать на русском языке. Каждое слово твоего ответа должно быть на русском.",
    "NL": "Je MOET in het Nederlands antwoorden. Elk woord van je antwoord moet in het Nederlands zijn.",
    "PL": "MUSISZ odpowiadać po polsku. Każde słowo twojej odpowiedzi musi być po polsku.",
    "SV": "Du MÅSTE svara på svenska. Varje ord i ditt svar måste vara på svenska.",
    "TR": "TÜRKÇE yanıt vermelisin. Cevabındaki her kelime Türkçe olmalıdır.",
    "KO": "반드시 한국어로 대답해야 합니다. 답변의 모든 단어는 한국어여야 합니다.",
    "HI": "आपको हिंदी में जवाब देना होगा। आपके जवाब का हर शब्द हिंदी में होना चाहिए।",
    "VI": "Bạn PHẢI trả lời bằng tiếng Việt. Mỗi từ trong câu trả lời phải bằng tiếng Việt.",
    "ID": "Anda HARUS menjawab dalam bahasa Indonesia. Setiap kata harus dalam bahasa Indonesia.",
    "CS": "MUSÍŠ odpovídat česky. Každé slovo tvé odpovědi musí být česky.",
    "RO": "TREBUIE să răspunzi în română. Fiecare cuvânt al răspunsului tău trebuie să fie în română.",
}

STYLE_INSTRUCTIONS = {
    "default":   "",
    "concise":   "Be concise and direct. No filler. Get straight to the point.",
    "detailed":  "Be thorough. Include explanations, nuances, and concrete examples.",
    "bullet":    "Structure your entire response as bullet points.",
    "academic":  "Use a formal, structured, academic tone with clear sections.",
    "casual":    "Use a casual, friendly, conversational tone.",
    "technical": "Use precise technical language. Assume deep expertise. Be exact.",
    "eli5":      "Explain as if to a 5-year-old. Simple words, short sentences, analogies.",
}

TONE_INSTRUCTIONS = {
    "neutral":     "",
    "encouraging": "Be positive, motivating, and encouraging.",
    "strict":      "Be direct and strict. No sugarcoating.",
    "humorous":    "Add appropriate humor to your response.",
    "empathetic":  "Be warm, empathetic, and understanding.",
    "socratic":    "Guide with questions rather than giving direct answers.",
}

FORMAT_INSTRUCTIONS = {
    "text":     "",
    "markdown": "Format using Markdown: ## headers, **bold**, bullet lists, ``` code blocks.",
    "json":     "Return ONLY a valid JSON object. No prose outside the JSON.",
    "html":     "Format using HTML: <h2>, <p>, <ul>, <li>, <code>, <strong>, <em>.",
}

EXPERTISE_INSTRUCTIONS = {
    "beginner":     "The user is a beginner. Explain everything from scratch, define all terms.",
    "intermediate": "The user has basic knowledge. Skip trivial explanations.",
    "expert":       "The user is an expert. Skip basics. Go deep and precise.",
}


class CandyUnavailableError(Exception):
    pass

class CandyQuotaError(Exception):
    pass

class CandyTimeoutError(Exception):
    pass


class CandyClient:

    def __init__(self):
        self._api_url: Optional[str] = None

    def _resolve_url(self, profile) -> str:
        if profile.cache_url and self._api_url:
            return self._api_url
        try:
            resp = httpx.get(CANDY_JSON_URL, timeout=10)
            resp.raise_for_status()
            data = resp.json()
        except Exception as e:
            raise CandyUnavailableError(f"Cannot reach candy.json: {e}")
        if data.get("is_closed", True):
            raise CandyUnavailableError("Cedric7-Thinking API is offline.")
        link = data.get("link", "").strip().rstrip("/")
        if not link:
            raise CandyUnavailableError("No API link in candy.json.")
        self._api_url = link
        return self._api_url

    def invalidate_cache(self):
        self._api_url = None

    def _build_suffix(self, profile) -> str:
        """
        Construit les instructions système supplémentaires.
        La langue est placée EN PREMIER et formulée de façon IMPÉRATIVE
        pour s'assurer que le modèle la respecte.
        """
        parts = []

        # ── Langue — TOUJOURS en premier, formulée de façon forte ───────────
        lang_instr = LANG_INSTRUCTIONS.get(
            profile.lang.upper(),
            f"You MUST respond in {profile.lang}. Every word must be in {profile.lang}."
        )
        parts.append(lang_instr)

        # ── Autres instructions ──────────────────────────────────────────────
        for m, k in [
            (STYLE_INSTRUCTIONS,     profile.style),
            (TONE_INSTRUCTIONS,      profile.tone),
            (FORMAT_INSTRUCTIONS,    profile.output_format),
            (EXPERTISE_INSTRUCTIONS, profile.expertise),
        ]:
            v = m.get(k, "")
            if v:
                parts.append(v)

        if not profile.include_examples:
            parts.append("Do not include examples.")
        if profile.safe_mode:
            parts.append("Refuse politely if the request involves harmful or illegal content.")
        if profile.max_tokens < 300:
            parts.append(f"Keep under {profile.max_tokens} tokens. Be extremely brief.")
        elif profile.max_tokens > 2000:
            parts.append(f"Be as detailed as needed (up to {profile.max_tokens} tokens).")

        return "\n".join(parts)

    def _build_prompt_with_lang(self, prompt: str, profile) -> str:
        """
        Préfixe le prompt avec la consigne de langue pour forcer le modèle.
        Double injection : dans le system prompt ET dans le prompt utilisateur.
        """
        lang = profile.lang.upper()
        lang_instr = LANG_INSTRUCTIONS.get(lang, f"Respond in {lang}.")
        return f"[INSTRUCTION OBLIGATOIRE: {lang_instr}]\n\n{prompt}"

    def ask(self, personality: str, prompt: str, profile,
            context: Optional[str] = None,
            stream: bool = False,
            history: Optional[list] = None):

        base = self._resolve_url(profile)
        url = f"{base}/ask/{personality}"

        # Injection langue dans le prompt lui-même (fix v6)
        augmented_prompt = self._build_prompt_with_lang(prompt, profile)

        payload = {
            "prompt": augmented_prompt,
            "stream": stream,
            "config": {
                "lang":             profile.lang,
                "max_tokens":       profile.max_tokens,
                "temperature":      profile.temperature,
                "style":            profile.style,
                "output_format":    profile.output_format,
                "tone":             profile.tone,
                "expertise":        profile.expertise,
                "include_examples": profile.include_examples,
                "safe_mode":        profile.safe_mode,
                "system_suffix":    self._build_suffix(profile),
            },
        }
        if context:
            payload["context"] = context
        if history:
            payload["history"] = history

        if getattr(profile, "verbose", False):
            pname = object.__getattribute__(profile, "_name")
            print(f"\033[90m[candy] POST {url} | profile={pname} | lang={profile.lang} | personality={personality}\033[0m")

        if stream:
            return self._stream(url, payload, profile)
        return self._post_with_retry(url, payload, profile)

    def _post_with_retry(self, url: str, payload: dict, profile) -> dict:
        attempts = max(1, getattr(profile, "retry", 1))
        last_err = None
        for attempt in range(attempts):
            try:
                resp = httpx.post(url, json=payload, timeout=profile.timeout)
                if resp.status_code == 429:
                    raise CandyQuotaError("Daily quota exceeded.")
                if resp.status_code == 404:
                    raise ValueError("Unknown personality.")
                resp.raise_for_status()
                return resp.json()
            except (CandyQuotaError, ValueError):
                raise
            except httpx.TimeoutException:
                last_err = CandyTimeoutError(f"Timeout after {profile.timeout}s.")
            except httpx.ConnectError:
                self._api_url = None
                last_err = CandyUnavailableError("Cannot connect to Cedric7-Thinking.")
            except Exception as e:
                last_err = e
            if attempt < attempts - 1:
                time.sleep(1.5 * (attempt + 1))
        raise last_err

    def _stream(self, url: str, payload: dict, profile) -> Generator[str, None, None]:
        try:
            with httpx.stream("POST", url, json=payload, timeout=profile.timeout) as resp:
                if resp.status_code == 429:
                    raise CandyQuotaError("Daily quota exceeded.")
                resp.raise_for_status()
                for line in resp.iter_lines():
                    if line:
                        try:
                            data = json.loads(line)
                            token = data.get("response", "")
                            if token:
                                yield token
                        except json.JSONDecodeError:
                            pass
        except httpx.TimeoutException:
            raise CandyTimeoutError(f"Stream timeout after {profile.timeout}s.")

    def batch(self, personality: str, prompts: list, profile,
              context: Optional[str] = None) -> list:
        results = []
        for prompt in prompts:
            try:
                result = self.ask(personality, prompt, profile, context=context)
                results.append(result.get("response", ""))
            except Exception as e:
                results.append(f"[ERREUR] {e}")
        return results

    def quota(self, profile) -> dict:
        base = self._resolve_url(profile)
        resp = httpx.get(f"{base}/quota/status", timeout=10)
        resp.raise_for_status()
        return resp.json()

    def personalities(self, profile) -> list:
        base = self._resolve_url(profile)
        resp = httpx.get(f"{base}/personalities", timeout=10)
        resp.raise_for_status()
        return resp.json()["personalities"]

    def ping(self, profile) -> float:
        try:
            base = self._resolve_url(profile)
            t = time.time()
            httpx.get(f"{base}/", timeout=5)
            return round((time.time() - t) * 1000, 1)
        except Exception:
            return -1.0

    def is_online(self, profile) -> bool:
        try:
            self._resolve_url(profile)
            return True
        except Exception:
            return False


_client = CandyClient()
