"""
Router - Complete routing pipeline.

Orchestrates the 5-node routing pipeline:
- Node 1: Task Detection
- Node 2: Capability Filter
- Node 3: Cost Optimization
- Node 4: Savings Calculation
- Node 5: Response Builder

Hybrid routing (async_route_prompt) adds confidence branching:
- High confidence (>=0.85): fast path (keyword only)
- Low confidence (<0.85): smart path (LLM classifier from TKT-028)
"""

from __future__ import annotations

from typing import Any

import structlog

from robot_resources.routing.classifier import ClassificationResult, classify_with_llm
from robot_resources.routing.selector import (
    CAPABILITY_THRESHOLD,
    MODELS_DB,
    calculate_savings,
    get_capable_models,
    rank_capable_models,
    select_cheapest_model,
)
from robot_resources.routing.task_detection import (
    TaskType,
    analyze_prompt,
)

logger = structlog.get_logger()

# Confidence threshold for hybrid routing.
# >= this value: fast path (keyword only)
# < this value: smart path (LLM classifier)
CONFIDENCE_THRESHOLD: float = 0.85

# Maps LLM classifier complexity (1-5) to capability threshold.
# Lower complexity → lower threshold → cheaper models eligible.
COMPLEXITY_THRESHOLD_MAP: dict[int, float] = {
    1: 0.60,
    2: 0.60,
    3: 0.70,
    4: 0.85,
    5: 0.85,
}


def route_prompt(
    prompt: str,
    models_db: list[dict] | None = None,
    threshold: float = CAPABILITY_THRESHOLD,
    baseline_model: dict | None = None,
) -> dict[str, Any]:
    """
    Execute the complete routing pipeline.

    This is the main entry point for routing decisions.

    Args:
        prompt: The user's prompt to route
        models_db: Optional custom models database
        threshold: Minimum capability threshold (default: 0.70)
        baseline_model: Personal baseline model for savings comparison

    Returns:
        Complete routing decision with model, cost, savings, and reasoning
    """
    db = models_db if models_db is not None else MODELS_DB

    # NODE 1: Detect task type
    analysis = analyze_prompt(prompt)
    task_type: TaskType = analysis["task_type"]
    confidence = analysis["confidence"]
    matched_keywords = analysis["matched_keywords"]

    # NODE 2: Get capable models
    capable_models = get_capable_models(task_type, db, threshold)

    # Fallback: if no capable models, lower threshold
    if not capable_models:
        capable_models = get_capable_models(task_type, db, threshold=0.5)

    # Final fallback: use all models
    if not capable_models:
        capable_models = db
        task_type = "general"

    # NODE 3: Select cheapest capable model
    selected_model = select_cheapest_model(capable_models)

    if selected_model is None:
        selected_model = db[0]

    # Rank all capable models for fallback (used by retry logic)
    ranked = rank_capable_models(capable_models)

    # NODE 4: Calculate savings
    savings = calculate_savings(
        selected_model, baseline_model=baseline_model, models_db=db
    )

    # NODE 5: Build response
    return _build_response(
        task_type=task_type,
        selected_model=selected_model,
        savings=savings,
        confidence=confidence,
        matched_keywords=matched_keywords,
        threshold=threshold,
        ranked_candidates=ranked,
    )


async def async_route_prompt(
    prompt: str,
    models_db: list[dict] | None = None,
    threshold: float = CAPABILITY_THRESHOLD,
    baseline_model: dict | None = None,
) -> dict[str, Any]:
    """Async routing pipeline with confidence branching.

    High confidence (>= 0.85): fast path — uses keyword detection only.
    Low confidence (< 0.85): smart path — calls LLM classifier for better
    accuracy, then adjusts capability threshold based on complexity.

    Returns the same dict as ``route_prompt()`` plus a
    ``classification_source`` field (``"keyword"`` or ``"llm"``).
    """
    db = models_db if models_db is not None else MODELS_DB

    # NODE 1: Detect task type via keywords
    analysis = analyze_prompt(prompt)
    keyword_task_type: TaskType = analysis["task_type"]
    confidence: float = analysis["confidence"]
    matched_keywords: list[str] = analysis["matched_keywords"]

    # Confidence branching
    classification_source = "keyword"
    task_type = keyword_task_type
    effective_threshold = threshold
    clf_result: ClassificationResult | None = None

    if confidence < CONFIDENCE_THRESHOLD:
        # Smart path: call LLM classifier
        try:
            clf_result = await classify_with_llm(prompt)
        except Exception:
            logger.warning("async_route_classifier_error", exc_info=True)
            clf_result = None

        if clf_result is not None and clf_result.task_type != keyword_task_type:
            # Classifier disagrees with keywords — use classifier result
            task_type = clf_result.task_type
            classification_source = "llm"
            effective_threshold = COMPLEXITY_THRESHOLD_MAP.get(
                clf_result.complexity, threshold
            )
            logger.info(
                "hybrid_routing_llm_override",
                keyword_type=keyword_task_type,
                llm_type=clf_result.task_type,
                complexity=clf_result.complexity,
                threshold=effective_threshold,
            )
        elif clf_result is not None and clf_result.task_type == keyword_task_type:
            # Same task type — use keyword (faster, same answer)
            # But still apply complexity threshold adjustment
            effective_threshold = COMPLEXITY_THRESHOLD_MAP.get(
                clf_result.complexity, threshold
            )
        # If clf_result is None → fallback to keyword result (no changes)

    # NODE 2–5: Standard pipeline with possibly adjusted task_type and threshold
    capable_models = get_capable_models(task_type, db, effective_threshold)

    if not capable_models:
        capable_models = get_capable_models(task_type, db, threshold=0.5)

    if not capable_models:
        capable_models = db
        task_type = "general"

    selected_model = select_cheapest_model(capable_models)
    if selected_model is None:
        selected_model = db[0]

    ranked = rank_capable_models(capable_models)
    savings = calculate_savings(
        selected_model, baseline_model=baseline_model, models_db=db
    )

    result = _build_response(
        task_type=task_type,
        selected_model=selected_model,
        savings=savings,
        confidence=confidence,
        matched_keywords=matched_keywords,
        threshold=effective_threshold,
        ranked_candidates=ranked,
    )
    result["classification_source"] = classification_source
    result["keyword_task_type"] = keyword_task_type
    result["keyword_confidence"] = confidence
    result["llm_task_type"] = clf_result.task_type if clf_result is not None else None
    result["llm_complexity"] = clf_result.complexity if clf_result is not None else None
    result["llm_model"] = clf_result.classifier_model if clf_result is not None else None
    result["capability_threshold"] = effective_threshold
    return result


def _build_response(
    task_type: TaskType,
    selected_model: dict,
    savings: dict,
    confidence: float,
    matched_keywords: list[str],
    threshold: float,
    ranked_candidates: list[dict] | None = None,
) -> dict[str, Any]:
    """NODE 5: Build the final routing response."""
    capability = selected_model["capabilities"].get(
        task_type, selected_model["capabilities"].get("overall", 0)
    )

    # Build fallback candidates list (exclude the selected model)
    fallbacks = []
    if ranked_candidates:
        fallbacks = [
            {
                "name": m["name"],
                "provider": m["provider"],
                "cost_per_1k_input": m["cost_per_1k_input"],
            }
            for m in ranked_candidates
            if m["name"] != selected_model["name"]
        ]

    return {
        # Model selection
        "selected_model": selected_model["name"],
        "provider": selected_model["provider"],
        # Cost info
        "cost_per_1k_input": selected_model["cost_per_1k_input"],
        "cost_per_1k_output": selected_model["cost_per_1k_output"],
        # Savings vs baseline
        "baseline_model": savings["baseline_model"],
        "baseline_cost": savings["baseline_cost"],
        "savings_percent": savings["savings_percent"],
        # Task analysis
        "task_type": task_type,
        "capability_score": round(capability, 2),
        "detection_confidence": confidence,
        # Reasoning
        "reasoning": (
            f"Selected {selected_model['name']} ({selected_model['provider']}) "
            f"as cheapest model capable of '{task_type}' tasks "
            f"(capability: {capability:.2f}, threshold: {threshold}). "
            f"Saves {savings['savings_percent']}% vs {savings['baseline_model']}."
        ),
        # Metadata
        "matched_keywords": matched_keywords,
        "max_tokens": selected_model.get("max_tokens"),
        # Fallback candidates for retry logic (sorted by cost, excludes selected)
        "ranked_candidates": fallbacks,
    }
