"""
candy.client — Client HTTP cedric-8EF v5
Nouveautés : retry, verbose, historique de conversation, batch
"""

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

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

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.",
    "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

    # ── URL ─────────────────────────────────────────────────────────────────

    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("cedric-8EF 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

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

    def _build_suffix(self, profile) -> str:
        parts = [LANG_INSTRUCTIONS.get(profile.lang.upper(), f"Always respond in {profile.lang}.")]
        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)

    # ── Core ask ─────────────────────────────────────────────────────────────

    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}"

        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 history:
            payload["history"] = history

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

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

    # ── HTTP ─────────────────────────────────────────────────────────────────

    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(f"Unknown personality.")
                resp.raise_for_status()
                return resp.json()
            except (CandyQuotaError, ValueError):
                raise
            except httpx.TimeoutException as e:
                last_err = CandyTimeoutError(f"Timeout after {profile.timeout}s.")
            except httpx.ConnectError as e:
                self._api_url = None
                last_err = CandyUnavailableError("Cannot connect to cedric-8EF.")
            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.")

    # ── Batch ────────────────────────────────────────────────────────────────

    def batch(self, personality: str, prompts: list, profile,
              context: Optional[str] = None) -> list:
        """
        Envoie plusieurs prompts et retourne une liste de réponses.
        Utile pour traiter plusieurs questions en une fois.
        """
        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

    # ── Utilitaires ──────────────────────────────────────────────────────────

    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()
