"""Ollama adapter for PULSE Protocol.

Run local AI models (Llama, Mistral, Phi, Gemma, etc.) using the same
PULSE interface as cloud providers. Your data never leaves your machine.

Example:
    >>> adapter = OllamaAdapter(model="llama3.2")
    >>> msg = PulseMessage(
    ...     action="ACT.QUERY.DATA",
    ...     parameters={"query": "Explain quantum computing"}
    ... )
    >>> response = adapter.send(msg)
    >>> print(response.content["parameters"]["result"])

GDPR / Privacy:
    Unlike cloud adapters, all inference runs locally. No data is sent
    to external APIs. Ideal for sensitive data, air-gapped environments,
    and EU GDPR compliance.

Requirements:
    Ollama must be installed and running: https://ollama.ai
    Pull a model first: ollama pull llama3.2
"""

from typing import Any, Dict, List, Optional

import requests as _requests

from pulse.message import PulseMessage
from pulse.adapter import PulseAdapter, AdapterError, AdapterConnectionError


# Map PULSE actions to system prompts
ACTION_PROMPTS = {
    "ACT.ANALYZE.SENTIMENT": (
        "Analyze the sentiment of the following text. "
        "Respond with a JSON object: "
        '{"sentiment": "positive|negative|neutral|mixed", '
        '"confidence": 0.0-1.0, "explanation": "brief reason"}'
    ),
    "ACT.ANALYZE.PATTERN": (
        "Analyze the following data for patterns and trends. "
        "Respond with a JSON object: "
        '{"patterns": ["pattern1", ...], "summary": "brief summary"}'
    ),
    "ACT.CREATE.TEXT": (
        "Generate text based on the following instructions."
    ),
    "ACT.TRANSFORM.TRANSLATE": (
        "Translate the following text. "
        "Output only the translation, nothing else."
    ),
    "ACT.TRANSFORM.SUMMARIZE": (
        "Summarize the following text concisely."
    ),
}

# Default Ollama models by action complexity
# These are just defaults — user can override with parameters={"model": "..."}
DEFAULT_MODELS = {
    "ACT.ANALYZE.SENTIMENT": "llama3.2",
    "ACT.ANALYZE.PATTERN": "llama3.2",
    "ACT.CREATE.TEXT": "llama3.2",
    "ACT.QUERY.DATA": "llama3.2",
    "ACT.TRANSFORM.TRANSLATE": "llama3.2",
    "ACT.TRANSFORM.SUMMARIZE": "llama3.2",
}


class OllamaAdapter(PulseAdapter):
    """PULSE adapter for Ollama — run local AI models with the PULSE interface.

    Connects to a running Ollama instance (default: http://localhost:11434).
    Supports any model installed in Ollama: llama3.2, mistral, phi3, gemma2, etc.

    Key advantage over cloud adapters:
        - No API key required
        - Data never leaves your machine
        - GDPR-compliant by design
        - Works offline / air-gapped

    Supported PULSE actions:
        - ACT.QUERY.DATA — ask a question, get an answer
        - ACT.CREATE.TEXT — generate text from instructions
        - ACT.ANALYZE.SENTIMENT — analyze text sentiment
        - ACT.ANALYZE.PATTERN — find patterns in data
        - ACT.TRANSFORM.TRANSLATE — translate text
        - ACT.TRANSFORM.SUMMARIZE — summarize text

    Example:
        >>> # Switch from cloud to local — one line changes
        >>> # adapter = OpenAIAdapter(api_key="sk-...")
        >>> adapter = OllamaAdapter(model="llama3.2")
        >>> msg = PulseMessage(
        ...     action="ACT.ANALYZE.SENTIMENT",
        ...     parameters={"text": "I love this product!"}
        ... )
        >>> response = adapter.send(msg)
    """

    def __init__(
        self,
        model: str = "llama3.2",
        host: str = "http://localhost:11434",
        timeout: int = 120,
        config: Optional[Dict[str, Any]] = None,
    ) -> None:
        """Initialize OllamaAdapter.

        Args:
            model: Default Ollama model name (e.g. "llama3.2", "mistral", "phi3").
                   Override per-request via parameters={"model": "..."}.
                   Run `ollama list` to see installed models.
            host: Ollama server URL. Default: http://localhost:11434.
                  Use a remote host for shared Ollama deployments.
            timeout: HTTP request timeout in seconds. Local inference can be slow
                     for large models — increase if needed.
            config: Additional configuration dict.
        """
        super().__init__(
            name="ollama",
            base_url=host,
            config=config or {},
        )
        self.default_model = model
        self.timeout = timeout
        self._session: Optional[_requests.Session] = None

    def connect(self) -> None:
        """Connect to Ollama server and verify it is running.

        Raises:
            AdapterConnectionError: If Ollama is not running or unreachable.
        """
        self._session = _requests.Session()
        try:
            r = self._session.get(f"{self.base_url}/api/tags", timeout=5)
            r.raise_for_status()
            self.connected = True
        except _requests.exceptions.ConnectionError as e:
            self._session = None
            raise AdapterConnectionError(
                f"Cannot connect to Ollama at {self.base_url}. "
                "Is Ollama running? Start it with: ollama serve"
            ) from e
        except _requests.exceptions.Timeout as e:
            self._session = None
            raise AdapterConnectionError(
                f"Ollama at {self.base_url} did not respond in 5 seconds."
            ) from e
        except _requests.exceptions.HTTPError as e:
            self._session = None
            raise AdapterConnectionError(
                f"Ollama server returned error: {e}"
            ) from e

    def disconnect(self) -> None:
        """Close the HTTP session."""
        if self._session:
            self._session.close()
        self._session = None
        self.connected = False

    def list_models(self) -> List[str]:
        """Return names of all locally installed Ollama models.

        Returns:
            List of model name strings (e.g. ["llama3.2:latest", "mistral:latest"])

        Raises:
            AdapterConnectionError: If Ollama is not running.

        Example:
            >>> adapter = OllamaAdapter()
            >>> adapter.list_models()
            ['llama3.2:latest', 'mistral:latest', 'phi3:latest']
        """
        if not self._session:
            self.connect()
        try:
            r = self._session.get(f"{self.base_url}/api/tags", timeout=5)
            r.raise_for_status()
            data = r.json()
            return [m["name"] for m in data.get("models", [])]
        except Exception as e:
            raise AdapterConnectionError(f"Failed to list models: {e}") from e

    def to_native(self, message: PulseMessage) -> Dict[str, Any]:
        """Convert PULSE message to Ollama /api/chat request.

        Args:
            message: PULSE message with action and parameters.

        Returns:
            Dictionary ready for POST to /api/chat.
        """
        action = message.content["action"]
        params = message.content.get("parameters", {})

        messages = []

        # Add system prompt for structured actions
        system_prompt = self._build_system_prompt(action, params)
        if system_prompt:
            messages.append({"role": "system", "content": system_prompt})

        # Add user message
        user_content = self._build_user_content(action, params)
        messages.append({"role": "user", "content": user_content})

        # Select model
        model = params.get("model", DEFAULT_MODELS.get(action, self.default_model))

        request = {
            "model": model,
            "messages": messages,
            "stream": False,
        }

        # Pass options if provided
        options = {}
        if "temperature" in params:
            options["temperature"] = params["temperature"]
        if "num_predict" in params:
            options["num_predict"] = params["num_predict"]
        if options:
            request["options"] = options

        return request

    def call_api(self, native_request: Dict[str, Any]) -> Dict[str, Any]:
        """Call Ollama /api/chat endpoint.

        Args:
            native_request: Ollama-formatted request dict from to_native().

        Returns:
            Dictionary with response content and metadata.

        Raises:
            AdapterConnectionError: If Ollama is not running.
            AdapterError: If the model is not found or inference fails.
        """
        if not self._session:
            self.connect()

        model = native_request.get("model", self.default_model)

        try:
            r = self._session.post(
                f"{self.base_url}/api/chat",
                json=native_request,
                timeout=self.timeout,
            )

            if r.status_code == 404:
                raise AdapterError(
                    f"Model '{model}' not found in Ollama. "
                    f"Pull it first: ollama pull {model}"
                )

            r.raise_for_status()
            data = r.json()

            message = data.get("message", {})
            content = message.get("content", "")

            return {
                "content": content,
                "model": data.get("model", model),
                "usage": {
                    "input_tokens": data.get("prompt_eval_count", 0),
                    "output_tokens": data.get("eval_count", 0),
                    "total_tokens": (
                        data.get("prompt_eval_count", 0) + data.get("eval_count", 0)
                    ),
                },
                "done": data.get("done", True),
                "total_duration_ns": data.get("total_duration", 0),
            }

        except _requests.exceptions.ConnectionError as e:
            raise AdapterConnectionError(
                f"Lost connection to Ollama at {self.base_url}: {e}"
            ) from e
        except _requests.exceptions.Timeout:
            raise AdapterError(
                f"Ollama inference timed out after {self.timeout}s. "
                "Try a smaller model or increase timeout."
            )
        except AdapterError:
            raise
        except Exception as e:
            raise AdapterError(f"Ollama API error: {e}") from e

    def from_native(self, native_response: Dict[str, Any]) -> PulseMessage:
        """Convert Ollama response to PULSE message.

        Args:
            native_response: Response dict from call_api().

        Returns:
            PULSE response message with result and metadata.
        """
        return PulseMessage(
            action="ACT.RESPOND",
            parameters={
                "result": native_response["content"],
                "model": native_response["model"],
                "usage": native_response["usage"],
                "done": native_response.get("done", True),
                "total_duration_ns": native_response.get("total_duration_ns", 0),
            },
            validate=False,
        )

    @property
    def supported_actions(self) -> List[str]:
        """Actions this adapter supports."""
        return [
            "ACT.QUERY.DATA",
            "ACT.CREATE.TEXT",
            "ACT.ANALYZE.SENTIMENT",
            "ACT.ANALYZE.PATTERN",
            "ACT.TRANSFORM.TRANSLATE",
            "ACT.TRANSFORM.SUMMARIZE",
        ]

    def _build_user_content(self, action: str, params: Dict[str, Any]) -> str:
        """Build user message content from PULSE parameters."""
        if "query" in params:
            return params["query"]
        if "text" in params:
            text = params["text"]
            if action == "ACT.TRANSFORM.TRANSLATE":
                target_lang = params.get("target_language", "English")
                return f"Translate to {target_lang}:\n\n{text}"
            return text
        if "instructions" in params:
            return params["instructions"]
        if "data" in params:
            return f"Analyze this data:\n\n{params['data']}"
        if "prompt" in params:
            return params["prompt"]
        raise AdapterError(
            f"Missing content parameter. For {action}, provide one of: "
            "'query', 'text', 'instructions', 'data', or 'prompt'."
        )

    def _build_system_prompt(self, action: str, params: Dict[str, Any]) -> Optional[str]:
        """Build system prompt based on PULSE action."""
        if "system_prompt" in params:
            return params["system_prompt"]
        return ACTION_PROMPTS.get(action)

    def __repr__(self) -> str:
        """Return string representation."""
        return (
            f"OllamaAdapter(host='{self.base_url}', model='{self.default_model}', "
            f"connected={self.connected})"
        )
