//! 模型路由器。
//!
//! 不接调用点(那是调用方:agentic.rs / title.rs / knowledge_inject.rs / project.rs /
//! df-ideas / df-nodes ai_node.rs)。
//!
//! ModelRouter 为单元结构,select 是无状态关联函数。

// 调用点经 `df_ai::router::{Modality, Capability, CostTier, IntelligenceTier}`
// 直接 import 维度枚举构造 TaskRequirements,re-export 避免调用点
// 各自从 df_ai_core::model 取(跨 crate 路径冗长)。select/select_model_id 仅借用枚举,无重定义。
// 注:CostTier/IntelligenceTier 路由已解耦——provider /v1/models API
// 不返回这两维度,数据无客观依据不可信,不参与硬路由;re-export 保留供未来真实判别源。
// ModelTier 从 crate::intent re-export(同 crate,无跨 crate 路径问题),供调用点构造
// `tier: suggested_model_tier(&intent)` 传入,router 同 weight 时按 tier tiebreak。
pub use crate::intent::ModelTier;
pub use df_ai_core::model::{Capability, CostTier, IntelligenceTier, Modality, ModelConfig};

/// 任务对模型的需求(4 维度)。
///
/// 由调用点构造,描述本次调用需要什么模态/能力/上下文/档位,
/// 交 ModelRouter::select 在候选池中选最优模型。
///
/// 路由已解耦:原 `min_intelligence`/`max_cost` 两字段删除。
/// provider /v1/models API 不返回 cost_tier/intelligence,这两维度 100% 靠预设表写死 +
/// 模型名启发式猜,数据无客观依据不可信,不应参与硬路由。枚举(CostTier/IntelligenceTier)
/// 保留供未来出现真实判别源时再接回。
///
/// `tier`(子项 2 根因修复):任务建议的模型档位(由 `intent::suggested_model_tier` 派生,
/// 或无意图场景传 None)。原 `max_by_key(weight)` 同 weight 返最后一个,顺序敏感无语义;
/// 接 tier 后,同 weight 时优先选 `intelligence` 满足 tier 下限的候选(见 `tier_match`)。
#[derive(Debug, Clone)]
pub struct TaskRequirements {
    /// 任务所需的模态集合(全子集匹配:任务所需模态都必须在模型模态里)
    pub modalities: Vec<Modality>,
    /// 是否需要工具调用能力(needs_tool_use=true 时候选必须含 Capability::ToolUse)
    pub needs_tool_use: bool,
    /// 预估上下文大小(tokens,模型 context_window 必须 >= 此值)。
    /// 调用点应传 TokenEstimator 估值而非 0(0 = 当前空操作,窗口过滤维度失效)。
    pub estimated_context: usize,
    /// 任务建议的模型档位(意图→ModelTier,无意图场景 None)。
    /// 同 weight 候选间按 tier tiebreak(满足 tier 下限的候选胜)。
    pub tier: Option<ModelTier>,
}

/// 模型路由器(单元结构,无状态)。
///
/// select 为关联函数:给定需求 + 候选池,执行过滤链选最优模型。
pub struct ModelRouter;

/// ModelTier → IntelligenceTier 下限映射(子项 2 tier tiebreak 用)。
///
/// 任务建议档位(ModelTier:F-Heavy)映射到模型智力下限(IntelligenceTier),
/// 同 weight 候选间优先选 `model.intelligence >= 下限` 的(满足任务复杂度需求)。
/// - `Fast` → `Lite`(轻量意图,任何模型都满足)
/// - `Standard` → `Standard`(日常,需 Standard 及以上)
/// - `Heavy` → `Plus`(复杂推理,需 Plus 及以上)
fn tier_min_intelligence(tier: ModelTier) -> IntelligenceTier {
    match tier {
        ModelTier::Fast => IntelligenceTier::Lite,
        ModelTier::Standard => IntelligenceTier::Standard,
        ModelTier::Heavy => IntelligenceTier::Plus,
    }
}

/// 同 weight tiebreak:候选是否满足任务建议档位的智力下限。
///
/// 返 `bool`(满足 = true)。调用方在 `max_by` 闭包内 `a_match.cmp(&b_match)` 把 bool 转 Ordering:
/// a 满足而 b 不满足 → Greater(a 胜);都满足/都不满足 → Equal(max_by 并列返最后一个)。
/// - `req.tier = None`(无意图场景,标题/扫描/压缩):恒 true(所有候选等价,保留旧行为)。
/// - `req.tier = Some(t)`:返 `model_intel >= tier_min_intelligence(t)`。
fn tier_match(model_intel: IntelligenceTier, req_tier: Option<ModelTier>) -> bool {
    req_tier
        .map(|t| model_intel >= tier_min_intelligence(t))
        .unwrap_or(true) // None → 视作满足(tiebreak 维度不参与,保旧行为)
}

impl ModelRouter {
    /// 在候选池中选出最优模型(过滤链)。
    ///
    /// 步骤:
    /// 1. enabled — 只选启用的
    /// 2. 模态匹配 — 任务所需模态全在模型模态里
    /// 3. 能力匹配 — needs_tool_use 时候选必须含 ToolUse
    /// 4. 窗口够大 — context_window >= estimated_context
    /// 5. max_by 选最优:**主键 weight 降序**(权重高者胜),**同 weight 时按 tier tiebreak**
    ///    (满足任务建议档位 `intelligence >= tier_min` 的候选胜)。
    ///
    /// tier tiebreak(子项 2 根因修复):原 `max_by_key(weight)` 同 weight 返最后一个,
    /// 顺序敏感无语义(intent suggested_model_tier 恒 None)→ 现接 `req.tier`
    /// (由 intent→ModelTier 派生),同 weight 时优先选满足档位下限的候选。
    /// `req.tier = None` 时 tiebreak 维度退化为等价(保留旧行为,标题/扫描路径无回归)。
    ///
    /// 路由已解耦:原「智力达标」/「成本可控」两步删除,
    /// 原第 7 步排序的 `Reverse(cost_tier)` 同权重选便宜也已删除——排序主键 weight 主导,
    /// tiebreak 由 tier(基于 intelligence,有客观档位映射依据)替代纯 max_by_key 顺序。
    pub fn select<'a>(req: &TaskRequirements, pool: &'a [ModelConfig]) -> Option<&'a ModelConfig> {
        pool.iter()
            .filter(|m| m.enabled) // 1. 只选启用的
            .filter(|m| req.modalities.iter().all(|r| m.modalities.contains(r))) // 2. 模态匹配
            .filter(|m| !req.needs_tool_use || m.capabilities.contains(&Capability::ToolUse)) // 3. 能力匹配
            .filter(|m| m.context_window >= req.estimated_context) // 4. 窗口够大
            // 5. 主键 weight 降序,同 weight 时 tier tiebreak(满足档位下限的候选胜)。
            // max_by 语义:comparator 返 a 相对 b 的 Ordering,Greater = a 胜;
            // 同 key(全 Equal)时 max_by 返最后一个(对齐原 max_by_key 并列返最后的语义)。
            .max_by(|a, b| {
                // 主键:weight,a 大则 a 胜(Greater)。
                let by_weight = a.weight.cmp(&b.weight);
                if by_weight != std::cmp::Ordering::Equal {
                    return by_weight;
                }
                // tiebreak:tier 满足度。a 满足档位下限而 b 不满足 → a 胜(Greater)。
                // tier_match 返 bool,bool 比较:true > false(满足 > 不满足)。
                let a_match = tier_match(a.intelligence, req.tier);
                let b_match = tier_match(b.intelligence, req.tier);
                a_match.cmp(&b_match)
            })
    }
}

/// 调用点 helper — 路由选模型并直接返回 model_id(纯函数)。
///
/// 给定 TaskRequirements + 候选池,返回最优模型的 `model_id`。
/// 调用点用法:`provider.model_configs`(Vec<ModelConfig>)→ `select_model_id(&req, &pool)`
///   → `Option<String>`;None 时兜底 `provider.default_model`(行为不变,平滑过渡)。
///
/// 行为不变保证:
/// - 池空(用户未通过 Settings 拉取模型)→ 返回 None → 调用点兜底 default_model
/// - 池非空但无候选满足需求 → 返回 None → 兜底 default_model
/// - 池非空命中 → 返回 model_id(F-01 路由目标,拉取即启用路由)
pub fn select_model_id(req: &TaskRequirements, pool: &[ModelConfig]) -> Option<String> {
    ModelRouter::select(req, pool).map(|m| m.model_id.clone())
}

#[cfg(test)]
mod tests {
    use super::*;

    /// 构造一个全维度可定制的 ModelConfig(默认全过过滤,调用方按需覆盖字段)。
    fn model(model_id: &str) -> ModelConfig {
        ModelConfig {
            model_id: model_id.into(),
            enabled: true,
            label: None,
            modalities: vec![Modality::Text],
            capabilities: vec![Capability::ToolUse],
            cost_tier: CostTier::Medium,
            intelligence: IntelligenceTier::Standard,
            weight: 50,
            context_window: 8192,
            probe_source: None,
        }
    }

    /// 构造一个宽松需求(默认全过过滤,调用方按需覆盖字段)。tier=None 保留旧行为。
    fn req() -> TaskRequirements {
        TaskRequirements {
            modalities: vec![Modality::Text],
            needs_tool_use: false,
            estimated_context: 0,
            tier: None,
        }
    }

    // ── select_model_id helper ──

    #[test]
    fn select_model_id_empty_pool_returns_none() {
        // 池空(用户未拉取模型)→ None,调用点兜底 default_model
        let pool: Vec<ModelConfig> = vec![];
        assert!(select_model_id(&req(), &pool).is_none());
    }

    #[test]
    fn select_model_id_hit_returns_model_id() {
        // 池非空命中 → 返回 model_id 字符串(非引用)
        let pool = vec![model("glm-4-flash")];
        assert_eq!(select_model_id(&req(), &pool).as_deref(), Some("glm-4-flash"));
    }

    // ── 步骤 1:enabled 过滤 ──

    #[test]
    fn empty_pool_returns_none() {
        let pool: Vec<ModelConfig> = vec![];
        assert!(ModelRouter::select(&req(), &pool).is_none());
    }

    #[test]
    fn all_disabled_returns_none() {
        let pool = vec![
            ModelConfig {
                enabled: false,
                ..model("a")
            },
            ModelConfig {
                enabled: false,
                ..model("b")
            },
        ];
        assert!(ModelRouter::select(&req(), &pool).is_none());
    }

    // ── 步骤 2:模态匹配 ──

    #[test]
    fn modality_mismatch_filtered() {
        // 任务需要 Vision,但池里模型只有 Text
        let pool = vec![model("text-only")];
        let r = TaskRequirements {
            modalities: vec![Modality::Vision],
            ..req()
        };
        assert!(ModelRouter::select(&r, &pool).is_none());
    }

    #[test]
    fn modality_subset_multimodal_required_filters_text_only() {
        // 多模态子集匹配:任务需 [Text, Vision](两个模态都得支持),
        // 模型仅 [Text](缺 Vision)→ 步骤 2 `.all(|r| m.modalities.contains(r))` 不成立,滤掉。
        // 区别于 modality_mismatch_filtered(单模态 Vision):本测覆盖「任务需多模态全子集」路径。
        let pool = vec![ModelConfig {
            modalities: vec![Modality::Text],
            ..model("text-only")
        }];
        let r = TaskRequirements {
            modalities: vec![Modality::Text, Modality::Vision],
            ..req()
        };
        assert!(ModelRouter::select(&r, &pool).is_none());

        // 对照:模型补全 Vision 后通过(Text+Vision ⊇ 任务需求 Text+Vision)
        let pool_ok = vec![ModelConfig {
            modalities: vec![Modality::Text, Modality::Vision],
            ..model("vision-capable")
        }];
        assert_eq!(
            ModelRouter::select(&r, &pool_ok).unwrap().model_id,
            "vision-capable"
        );
    }

    #[test]
    fn enabled_false_excluded_from_routing() {
        // enabled=false 不参与路由:混池里禁用候选 weight 更高(100),
        // 但应被步骤 1 `.filter(|m| m.enabled)` 滤掉,只选 enabled=true 的低 weight 候选。
        // 区别于 all_disabled_returns_none(全禁用返 None):本测覆盖「部分禁用」混池场景。
        let pool = vec![
            ModelConfig {
                enabled: false,
                weight: 100, // 若未被 enabled 过滤,weight 100 会胜
                ..model("disabled-heavy")
            },
            ModelConfig {
                enabled: true,
                weight: 30,
                ..model("enabled-light")
            },
        ];
        assert_eq!(
            ModelRouter::select(&req(), &pool).unwrap().model_id,
            "enabled-light"
        );
    }

    #[test]
    fn empty_candidate_pool_returns_none() {
        // 空候选池 → None(与 select_model_id_empty_pool_returns_none 对应,
        // 本测覆盖 select() 关联函数本身而非 helper;语义等价但断言点不同)。
        let pool: Vec<ModelConfig> = vec![];
        assert!(ModelRouter::select(&req(), &pool).is_none());
    }

    // ── 步骤 3:能力匹配(ToolUse) ──

    #[test]
    fn needs_tool_use_filters_non_tool() {
        // 任务需要 ToolUse,池里模型 capabilities 无 ToolUse
        let pool = vec![ModelConfig {
            capabilities: vec![Capability::Embedding],
            ..model("embed-only")
        }];
        let r = TaskRequirements {
            needs_tool_use: true,
            ..req()
        };
        assert!(ModelRouter::select(&r, &pool).is_none());
    }

    #[test]
    fn needs_tool_use_false_allows_non_tool() {
        // 任务不需 ToolUse,池里模型无 ToolUse 仍入选
        let pool = vec![ModelConfig {
            capabilities: vec![Capability::Embedding],
            ..model("embed-only")
        }];
        let r = TaskRequirements {
            needs_tool_use: false,
            ..req()
        };
        assert_eq!(
            ModelRouter::select(&r, &pool).unwrap().model_id,
            "embed-only"
        );
    }

    // ── 步骤 4(原智力/成本过滤已解耦):窗口够大 ──

    #[test]
    fn context_window_insufficient() {
        // 任务预估 100000,池里模型 8192
        let pool = vec![model("small-window")];
        let r = TaskRequirements {
            estimated_context: 100_000,
            ..req()
        };
        assert!(ModelRouter::select(&r, &pool).is_none());
    }

    #[test]
    fn estimated_context_filters_small_window_model() {
        // 子项 1 根因修复回归测:estimated_context 非零(调用点传 TokenEstimator 估值,非死代码 0)
        // → 步骤 4 窗口过滤生效。两候选:小窗口(4K)weight 90(高诱惑)+ 大窗口(128K)weight 50。
        // 任务预估 8K 上下文 → 小窗口模型被滤,只剩大窗口候选胜(即使 weight 低)。
        // 若调用点回退传 0(原 bug),两候选窗口都 >= 0,weight 90 的小窗口模型会胜(误选)。
        let pool = vec![
            ModelConfig {
                weight: 90,
                context_window: 4096, // 小窗口,高 weight 诱惑
                ..model("small-window-heavy")
            },
            ModelConfig {
                weight: 50,
                context_window: 131072, // 大窗口,低 weight
                ..model("large-window-light")
            },
        ];
        let r = TaskRequirements {
            estimated_context: 8000, // 任务预估 8K,小窗口模型装不下
            ..req()
        };
        assert_eq!(
            ModelRouter::select(&r, &pool).unwrap().model_id,
            "large-window-light",
            "estimated_context 非零应滤掉小窗口候选,即使其 weight 更高"
        );
    }

    // ── 步骤 5:max_by(weight 主键,tier tiebreak) ──

    #[test]
    fn single_match_returns_it() {
        let pool = vec![model("only-one")];
        assert_eq!(
            ModelRouter::select(&req(), &pool).unwrap().model_id,
            "only-one"
        );
    }

    #[test]
    fn weight_priority_higher_wins() {
        // 两候选都满足,weight 70 胜 50
        let pool = vec![
            ModelConfig {
                weight: 50,
                ..model("low-weight")
            },
            ModelConfig {
                weight: 70,
                ..model("high-weight")
            },
        ];
        assert_eq!(
            ModelRouter::select(&req(), &pool).unwrap().model_id,
            "high-weight"
        );
    }

    #[test]
    fn same_weight_picks_first_match() {
        // 同 weight 70,tier=None(req() 默认):tiebreak 维度退等价,max_by 遇并列返最后一个
        // (rust Iterator::max_by 语义,与原 max_by_key 一致)。验证同 weight + tier=None
        // 不再按 cost 取舍,行为对齐接入 tier tiebreak 前的语义(标题/扫描路径无回归)。
        let pool = vec![
            ModelConfig {
                weight: 70,
                cost_tier: CostTier::High,
                ..model("expensive")
            },
            ModelConfig {
                weight: 70,
                cost_tier: CostTier::Low,
                ..model("low-cost")
            },
        ];
        assert_eq!(
            ModelRouter::select(&req(), &pool).unwrap().model_id,
            "low-cost"
        );
    }

    #[test]
    fn all_dimensions_match_picks_best() {
        // 3+ 候选各维度参差,验证过滤链全过 + max_by(weight, tier) 选最优。
        // (智力/成本过滤已解耦,原步骤 4/5 删除,候选 d 不再因 intelligence 滤掉)
        //
        // 候选:
        //  a: weight 60  → 通过全部过滤
        //  b: weight 80  → 通过 — weight 次高档(与 c 并列,但 tier=None 故 tiebreak 退等价)
        //  c: weight 80  → 通过 — 同 weight 80,tier=None 时 max_by 返并列最后一个
        //  d: weight 90  → 通过(intelligence 不参与过滤)— weight 最高,胜
        //  e: enabled=false → 步骤 1 滤掉
        //
        // 预期:d 胜(weight 90 最高,tier tiebreak 不触发因 weight 已决出胜负)
        let pool = vec![
            ModelConfig {
                weight: 60,
                cost_tier: CostTier::Medium,
                intelligence: IntelligenceTier::Standard,
                ..model("a")
            },
            ModelConfig {
                weight: 80,
                cost_tier: CostTier::High,
                intelligence: IntelligenceTier::Plus,
                ..model("b")
            },
            ModelConfig {
                weight: 80,
                cost_tier: CostTier::Low,
                intelligence: IntelligenceTier::Plus,
                ..model("c")
            },
            ModelConfig {
                weight: 90,
                cost_tier: CostTier::Low,
                intelligence: IntelligenceTier::Lite,
                ..model("d")
            },
            ModelConfig {
                enabled: false,
                weight: 100,
                ..model("e")
            },
        ];
        let r = TaskRequirements {
            modalities: vec![Modality::Text],
            needs_tool_use: true,
            estimated_context: 0,
            tier: None,
        };
        assert_eq!(ModelRouter::select(&r, &pool).unwrap().model_id, "d");
    }

    // ── 步骤 5 tiebreak(子项 2):同 weight 时 tier 决胜 ──

    #[test]
    fn tier_tiebreak_heavy_prefers_meeting_model() {
        // 子项 2 根因修复:同 weight 时,任务建议 Heavy(req.tier=Some(Heavy))→ tier_min=Plus,
        // 满足 intelligence>=Plus 的候选胜过不满足的。
        // 候选 a:Standard(不满足 Plus),候选 b:Plus(满足),同 weight 50。
        // 预期:b 胜(满足 Heavy 档位下限)。原 max_by_key 会返最后一个(顺序敏感无语义)。
        let pool = vec![
            ModelConfig {
                weight: 50,
                intelligence: IntelligenceTier::Standard,
                ..model("a_standard")
            },
            ModelConfig {
                weight: 50,
                intelligence: IntelligenceTier::Plus,
                ..model("b_plus")
            },
        ];
        let r = TaskRequirements {
            tier: Some(ModelTier::Heavy),
            ..req()
        };
        assert_eq!(
            ModelRouter::select(&r, &pool).unwrap().model_id,
            "b_plus",
            "同 weight 时 Heavy 档位应优先选 Plus(满足)而非 Standard(不满足)"
        );
    }

    #[test]
    fn tier_tiebreak_none_preserves_max_by_key_semantics() {
        // tier=None(标题/扫描/压缩无意图场景)→ tiebreak 维度退等价,
        // max_by 同 key 返最后一个(对齐原 max_by_key 行为,无回归)。
        // 候选 a/b 同 weight 70,顺序 a 在前 b 在后 → 预期返 b(max_by 并列返最后)。
        let pool = vec![
            ModelConfig {
                weight: 70,
                intelligence: IntelligenceTier::Standard,
                ..model("a")
            },
            ModelConfig {
                weight: 70,
                intelligence: IntelligenceTier::Plus,
                ..model("b")
            },
        ];
        // tier=None 时即使 b 的 intelligence 更高也不应胜(tiebreak 不参与),保 max_by_key 语义。
        assert_eq!(ModelRouter::select(&req(), &pool).unwrap().model_id, "b");
    }

    #[test]
    fn tier_tiebreak_chat_fast_any_model_meets_lite() {
        // 任务建议 Fast → tier_min=Lite,任何模型 intelligence>=Lite(Lite 是最低档)→ 都满足。
        // 故 Fast 档位下 tiebreak 退等价(都满足),max_by 同 weight 返最后一个,行为不变。
        let pool = vec![
            ModelConfig {
                weight: 50,
                intelligence: IntelligenceTier::Lite,
                ..model("a_lite")
            },
            ModelConfig {
                weight: 50,
                intelligence: IntelligenceTier::Ultra,
                ..model("b_ultra")
            },
        ];
        let r = TaskRequirements {
            tier: Some(ModelTier::Fast),
            ..req()
        };
        // 都满足 Lite 下限 → tiebreak 等价 → max_by 返最后一个 = b_ultra
        assert_eq!(ModelRouter::select(&r, &pool).unwrap().model_id, "b_ultra");
    }

    #[test]
    fn tier_tiebreak_weight_still_dominates() {
        // tier 不凌驾 weight:weight 高者永远胜,即使低 weight 候选满足 tier 而高 weight 不满足。
        // 候选 a:weight 90,Standard(不满足 Heavy/Plus)。
        // 候选 b:weight 50,Plus(满足 Heavy)。
        // 预期:a 胜(weight 主键优先,tiebreak 只在 weight 相同时触发)。
        let pool = vec![
            ModelConfig {
                weight: 90,
                intelligence: IntelligenceTier::Standard,
                ..model("a_heavy_weight")
            },
            ModelConfig {
                weight: 50,
                intelligence: IntelligenceTier::Plus,
                ..model("b_meets_tier")
            },
        ];
        let r = TaskRequirements {
            tier: Some(ModelTier::Heavy),
            ..req()
        };
        assert_eq!(
            ModelRouter::select(&r, &pool).unwrap().model_id,
            "a_heavy_weight",
            "weight 主键应凌驾 tier tiebreak"
        );
    }
}
