"""AI provider abstractions and implementations."""

from __future__ import annotations

import json
from abc import ABC, abstractmethod
from typing import Any

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

from app.core.config import settings

GEMINI_API_BASE = "https://generativelanguage.googleapis.com/v1beta"
OPENAI_API_BASE = "https://api.openai.com/v1"
REQUEST_TIMEOUT_SECONDS = 45
DEFAULT_GEMINI_MODEL = "gemini-2.5-flash"


class AIConfigError(RuntimeError):
    """Raised when AI provider configuration is invalid or missing."""


class AIAnalysisError(RuntimeError):
    """Raised when AI provider request/response fails."""


class AIProvider(ABC):
    """Common provider interface for AI analysis calls."""

    @abstractmethod
    def analyze(self, prompt: str) -> dict[str, Any]:
        """Run model analysis and return parsed JSON payload."""


def _response_error_detail(response: Response) -> str:
    """Extract human-friendly API error detail."""
    if response.status_code in {401, 403}:
        return "Authentication or permission error from provider"

    try:
        payload = response.json()
    except ValueError:
        text = response.text.strip()
        return text or "Provider request failed"

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

    return "Provider request failed"


def _preview(value: Any, max_chars: int = 220) -> str:
    """Return short preview for diagnostics."""
    rendered = repr(value)
    if len(rendered) <= max_chars:
        return rendered
    return f"{rendered[:max_chars]}..."


def _parse_json_text(provider_name: str, raw_text: str) -> dict[str, Any]:
    """Parse strict JSON object from model text output."""
    try:
        payload = json.loads(raw_text)
    except json.JSONDecodeError as exc:
        raise AIAnalysisError(f"{provider_name} returned non-JSON output") from exc

    if not isinstance(payload, dict):
        raise AIAnalysisError(f"{provider_name} returned JSON that is not an object")
    return payload


class GeminiProvider(AIProvider):
    """Google Gemini provider via REST API v1beta generateContent."""

    def __init__(self, api_key: str, model: str):
        self.api_key = api_key
        self.model = str(model or "").strip() or DEFAULT_GEMINI_MODEL

    def analyze(self, prompt: str) -> dict[str, Any]:
        """Analyze prompt with Gemini and return parsed JSON dict."""
        url = f"{GEMINI_API_BASE}/models/{self.model}:generateContent"
        body = {
            "contents": [{"parts": [{"text": prompt}]}],
            "generationConfig": {"temperature": 0.2},
        }

        try:
            response = requests.post(
                url=url,
                params={"key": self.api_key},
                json=body,
                timeout=REQUEST_TIMEOUT_SECONDS,
            )
        except RequestException as exc:
            raise AIAnalysisError(f"Gemini request failed: {exc}") from exc

        if response.status_code != 200:
            detail = _response_error_detail(response)
            raise AIAnalysisError(
                f"Gemini API request failed ({response.status_code}): {detail}",
            )

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

        try:
            text = payload["candidates"][0]["content"]["parts"][0]["text"]
        except (KeyError, IndexError, TypeError) as exc:
            raise AIAnalysisError(
                f"Gemini response structure is unexpected: {_preview(payload)}",
            ) from exc

        if not isinstance(text, str) or not text.strip():
            raise AIAnalysisError("Gemini returned empty analysis response")

        return _parse_json_text("Gemini", text)


class OpenAIProvider(AIProvider):
    """OpenAI provider via chat completions REST API."""

    def __init__(self, api_key: str, model: str):
        self.api_key = api_key
        self.model = model

    def analyze(self, prompt: str) -> dict[str, Any]:
        """Analyze prompt with OpenAI and return parsed JSON dict."""
        body = {
            "model": self.model,
            "messages": [{"role": "user", "content": prompt}],
            "response_format": {"type": "json_object"},
            "temperature": 0.2,
        }
        headers = {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

        try:
            response = requests.post(
                url=f"{OPENAI_API_BASE}/chat/completions",
                headers=headers,
                json=body,
                timeout=REQUEST_TIMEOUT_SECONDS,
            )
        except RequestException as exc:
            raise AIAnalysisError(f"OpenAI request failed: {exc}") from exc

        if response.status_code != 200:
            detail = _response_error_detail(response)
            raise AIAnalysisError(
                f"OpenAI API request failed ({response.status_code}): {detail}",
            )

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

        try:
            text = payload["choices"][0]["message"]["content"]
        except (KeyError, IndexError, TypeError) as exc:
            raise AIAnalysisError(
                f"OpenAI response structure is unexpected: {_preview(payload)}",
            ) from exc

        if not isinstance(text, str) or not text.strip():
            raise AIAnalysisError("OpenAI returned empty analysis response")

        return _parse_json_text("OpenAI", text)


class OllamaProvider(AIProvider):
    """Local Ollama provider via generate endpoint."""

    def __init__(self, base_url: str, model: str):
        self.base_url = base_url.rstrip("/")
        self.model = model

    def analyze(self, prompt: str) -> dict[str, Any]:
        """Analyze prompt with Ollama and return parsed JSON dict."""
        body = {
            "model": self.model,
            "prompt": prompt,
            "stream": False,
            "options": {"temperature": 0.2},
        }

        try:
            response = requests.post(
                url=f"{self.base_url}/api/generate",
                json=body,
                timeout=REQUEST_TIMEOUT_SECONDS,
            )
        except RequestException as exc:
            raise AIAnalysisError(f"Ollama request failed: {exc}") from exc

        if response.status_code != 200:
            detail = _response_error_detail(response)
            raise AIAnalysisError(
                f"Ollama API request failed ({response.status_code}): {detail}",
            )

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

        text = payload.get("response") if isinstance(payload, dict) else None
        if not isinstance(text, str) or not text.strip():
            raise AIAnalysisError(
                f"Ollama response structure is unexpected: {_preview(payload)}",
            )

        return _parse_json_text("Ollama", text)


def get_ai_provider(provider_name: str | None = None) -> AIProvider:
    """Build provider implementation based on configured AI_PROVIDER."""
    selected = (provider_name or settings.ai_provider or "gemini").strip().lower()

    if selected == "gemini":
        if not settings.gemini_api_key:
            raise AIConfigError(
                "GEMINI_API_KEY is not configured (required when AI_PROVIDER=gemini)",
            )
        return GeminiProvider(api_key=settings.gemini_api_key, model=settings.gemini_model)

    if selected == "openai":
        if not settings.openai_api_key:
            raise AIConfigError(
                "OPENAI_API_KEY is not configured (required when AI_PROVIDER=openai)",
            )
        return OpenAIProvider(api_key=settings.openai_api_key, model=settings.openai_model)

    if selected == "ollama":
        if not settings.ollama_base_url:
            raise AIConfigError("OLLAMA_BASE_URL is not configured")
        if not settings.ollama_model:
            raise AIConfigError("OLLAMA_MODEL is not configured")
        return OllamaProvider(base_url=settings.ollama_base_url, model=settings.ollama_model)

    raise AIConfigError(
        "Unsupported AI_PROVIDER. Use one of: gemini, openai, ollama",
    )
