import logging
from abc import ABC, abstractmethod
from typing import AsyncGenerator

import httpx

from soagila.config import get_settings

logger = logging.getLogger("soagila.llm.connector")


class BaseLLMConnector(ABC):
    """Abstract LLM connector interface."""

    @abstractmethod
    async def generate(self, prompt: str, system_prompt: str, model: str | None = None) -> str:
        ...

    @abstractmethod
    async def stream_generate(self, prompt: str, system_prompt: str, model: str | None = None) -> AsyncGenerator[str, None]:
        ...

    @abstractmethod
    async def health_check(self) -> bool:
        ...

    @abstractmethod
    async def list_models(self) -> list[str]:
        ...


class OllamaConnector(BaseLLMConnector):
    """Connector for Ollama (local LLM inference)."""

    def __init__(self, base_url: str | None = None, default_model: str = "llama3.1:8b"):
        settings = get_settings()
        self.base_url = base_url or settings.ollama_url
        self.default_model = default_model
        self._client = httpx.AsyncClient(base_url=self.base_url, timeout=120.0)
        logger.info(f"Ollama connector initialized: {self.base_url} (model={default_model})")

    async def generate(self, prompt: str, system_prompt: str, model: str | None = None) -> str:
        """Generate a complete response from Ollama."""
        model = model or self.default_model

        try:
            response = await self._client.post(
                "/api/generate",
                json={
                    "model": model,
                    "prompt": prompt,
                    "system": system_prompt,
                    "stream": False,
                    "options": {
                        "temperature": 0.7,
                        "top_p": 0.9,
                        "num_predict": 256,
                    },
                },
            )
            response.raise_for_status()
            data = response.json()
            return data.get("response", "")

        except httpx.ConnectError:
            logger.warning("Ollama not available, returning fallback response")
            return self._fallback_response(prompt)
        except Exception as e:
            logger.error(f"Ollama generation error: {e}")
            return self._fallback_response(prompt)

    async def stream_generate(self, prompt: str, system_prompt: str, model: str | None = None) -> AsyncGenerator[str, None]:
        """Stream response tokens from Ollama."""
        model = model or self.default_model

        try:
            async with self._client.stream(
                "POST",
                "/api/generate",
                json={
                    "model": model,
                    "prompt": prompt,
                    "system": system_prompt,
                    "stream": True,
                    "options": {
                        "temperature": 0.7,
                        "top_p": 0.9,
                        "num_predict": 256,
                    },
                },
            ) as response:
                import json
                async for line in response.aiter_lines():
                    if line:
                        data = json.loads(line)
                        token = data.get("response", "")
                        if token:
                            yield token
                        if data.get("done", False):
                            break

        except httpx.ConnectError:
            logger.warning("Ollama not available for streaming, yielding fallback")
            yield self._fallback_response(prompt)
        except Exception as e:
            logger.error(f"Ollama streaming error: {e}")
            yield self._fallback_response(prompt)

    async def health_check(self) -> bool:
        """Check if Ollama is running and responsive."""
        try:
            response = await self._client.get("/api/tags")
            return response.status_code == 200
        except Exception:
            return False

    async def list_models(self) -> list[str]:
        """List available models in Ollama."""
        try:
            response = await self._client.get("/api/tags")
            response.raise_for_status()
            data = response.json()
            return [m["name"] for m in data.get("models", [])]
        except Exception as e:
            logger.error(f"Failed to list Ollama models: {e}")
            return []

    def _fallback_response(self, prompt: str) -> str:
        """Fallback response when Ollama is not available."""
        return (
            "I understand your question. While the LLM service is currently "
            "initializing, I can confirm that your message has been processed "
            "through SOAGILA's cognitive memory system. All relevant memories "
            "have been queried and stored. Please try again in a moment for "
            "a full AI-powered response."
        )


class MockConnector(BaseLLMConnector):
    """Mock connector for testing without any LLM backend."""

    async def generate(self, prompt: str, system_prompt: str, model: str | None = None) -> str:
        # Extract key info from prompt to generate contextual mock response
        if "memory" in prompt.lower() or "remember" in prompt.lower():
            return (
                "Based on SOAGILA's cognitive memory system, I can see your previous "
                "interactions and relevant context. Your memories have been retrieved "
                "from all three tiers — working, episodic, and semantic — to provide "
                "you with the most comprehensive response."
            )
        return (
            "Thank you for your message. I've processed it through SOAGILA's "
            "memory engine, and I'm ready to assist you. How can I help further?"
        )

    async def stream_generate(self, prompt: str, system_prompt: str, model: str | None = None) -> AsyncGenerator[str, None]:
        response = await self.generate(prompt, system_prompt, model)
        # Simulate streaming by yielding word by word
        for word in response.split():
            yield word + " "

    async def health_check(self) -> bool:
        return True

    async def list_models(self) -> list[str]:
        return ["mock-model"]


# =============================================
# FACTORY
# =============================================

_connector: BaseLLMConnector | None = None


def get_llm_connector() -> BaseLLMConnector:
    """Get the LLM connector singleton based on configuration."""
    global _connector
    if _connector is None:
        settings = get_settings()
        # Try Ollama first, fall back to mock
        _connector = OllamaConnector(
            base_url=settings.ollama_url,
            default_model="llama3.2:latest",
        )
        logger.info("LLM connector: OllamaConnector (with fallback)")
    return _connector
