"""Configuration loader — YAML config with env var interpolation and pydantic validation."""

from __future__ import annotations

import os
import re
from pathlib import Path
from typing import Optional

import yaml
from pydantic import BaseModel, Field


def _interpolate_env(value: str) -> str:
    """Replace ${VAR} and $VAR patterns with environment variable values."""
    def _replace(match):
        var_name = match.group(1) or match.group(2)
        return os.environ.get(var_name, match.group(0))
    return re.sub(r'\$\{([^}]+)\}|\$([A-Z_][A-Z0-9_]*)', _replace, value)


def _deep_interpolate(obj):
    """Recursively interpolate env vars in all string values."""
    if isinstance(obj, str):
        return _interpolate_env(obj)
    if isinstance(obj, dict):
        return {k: _deep_interpolate(v) for k, v in obj.items()}
    if isinstance(obj, list):
        return [_deep_interpolate(v) for v in obj]
    return obj


class ModelConfig(BaseModel):
    id: str
    input_cost_per_mtok: float = 0.0
    output_cost_per_mtok: float = 0.0
    # Anthropic prompt caching costs (per million tokens)
    cache_read_cost_per_mtok: float = 0.0  # Reading from cache (cheaper than input)
    cache_creation_cost_per_mtok: float = 0.0  # Writing to cache (more expensive than input)
    max_context: int = 128000
    supports_tools: bool = False
    supports_vision: bool = False
    # Language filter: ISO 639-1 codes (e.g. ["en", "it"]). None = all languages.
    supported_languages: Optional[list[str]] = None
    # Calibration settings
    calibration_enabled: bool = True  # Include in calibration runs
    calibration_samples: int = 200  # Number of tasks to evaluate for learning vector


class ProviderConfig(BaseModel):
    id: str
    type: str = "openai"  # openai | anthropic | bedrock | openai-compatible
    base_url: str
    api_key: str = ""
    models: list[ModelConfig] = Field(default_factory=list)


class RoutingConfig(BaseModel):
    default_strategy: str = "auto"  # auto | cheap | best
    enable_fallback: bool = True  # Allow fallback to next model on error
    max_retries: int = 1
    circuit_breaker_threshold: int = 3
    # Vector-based routing settings
    vectors_path: str = "data/model_vectors.json"  # Path to learned model vectors
    embedder_model: str = "sentence-transformers/distiluse-base-multilingual-cased-v1"
    cost_weight: float = 0.3  # λ for auto strategy: score - λ*log(cost)
    score_threshold: float = 0.0  # Minimum score for cheap strategy


class CalibrationConfig(BaseModel):
    judge_model: str = "anthropic/claude-sonnet-4-20250514"
    concurrency_per_provider: int = 3
    custom_tasks_dir: Optional[str] = None
    # Task filtering options
    max_tasks: Optional[int] = None  # Limit total tasks (null = all)
    categories: Optional[list[str]] = None  # Filter by category (null = all)
    exclude_categories: Optional[list[str]] = None  # Exclude specific categories
    languages: Optional[list[str]] = None  # Filter by language, e.g. ["en", "it"] (null = all)
    difficulties: Optional[list[str]] = None  # Filter by difficulty (null = all)
    # Model vector learning
    samples_per_model: int = 200  # Default samples for learning model vector
    use_binary_scoring: bool = True  # Use pass/fail instead of 1-5 scores


class ServerConfig(BaseModel):
    host: str = "0.0.0.0"
    port: int = 8000


class EcateConfig(BaseModel):
    server: ServerConfig = Field(default_factory=ServerConfig)
    routing: RoutingConfig = Field(default_factory=RoutingConfig)
    calibration: CalibrationConfig = Field(default_factory=CalibrationConfig)
    providers: list[ProviderConfig] = Field(default_factory=list)

    def get_model(self, model_id: str) -> tuple[ProviderConfig, ModelConfig] | None:
        """Resolve a model_id like 'anthropic/gpt-4o' or just 'gpt-4o' to (provider, model)."""
        # Try provider/model format first
        if "/" in model_id:
            provider_id, model_name = model_id.split("/", 1)
            for p in self.providers:
                if p.id == provider_id:
                    for m in p.models:
                        if m.id == model_name:
                            return p, m
        # Fall back to searching all providers
        for p in self.providers:
            for m in p.models:
                if m.id == model_id:
                    return p, m
        return None

    def all_models(self) -> list[tuple[ProviderConfig, ModelConfig]]:
        """Return all configured (provider, model) pairs."""
        result = []
        for p in self.providers:
            for m in p.models:
                result.append((p, m))
        return result


def load_config(path: str | Path = "config.yaml") -> EcateConfig:
    """Load and validate configuration from a YAML file."""
    path = Path(path)
    if not path.exists():
        return EcateConfig()

    with open(path) as f:
        raw = yaml.safe_load(f) or {}

    raw = _deep_interpolate(raw)
    config = EcateConfig(**raw)
    config._config_path = path  # Store for save_config
    return config


def save_config(config: EcateConfig, path: str | Path | None = None) -> None:
    """Save configuration to a YAML file.
    
    Note: Environment variable references are NOT preserved — they are expanded
    on load. If you need to preserve ${VAR} syntax, manage config externally.
    """
    if path is None:
        path = getattr(config, "_config_path", Path("config.yaml"))
    path = Path(path)
    
    # Convert to dict, excluding internal fields
    data = {
        "server": config.server.model_dump(),
        "routing": config.routing.model_dump(),
        "calibration": config.calibration.model_dump(),
        "providers": [
            {
                "id": p.id,
                "type": p.type,
                "base_url": p.base_url,
                "api_key": p.api_key,
                "models": [m.model_dump() for m in p.models],
            }
            for p in config.providers
        ],
    }
    
    # Write with backup
    if path.exists():
        backup = path.with_suffix(".yaml.bak")
        backup.write_text(path.read_text())
    
    with open(path, "w") as f:
        yaml.safe_dump(data, f, default_flow_style=False, sort_keys=False)
