"""LLMHosts configuration -- TOML-based config with env var overrides.

Loads configuration from ``~/.llmhosts/config.toml``, merges environment
variable overrides, and provides typed Pydantic v2 models for every
subsystem (server, ollama, router, cache, dashboard).

Priority: env vars > config file > defaults
Env var format: ``LLMHOSTS_<SECTION>_<KEY>`` (e.g. ``LLMHOSTS_SERVER_PORT=8080``).
"""

from __future__ import annotations

import logging
import os
from pathlib import Path
from typing import Any

import toml  # type: ignore[import-untyped]
from pydantic import BaseModel, Field

from llmhosts.constants import (
    CONFIG_FILENAME,
    DATA_DIR_NAME,
    DEFAULT_HOST,
    DEFAULT_PORT,
    OLLAMA_DEFAULT_HOST,
    VLLM_DEFAULT_HOST,
)

logger = logging.getLogger(__name__)

# ---------------------------------------------------------------------------
# Section models
# ---------------------------------------------------------------------------


class ServerConfig(BaseModel):
    """HTTP server settings."""

    host: str = DEFAULT_HOST
    port: int = DEFAULT_PORT
    workers: int = 1
    log_level: str = "info"
    cors_origins: list[str] = Field(default_factory=lambda: ["*"])


class OllamaConfig(BaseModel):
    """Ollama backend connection settings."""

    host: str = OLLAMA_DEFAULT_HOST
    timeout: float = 120.0
    auto_discover: bool = True


class RouterConfig(BaseModel):
    """Intelligent router settings."""

    prefer_local: bool = True
    cost_optimize: bool = True
    fallback_to_cloud: bool = True
    # Tier control -- advanced tiers require optional dependencies
    enable_knn: bool = False  # requires [smart] pip tier
    enable_modernbert: bool = False  # requires [smart] pip tier
    enable_qwen: bool = False  # requires [full] pip tier


class CacheConfig(BaseModel):
    """Semantic cache settings."""

    enabled: bool = True
    max_size_mb: int = 512
    ttl_seconds: int = 3600
    # Tier control
    enable_namespace: bool = False  # requires [smart] pip tier
    enable_vcache: bool = False  # requires [smart] pip tier
    delta: float = 0.01  # max false positive rate for vCache
    # Per-tier LRU limits
    exact_max_entries: int = 10_000  # max entries in exact hash tier
    namespace_max_namespaces: int = 500  # max namespace buckets
    vcache_max_bytes: int = 256 * 1024 * 1024  # 256 MB max for vCache embeddings


class DashboardConfig(BaseModel):
    """Dashboard (TUI + web) settings."""

    enabled: bool = True
    tui: bool = True
    web: bool = True


class ConnectionPoolConfig(BaseModel):
    """Per-backend HTTP connection pool settings.

    Each backend URL gets its own ``httpx.AsyncClient`` with these limits.
    HTTP/2 is attempted when the ``h2`` package is installed; otherwise
    httpx falls back to HTTP/1.1 transparently.
    """

    max_connections: int = 20
    max_keepalive: int = 5
    idle_timeout: int = 30
    http2: bool = True


class BatchConfig(BaseModel):
    """Micro-batching settings for non-streaming inference requests.

    When enabled, the proxy accumulates incoming non-streaming requests
    within a short time window and dispatches them together to improve
    GPU utilization.  Streaming requests are never batched.
    """

    enabled: bool = False  # Opt-in; disabled by default
    window_ms: float = 50.0  # Max wait before flushing a partial batch
    max_size: int = 16  # Flush immediately when this many requests queue up


class EngineConfig(BaseModel):
    """Inference engine selection (Ollama vs vLLM)."""

    type: str = "ollama"  # ollama | vllm | auto
    vllm_host: str = VLLM_DEFAULT_HOST


class PromptGuardConfig(BaseModel):
    """PromptGuard prompt injection defense settings (TrustShield Pillar 1)."""

    enabled: bool = True
    mode: str = "warn"  # off | warn | block | sanitize
    threshold: float = 0.7
    log_all: bool = True
    custom_patterns: list[str] = Field(default_factory=list)


class RateLimitConfig(BaseModel):
    """Adaptive rate limiting with model-weighted sliding window (TrustShield Pillar 3)."""

    enabled: bool = True
    algorithm: str = "sliding_window"
    window_seconds: int = 60
    burst_multiplier: float = 1.5
    burst_window_seconds: int = 10
    model_weights: dict[str, int] = Field(
        default_factory=lambda: {
            "gpt-4": 4,
            "gpt-4-turbo": 3,
            "gpt-4o": 2,
            "gpt-3.5-turbo": 1,
            "claude-3-opus": 4,
            "claude-3-sonnet": 2,
            "claude-3-haiku": 1,
            "llama-3-70b": 2,
            "llama-3-8b": 1,
        },
    )
    default_weight: int = 1
    cleanup_interval_seconds: float = 3600.0


class TransparencyConfig(BaseModel):
    """Transparency and privacy configuration (TrustShield Pillar 5)."""

    privacy_manifest: bool = True
    resource_monitor: bool = True
    activity_log_retention_hours: int = 24


class MTLSConfig(BaseModel):
    """Mutual TLS configuration for backend connections (TrustShield Pillar 4)."""

    enabled: bool = False  # Opt-in (not needed for localhost Ollama)
    cert_expiry_days: int = 365
    auto_rotate: bool = True
    rotate_before_days: int = 30


class SecurityConfig(BaseModel):
    """Proxy security settings -- authentication, rate limiting, CORS, TrustShield."""

    require_auth: bool = False  # When True, all /v1/* endpoints require Bearer token
    rate_limit_rpm: int = 60  # Requests per minute per API key (or per IP if auth disabled)
    rate_limit_burst: int = 10  # Burst allowance above steady-state rate
    cors_restrict: bool = False  # When True, use server.cors_origins instead of ["*"]
    # TrustShield pillars
    prompt_guard: PromptGuardConfig = Field(default_factory=PromptGuardConfig)
    rate_limit: RateLimitConfig = Field(default_factory=RateLimitConfig)
    transparency: TransparencyConfig = Field(default_factory=TransparencyConfig)
    mtls: MTLSConfig = Field(default_factory=MTLSConfig)


class TracingConfig(BaseModel):
    """OpenTelemetry distributed tracing settings."""

    enabled: bool = False
    exporter: str = "otlp"  # otlp | jaeger | console
    endpoint: str = "http://localhost:4317"
    service_name: str = "llmhosts-proxy"
    sample_rate: float = 1.0


class ObservabilityConfig(BaseModel):
    """Observability, error tracking, and structured logging."""

    sentry_dsn: str = ""  # Empty = disabled; set to Sentry DSN to enable
    log_format: str = "text"  # "text" (human-readable) | "json" (structured)
    tracing: TracingConfig = Field(default_factory=TracingConfig)


class PluginConfig(BaseModel):
    """Plugin subsystem settings."""

    enabled: bool = True
    plugin_dirs: list[str] = Field(
        default_factory=lambda: ["~/.llmhosts/plugins"],
        description="Directories to scan for plugin Python files.",
    )
    disabled_plugins: list[str] = Field(
        default_factory=list,
        description="Plugin names to skip even when discovered.",
    )


class AlertRuleConfig(BaseModel):
    """Configuration for a single alerting rule."""

    name: str
    condition: str  # e.g. "backend_latency_ms > 500"
    duration_seconds: int = 0
    severity: str = "warning"  # warning | critical
    channels: list[str] = Field(default_factory=lambda: ["console"])


class AlertingConfig(BaseModel):
    """Configurable alerting system settings."""

    enabled: bool = False
    check_interval_seconds: int = 30
    rules: list[AlertRuleConfig] = Field(default_factory=list)
    webhooks: dict[str, str] = Field(default_factory=dict)  # name -> URL


class DiscoveryConfig(BaseModel):
    """Zero-config auto-discovery settings for the Infrastructure Autopilot."""

    auto_provision: bool = True  # Auto-install Ollama + pull model on clean box
    model_poll_interval: float = 5.0  # Seconds between lightweight model checks on known engines
    engine_scan_interval: float = 30.0  # Seconds between full engine re-scans
    scan_timeout: float = 3.0  # Per-probe timeout for engine scanning
    extra_hosts: list[str] = Field(default_factory=list)  # User-defined engine URLs to probe
    savings_tracking: bool = True  # Track cloud-equivalent cost savings


class FallbackConfig(BaseModel):
    """At-capacity cloud fallback settings."""

    enabled: bool = False
    queue_depth_threshold: int = 10
    latency_threshold_ms: int = 5000
    latency_window_size: int = 50
    model_mapping: dict[str, str] = Field(
        default_factory=lambda: {
            "qwen3:14b": "gpt-4o-mini",
            "qwen3:32b": "gpt-4o",
            "llama3.3:70b": "gpt-4o",
            "qwen3:8b": "gpt-4o-mini",
            "qwen3:4b": "gpt-4o-mini",
        }
    )
    notify_user: bool = True


class RemitScheduleEntry(BaseModel):
    """Time-of-day schedule for GPU availability."""

    start_hour: int = Field(default=0, ge=0, le=23)
    end_hour: int = Field(default=24, ge=0, le=24)
    days: list[str] = Field(default_factory=lambda: ["mon", "tue", "wed", "thu", "fri", "sat", "sun"])


class RemitConfig(BaseModel):
    """GPU remit -- controls which backends/GPUs LLMHosts is allowed to use.

    Modes:
    - 'all': Use all discovered backends (default)
    - 'allowlist': Only use explicitly allowed backends
    - 'denylist': Use all except explicitly denied backends
    """

    mode: str = "all"  # "all" | "allowlist" | "denylist"
    allowlist: list[str] = Field(default_factory=list)  # backend IDs or GPU names
    denylist: list[str] = Field(default_factory=list)  # backend IDs or GPU names
    schedule: list[RemitScheduleEntry] = Field(default_factory=list)
    hot_reload: bool = True  # Watch config file for changes


# ---------------------------------------------------------------------------
# Root config
# ---------------------------------------------------------------------------


class LLMHostsConfig(BaseModel):
    """Root configuration model aggregating all sections."""

    server: ServerConfig = Field(default_factory=ServerConfig)
    ollama: OllamaConfig = Field(default_factory=OllamaConfig)
    router: RouterConfig = Field(default_factory=RouterConfig)
    cache: CacheConfig = Field(default_factory=CacheConfig)
    dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
    engine: EngineConfig = Field(default_factory=EngineConfig)
    security: SecurityConfig = Field(default_factory=SecurityConfig)
    observability: ObservabilityConfig = Field(default_factory=ObservabilityConfig)
    alerting: AlertingConfig = Field(default_factory=AlertingConfig)
    connection_pool: ConnectionPoolConfig = Field(default_factory=ConnectionPoolConfig)
    batch: BatchConfig = Field(default_factory=BatchConfig)
    plugins: PluginConfig = Field(default_factory=PluginConfig)
    discovery: DiscoveryConfig = Field(default_factory=DiscoveryConfig)
    remit: RemitConfig = Field(default_factory=RemitConfig)
    fallback: FallbackConfig = Field(default_factory=FallbackConfig)
    # Runtime-only: models selected via --interactive (not persisted to TOML)
    selected_models: list[str] = Field(default_factory=list, exclude=True)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

_ENV_PREFIX = "LLMHOSTS_"

# Maps of (section, field) -> Python type for coercion
_BOOL_TRUTHY = {"1", "true", "yes", "on"}
_BOOL_FALSY = {"0", "false", "no", "off"}


def llmhosts_dir() -> Path:
    """Return ``~/.llmhosts``, creating the directory if needed."""
    p = Path.home() / DATA_DIR_NAME
    p.mkdir(parents=True, exist_ok=True)
    return p


def _config_path() -> Path:
    """Return the default config file path (``~/.llmhosts/config.toml``)."""
    return llmhosts_dir() / CONFIG_FILENAME


def _coerce_value(value: str, target_type: type) -> Any:
    """Coerce a string env var value to the target Python type."""
    if target_type is bool:
        low = value.strip().lower()
        if low in _BOOL_TRUTHY:
            return True
        if low in _BOOL_FALSY:
            return False
        raise ValueError(f"Cannot coerce {value!r} to bool")
    if target_type is int:
        return int(value)
    if target_type is float:
        return float(value)
    if target_type is list:
        # Comma-separated list
        return [v.strip() for v in value.split(",") if v.strip()]
    return value


def _apply_env_overrides(data: dict[str, Any]) -> dict[str, Any]:
    """Scan ``LLMHOSTS_*`` environment variables and merge into *data*.

    Format: ``LLMHOSTS_<SECTION>_<KEY>=<VALUE>``
    Example: ``LLMHOSTS_SERVER_PORT=8080`` -> ``data["server"]["port"] = 8080``
    """
    # Build a type map from the section models
    section_models: dict[str, type[BaseModel]] = {
        "server": ServerConfig,
        "ollama": OllamaConfig,
        "router": RouterConfig,
        "cache": CacheConfig,
        "dashboard": DashboardConfig,
        "engine": EngineConfig,
        "security": SecurityConfig,
        "observability": ObservabilityConfig,
        "alerting": AlertingConfig,
        "connection_pool": ConnectionPoolConfig,
        "batch": BatchConfig,
        "plugins": PluginConfig,
        "discovery": DiscoveryConfig,
        "remit": RemitConfig,
        "fallback": FallbackConfig,
    }

    for env_key, env_val in os.environ.items():
        if not env_key.startswith(_ENV_PREFIX):
            continue
        parts = env_key[len(_ENV_PREFIX) :].lower().split("_", 1)
        if len(parts) != 2:
            continue
        section, field = parts
        if section not in section_models:
            continue

        model_cls = section_models[section]
        field_info = model_cls.model_fields.get(field)
        if field_info is None:
            logger.debug("Ignoring unknown env override: %s", env_key)
            continue

        # Determine the target Python type from the annotation
        annotation = field_info.annotation
        # Handle Optional / union types by taking the first real type
        origin = getattr(annotation, "__origin__", None)
        target_type: type
        if origin is list:
            target_type = list
        elif annotation in (bool, int, float, str):
            target_type = annotation  # type: ignore[assignment]
        else:
            target_type = str

        try:
            coerced = _coerce_value(env_val, target_type)
        except (ValueError, TypeError) as exc:
            logger.warning("Invalid env override %s=%s: %s", env_key, env_val, exc)
            continue

        data.setdefault(section, {})[field] = coerced
        logger.debug("Env override: %s.%s = %r", section, field, coerced)

    return data


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------


def load_config(config_path: Path | None = None) -> LLMHostsConfig:
    """Load configuration from a TOML file, merged with env var overrides.

    Priority: env vars > config file > defaults.

    Parameters
    ----------
    config_path:
        Explicit path to a TOML file.  When *None*, falls back to
        ``~/.llmhosts/config.toml`` (missing file is not an error --
        defaults are used).

    Returns
    -------
    LLMHostsConfig
        Fully-resolved configuration.
    """
    data: dict[str, Any] = {}
    path = config_path or _config_path()

    if path.is_file():
        try:
            data = toml.load(path)
            logger.info("Loaded config from %s", path)
        except toml.TomlDecodeError as exc:
            logger.warning("Failed to parse config file %s: %s -- using defaults", path, exc)
    else:
        logger.debug("No config file at %s -- using defaults", path)

    # Merge env var overrides on top of file values
    data = _apply_env_overrides(data)

    return LLMHostsConfig.model_validate(data)


def save_config(config: LLMHostsConfig, config_path: Path | None = None) -> None:
    """Write the current configuration to a TOML file.

    Parameters
    ----------
    config:
        The configuration to persist.
    config_path:
        Explicit path. Defaults to ``~/.llmhosts/config.toml``.
    """
    path = config_path or _config_path()
    path.parent.mkdir(parents=True, exist_ok=True)
    data = config.model_dump(mode="python", exclude={"selected_models"})
    path.write_text(toml.dumps(data), encoding="utf-8")
    logger.info("Saved config to %s", path)


def save_default_config(path: Path) -> None:
    """Write an annotated default ``config.toml`` to *path*.

    Existing files are **not** overwritten -- a ``FileExistsError`` is raised.
    """
    if path.exists():
        raise FileExistsError(f"Config file already exists: {path}")

    path.parent.mkdir(parents=True, exist_ok=True)

    content = """\
# LLMHosts configuration
# Docs: https://llmhosts.com/docs/config
# Env overrides: LLMHOSTS_<SECTION>_<KEY>=<VALUE>

[server]
host = "127.0.0.1"
port = 4000
workers = 1
log_level = "info"
cors_origins = ["*"]

# Inference engine connection (backend auto-discovered by LLMHosts)
[ollama]
host = "http://127.0.0.1:11434"
timeout = 120.0
auto_discover = true

[router]
prefer_local = true
cost_optimize = true
fallback_to_cloud = true
# Tier control (requires optional pip dependencies)
enable_knn = false        # pip install llmhosts[smart]
enable_modernbert = false # pip install llmhosts[smart]
enable_qwen = false       # pip install llmhosts[full]

[cache]
enabled = true
max_size_mb = 512
ttl_seconds = 3600
# Tier control
enable_namespace = false  # pip install llmhosts[smart]
enable_vcache = false     # pip install llmhosts[smart]
delta = 0.01              # max false positive rate for vCache

[dashboard]
enabled = true
tui = true
web = true

[engine]
type = "ollama"
vllm_host = "http://localhost:8000"

[security]
require_auth = false      # Require API key on /v1/* endpoints
rate_limit_rpm = 60       # Requests per minute per key/IP
rate_limit_burst = 10     # Burst allowance
cors_restrict = false     # When true, only allow cors_origins from [server]

[security.prompt_guard]
enabled = true
mode = "warn"             # off | warn | block | sanitize
threshold = 0.7           # Score above which action triggers
log_all = true            # Log even clean prompts for audit
custom_patterns = []      # User-defined regex patterns

[security.rate_limit]
enabled = true
algorithm = "sliding_window"
window_seconds = 60
default_weight = 1

[security.transparency]
privacy_manifest = true
resource_monitor = true
activity_log_retention_hours = 24

[security.mtls]
enabled = false              # Opt-in: enable mTLS for backend connections
cert_expiry_days = 365
auto_rotate = true
rotate_before_days = 30

[connection_pool]
max_connections = 20      # Max concurrent connections per backend
max_keepalive = 5         # Max idle keep-alive connections per backend
idle_timeout = 30         # Seconds before idle connections are closed
http2 = true              # Attempt HTTP/2 (requires h2 package; falls back to HTTP/1.1)

[batch]
enabled = false           # Opt-in: batch non-streaming requests for GPU efficiency
window_ms = 50.0          # Max milliseconds to wait before flushing a partial batch
max_size = 16             # Flush immediately when this many requests queue up

[observability]
sentry_dsn = ""           # Set to your Sentry DSN to enable error tracking
log_format = "text"       # "text" or "json"

[observability.tracing]
enabled = false           # Enable OpenTelemetry distributed tracing
exporter = "otlp"         # "otlp", "jaeger", or "console"
endpoint = "http://localhost:4317"  # Exporter endpoint (OTLP gRPC or Jaeger)
service_name = "llmhosts-proxy"     # Service name in traces
sample_rate = 1.0         # Sampling rate (0.0 to 1.0)

[alerting]
enabled = false           # Enable configurable alerting rules
check_interval_seconds = 30
# webhooks = { slack = "https://hooks.slack.com/..." }

# Example rules (uncomment to enable):
# [[alerting.rules]]
# name = "high-latency"
# condition = "backend_latency_ms > 500"
# duration_seconds = 60
# severity = "warning"
# channels = ["console"]
#
# [[alerting.rules]]
# name = "high-error-rate"
# condition = "error_rate > 0.1"
# severity = "critical"
# channels = ["console", "slack"]
"""
    path.write_text(content, encoding="utf-8")
    logger.info("Wrote default config to %s", path)
