from __future__ import annotations

from time import perf_counter
from types import SimpleNamespace
from typing import Any

from google import genai
from google.genai import types
from pydantic import BaseModel, ValidationError

from app.core.logging import get_audit_logger
from app.llm.models import (
    LLMErrorKind,
    LLMProviderAvailability,
    LLMProviderCapabilities,
    LLMProviderError,
    LLMStructuredRequest,
    LLMStructuredResult,
    LLMTextRequest,
    LLMTextResult,
)


def _text_size(value: Any) -> int:
    if value is None:
        return 0
    if isinstance(value, str):
        return len(value)
    if isinstance(value, (list, tuple)):
        return sum(_text_size(item) for item in value)
    return len(str(value))


def _build_generation_config(generation_config: Any) -> Any:
    if generation_config is None:
        return None
    if isinstance(generation_config, types.GenerateContentConfig):
        return generation_config
    if isinstance(generation_config, dict):
        try:
            return types.GenerateContentConfig(**generation_config)
        except TypeError:
            return dict(generation_config)
    return generation_config


class GeminiAdapter:
    def __init__(
        self,
        *,
        api_key: str | None,
        default_model: str,
        service: str = "llm_gemini_adapter",
        client: Any | None = None,
    ) -> None:
        self._api_key = api_key
        self._default_model = default_model
        self._service = service
        self._client = client
        self._audit_logger = get_audit_logger()

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

    def availability(self) -> LLMProviderAvailability:
        configured = bool(str(self._api_key or "").strip() or self._client is not None)
        reason = None if configured else "GEMINI_API_KEY no configurada"
        return LLMProviderAvailability(configured=configured, reason=reason)

    def capabilities(self) -> LLMProviderCapabilities:
        return LLMProviderCapabilities(
            text_generation=True,
            structured_generation=True,
            native_structured_output=True,
        )

    def close(self) -> None:
        self._client = None

    def _client_or_raise(self) -> Any:
        availability = self.availability()
        if not availability.configured:
            raise LLMProviderError(
                availability.reason or "Proveedor Gemini no configurado",
                kind=LLMErrorKind.INVALID_CONFIGURATION,
                provider=self.provider_name(),
                model=self._default_model,
            )
        if self._client is None:
            self._client = genai.Client(api_key=self._api_key)
        return self._client

    def _normalize_contents(self, request: LLMTextRequest) -> Any:
        if request.contents is not None:
            return request.contents
        if request.system_prompt:
            return f"{request.system_prompt}\n\n{request.prompt}"
        return request.prompt

    def _classify_exception(self, exc: Exception, *, model: str) -> LLMProviderError:
        text = str(exc)
        if "503" in text or "UNAVAILABLE" in text:
            return LLMProviderError(
                text or "Gemini temporalmente no disponible",
                kind=LLMErrorKind.TRANSIENT,
                provider=self.provider_name(),
                model=model,
                retryable=True,
            )
        if "429" in text or "RESOURCE_EXHAUSTED" in text:
            return LLMProviderError(
                text or "Gemini alcanzó límite de cuota",
                kind=LLMErrorKind.RATE_LIMITED,
                provider=self.provider_name(),
                model=model,
                retryable=True,
            )
        return LLMProviderError(
            text or "Fallo del proveedor Gemini",
            kind=LLMErrorKind.PROVIDER_UNAVAILABLE,
            provider=self.provider_name(),
            model=model,
        )

    def _invoke_generate_content(self, *, model: str, contents: Any, generation_config: Any = None) -> Any:
        client = self._client_or_raise()
        return client.models.generate_content(
            model=model,
            contents=contents,
            config=_build_generation_config(generation_config),
        )

    def generate_content_raw(self, contents: Any, generation_config: Any = None, *, model: str | None = None) -> Any:
        resolved_model = model or self._default_model
        return self._invoke_generate_content(
            model=resolved_model,
            contents=contents,
            generation_config=generation_config,
        )

    def generate_content_legacy(
        self,
        contents: Any,
        generation_config: Any = None,
        *,
        model: str | None = None,
    ) -> Any:
        response = self.generate_content_raw(contents, generation_config=generation_config, model=model)
        return SimpleNamespace(text=getattr(response, "text", "") or "")

    def generate_text(self, request: LLMTextRequest) -> LLMTextResult:
        model = request.model or self._default_model
        contents = self._normalize_contents(request)
        started_at = perf_counter()
        try:
            response = self._invoke_generate_content(
                model=model,
                contents=contents,
                generation_config=request.generation_config,
            )
            text = getattr(response, "text", "") or ""
            metrics = {
                "duration_ms": round((perf_counter() - started_at) * 1000, 3),
                "input_chars": _text_size(contents),
                "output_chars": _text_size(text),
            }
            self._audit_logger.llm_event(
                action="generate_text",
                outcome="success",
                service=self._service,
                provider=self.provider_name(),
                model=model,
                metrics=metrics,
                resource={"operation": "generate_text"},
            )
            return LLMTextResult(
                content=text,
                provider=self.provider_name(),
                model=model,
                metrics=metrics,
            )
        except LLMProviderError as exc:
            self._audit_logger.llm_event(
                action="generate_text",
                outcome="error",
                service=self._service,
                provider=self.provider_name(),
                model=model,
                metrics={"duration_ms": round((perf_counter() - started_at) * 1000, 3)},
                resource={"operation": "generate_text"},
                error=exc.to_error_payload(),
            )
            raise
        except Exception as exc:
            mapped = self._classify_exception(exc, model=model)
            self._audit_logger.llm_event(
                action="generate_text",
                outcome="error",
                service=self._service,
                provider=self.provider_name(),
                model=model,
                metrics={"duration_ms": round((perf_counter() - started_at) * 1000, 3)},
                resource={"operation": "generate_text"},
                error=mapped.to_error_payload(),
            )
            raise mapped from exc

    def generate_structured(self, request: LLMStructuredRequest) -> LLMStructuredResult:
        model = request.model or self._default_model
        output_model = request.output_model
        if not isinstance(output_model, type) or not issubclass(output_model, BaseModel):
            raise LLMProviderError(
                "Gemini structured output requiere un modelo Pydantic",
                kind=LLMErrorKind.INVALID_CONFIGURATION,
                provider=self.provider_name(),
                model=model,
            )

        started_at = perf_counter()
        contents = request.prompt
        if request.system_prompt:
            contents = f"{request.system_prompt}\n\n{request.prompt}"
        try:
            response = self._invoke_generate_content(
                model=model,
                contents=contents,
                generation_config={
                    "temperature": 0.0,
                    "response_mime_type": "application/json",
                    "response_schema": output_model,
                },
            )
            parsed_model = self._validate_structured_response(
                response=response,
                output_model=output_model,
                model=model,
                original_contents=contents,
            )
            metrics = {
                "duration_ms": round((perf_counter() - started_at) * 1000, 3),
                "input_chars": _text_size(contents),
                "output_chars": _text_size(getattr(response, "text", "") or ""),
            }
            return LLMStructuredResult(
                content=parsed_model.model_dump(by_alias=True, exclude_none=True),
                provider=self.provider_name(),
                model=model,
                output_kind=request.output_kind,
                metrics=metrics,
            )
        except LLMProviderError:
            raise
        except Exception as exc:
            mapped = self._classify_exception(exc, model=model)
            raise mapped from exc

    def _validate_structured_response(
        self,
        *,
        response: Any,
        output_model: type[BaseModel],
        model: str,
        original_contents: Any,
    ) -> BaseModel:
        parsed = getattr(response, "parsed", None)
        if isinstance(parsed, output_model):
            return parsed
        if parsed is not None:
            try:
                return output_model.model_validate(parsed)
            except ValidationError:
                pass

        text = getattr(response, "text", "") or ""
        try:
            return output_model.model_validate_json(self._strip_json_fence(text))
        except ValidationError as exc:
            retry_response = self._invoke_generate_content(
                model=model,
                contents=(
                    "Devuelve únicamente JSON válido que cumpla el esquema solicitado. "
                    "No incluyas comentarios, Markdown ni texto adicional.\n\n"
                    + str(original_contents or "")
                ),
                generation_config={
                    "temperature": 0.0,
                    "response_mime_type": "application/json",
                    "response_schema": output_model,
                },
            )
            retry_parsed = getattr(retry_response, "parsed", None)
            if isinstance(retry_parsed, output_model):
                return retry_parsed
            retry_text = getattr(retry_response, "text", "") or ""
            try:
                return output_model.model_validate_json(self._strip_json_fence(retry_text))
            except ValidationError as retry_exc:
                raise LLMProviderError(
                    "Gemini devolvió un JSON inválido para el esquema solicitado",
                    kind=LLMErrorKind.UNSUPPORTED_CAPABILITY,
                    provider=self.provider_name(),
                    model=model,
                    details={"validation_error": str(retry_exc)},
                ) from exc

    @staticmethod
    def _strip_json_fence(value: str) -> str:
        text = str(value or "").strip()
        if text.startswith("```"):
            text = text.split("\n", 1)[1] if "\n" in text else text[3:]
            if text.endswith("```"):
                text = text[:-3]
        return text.strip()
