"""
Provider configuration loader.
Priority: env vars > config/providers.yaml > built-in defaults.

The .env file (at the project root, one level above backend/) is loaded
via python-dotenv at import time so that API keys set there are visible
to os.environ.get() even when the backend is started by a process that
does not inherit the user's shell profile (e.g. uvicorn launched from
an IDE, a CI runner, or a subprocess-based tool host).
"""
from __future__ import annotations

import os
from dataclasses import dataclass, field
from pathlib import Path

try:
    from dotenv import dotenv_values as _dotenv_values
except ImportError:
    _dotenv_values = None

import logging
logger = logging.getLogger(__name__)

# Load .env from the project root (smriti/) — one level above
# backend/. We track which values came from dotenv so reset_config()
# can safely refresh them later without clobbering real process env.
_PROJECT_ROOT_ENV = Path(__file__).parent.parent.parent / ".env"
_BACKEND_DIR_ENV = Path(__file__).parent.parent / ".env"
_PROCESS_ENV_KEYS = frozenset(os.environ)
_DOTENV_MANAGED_VALUES: dict[str, str] = {}

try:
    import yaml
    _YAML_AVAILABLE = True
except ImportError:
    _YAML_AVAILABLE = False


# ── Typed config objects ──────────────────────────────────────────────────────

@dataclass
class ProviderConfig:
    enabled: bool = False
    api_key: str = ""
    default_model: str = ""
    base_url: str = ""
    missing_package: bool = False


@dataclass
class ChatConfig:
    default_provider: str = "openrouter"
    autosave_turns: int = 50
    auto_commit: bool = False

@dataclass
class BackgroundConfig:
    provider: str = "openai"
    model: str = "gpt-4o-mini"


@dataclass
class AppProviderConfig:
    openai: ProviderConfig = field(default_factory=ProviderConfig)
    anthropic: ProviderConfig = field(default_factory=ProviderConfig)
    openrouter: ProviderConfig = field(default_factory=ProviderConfig)
    generic: ProviderConfig = field(default_factory=ProviderConfig)
    chat: ChatConfig = field(default_factory=ChatConfig)
    background: BackgroundConfig = field(default_factory=BackgroundConfig)


class ProviderNotConfiguredError(Exception):
    """Raised when a requested provider is missing its API key."""


# ── Config loader ─────────────────────────────────────────────────────────────

_CONFIG_PATH = Path(__file__).parent.parent / "config" / "providers.yaml"


def _resolve_dotenv_path() -> Path | None:
    if _PROJECT_ROOT_ENV.is_file():
        return _PROJECT_ROOT_ENV
    if _BACKEND_DIR_ENV.is_file():
        return _BACKEND_DIR_ENV
    return None


def _sync_dotenv_into_environ() -> None:
    """Refresh dotenv-managed values while preserving real process env.

    Keys that were present before this module loaded are treated as
    externally managed and are never overwritten. Keys previously
    sourced from .env are updated in place when the file changes, and
    removed from os.environ if they disappear from the file.
    """
    global _DOTENV_MANAGED_VALUES

    if _dotenv_values is None:
        return

    env_path = _resolve_dotenv_path()
    parsed = _dotenv_values(env_path) if env_path is not None else {}
    parsed_values = {key: value for key, value in parsed.items() if value is not None}

    previous_managed = _DOTENV_MANAGED_VALUES

    for key, old_value in previous_managed.items():
        if key in parsed_values:
            continue
        if os.environ.get(key) == old_value:
            os.environ.pop(key, None)

    new_managed: dict[str, str] = {}
    for key, value in parsed_values.items():
        if key in _PROCESS_ENV_KEYS:
            continue

        current_value = os.environ.get(key)
        old_value = previous_managed.get(key)
        if current_value is None or (old_value is not None and current_value == old_value):
            os.environ[key] = value
            new_managed[key] = value

    _DOTENV_MANAGED_VALUES = new_managed


_sync_dotenv_into_environ()

def _load_yaml() -> dict:
    if not _YAML_AVAILABLE:
        logger.warning("PyYAML not installed, skipping providers.yaml config load")
        return {}
    if not _CONFIG_PATH.exists():
        logger.info(f"Config file not found: {_CONFIG_PATH} (using env vars / defaults)")
        return {}
    with _CONFIG_PATH.open("r") as f:
        return yaml.safe_load(f) or {}


def load_config() -> AppProviderConfig:
    _sync_dotenv_into_environ()
    raw = _load_yaml()
    providers_raw = raw.get("providers", {})
    chat_raw = raw.get("chat", {})

    def _provider(name: str, env_var: str, base_url_default: str = "") -> ProviderConfig:
        """Merge YAML config + env override for a provider."""
        p = providers_raw.get(name, {})
        api_key = (
            os.environ.get(env_var)
            or p.get("api_key", "")
        )
        enabled = bool(api_key) and p.get("enabled", bool(api_key))
        return ProviderConfig(
            enabled=enabled,
            api_key=api_key,
            default_model=p.get("default_model", ""),
            base_url=p.get("base_url", base_url_default),
        )

    openai = _provider("openai", "OPENAI_API_KEY")
    anthropic = _provider("anthropic", "ANTHROPIC_API_KEY")
    openrouter = _provider(
        "openrouter", "OPENROUTER_API_KEY",
        base_url_default="https://openrouter.ai/api/v1",
    )

    # Generic OpenAI-compatible provider (Ollama, LM Studio, vLLM, Together,
    # Groq, etc.). Enabled when base_url is set — api_key is optional because
    # local servers like Ollama don't require one.
    g = providers_raw.get("generic", {})
    generic_base_url = (
        os.environ.get("SMRITI_GENERIC_API_URL")
        or g.get("base_url", "")
    )
    generic_api_key = (
        os.environ.get("SMRITI_GENERIC_API_KEY")
        or g.get("api_key", "")
    )
    generic_model = (
        os.environ.get("SMRITI_GENERIC_MODEL")
        or g.get("default_model", "")
    )
    generic = ProviderConfig(
        enabled=bool(generic_base_url),
        api_key=generic_api_key or "not-required",
        default_model=generic_model,
        base_url=generic_base_url,
    )

    chat = ChatConfig(
        default_provider=os.environ.get("SMRITI_DEFAULT_PROVIDER")
            or chat_raw.get("default_provider", "openrouter"),
        autosave_turns=int(chat_raw.get("autosave_turns", 50)),
        auto_commit=bool(chat_raw.get("auto_commit", False)),
    )
    
    bg_raw = raw.get("background_intelligence", {})
    background = BackgroundConfig(
        provider=bg_raw.get("provider", "openai"),
        model=bg_raw.get("model", "gpt-4o-mini")
    )

    return AppProviderConfig(
        openai=openai,
        anthropic=anthropic,
        openrouter=openrouter,
        generic=generic,
        chat=chat,
        background=background,
    )


# Singleton — loaded once on first call to get_config().
_config: AppProviderConfig | None = None


def get_config() -> AppProviderConfig:
    global _config
    if _config is None:
        _config = load_config()
    return _config


def reset_config() -> None:
    """Clear the cached singleton so the next get_config() call re-reads
    .env-backed env vars, current os.environ, and providers.yaml.

    Useful during development when editing .env or providers.yaml without
    restarting the server. Not intended for production use — hot-reloading
    config in a running server can cause mid-request inconsistency if
    two requests see different configs.
    """
    global _config
    _config = None


def get_provider_config(provider: str) -> ProviderConfig:
    cfg = get_config()
    pc = getattr(cfg, provider.lower(), None)
    if pc is None:
        raise ProviderNotConfiguredError(f"Unknown provider: {provider}")
    if not pc.api_key:
        raise ProviderNotConfiguredError(
            f"Provider '{provider}' has no API key configured. "
            f"Set the environment variable or add it to config/providers.yaml."
        )
    return pc


def _check_package(provider: str) -> bool:
    try:
        if provider in ("openai", "openrouter"):
            import openai  # noqa
        elif provider == "anthropic":
            import anthropic  # noqa
        return False
    except ImportError:
        return True


def providers_status() -> dict[str, dict]:
    """Return a dict summarising which providers are enabled — safe to expose in API."""
    cfg = get_config()
    status = {
        name: {
            "enabled": getattr(cfg, name).enabled,
            "has_key": bool(getattr(cfg, name).api_key),
            "missing_package": _check_package(name),
            "configured": getattr(cfg, name).enabled and bool(getattr(cfg, name).api_key) and not _check_package(name),
            "status_label": "Ready" if getattr(cfg, name).enabled else "Disabled",
            "default_model": getattr(cfg, name).default_model,
        }
        for name in ("openai", "anthropic", "openrouter", "generic")
    }
    # Background intelligence uses one of the above providers. Report
    # its ACTUAL key/enabled/configured state instead of hardcoding True,
    # so startup logs and the /providers endpoint surface mock-fallback
    # situations honestly.
    bg_provider_name = cfg.background.provider.lower()
    bg_pc = getattr(cfg, bg_provider_name, None)
    bg_has_key = bool(bg_pc.api_key) if bg_pc else False
    bg_enabled = bg_pc.enabled if bg_pc else False
    bg_missing_pkg = _check_package(bg_provider_name) if bg_pc else True
    bg_configured = bg_enabled and bg_has_key and not bg_missing_pkg
    status["background_intelligence"] = {
        "provider": cfg.background.provider,
        "model": cfg.background.model,
        "enabled": bg_enabled,
        "has_key": bg_has_key,
        "missing_package": bg_missing_pkg,
        "configured": bg_configured,
        "status_label": "Ready" if bg_configured else "Disabled — extraction will use MockAdapter",
    }
    return status
