use super::*;

const DEFAULT_EQUIVALENT_FAILOVER_MAX_ROUTES: usize = 3;

pub(super) fn quality_rank(tier: &str) -> i32 {
    match tier.to_ascii_lowercase().as_str() {
        "small" => 0,
        "mid" | "medium" => 1,
        "frontier" | "large" => 2,
        _ => 1,
    }
}

pub(super) fn route_target_from_short(
    target: &str,
) -> Result<(String, String), crate::value::VmError> {
    let target = target.trim();
    if target.is_empty() {
        return Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
            "route_policy: target must not be empty",
        ))));
    }
    if let Some((provider, model)) = target.split_once(':') {
        let provider_known = provider == "mock"
            || crate::llm_config::provider_config(provider).is_some()
            || crate::llm::provider::is_provider_registered(provider);
        if provider_known && !model.trim().is_empty() {
            let (resolved_model, _) = crate::llm_config::resolve_model(model.trim());
            return Ok((resolved_model, provider.trim().to_string()));
        }
    }
    let resolved = crate::llm_config::resolve_model_info(target);
    Ok((resolved.id, resolved.provider))
}

pub(super) fn parse_route_policy_text(
    text: &str,
) -> Result<crate::llm::api::LlmRoutePolicy, VmError> {
    use crate::llm::api::LlmRoutePolicy;
    let text = text.trim();
    let lower = text.to_ascii_lowercase();
    let arg = |name: &str| -> Option<String> {
        lower
            .strip_prefix(name)
            .and_then(|rest| rest.strip_prefix('('))
            .and_then(|rest| rest.strip_suffix(')'))
            .map(|_| text[name.len() + 1..text.len() - 1].trim().to_string())
    };
    if text.is_empty() || lower == "manual" {
        return Ok(LlmRoutePolicy::Manual);
    }
    if let Some(target) = arg("always") {
        return Ok(LlmRoutePolicy::Always(target));
    }
    if let Some(target) = arg("cheapest_over_quality") {
        return Ok(LlmRoutePolicy::CheapestOverQuality(target));
    }
    if let Some(target) = arg("fastest_over_quality") {
        return Ok(LlmRoutePolicy::FastestOverQuality(target));
    }
    Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(format!(
        "route_policy: expected manual, always(id), cheapest_over_quality(t), or fastest_over_quality(t), got {text:?}"
    )))))
}

pub(super) fn vm_string_list(value: &VmValue) -> Vec<String> {
    let mut out = Vec::new();
    let mut push = |text: String| {
        let text = text.trim().to_string();
        if !text.is_empty() && !out.iter().any(|existing| existing == &text) {
            out.push(text);
        }
    };
    match value {
        VmValue::List(items) => {
            for item in items.iter() {
                push(item.display());
            }
        }
        VmValue::String(text) => {
            for item in text.split(',') {
                push(item.to_string());
            }
        }
        other => push(other.display()),
    }
    out
}

pub(super) fn parse_route_policy_option(
    options: Option<&BTreeMap<String, VmValue>>,
) -> Result<crate::llm::api::LlmRoutePolicy, VmError> {
    use crate::llm::api::LlmRoutePolicy;
    let Some(raw) = options.and_then(|o| o.get("route_policy")) else {
        if let Some(prefer) = options.and_then(|o| o.get("prefer")) {
            let targets = vm_string_list(prefer);
            if !targets.is_empty() {
                let strategy = options
                    .and_then(|o| o.get("fallback_strategy").or_else(|| o.get("strategy")))
                    .map(|value| value.display())
                    .unwrap_or_else(|| "prefer_order".to_string());
                return Ok(LlmRoutePolicy::PreferenceList { targets, strategy });
            }
        }
        return Ok(LlmRoutePolicy::Manual);
    };
    match raw {
        VmValue::Nil => Ok(LlmRoutePolicy::Manual),
        VmValue::Bool(false) => Ok(LlmRoutePolicy::Manual),
        VmValue::String(text) => parse_route_policy_text(text),
        VmValue::Dict(d) => {
            let mode = d
                .get("mode")
                .map(|value| value.display())
                .unwrap_or_else(|| "manual".to_string());
            let target = d
                .get("target")
                .or_else(|| d.get("quality"))
                .or_else(|| d.get("id"))
                .map(|value| value.display())
                .unwrap_or_default();
            match mode.as_str() {
                "manual" => Ok(LlmRoutePolicy::Manual),
                "always" => Ok(LlmRoutePolicy::Always(target)),
                "cheapest_over_quality" => Ok(LlmRoutePolicy::CheapestOverQuality(target)),
                "fastest_over_quality" => Ok(LlmRoutePolicy::FastestOverQuality(target)),
                "preference_list" | "prefer" => {
                    let targets = d
                        .get("targets")
                        .or_else(|| d.get("prefer"))
                        .map(vm_string_list)
                        .unwrap_or_default();
                    if targets.is_empty() {
                        return Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
                            "route_policy.prefer: expected at least one model/provider target",
                        ))));
                    }
                    let strategy = d
                        .get("strategy")
                        .or_else(|| d.get("fallback_strategy"))
                        .map(|value| value.display())
                        .unwrap_or_else(|| "prefer_order".to_string());
                    Ok(LlmRoutePolicy::PreferenceList { targets, strategy })
                }
                other => Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
                    format!("route_policy.mode: unsupported value {other:?}"),
                )))),
            }
        }
        _ => Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
            "route_policy: expected string or dict",
        )))),
    }
}

pub(super) fn parse_fallback_chain_option(
    options: Option<&BTreeMap<String, VmValue>>,
) -> Vec<String> {
    let Some(raw) = options.and_then(|o| o.get("fallback_chain")) else {
        return Vec::new();
    };
    let mut out = Vec::new();
    let mut push = |value: String| {
        let value = value.trim().to_string();
        if !value.is_empty() && !out.iter().any(|existing| existing == &value) {
            out.push(value);
        }
    };
    match raw {
        VmValue::List(list) => {
            for item in list.iter() {
                push(item.display());
            }
        }
        VmValue::String(text) => {
            for item in text.split(',') {
                push(item.to_string());
            }
        }
        _ => {}
    }
    out
}

pub(super) fn parse_equivalent_failover_option(
    options: Option<&BTreeMap<String, VmValue>>,
    provider: &str,
    model: &str,
    explicit_routing_policy: bool,
) -> Result<Option<std::sync::Arc<crate::llm::routing::RoutingPolicyConfig>>, VmError> {
    let Some(raw) = options.and_then(|o| o.get("equivalent_failover")) else {
        return Ok(None);
    };

    let mut enabled = true;
    let mut max_routes = DEFAULT_EQUIVALENT_FAILOVER_MAX_ROUTES;
    match raw {
        VmValue::Nil | VmValue::Bool(false) => return Ok(None),
        VmValue::Bool(true) => {}
        VmValue::Dict(dict) => {
            if let Some(value) = dict.get("enabled") {
                enabled = match value {
                    VmValue::Nil => true,
                    VmValue::Bool(value) => *value,
                    other => {
                        return Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
                            format!(
                                "equivalent_failover.enabled: expected bool, got {}",
                                other.type_name()
                            ),
                        ))));
                    }
                };
            }
            if let Some(value) = dict.get("max_routes") {
                let raw_max = value.as_int().ok_or_else(|| {
                    VmError::Thrown(VmValue::String(std::sync::Arc::from(
                        "equivalent_failover.max_routes: expected a positive integer",
                    )))
                })?;
                if raw_max < 1 {
                    return Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
                        "equivalent_failover.max_routes: expected a positive integer",
                    ))));
                }
                max_routes = raw_max as usize;
            }
        }
        other => {
            return Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
                format!(
                    "equivalent_failover: expected bool or dict, got {}",
                    other.type_name()
                ),
            ))));
        }
    }

    if !enabled {
        return Ok(None);
    }
    if explicit_routing_policy {
        return Err(VmError::Thrown(VmValue::String(std::sync::Arc::from(
            "equivalent_failover cannot be combined with explicit routing_policy(...)",
        ))));
    }

    Ok(crate::llm::routing::build_equivalent_failover_policy(
        provider, model, max_routes,
    ))
}

pub(super) fn route_alternative(
    provider: String,
    model: String,
    selected: bool,
    reason: String,
) -> crate::llm::api::LlmRouteAlternative {
    let quality_tier = crate::llm_config::model_tier(&model);
    let pricing = crate::llm::cost::pricing_per_1k_for(&provider, &model);
    crate::llm::api::LlmRouteAlternative {
        available: provider_key_available(&provider),
        cost_per_1k_in: pricing.map(|p| p.0),
        cost_per_1k_out: pricing.map(|p| p.1),
        latency_p50_ms: crate::llm::cost::latency_p50_ms_for(&provider),
        provider,
        model,
        quality_tier,
        selected,
        reason,
    }
}

pub(super) fn resolve_route_policy(
    policy: &crate::llm::api::LlmRoutePolicy,
    current_provider: &str,
    current_model: &str,
) -> Result<Option<crate::llm::api::LlmRoutingDecision>, VmError> {
    use crate::llm::api::{LlmRoutePolicy, LlmRoutingDecision};

    match policy {
        LlmRoutePolicy::Manual => Ok(None),
        LlmRoutePolicy::Always(target) => {
            let (model, provider) = route_target_from_short(target)?;
            Ok(Some(LlmRoutingDecision {
                policy: policy.as_label(),
                requested_quality: None,
                selected_provider: provider.clone(),
                selected_model: model.clone(),
                alternatives: vec![route_alternative(
                    provider,
                    model,
                    true,
                    "pinned by always".to_string(),
                )],
            }))
        }
        LlmRoutePolicy::CheapestOverQuality(target)
        | LlmRoutePolicy::FastestOverQuality(target) => {
            let requested_rank = quality_rank(target);
            let mut alternatives = crate::llm_config::all_model_candidates()
                .into_iter()
                .filter(|(model, _)| {
                    quality_rank(&crate::llm_config::model_tier(model)) >= requested_rank
                })
                .map(|(model, provider)| {
                    route_alternative(provider, model, false, "candidate".to_string())
                })
                .collect::<Vec<_>>();

            if alternatives.is_empty() {
                alternatives.push(route_alternative(
                    current_provider.to_string(),
                    current_model.to_string(),
                    false,
                    "fallback_current_route".to_string(),
                ));
            }

            let score_cost = |alt: &crate::llm::api::LlmRouteAlternative| -> f64 {
                alt.cost_per_1k_in.unwrap_or(f64::INFINITY)
                    + alt.cost_per_1k_out.unwrap_or(f64::INFINITY)
            };
            let selected_idx = alternatives
                .iter()
                .enumerate()
                .filter(|(_, alt)| alt.available)
                .min_by(|(_, left), (_, right)| {
                    let left_score = match policy {
                        LlmRoutePolicy::CheapestOverQuality(_) => score_cost(left),
                        LlmRoutePolicy::FastestOverQuality(_) => {
                            left.latency_p50_ms.unwrap_or(u64::MAX) as f64
                        }
                        _ => unreachable!(),
                    };
                    let right_score = match policy {
                        LlmRoutePolicy::CheapestOverQuality(_) => score_cost(right),
                        LlmRoutePolicy::FastestOverQuality(_) => {
                            right.latency_p50_ms.unwrap_or(u64::MAX) as f64
                        }
                        _ => unreachable!(),
                    };
                    left_score
                        .partial_cmp(&right_score)
                        .unwrap_or(std::cmp::Ordering::Equal)
                        .then_with(|| left.provider.cmp(&right.provider))
                        .then_with(|| left.model.cmp(&right.model))
                })
                .map(|(idx, _)| idx)
                .unwrap_or(0);

            alternatives[selected_idx].selected = true;
            alternatives[selected_idx].reason = "selected".to_string();
            let selected = alternatives[selected_idx].clone();
            Ok(Some(LlmRoutingDecision {
                policy: policy.as_label(),
                requested_quality: Some(target.clone()),
                selected_provider: selected.provider,
                selected_model: selected.model,
                alternatives,
            }))
        }
        LlmRoutePolicy::PreferenceList { targets, strategy } => {
            let mut alternatives = Vec::new();
            for target in targets {
                let (model, provider) = route_target_from_short(target)?;
                if alternatives
                    .iter()
                    .any(|alt: &crate::llm::api::LlmRouteAlternative| {
                        alt.provider == provider && alt.model == model
                    })
                {
                    continue;
                }
                alternatives.push(route_alternative(
                    provider,
                    model,
                    false,
                    "candidate".to_string(),
                ));
            }
            if alternatives.is_empty() {
                alternatives.push(route_alternative(
                    current_provider.to_string(),
                    current_model.to_string(),
                    false,
                    "fallback_current_route".to_string(),
                ));
            }
            let normalized = strategy.trim().to_ascii_lowercase();
            let score_cost = |alt: &crate::llm::api::LlmRouteAlternative| -> f64 {
                alt.cost_per_1k_in.unwrap_or(f64::INFINITY)
                    + alt.cost_per_1k_out.unwrap_or(f64::INFINITY)
            };
            let selected_idx = alternatives
                .iter()
                .enumerate()
                .filter(|(_, alt)| alt.available)
                .min_by(
                    |(left_idx, left), (right_idx, right)| match normalized.as_str() {
                        "cheapest_first" | "cheapest" => score_cost(left)
                            .partial_cmp(&score_cost(right))
                            .unwrap_or(std::cmp::Ordering::Equal)
                            .then_with(|| left_idx.cmp(right_idx)),
                        "fastest_first" | "fastest" => left
                            .latency_p50_ms
                            .unwrap_or(u64::MAX)
                            .cmp(&right.latency_p50_ms.unwrap_or(u64::MAX))
                            .then_with(|| left_idx.cmp(right_idx)),
                        _ => left_idx.cmp(right_idx),
                    },
                )
                .map(|(idx, _)| idx)
                .unwrap_or(0);
            alternatives[selected_idx].selected = true;
            alternatives[selected_idx].reason = "selected".to_string();
            let selected = alternatives[selected_idx].clone();
            Ok(Some(LlmRoutingDecision {
                policy: policy.as_label(),
                requested_quality: None,
                selected_provider: selected.provider,
                selected_model: selected.model,
                alternatives,
            }))
        }
    }
}
