"""
candy.client — Client HTTP cedric-8EF v4
"""

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

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

# ── Instructions par paramètre ───────────────────────────────────────────────

LANG_INSTRUCTIONS = {
    "FR": "Réponds toujours en français.",
    "EN": "Always respond in English.",
    "ES": "Responde siempre en español.",
    "DE": "Antworte immer auf Deutsch.",
    "IT": "Rispondi sempre in italiano.",
    "PT": "Responda sempre em português.",
    "ZH": "始终用中文回答。",
    "JA": "常に日本語で答えてください。",
    "AR": "أجب دائمًا باللغة العربية.",
    "RU": "Всегда отвечай на русском языке.",
    "NL": "Antwoord altijd in het Nederlands.",
    "PL": "Zawsze odpowiadaj po polsku.",
    "SV": "Svara alltid på svenska.",
    "TR": "Her zaman Türkçe yanıt ver.",
    "KO": "항상 한국어로 대답하세요.",
    "HI": "हमेशा हिंदी में जवाब दें।",
    "VI": "Luôn trả lời bằng tiếng Việt.",
    "ID": "Selalu jawab dalam bahasa Indonesia.",
    "CS": "Vždy odpovídej česky.",
    "RO": "Răspunde întotdeauna î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 and citations where relevant.",
    "casual":    "Use a casual, friendly, conversational tone as if talking to a friend.",
    "technical": "Use precise technical language. Assume deep expertise. Be exact and terse.",
    "eli5":      "Explain as if talking to a 5-year-old. Use very simple words, analogies, and short sentences.",
}

TONE_INSTRUCTIONS = {
    "neutral":     "",
    "encouraging": "Be positive, motivating, and encouraging throughout your response.",
    "strict":      "Be direct and strict. No sugarcoating. Get to the point immediately.",
    "humorous":    "Add a touch of appropriate humor to make your response enjoyable.",
    "empathetic":  "Be warm, empathetic, and understanding in your response.",
    "socratic":    "Use the Socratic method: guide the user with questions rather than giving direct answers.",
}

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

EXPERTISE_INSTRUCTIONS = {
    "beginner":     "The user is a complete beginner. Explain everything from scratch, define all technical terms.",
    "intermediate": "The user has basic knowledge. Skip trivial explanations but clarify non-obvious concepts.",
    "expert":       "The user is an expert. Skip all basics. Go deep, be precise, use correct terminology.",
}


class CandyUnavailableError(Exception):
    pass

class CandyQuotaError(Exception):
    pass

class CandyTimeoutError(Exception):
    pass


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

    # ── URL resolution ───────────────────────────────────────────────────────

    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("The cedric-8EF API is currently 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):
        """Force un re-fetch de l'URL au prochain appel."""
        self._api_url = None

    # ── Prompt builder ───────────────────────────────────────────────────────

    def _build_suffix(self, profile) -> str:
        parts = []
        parts.append(LANG_INSTRUCTIONS.get(profile.lang.upper(), f"Always respond in {profile.lang}."))
        for instr_map, key in [
            (STYLE_INSTRUCTIONS,   profile.style),
            (TONE_INSTRUCTIONS,    profile.tone),
            (FORMAT_INSTRUCTIONS,  profile.output_format),
            (EXPERTISE_INSTRUCTIONS, profile.expertise),
        ]:
            v = instr_map.get(key, "")
            if v:
                parts.append(v)
        if not profile.include_examples:
            parts.append("Do not include examples in your response.")
        if profile.safe_mode:
            parts.append("Refuse politely if the request involves harmful, illegal, or unethical content.")
        if profile.max_tokens < 300:
            parts.append(f"Keep your response under {profile.max_tokens} tokens. Be extremely brief.")
        elif profile.max_tokens > 2000:
            parts.append(f"You may be as detailed as needed (up to {profile.max_tokens} tokens).")
        return "\n".join(parts)

    # ── Core request ─────────────────────────────────────────────────────────

    def ask(self, personality: str, prompt: str, profile,
            context: Optional[str] = None, stream: bool = False):
        base = self._resolve_url(profile)
        url = f"{base}/ask/{personality}"
        payload = {
            "prompt": 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 stream:
            return self._stream(url, payload, profile)
        return self._post(url, payload, profile)

    def _post(self, url: str, payload: dict, profile) -> dict:
        try:
            resp = httpx.post(url, json=payload, timeout=profile.timeout)
        except httpx.ConnectError:
            self._api_url = None
            raise CandyUnavailableError("Cannot connect to cedric-8EF API.")
        except httpx.TimeoutException:
            raise CandyTimeoutError(f"Request timed out after {profile.timeout}s.")
        if resp.status_code == 429:
            raise CandyQuotaError("Daily quota exceeded. Try again tomorrow.")
        if resp.status_code == 404:
            raise ValueError(f"Unknown personality in URL: {url}")
        resp.raise_for_status()
        return resp.json()

    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 timed out after {profile.timeout}s.")

    # ── Utility endpoints ────────────────────────────────────────────────────

    def quota(self, profile) -> dict:
        """Retourne le quota journalier restant."""
        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:
        """Liste toutes les personnalités disponibles."""
        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:
        """Mesure la latence de l'API en ms. Retourne -1 si hors ligne."""
        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:
        """Vérifie si l'API est en ligne."""
        try:
            self._resolve_url(profile)
            return True
        except CandyUnavailableError:
            return False


# ── Singleton partagé ────────────────────────────────────────────────────────
_client = CandyClient()
