"""
Model Selector - Capability filtering and cost optimization.

Implements nodes 2-4 of the routing pipeline:
- Node 2: Filter models by capability threshold
- Node 3: Select cheapest capable model
- Node 4: Calculate savings vs baseline
"""

from __future__ import annotations

import datetime
import json
from pathlib import Path
from typing import Any

import structlog

from robot_resources.routing.task_detection import TaskType

logger = structlog.get_logger()

# Load models database
_DB_PATH = Path(__file__).parent / "models_db.json"

# Providers with working client implementations in proxy/providers/
IMPLEMENTED_PROVIDERS: frozenset[str] = frozenset({"openai", "anthropic", "google"})

# Staleness threshold in days
_STALENESS_DAYS = 30


def validate_models_db(models_db: list[dict]) -> list[dict]:
    """Validate that all models use implemented providers.

    Raises ``ValueError`` if any model has an unimplemented or missing
    provider. Returns the input list unchanged on success.
    """
    if not models_db:
        return models_db

    missing_provider: list[str] = []
    invalid_providers: dict[str, list[str]] = {}

    for model in models_db:
        name = model.get("name", "<unnamed>")
        provider = model.get("provider")

        if provider is None:
            missing_provider.append(name)
            continue

        if provider not in IMPLEMENTED_PROVIDERS:
            invalid_providers.setdefault(provider, []).append(name)

    errors: list[str] = []
    if missing_provider:
        errors.append(
            f"Models with missing provider field: {', '.join(missing_provider)}"
        )
    if invalid_providers:
        parts = [
            f"{prov} ({', '.join(names)})"
            for prov, names in sorted(invalid_providers.items())
        ]
        errors.append(
            f"Unimplemented providers: {'; '.join(parts)}. "
            f"Supported: {', '.join(sorted(IMPLEMENTED_PROVIDERS))}"
        )

    if errors:
        raise ValueError("; ".join(errors))

    return models_db


def check_pricing_staleness(
    models_db: list[dict],
    *,
    warn: bool = False,
    threshold_days: int = _STALENESS_DAYS,
) -> list[dict]:
    """Check which models have stale pricing data.

    A model is stale if its ``last_updated`` field is missing, unparseable,
    or older than *threshold_days*.  Future dates trigger a clock-skew
    warning but are **not** considered stale.

    Args:
        models_db: The models database to check.
        warn: If ``True``, emit an aggregate structlog warning for stale models.
        threshold_days: Days after which pricing is stale (default 30).

    Returns:
        List of model dicts that are stale.
    """
    if not models_db:
        return []

    today = datetime.date.today()
    stale: list[dict] = []

    for model in models_db:
        name = model.get("name", "<unnamed>")
        raw = model.get("last_updated")

        if raw is None:
            stale.append(model)
            continue

        try:
            updated = datetime.date.fromisoformat(str(raw))
        except (ValueError, TypeError):
            logger.warning(
                "models_db_invalid_date",
                model=name,
                last_updated=raw,
                note="Malformed last_updated — treating as stale",
            )
            stale.append(model)
            continue

        if updated > today:
            logger.warning(
                "models_db_future_date",
                model=name,
                last_updated=raw,
                note="last_updated is in the future — possible clock skew",
            )
            continue  # not stale, just suspicious

        age = (today - updated).days
        if age > threshold_days:
            stale.append(model)

    if warn and stale:
        names = [m.get("name", "?") for m in stale]
        logger.warning(
            "models_db_stale_pricing",
            stale_count=len(stale),
            models=names,
            threshold_days=threshold_days,
            note=f"{len(stale)} model(s) have pricing older than {threshold_days} days",
        )

    return stale


def load_models_db() -> list[dict]:
    """Load and validate the models capability database."""
    with open(_DB_PATH) as f:
        db = json.load(f)
    validate_models_db(db)
    check_pricing_staleness(db, warn=True)
    return db


# Singleton DB instance
MODELS_DB: list[dict] = load_models_db()

# Default baseline model for savings comparison (most expensive in DB).
# Used as fallback when no personal baseline is available yet.
DEFAULT_BASELINE = max(MODELS_DB, key=lambda m: m["cost_per_1k_input"])["name"]

# Minimum capability threshold for model selection
CAPABILITY_THRESHOLD = 0.70


def get_capable_models(
    task_type: TaskType,
    models_db: list[dict] | None = None,
    threshold: float = CAPABILITY_THRESHOLD,
) -> list[dict]:
    """
    NODE 2: Filter models by capability threshold for a task type.

    Args:
        task_type: The detected task type
        models_db: Optional custom models database
        threshold: Minimum capability score (default: 0.70)

    Returns:
        List of models that meet the capability threshold
    """
    db = models_db if models_db is not None else MODELS_DB

    capable = []
    for model in db:
        capability = model["capabilities"].get(task_type)
        if capability is None:
            capability = model["capabilities"].get("overall", 0)

        if capability >= threshold:
            capable.append(model)

    return capable


def select_cheapest_model(models: list[dict]) -> dict | None:
    """
    NODE 3: Select the cheapest model from a list.

    Args:
        models: List of capable models

    Returns:
        The cheapest model, or None if list is empty
    """
    if not models:
        return None

    return min(models, key=lambda m: m["cost_per_1k_input"])


def calculate_savings(
    selected_model: dict,
    baseline_model: dict | None = None,
    models_db: list[dict] | None = None,
) -> dict[str, Any]:
    """
    NODE 4: Calculate cost savings vs baseline.

    Args:
        selected_model: The model we're routing to
        baseline_model: Optional baseline for comparison
        models_db: Optional custom models database

    Returns:
        Dict with savings metrics
    """
    db = models_db if models_db is not None else MODELS_DB

    if baseline_model is None:
        baseline_model = next((m for m in db if m["name"] == DEFAULT_BASELINE), db[0])

    baseline_cost = baseline_model["cost_per_1k_input"]
    selected_cost = selected_model["cost_per_1k_input"]

    if baseline_cost > 0:
        savings_percent = ((baseline_cost - selected_cost) / baseline_cost) * 100
    else:
        savings_percent = 0.0

    return {
        "baseline_model": baseline_model["name"],
        "selected_model": selected_model["name"],
        "baseline_cost": baseline_cost,
        "selected_cost": selected_cost,
        "savings_percent": round(savings_percent, 1),
        "savings_per_1k_tokens": round(baseline_cost - selected_cost, 6),
    }


def rank_capable_models(models: list[dict]) -> list[dict]:
    """Rank capable models by cost (cheapest first).

    Returns all models sorted by ``cost_per_1k_input`` ascending.
    Used by retry logic to fall back to next-cheapest model from a
    different provider.
    """
    if not models:
        return []
    return sorted(models, key=lambda m: m["cost_per_1k_input"])


def get_model_by_name(name: str, models_db: list[dict] | None = None) -> dict | None:
    """Get a model by name from the database."""
    db = models_db if models_db is not None else MODELS_DB
    return next((m for m in db if m["name"] == name), None)
