"""Gemini AI provider implementation."""

from __future__ import annotations

import json
import logging
import re
from difflib import get_close_matches
from typing import Any

import requests
from requests import Response
from requests.exceptions import RequestException

from app.services.ai.providers.base import (
    AIProvider,
    AIProviderConfigError,
    AIProviderResponseError,
    AIProviderUnavailableError,
)

GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta"
DEFAULT_TIMEOUT_SECONDS = 45
DEFAULT_GEMINI_MODEL = "gemini-2.5-flash"
MODEL_SUGGESTION_LIMIT = 6
LOGGER = logging.getLogger("sentineledge.ai.gemini")


def _response_error_detail(response: Response) -> str:
    """Extract concise API error details."""
    try:
        payload = response.json()
    except ValueError:
        text = response.text.strip()
        return text or "Gemini request failed"

    if isinstance(payload, dict):
        error = payload.get("error")
        if isinstance(error, dict):
            message = error.get("message")
            if isinstance(message, str) and message.strip():
                return message.strip()
        for key in ("message", "detail"):
            value = payload.get(key)
            if isinstance(value, str) and value.strip():
                return value.strip()
    return "Gemini request failed"


def _prompt_with_context(prompt: str, context: dict[str, Any] | None) -> str:
    """Merge context into prompt without forcing a rigid schema."""
    clean_prompt = str(prompt or "").strip()
    if not context:
        return clean_prompt
    context_json = json.dumps(context, ensure_ascii=False, indent=2)
    return f"{clean_prompt}\n\nContext:\n{context_json}"


def _sanitize_error_message(message: str) -> str:
    """Redact credential query parameters from exception text."""
    text = str(message or "")
    if not text:
        return text
    return re.sub(r"([?&]key=)[^&\s]+", r"\1REDACTED", text)


def _normalize_model_id(value: object | None) -> str:
    """Normalize Gemini model id from API payload and user input."""
    text = str(value or "").strip()
    if text.startswith("models/"):
        return text.split("/", 1)[1].strip()
    return text


class GeminiProvider(AIProvider):
    """Remote Gemini provider using generateContent REST API."""

    def __init__(self, api_key: str | None, model: str, timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS):
        self._api_key = (api_key or "").strip()
        self._model = str(model or "").strip() or DEFAULT_GEMINI_MODEL
        self._timeout_seconds = max(float(timeout_seconds), 1.0)

    def provider_name(self) -> str:
        return "gemini"

    def _request(
        self,
        *,
        prompt: str,
        max_output_tokens: int | None = None,
        timeout_seconds: float | None = None,
    ) -> Response:
        if not self._api_key:
            raise AIProviderConfigError("Gemini API key is missing")

        body: dict[str, Any] = {
            "contents": [{"parts": [{"text": prompt}]}],
            "generationConfig": {"temperature": 0.35},
        }
        if max_output_tokens is not None:
            body["generationConfig"]["maxOutputTokens"] = int(max_output_tokens)
        timeout = max(float(timeout_seconds or self._timeout_seconds), 1.0)
        LOGGER.debug(
            "Gemini request path=app.services.ai.providers.gemini_provider.GeminiProvider._request endpoint=%s model=%s api_key_present=%s prompt_len=%s max_tokens=%s timeout=%s",
            f"{GEMINI_API_BASE}/models/{{model}}:generateContent",
            self._model,
            bool(self._api_key),
            len(str(prompt or "")),
            max_output_tokens,
            timeout,
        )

        try:
            return requests.post(
                url=f"{GEMINI_API_BASE}/models/{self._model}:generateContent",
                params={"key": self._api_key},
                json=body,
                timeout=timeout,
            )
        except RequestException as exc:
            safe_error = _sanitize_error_message(str(exc))
            LOGGER.warning(
                "Gemini request failed path=app.services.ai.providers.gemini_provider.GeminiProvider._request error_class=%s error=%s model=%s",
                exc.__class__.__name__,
                safe_error,
                self._model,
            )
            raise AIProviderUnavailableError(f"Gemini request failed: {safe_error}") from exc

    def _list_generate_content_models(self) -> list[str]:
        """Return available model ids that support generateContent."""
        if not self._api_key:
            return []
        try:
            response = requests.get(
                url=f"{GEMINI_API_BASE}/models",
                params={"key": self._api_key},
                timeout=min(self._timeout_seconds, 10.0),
            )
        except RequestException as exc:
            safe_error = _sanitize_error_message(str(exc))
            LOGGER.debug(
                "Gemini ListModels failed path=app.services.ai.providers.gemini_provider.GeminiProvider._list_generate_content_models error_class=%s error=%s",
                exc.__class__.__name__,
                safe_error,
            )
            return []

        if response.status_code != 200:
            LOGGER.debug(
                "Gemini ListModels non_200 path=app.services.ai.providers.gemini_provider.GeminiProvider._list_generate_content_models status=%s detail=%s",
                response.status_code,
                _response_error_detail(response),
            )
            return []

        try:
            payload = response.json()
        except ValueError:
            return []

        if not isinstance(payload, dict):
            return []

        models_raw = payload.get("models")
        if not isinstance(models_raw, list):
            return []

        models: list[str] = []
        seen: set[str] = set()
        for item in models_raw:
            if not isinstance(item, dict):
                continue
            model_id = _normalize_model_id(item.get("name"))
            if not model_id or model_id in seen:
                continue
            methods = item.get("supportedGenerationMethods")
            if isinstance(methods, list) and "generateContent" not in {str(method).strip() for method in methods}:
                continue
            seen.add(model_id)
            models.append(model_id)
        return models

    def _invalid_model_detail(self) -> str:
        """Build actionable invalid-model message with ListModels suggestions."""
        requested_model = _normalize_model_id(self._model)
        available = self._list_generate_content_models()
        if not available:
            return (
                f"Model '{requested_model}' is not available for generateContent. "
                "Change Gemini model in Settings (for example: gemini-2.5-flash)."
            )

        if requested_model in available:
            return (
                f"Model '{requested_model}' was rejected by Gemini API. "
                "Verify model availability and API version in Settings."
            )

        suggestions = get_close_matches(requested_model, available, n=MODEL_SUGGESTION_LIMIT, cutoff=0.3)
        if not suggestions:
            prefix = requested_model.split("-", 2)[:2]
            prefix_hint = "-".join(prefix)
            suggestions = [model for model in available if model.startswith(prefix_hint)]
        if not suggestions:
            suggestions = available[:MODEL_SUGGESTION_LIMIT]

        suggestion_text = ", ".join(suggestions[:MODEL_SUGGESTION_LIMIT])
        base = f"Model '{requested_model}' is not available for generateContent."
        if suggestion_text:
            return f"{base} Available models: {suggestion_text}. Change model in Settings."
        return f"{base} Change model in Settings."

    def generate(self, prompt: str, context: dict[str, Any] | None = None) -> str:
        LOGGER.debug(
            "Gemini generate path=app.services.ai.providers.gemini_provider.GeminiProvider.generate model=%s api_key_present=%s",
            self._model,
            bool(self._api_key),
        )
        response = self._request(prompt=_prompt_with_context(prompt, context))
        if response.status_code in {401, 403}:
            LOGGER.warning(
                "Gemini auth error path=app.services.ai.providers.gemini_provider.GeminiProvider.generate status=%s error_class=%s detail=%s model=%s",
                response.status_code,
                AIProviderConfigError.__name__,
                _response_error_detail(response),
                self._model,
            )
            raise AIProviderConfigError(f"Gemini authentication failed: {_response_error_detail(response)}")
        if response.status_code != 200:
            detail = _response_error_detail(response)
            if response.status_code == 404:
                detail = self._invalid_model_detail()
            LOGGER.warning(
                "Gemini response error path=app.services.ai.providers.gemini_provider.GeminiProvider.generate status=%s error_class=%s detail=%s model=%s",
                response.status_code,
                AIProviderResponseError.__name__,
                detail,
                self._model,
            )
            raise AIProviderResponseError(
                f"Gemini API request failed ({response.status_code}): {detail}",
            )

        try:
            payload = response.json()
        except ValueError as exc:
            raise AIProviderResponseError("Gemini returned invalid JSON body") from exc

        try:
            text = payload["candidates"][0]["content"]["parts"][0]["text"]
        except (KeyError, IndexError, TypeError) as exc:
            raise AIProviderResponseError("Gemini returned an unexpected response structure") from exc

        if not isinstance(text, str) or not text.strip():
            raise AIProviderResponseError("Gemini returned an empty response")
        return text.strip()

    def health_check(self) -> bool:
        if not self._api_key:
            return False
        try:
            response = self._request(
                prompt="Reply exactly: ok",
                max_output_tokens=2,
                timeout_seconds=min(self._timeout_seconds, 8.0),
            )
        except (AIProviderConfigError, AIProviderUnavailableError) as exc:
            LOGGER.debug(
                "Gemini health check failed path=app.services.ai.providers.gemini_provider.GeminiProvider.health_check error_class=%s error=%s model=%s",
                exc.__class__.__name__,
                _sanitize_error_message(str(exc)),
                self._model,
            )
            return False
        return response.status_code == 200
