"""LLM integration manager.

Provides the ``LLMIntegration`` class that manages provider configuration,
analysis hooks, and the high-level analyze/explain API.
"""

from __future__ import annotations

import hashlib
from collections.abc import Callable
from typing import Any

from oscura.api.integrations.llm.types import (
    AnalysisHook,
    LLMClient,
    LLMConfig,
    LLMError,
    LLMProvider,
    LLMResponse,
)


class LLMIntegration:
    """LLM integration manager.

    Provides hooks for LLM-assisted analysis and natural language interfaces.
    """

    def __init__(self, config: LLMConfig | None = None) -> None:
        """Initialize LLM integration.

        Args:
            config: LLM configuration (defaults to privacy mode)
        """
        self.config = config or LLMConfig()
        self._client: LLMClient | None = None
        self._hooks: dict[AnalysisHook, list[Callable[..., Any]]] = {
            AnalysisHook.BEFORE_ANALYSIS: [],
            AnalysisHook.AFTER_ANALYSIS: [],
            AnalysisHook.ON_ERROR: [],
        }

    def configure(
        self, provider: str, model: str, api_key: str | None = None, **kwargs: Any
    ) -> None:
        """Configure LLM provider.

        Args:
            provider: Provider name ('openai', 'anthropic', 'local', 'custom')
            model: Model identifier
            api_key: API key for cloud providers
            **kwargs: Additional configuration options

        Raises:
            LLMError: If provider is unknown
        """
        try:
            provider_enum = LLMProvider(provider.lower())
        except ValueError:
            raise LLMError(f"Unknown provider: {provider}")

        self.config = LLMConfig(
            provider=provider_enum,
            model=model,
            api_key=api_key,
            base_url=kwargs.get("base_url"),
            privacy_mode=kwargs.get("privacy_mode", provider_enum == LLMProvider.LOCAL),
            timeout=kwargs.get("timeout", 30.0),
            max_retries=kwargs.get("max_retries", 3),
            requests_per_minute=kwargs.get("requests_per_minute", 60),
        )

        # Reset client to force reinitialization
        self._client = None

    def _get_client(self) -> LLMClient:
        """Get or create LLM client.

        Returns:
            LLM client instance
        """
        if self._client is None:
            self._client = self._create_client()
        return self._client

    def _create_client(self) -> LLMClient:
        """Create LLM client based on configuration.

        Returns:
            LLM client instance

        Raises:
            LLMError: If client cannot be created
        """
        # Import providers lazily to avoid circular imports
        from oscura.api.integrations.llm.anthropic_provider import AnthropicClient
        from oscura.api.integrations.llm.local_provider import LocalLLMClient
        from oscura.api.integrations.llm.openai_provider import OpenAIClient

        if self.config.provider == LLMProvider.OPENAI:
            return self._create_openai_client(OpenAIClient)
        elif self.config.provider == LLMProvider.ANTHROPIC:
            return self._create_anthropic_client(AnthropicClient)
        elif self.config.provider == LLMProvider.LOCAL:
            return LocalLLMClient(self.config)
        else:
            raise LLMError(f"Provider not implemented: {self.config.provider.value}")

    def _create_openai_client(self, client_cls: type[Any]) -> LLMClient:
        """Create OpenAI client.

        Args:
            client_cls: OpenAIClient class.

        Returns:
            OpenAI client

        Raises:
            LLMError: If OpenAI package not available or configuration invalid
        """
        try:
            import openai  # type: ignore[import-not-found]  # noqa: F401
        except ImportError:
            raise LLMError("OpenAI package not installed. Install with: pip install openai")

        if not self.config.api_key:
            raise LLMError("OpenAI API key required")

        if self.config.privacy_mode:
            raise LLMError("Privacy mode not compatible with OpenAI (cloud provider)")

        return client_cls(self.config)  # type: ignore[no-any-return]

    def _create_anthropic_client(self, client_cls: type[Any]) -> LLMClient:
        """Create Anthropic client.

        Args:
            client_cls: AnthropicClient class.

        Returns:
            Anthropic client

        Raises:
            LLMError: If Anthropic package not available or configuration invalid
        """
        try:
            import anthropic  # type: ignore[import-not-found]  # noqa: F401
        except ImportError:
            raise LLMError("Anthropic package not installed. Install with: pip install anthropic")

        if not self.config.api_key:
            raise LLMError("Anthropic API key required")

        if self.config.privacy_mode:
            raise LLMError("Privacy mode not compatible with Anthropic (cloud provider)")

        return client_cls(self.config)  # type: ignore[no-any-return]

    def register_hook(self, hook: AnalysisHook, callback: Callable[..., Any]) -> None:
        """Register callback for analysis hook.

        Args:
            hook: Hook point
            callback: Callback function
        """
        self._hooks[hook].append(callback)

    def trigger_hook(self, hook: AnalysisHook, *args: Any, **kwargs: Any) -> None:
        """Trigger all callbacks for a hook.

        Args:
            hook: Hook point
            *args: Positional arguments for callbacks
            **kwargs: Keyword arguments for callbacks
        """
        for callback in self._hooks[hook]:
            try:
                callback(*args, **kwargs)
            except Exception as e:
                # Don't let hook errors break analysis
                print(f"Warning: Hook {hook.value} failed: {e}")

    def prepare_context(self, trace: Any) -> dict[str, Any]:
        """Prepare trace metadata for LLM context.

        Args:
            trace: Trace object

        Returns:
            Context dictionary with trace metadata
        """
        context: dict[str, Any] = {
            "type": type(trace).__name__,
        }

        # Extract common metadata
        if hasattr(trace, "metadata"):
            meta = trace.metadata
            context.update(
                {
                    "sample_rate": getattr(meta, "sample_rate", None),
                    "num_samples": getattr(meta, "num_samples", None),
                    "duration": getattr(meta, "duration", None),
                }
            )

        # Data statistics (without sending actual data in privacy mode)
        if hasattr(trace, "data") and not self.config.privacy_mode:
            import numpy as np

            data = trace.data
            context["statistics"] = {
                "mean": float(np.mean(data)),
                "std": float(np.std(data)),
                "min": float(np.min(data)),
                "max": float(np.max(data)),
            }
        elif self.config.privacy_mode:
            # Compute hash of data for change detection without sending data
            if hasattr(trace, "data"):
                data_bytes = trace.data.tobytes()
                context["data_hash"] = hashlib.sha256(data_bytes).hexdigest()[:16]

        return context

    def analyze(self, trace: Any, question: str) -> LLMResponse:
        """Analyze trace with natural language question.

        Args:
            trace: Trace object
            question: Natural language question

        Returns:
            LLM response with answer and suggestions

        Raises:
            LLMError: If analysis fails
        """
        self.trigger_hook(AnalysisHook.BEFORE_ANALYSIS, trace, question)

        try:
            client = self._get_client()
            response = client.analyze(trace, question)
            self.trigger_hook(AnalysisHook.AFTER_ANALYSIS, trace, response)
            return response

        except Exception as e:
            self.trigger_hook(AnalysisHook.ON_ERROR, trace, question, e)
            raise LLMError(f"LLM analysis failed: {e}")

    def explain(self, measurement: Any) -> str:
        """Explain a measurement result.

        Args:
            measurement: Measurement result to explain

        Returns:
            Explanation text
        """
        client = self._get_client()
        return client.explain(measurement)
