# Familiar - Self-Hosted AI Companion Platform
# Copyright (c) 2026 George Scott Foley
# ORCID: 0009-0006-4957-0540
# Email: Georgescottfoley@proton.me
# Licensed under the MIT License - see LICENSE file for details

"""
ProviderManager — extracted from agent.py (PR 2).

Handles LLM provider lifecycle:
- Provider initialization with fallback cascade
- Model router setup for multi-LLM orchestration
- Provider selection per request (auto/manual routing, PHI guard)
- Provider fallback on failure
- Routing configuration (get/set)
"""

import logging
import os

from .providers import (
    LLMProvider,
    get_best_ollama_model,
    get_provider,
)
from .resilience import ProviderCapacityTracker

try:
    from .providers import OllamaProvider as _OllamaProvider
except ImportError:
    _OllamaProvider = None

logger = logging.getLogger(__name__)


class ProviderManager:
    """Manages LLM provider initialization, routing, selection, and fallback."""

    def __init__(self, agent_ref):
        self._agent = agent_ref
        self.model_router = None
        self._routing_mode = "auto"
        self._manual_routes = {}  # maps QueryType value → model name
        self.capacity_tracker = ProviderCapacityTracker()

    def init_provider(self) -> LLMProvider:
        """Initialize the LLM provider.

        Checks both API key presence AND package availability
        before selecting a provider. Falls through gracefully:
        anthropic → openai → ollama (native, zero deps).
        """
        config = self._agent.config
        provider_name = config.llm.default_provider

        has_anthropic_key = bool(os.environ.get("ANTHROPIC_API_KEY"))
        has_openai_key = bool(os.environ.get("OPENAI_API_KEY"))

        try:
            import anthropic  # noqa: F401

            has_anthropic_pkg = True
        except ImportError:
            has_anthropic_pkg = False

        try:
            import openai  # noqa: F401

            has_openai_pkg = True
        except ImportError:
            has_openai_pkg = False

        has_anthropic = has_anthropic_key and has_anthropic_pkg
        has_openai = has_openai_key and has_openai_pkg

        if has_anthropic_key and not has_anthropic_pkg:
            logger.warning("ANTHROPIC_API_KEY is set but 'anthropic' package not installed.")
        if has_openai_key and not has_openai_pkg:
            logger.warning("OPENAI_API_KEY is set but 'openai' package not installed.")

        # Fallback logic
        if provider_name in ["anthropic", "claude"] and not has_anthropic:
            if has_openai:
                provider_name = "openai"
                logger.warning("Anthropic unavailable, falling back to OpenAI")
            else:
                provider_name = "ollama"
                logger.warning("No cloud providers available, falling back to Ollama (local)")

        if provider_name in ["openai", "gpt"] and not has_openai:
            if has_anthropic:
                provider_name = "anthropic"
                logger.warning("OpenAI unavailable, falling back to Anthropic")
            else:
                provider_name = "ollama"
                logger.warning("No cloud providers available, falling back to Ollama (local)")

        return get_provider(provider_name, config)

    def init_model_router(self):
        """Initialize multi-LLM model router if multiple providers are available."""
        config = self._agent.config
        try:
            from .model_router import ModelRouter, RoutingStrategy
        except ImportError:
            logger.debug("Model router module not available")
            return

        has_anthropic = bool(os.environ.get("ANTHROPIC_API_KEY"))
        has_openai = bool(os.environ.get("OPENAI_API_KEY"))
        has_gemini = bool(os.environ.get("GEMINI_API_KEY") or os.environ.get("GOOGLE_API_KEY"))
        has_ollama = False
        ollama_models = []

        try:
            import urllib.request

            req = urllib.request.Request(
                f"{config.llm.ollama_base_url}/api/tags",
                method="GET",
            )
            with urllib.request.urlopen(req, timeout=2) as resp:
                import json

                data = json.loads(resp.read())
                ollama_models = [m["name"] for m in data.get("models", [])]
                has_ollama = len(ollama_models) > 0
        except Exception:
            pass

        available_count = sum([has_anthropic, has_openai, has_gemini, has_ollama])
        if available_count < 2:
            logger.debug(f"Model router: {available_count} provider(s), skipping (need 2+)")
            return

        router = ModelRouter(
            default_model=self._agent.provider.name,
            default_provider=config.llm.default_provider,
            strategy=RoutingStrategy.FIRST_MATCH,
        )

        # Private data → always route to local Ollama
        if has_ollama:
            ollama_model = config.llm.ollama_model
            router.add_model(
                name=f"ollama/{ollama_model}",
                provider="ollama",
                predicate=lambda q: q.is_private,
                priority=5,
            )

        # Complex reasoning or tool use → best cloud model
        if has_anthropic:
            router.add_model(
                name="claude-sonnet",
                provider="anthropic",
                predicate=lambda q: q.complexity > 0.5 or q.requires_reasoning or q.requires_tools,
                priority=10,
            )
        elif has_openai:
            router.add_model(
                name="gpt-4o",
                provider="openai",
                predicate=lambda q: q.complexity > 0.5 or q.requires_reasoning or q.requires_tools,
                priority=10,
            )
        elif has_gemini:
            router.add_model(
                name="gemini-2.5-flash",
                provider="gemini",
                predicate=lambda q: q.complexity > 0.5 or q.requires_reasoning or q.requires_tools,
                priority=10,
            )

        # Simple/fast queries → lightweight
        if has_anthropic:
            router.add_model(
                name="claude-haiku",
                provider="anthropic",
                predicate=lambda q: q.is_simple,
                priority=30,
            )
        elif has_openai:
            router.add_model(
                name="gpt-4o-mini",
                provider="openai",
                predicate=lambda q: q.is_simple,
                priority=30,
            )
        elif has_gemini:
            router.add_model(
                name="gemini-2.5-flash",
                provider="gemini",
                predicate=lambda q: q.is_simple,
                priority=30,
            )

        # Register remaining provider models
        extra_models = {}
        if has_anthropic:
            extra_models.update(
                {
                    "claude-sonnet": "anthropic",
                    "claude-haiku": "anthropic",
                }
            )
        if has_openai:
            extra_models.update(
                {
                    "gpt-4o": "openai",
                    "gpt-4o-mini": "openai",
                }
            )
        if has_gemini:
            extra_models.update(
                {
                    "gemini-2.5-flash": "gemini",
                }
            )
        if has_ollama:
            for om in ollama_models:
                extra_models[f"ollama/{om}"] = "ollama"
        for name, prov in extra_models.items():
            if name not in router._models:
                router.add_model(name=name, provider=prov)

        # Build fallback chain
        fallbacks = []
        if has_anthropic:
            fallbacks.append("claude-sonnet")
        if has_openai:
            fallbacks.append("gpt-4o")
        if has_gemini:
            fallbacks.append("gemini-2.5-flash")
        if has_ollama:
            fallbacks.append(f"ollama/{config.llm.ollama_model}")
        router.set_fallback_chain(fallbacks)

        self.model_router = router
        logger.info(
            f"Model router active: {available_count} providers, "
            f"{len(router._models)} models configured, "
            f"fallback chain: {' → '.join(fallbacks)}"
        )

    def select_provider(self, message: str, session):
        """
        Route message to the best available provider.

        Returns (active_provider, model_name).
        """
        agent = self._agent
        active_provider = agent.provider
        model_name = agent.config.llm.default_provider
        manual_routed = False

        # Manual routing
        if self.model_router and self._routing_mode == "manual" and self._manual_routes:
            try:
                analyzer = self.model_router.analyzer
                qc = analyzer.analyze(message)
                route_model = self._manual_routes.get(qc.query_type.value)
                if route_model and route_model in self.model_router._models:
                    cfg = self.model_router._models[route_model]
                    routed = get_provider(cfg.provider, agent.config)
                    active_provider = routed
                    model_name = route_model
                    manual_routed = True
                    logger.info(
                        f"Manual route: {qc.query_type.value} -> {route_model} ({cfg.provider})"
                    )
            except Exception as e:
                logger.debug(f"Manual routing fallback to auto: {e}")

        # Auto routing (skipped if manual routing already succeeded)
        if self.model_router and not manual_routed:
            try:
                model_name, provider_name = self.model_router.route(message)
                routed = get_provider(provider_name, agent.config)
                active_provider = routed
                if provider_name != agent.config.llm.default_provider:
                    logger.info(f"Routed to {model_name} ({provider_name})")
            except Exception as e:
                logger.debug(f"Router fallback to default: {e}")

        # PHI guard — hard constraint: PHI never leaves device in HIPAA mode
        if agent.compliance_mode and hasattr(active_provider, "name"):
            try:
                from .compliance import check_phi_content

                if check_phi_content(message, agent.compliance_mode):
                    is_cloud = any(
                        p in active_provider.name.lower() for p in ("anthropic", "openai", "gemini")
                    )
                    if is_cloud:
                        try:
                            local_provider = get_provider("ollama", agent.config)
                            logger.warning(
                                f"PHI detected — rerouted from {model_name} to "
                                f"local Ollama ({agent.config.llm.ollama_model})"
                            )
                            return local_provider, model_name
                        except Exception:
                            logger.error("PHI detected but no local provider available")
                            raise RuntimeError("PHI_NO_LOCAL_FALLBACK")
            except ImportError:
                pass

        return active_provider, model_name

    def provider_fallback(self, active_provider, primary_error: Exception):
        """
        Attempt a provider fallback after a failure.

        Returns the fallback provider or None if no fallback is available.
        """
        agent = self._agent

        # Record circuit breaker failure
        if self.model_router and hasattr(active_provider, "name"):
            try:
                for name, cb in self.model_router._circuit_breakers.items():
                    if active_provider.name.lower() in name.lower():
                        cb.record_failure()
                        logger.info(f"Circuit breaker recorded failure for {name}")
            except Exception as e:
                logger.debug(f"Circuit breaker update failed: {e}")

        # Record rate limit events for capacity tracking
        err_type = type(primary_error).__name__
        err_str = str(primary_error).lower()
        if "ratelimit" in err_type.lower() or "rate limit" in err_str or "429" in err_str:
            provider_key = active_provider.name.split("(")[0].strip().lower()
            self.capacity_tracker.record_rate_limit(provider_key)
            logger.info(f"Capacity tracker: recorded rate limit for {provider_key}")

        fallback = None
        provider_name = active_provider.name.lower()

        fallback_order = ["ollama", "anthropic", "openai", "gemini"]
        for fb_name in fallback_order:
            if fb_name == provider_name or fb_name in provider_name:
                continue
            if self.capacity_tracker.is_rate_limited(fb_name):
                logger.info(f"Skipping {fb_name} fallback — rate limited (capacity tracker)")
                continue
            try:
                if fb_name == "ollama":
                    fallback = get_best_ollama_model(agent.config)
                    if not fallback:
                        fallback = get_provider("ollama", agent.config)
                else:
                    fallback = get_provider(fb_name, agent.config)
                logger.warning(f"{active_provider.name} failed — falling back to {fallback.name}")
                break
            except Exception:
                continue

        return fallback

    def get_routing_config(self) -> dict:
        """Return current routing configuration for the dashboard."""
        agent = self._agent
        from .model_router import QueryContext, QueryType

        result = {
            "mode": self._routing_mode,
            "router_active": self.model_router is not None,
            "available_models": [],
            "categories": {},
        }

        if not self.model_router:
            result["available_models"] = [agent.provider.name]
            return result

        result["available_models"] = list(self.model_router._models.keys())

        category_descriptions = {
            "simple": "Quick factual answers, greetings",
            "moderate": "Standard Q&A conversations",
            "complex": "Multi-step reasoning tasks",
            "coding": "Code generation and review",
            "creative": "Writing and brainstorming",
            "private": "Contains sensitive data (local only)",
            "long": "Large input context",
        }
        category_flags = {
            "simple": {"is_simple": True, "complexity": 0.1},
            "moderate": {"complexity": 0.5},
            "complex": {"is_complex": True, "complexity": 0.9, "requires_reasoning": True},
            "coding": {"is_coding": True, "complexity": 0.6, "requires_tools": True},
            "creative": {"is_creative": True, "complexity": 0.5},
            "private": {"is_private": True, "complexity": 0.5},
            "long": {"is_long_context": True, "complexity": 0.5},
        }

        for qt in QueryType:
            flags = category_flags.get(qt.value, {"complexity": 0.5})
            ctx = QueryContext(**flags)
            try:
                auto_model, _ = self.model_router.route(ctx)
            except Exception:
                auto_model = self.model_router.default_model
            manual_model = self._manual_routes.get(qt.value, auto_model)
            result["categories"][qt.value] = {
                "auto_model": auto_model,
                "model": manual_model,
                "description": category_descriptions.get(qt.value, ""),
            }

        return result

    def set_routing_config(self, mode: str, routes: dict = None) -> dict:
        """Update routing mode and manual route assignments."""
        if mode not in ("auto", "manual"):
            raise ValueError(f"Invalid routing mode: {mode!r} (expected 'auto' or 'manual')")
        self._routing_mode = mode
        if routes is not None:
            self._manual_routes = dict(routes)
        return self.get_routing_config()

    def switch_provider(self, provider_name: str) -> str:
        """Switch to a different LLM provider."""
        agent = self._agent
        try:
            agent.provider = get_provider(provider_name, agent.config)
            self.init_model_router()
            return f"Switched to {agent.provider.name}"
        except ValueError as e:
            return str(e)
