"""
DeepSeek Provider - Cloud LLM translation via DeepSeek API.

DeepSeek uses an OpenAI-compatible Chat Completions API.
"""

import threading
import time
from datetime import datetime, timezone
from typing import Any, Dict, List, Optional

import requests
from requests.exceptions import Timeout, ConnectionError as RequestsConnectionError

from core.logging import get_logger
from .base import TranslationProvider
from .schemas import ProviderHealthStatus, TranslationRequest, TranslationResponse

logger = get_logger(__name__)

DEEPSEEK_RATE_LIMITED = "DEEPSEEK_RATE_LIMITED"
DEEPSEEK_INVALID_KEY = "DEEPSEEK_INVALID_KEY"
DEEPSEEK_TIMEOUT = "DEEPSEEK_TIMEOUT"
DEEPSEEK_SERVICE_ERROR = "DEEPSEEK_SERVICE_ERROR"

_RETRYABLE_ERRORS = {DEEPSEEK_RATE_LIMITED, DEEPSEEK_TIMEOUT, DEEPSEEK_SERVICE_ERROR}

DEFAULT_TRANSLATION_PROMPT = """You are a professional translator. Translate the following text from {source_lang} to {target_lang}.

Rules:
- Translate ONLY the text, do not add explanations or notes
- Preserve the original formatting, line breaks, and structure
- Maintain the original tone and style
- Translate technical terms, jargon, and labels using the standard target-language equivalent
- Chart elements (titles, axis labels, legend entries, category labels, series names) MUST always be translated, even when they look like a title or a proper noun
- Translate month and weekday abbreviations (Jan → janvier, Mon → lundi) to the target language
- Keep ONLY real proper nouns unchanged: people's names, place names, company names (Google, Microsoft), and product names (GitHub, HuggingFace)
- Do not invent content; if a term is an acronym with no translation (API, URL, HTTP, JSON), keep it as-is"""


def _get_language_name(code: str) -> str:
    """Convert language code to full name (all supported languages)."""
    from core.languages import language_name

    return language_name(code)


def _build_system_prompt(
    source_lang: str, target_lang: str, custom_prompt: Optional[str] = None
) -> str:
    """Build system prompt for translation.

    The base translation instructions are ALWAYS present — a custom prompt
    (glossary, tone, context) is appended as additional directives, never a
    replacement. Same contract as the OpenAI provider.
    """
    base = DEFAULT_TRANSLATION_PROMPT.format(
        source_lang=source_lang, target_lang=target_lang
    )
    if custom_prompt and custom_prompt.strip():
        return f"{base}\n\nADDITIONAL CONTEXT AND INSTRUCTIONS:\n{custom_prompt.strip()}"
    return base


class DeepSeekProviderError(Exception):
    def __init__(self, code: str, message: str, details: Optional[Dict[str, Any]] = None):
        self.code = code
        self.message = message
        self.details = details or {}
        super().__init__(message)


class DeepSeekTranslationProvider(TranslationProvider):
    """
    DeepSeek translation provider using OpenAI-compatible API.

    Supports DeepSeek Chat and DeepSeek Reasoner models.
    """

    def __init__(
        self,
        api_key: str,
        model: str = "deepseek-chat",
        timeout: int = 60,
        max_retries: int = 3,
        retry_delay: float = 1.0,
        base_url: str = "https://api.deepseek.com/v1",
    ):
        if not api_key or not api_key.strip():
            raise ValueError("DeepSeek API key cannot be empty")

        self._api_key = api_key
        self._model = model
        self._base_url = base_url.rstrip("/")
        self._provider_name = "deepseek"
        self._timeout = timeout
        self._max_retries = max_retries
        self._retry_delay = retry_delay
        self._health_cache: Dict[str, Any] = {}
        self._health_cache_ttl = 60
        self._health_cache_lock = threading.Lock()

    def _make_api_request(self, text: str, system_prompt: str) -> tuple:
        if not text or not text.strip():
            return text, {}

        url = f"{self._base_url}/chat/completions"
        headers = {
            "Authorization": f"Bearer {self._api_key}",
            "Content-Type": "application/json",
        }
        payload = {
            "model": self._model,
            "messages": [
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": text},
            ],
            "temperature": 0.3,
            "max_tokens": 4096,
        }

        try:
            response = requests.post(url, headers=headers, json=payload, timeout=self._timeout)

            if response.status_code == 401:
                raise DeepSeekProviderError(DEEPSEEK_INVALID_KEY, "Cle API DeepSeek invalide.")
            if response.status_code == 429:
                raise DeepSeekProviderError(DEEPSEEK_RATE_LIMITED, "Limite de requetes DeepSeek atteinte.")
            if response.status_code >= 500:
                raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Service DeepSeek temporairement indisponible.")
            if response.status_code != 200:
                raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, f"Erreur DeepSeek: {response.text[:200]}")

            data = response.json()
            choices = data.get("choices", [])
            if not choices:
                raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Reponse DeepSeek vide")

            content = choices[0].get("message", {}).get("content", "")
            if not content:
                raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Reponse DeepSeek vide")

            return content.strip(), data.get("usage", {})

        except Timeout:
            raise DeepSeekProviderError(DEEPSEEK_TIMEOUT, "Delai d'attente DeepSeek depasse.")
        except RequestsConnectionError:
            raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, "Service DeepSeek indisponible.")
        except DeepSeekProviderError:
            raise
        except Exception as e:
            raise DeepSeekProviderError(DEEPSEEK_SERVICE_ERROR, f"Erreur DeepSeek: {str(e)[:100]}")

    def get_name(self) -> str:
        return self._provider_name

    def is_available(self) -> bool:
        try:
            headers = {"Authorization": f"Bearer {self._api_key}"}
            response = requests.get(f"{self._base_url}/models", headers=headers, timeout=5)
            return response.status_code == 200
        except Exception:
            return False

    def translate_text(self, request: TranslationRequest) -> TranslationResponse:
        text = request.text
        target_language = request.target_language
        source_language = request.source_language or "auto"

        if not text or not text.strip():
            return TranslationResponse(translated_text=text, provider_name=self._provider_name, from_cache=False)

        source_lang_name = _get_language_name(source_language)
        target_lang_name = _get_language_name(target_language)
        custom_prompt = request.metadata.get("custom_prompt") if request.metadata else None
        # Base translation instructions always present; the custom prompt
        # (glossary/tone/context) is appended, never a replacement.
        system_prompt = _build_system_prompt(
            source_lang_name, target_lang_name, custom_prompt
        )

        last_error = None
        for attempt in range(self._max_retries + 1):
            try:
                start_time = time.time()
                result, usage = self._make_api_request(text, system_prompt)
                latency = time.time() - start_time

                logger.info("deepseek_translation_success",
                    chars=len(text), source_lang=source_language, target_lang=target_language,
                    model=self._model, latency_ms=round(latency * 1000, 2), retries=attempt)

                return TranslationResponse(
                    translated_text=result, provider_name=self._provider_name,
                    from_cache=False, source_language=source_language)

            except DeepSeekProviderError as e:
                last_error = e
                if e.code not in _RETRYABLE_ERRORS or attempt >= self._max_retries:
                    break
                delay = self._retry_delay * (2 ** attempt)
                time.sleep(delay)

        return TranslationResponse(
            translated_text=text, provider_name=self._provider_name, from_cache=False,
            error=last_error.message if last_error else "Unknown error",
            error_code=last_error.code if last_error else DEEPSEEK_SERVICE_ERROR)

    def _make_batch_api_request(
        self, texts: List[str], system_prompt: str
    ) -> Optional[List[str]]:
        """Translate a whole chunk in ONE request via a numbered JSON list.

        The user message is a JSON array; the model must answer with a JSON
        array of the same length. Returns None when the answer cannot be
        parsed confidently — callers then fall back to per-item calls
        (correctness over latency). Mirrors the OpenAI provider batch mode.
        """
        import json as _json

        numbered = _json.dumps(
            [{"id": i, "text": t} for i, t in enumerate(texts)],
            ensure_ascii=False,
        )
        batch_system = (
            system_prompt
            + "\n\nBATCH MODE: the user message is a JSON array of items with "
            "unique ids. Answer with ONLY a JSON array of objects "
            '[{"id": <same id>, "translation": "<translated text>"}], same '
            "length and same ids, in the same order. Translate every item; "
            "keep ids unchanged; no comments, no markdown fence."
        )

        try:
            content, _usage = self._make_api_request(numbered, batch_system)
            raw = content.strip()
            # Strip an optional markdown fence
            if raw.startswith("```"):
                raw = raw.strip("`")
                if raw.lower().startswith("json"):
                    raw = raw[4:]
                raw = raw.strip()
            parsed = _json.loads(raw)
            if not isinstance(parsed, list) or len(parsed) != len(texts):
                return None
            out: List[str] = [""] * len(texts)
            for item in parsed:
                if not isinstance(item, dict):
                    return None
                idx = item.get("id")
                translation = item.get("translation")
                if not isinstance(idx, int) or not 0 <= idx < len(texts):
                    return None
                if not isinstance(translation, str) or not translation.strip():
                    return None
                out[idx] = translation.strip()
            return out
        except DeepSeekProviderError:
            raise
        except Exception:
            return None

    def translate_batch(
        self, requests: List[TranslationRequest]
    ) -> List[TranslationResponse]:
        """
        Translate multiple texts.

        Chunks arrive from the translators as ~15 texts. When every request
        shares the same language pair and metadata, they are sent in ONE
        call (numbered JSON list — ~15x fewer requests, better contextual
        consistency across neighbouring segments). Any parse/API doubt falls
        back to the per-item path so a batch failure never corrupts output.
        """
        if not requests:
            return []

        same_pair = len({(r.source_language, r.target_language) for r in requests}) == 1
        same_meta = len(
            {tuple(sorted((r.metadata or {}).items())) for r in requests}
        ) == 1

        if same_pair and same_meta and len(requests) > 1:
            try:
                source_lang_name = _get_language_name(
                    requests[0].source_language or "auto"
                ) or "the source language (auto-detect)"
                target_lang_name = _get_language_name(requests[0].target_language)
                custom_prompt = None
                if requests[0].metadata:
                    custom_prompt = requests[0].metadata.get("custom_prompt")
                system_prompt = _build_system_prompt(
                    source_lang_name, target_lang_name, custom_prompt
                )
                texts = [r.text for r in requests]
                translations = self._make_batch_api_request(texts, system_prompt)
                if translations is not None:
                    logger.info(
                        "deepseek_batch_translation_success",
                        items=len(requests),
                        model=self._model,
                    )
                    return [
                        TranslationResponse(
                            translated_text=t,
                            provider_name=self._provider_name,
                            from_cache=False,
                        )
                        for t in translations
                    ]
                logger.warning(
                    "deepseek_batch_translation_fallback",
                    reason="unparseable_response",
                    items=len(requests),
                )
            except Exception as e:
                logger.warning(
                    "deepseek_batch_translation_fallback",
                    reason=type(e).__name__,
                    items=len(requests),
                )

        return [self.translate_text(req) for req in requests]

    def health_check(self) -> ProviderHealthStatus:
        start_time = time.time()
        try:
            headers = {"Authorization": f"Bearer {self._api_key}"}
            response = requests.get(f"{self._base_url}/models", headers=headers, timeout=5)
            latency_ms = (time.time() - start_time) * 1000
            return ProviderHealthStatus(
                name=self._provider_name, available=response.status_code == 200,
                latency_ms=round(latency_ms, 2), last_check=datetime.now(timezone.utc).isoformat(),
                model=self._model)
        except Exception as e:
            return ProviderHealthStatus(
                name=self._provider_name, available=False,
                latency_ms=round((time.time() - start_time) * 1000, 2),
                error=str(e)[:100], last_check=datetime.now(timezone.utc).isoformat(),
                model=self._model)


_provider_instance: Optional[DeepSeekTranslationProvider] = None
_provider_lock = threading.Lock()


def get_deepseek_provider() -> DeepSeekTranslationProvider:
    global _provider_instance
    if _provider_instance is None:
        with _provider_lock:
            if _provider_instance is None:
                from .config import ProvidersConfig
                _provider_instance = DeepSeekTranslationProvider(
                    api_key=ProvidersConfig.DEEPSEEK_API_KEY,
                    model=ProvidersConfig.DEEPSEEK_MODEL,
                    timeout=ProvidersConfig.DEEPSEEK_TIMEOUT,
                    max_retries=ProvidersConfig.DEEPSEEK_MAX_RETRIES,
                    retry_delay=ProvidersConfig.DEEPSEEK_RETRY_DELAY,
                    base_url=ProvidersConfig.DEEPSEEK_BASE_URL,
                )
    return _provider_instance


def register_deepseek_provider():
    from .registry import registry
    provider = get_deepseek_provider()
    registry.register("deepseek", provider)
    return provider


def reset_deepseek_provider():
    global _provider_instance
    with _provider_lock:
        _provider_instance = None
