use std::collections::BTreeMap;

use crate::value::{VmError, VmValue};

tokio::task_local! {
    static COST_ROUTE_STACK: Vec<BTreeMap<String, VmValue>>;
}

fn strategy_text(config: &BTreeMap<String, VmValue>) -> Option<String> {
    config
        .get("fallback_strategy")
        .or_else(|| config.get("strategy"))
        .map(|value| value.display())
        .filter(|value| !value.trim().is_empty())
}

fn quality_text(config: &BTreeMap<String, VmValue>) -> String {
    config
        .get("quality")
        .or_else(|| config.get("min_quality"))
        .map(|value| value.display())
        .filter(|value| !value.trim().is_empty())
        .unwrap_or_else(|| "mid".to_string())
}

fn route_policy_dict(
    mode: &str,
    target: Option<String>,
    prefer: Option<VmValue>,
    strategy: Option<String>,
) -> VmValue {
    let mut dict = BTreeMap::new();
    dict.insert(
        "mode".to_string(),
        VmValue::String(std::sync::Arc::from(mode.to_string())),
    );
    if let Some(target) = target {
        dict.insert(
            "target".to_string(),
            VmValue::String(std::sync::Arc::from(target)),
        );
    }
    if let Some(prefer) = prefer {
        dict.insert("prefer".to_string(), prefer);
    }
    if let Some(strategy) = strategy {
        dict.insert(
            "strategy".to_string(),
            VmValue::String(std::sync::Arc::from(strategy)),
        );
    }
    VmValue::Dict(std::sync::Arc::new(dict))
}

fn merge_budget_aliases(options: &mut BTreeMap<String, VmValue>) {
    let Some(budget_usd) = options.get("budget_usd").cloned() else {
        return;
    };
    let mut budget = match options.get("budget") {
        Some(VmValue::Dict(existing)) => existing.as_ref().clone(),
        _ => BTreeMap::new(),
    };
    budget
        .entry("max_cost_usd".to_string())
        .or_insert(budget_usd.clone());
    options
        .entry("max_cost_usd".to_string())
        .or_insert(budget_usd);
    options.insert(
        "budget".to_string(),
        VmValue::Dict(std::sync::Arc::new(budget)),
    );
}

fn normalize_config(mut config: BTreeMap<String, VmValue>) -> BTreeMap<String, VmValue> {
    merge_budget_aliases(&mut config);
    if config.contains_key("route_policy") {
        return config;
    }

    if let Some(prefer) = config.get("prefer").cloned() {
        let strategy = strategy_text(&config).unwrap_or_else(|| "prefer_order".to_string());
        config.insert(
            "route_policy".to_string(),
            route_policy_dict("preference_list", None, Some(prefer), Some(strategy)),
        );
        return config;
    }

    if let Some(strategy) = strategy_text(&config) {
        let normalized = strategy.trim().to_ascii_lowercase();
        let mode = match normalized.as_str() {
            "cheapest_first" | "cheapest" => Some("cheapest_over_quality"),
            "fastest_first" | "fastest" => Some("fastest_over_quality"),
            _ => None,
        };
        if let Some(mode) = mode {
            config.insert(
                "route_policy".to_string(),
                route_policy_dict(mode, Some(quality_text(&config)), None, None),
            );
        }
    }

    config
}

fn merge_budget(inherited: Option<&VmValue>, explicit: Option<&VmValue>) -> Option<VmValue> {
    let mut merged = match inherited {
        Some(VmValue::Dict(dict)) => dict.as_ref().clone(),
        Some(value) => {
            let mut dict = BTreeMap::new();
            dict.insert("max_cost_usd".to_string(), value.clone());
            dict
        }
        None => BTreeMap::new(),
    };
    if let Some(VmValue::Dict(dict)) = explicit {
        for (key, value) in dict.iter() {
            merged.insert(key.clone(), value.clone());
        }
    } else if let Some(value) = explicit {
        merged.insert("max_cost_usd".to_string(), value.clone());
    }
    (!merged.is_empty()).then(|| VmValue::Dict(std::sync::Arc::new(merged)))
}

pub(crate) fn merge_context_options(
    explicit: Option<BTreeMap<String, VmValue>>,
) -> Option<BTreeMap<String, VmValue>> {
    let inherited = COST_ROUTE_STACK
        .try_with(|stack| {
            let mut merged = BTreeMap::new();
            for frame in stack.iter() {
                for (key, value) in frame {
                    merged.insert(key.clone(), value.clone());
                }
            }
            merged
        })
        .unwrap_or_default();

    if inherited.is_empty() {
        return explicit;
    }

    let mut merged = inherited;
    if let Some(explicit) = explicit {
        let budget = merge_budget(merged.get("budget"), explicit.get("budget"));
        for (key, value) in explicit {
            merged.insert(key, value);
        }
        if let Some(budget) = budget {
            merged.insert("budget".to_string(), budget);
        }
    }
    Some(merged)
}

pub(crate) async fn cost_route_impl(
    ctx: &crate::vm::AsyncBuiltinCtx,
    args: Vec<VmValue>,
) -> Result<VmValue, VmError> {
    let config = match args.first().and_then(VmValue::as_dict) {
        Some(config) => normalize_config(config.clone()),
        None => {
            return Err(VmError::Runtime(
                "cost_route: first argument must be a config dict".to_string(),
            ))
        }
    };
    let closure = match args.get(1) {
        Some(VmValue::Closure(closure)) => closure.clone(),
        _ => {
            return Err(VmError::Runtime(
                "cost_route: second argument must be a closure".to_string(),
            ))
        }
    };

    let mut child_vm = ctx.child_vm();
    let mut stack = COST_ROUTE_STACK
        .try_with(|current| current.clone())
        .unwrap_or_default();
    stack.push(config);
    let result = COST_ROUTE_STACK
        .scope(stack, child_vm.call_closure_pub(&closure, &[]))
        .await;
    ctx.forward_output(&child_vm.take_output());
    result
}
