//! 飞轮阶段三：执行质量风险画像（预测层）。
//!
//! 从 commit_metrics（scope 级）+ commit_files（文件级）聚合历史返工率，
//! 供 agent 开工前通过 MCP 工具 `predict_task_risk` 评估任务风险。
//! 全部查询走 conn 注入（R06），聚合与分级逻辑可用 in-memory DB 独立单测。

use rusqlite::Connection;
use serde::Serialize;

/// scope 级返工画像。
#[derive(Debug, Serialize)]
pub struct ScopeRisk {
    pub scope: String,
    pub commits: u32,
    pub rework: u32,
    pub rework_rate: f64,
}

/// 单文件返工画像。
#[derive(Debug, Serialize)]
pub struct FileRisk {
    pub path: String,
    pub commits: u32,
    pub rework: u32,
    pub rework_rate: f64,
}

/// predict_task_risk 的完整返回。
#[derive(Debug, Serialize)]
pub struct TaskRiskPrediction {
    pub project_id: String,
    pub scope_stats: Option<ScopeRisk>,
    pub file_stats: Vec<FileRisk>,
    /// 结论依据的样本量（scope 命中 commit 数；无 scope 时取文件最大命中数）
    pub sample_size: u32,
    /// low = 样本 <5 或历史数据脏（conventional 率 <0.5），结论仅供参考
    pub confidence: String,
    /// low / medium / high，取 scope 与文件返工率的较高者分级
    pub risk_level: String,
    pub notes: Vec<String>,
}

/// 返工率分级阈值：≥0.35 high、≥0.15 medium、其余 low。纯函数。
pub fn classify_risk(rate: f64) -> &'static str {
    if rate >= 0.35 {
        "high"
    } else if rate >= 0.15 {
        "medium"
    } else {
        "low"
    }
}

fn rate(rework: u32, commits: u32) -> f64 {
    if commits == 0 {
        0.0
    } else {
        (rework as f64 / commits as f64 * 1000.0).round() / 1000.0
    }
}

/// scope 级聚合：精确匹配或前缀匹配（"auth" 命中 "auth" 与 "auth/login"）。
fn scope_risk(conn: &Connection, project_id: &str, scope: &str) -> Result<Option<ScopeRisk>, String> {
    let (commits, rework): (u32, u32) = conn
        .query_row(
            "SELECT COUNT(*),
                    COALESCE(SUM(CASE WHEN is_rework = 1 AND is_ci_auto = 0 THEN 1 ELSE 0 END), 0)
             FROM commit_metrics
             WHERE project_id = ?1 AND (scope = ?2 OR scope LIKE ?2 || '/%')",
            rusqlite::params![project_id, scope],
            |r| Ok((r.get(0)?, r.get(1)?)),
        )
        .map_err(|e| e.to_string())?;
    if commits == 0 {
        return Ok(None);
    }
    Ok(Some(ScopeRisk {
        scope: scope.to_string(),
        rework_rate: rate(rework, commits),
        commits,
        rework,
    }))
}

/// 文件级聚合：每个文件在该项目历史中被多少 commit 触碰、其中多少是返工。
fn file_risks(conn: &Connection, project_id: &str, files: &[String]) -> Result<Vec<FileRisk>, String> {
    let mut result = Vec::new();
    for path in files {
        let (commits, rework): (u32, u32) = conn
            .query_row(
                "SELECT COUNT(*),
                        COALESCE(SUM(CASE WHEN cm.is_rework = 1 AND cm.is_ci_auto = 0 THEN 1 ELSE 0 END), 0)
                 FROM commit_files cf
                 JOIN commit_metrics cm ON cm.sha = cf.sha
                 WHERE cm.project_id = ?1 AND cf.path = ?2",
                rusqlite::params![project_id, path],
                |r| Ok((r.get(0)?, r.get(1)?)),
            )
            .map_err(|e| e.to_string())?;
        if commits > 0 {
            result.push(FileRisk {
                path: path.clone(),
                rework_rate: rate(rework, commits),
                commits,
                rework,
            });
        }
    }
    // 返工次数降序，agent 一眼看到最烫的文件
    result.sort_by(|a, b| b.rework.cmp(&a.rework));
    Ok(result)
}

/// 跨项目高风险 scope（梦核数据段）。
#[derive(Debug, Serialize)]
pub struct HighRiskScope {
    pub project_name: String,
    pub scope: String,
    pub commits: u32,
    pub rework: u32,
    pub rework_rate: f64,
}

/// 跨项目聚合：样本 ≥ min_commits 且至少 1 次返工的 scope，按返工率降序取 top limit。
/// 项目名从 project_profiles 解析，登记缺失时回退 project_id。
pub fn high_risk_scopes(
    conn: &Connection,
    min_commits: u32,
    limit: usize,
) -> Result<Vec<HighRiskScope>, String> {
    let mut stmt = conn
        .prepare(
            "SELECT COALESCE(pp.name, cm.project_id) AS pname, cm.scope,
                    COUNT(*) AS c,
                    COALESCE(SUM(CASE WHEN cm.is_rework = 1 AND cm.is_ci_auto = 0 THEN 1 ELSE 0 END), 0) AS rw
             FROM commit_metrics cm
             LEFT JOIN project_workspaces pw ON pw.id = cm.project_id
             LEFT JOIN project_profiles pp ON pp.id = pw.profile_id
             WHERE cm.scope IS NOT NULL
             GROUP BY cm.project_id, cm.scope
             HAVING c >= ?1 AND rw > 0
             ORDER BY CAST(rw AS REAL) / c DESC, c DESC
             LIMIT ?2",
        )
        .map_err(|e| e.to_string())?;
    let rows = stmt
        .query_map(rusqlite::params![min_commits, limit as i64], |r| {
            Ok((
                r.get::<_, String>(0)?,
                r.get::<_, String>(1)?,
                r.get::<_, u32>(2)?,
                r.get::<_, u32>(3)?,
            ))
        })
        .map_err(|e| e.to_string())?;
    let mut result = Vec::new();
    for row in rows {
        let (project_name, scope, commits, rework) = row.map_err(|e| e.to_string())?;
        result.push(HighRiskScope {
            project_name,
            scope,
            rework_rate: rate(rework, commits),
            commits,
            rework,
        });
    }
    Ok(result)
}

/// Tauri command：跨项目返工热区（FlywheelPanel「返工热区」块）。薄包装，逻辑在 high_risk_scopes。
#[tauri::command]
pub fn get_high_risk_scopes(
    min_commits: Option<u32>,
    limit: Option<u32>,
) -> Result<Vec<HighRiskScope>, String> {
    let conn = crate::db::pool().get().map_err(|e| e.to_string())?;
    high_risk_scopes(&conn, min_commits.unwrap_or(5), limit.unwrap_or(10) as usize)
}

/// 核心入口：按 scope 和/或文件清单预测任务风险。
pub fn predict_task_risk(
    conn: &Connection,
    project_id: &str,
    scope: Option<&str>,
    files: &[String],
) -> Result<TaskRiskPrediction, String> {
    if scope.is_none() && files.is_empty() {
        return Err("scope 与 files 至少提供一个".to_string());
    }

    let scope_stats = match scope {
        Some(s) if !s.trim().is_empty() => scope_risk(conn, project_id, s.trim())?,
        _ => None,
    };
    let file_stats = file_risks(conn, project_id, files)?;

    let mut notes = Vec::new();

    // 脏历史检测：conventional 率 <0.5 时结论不可信（数据可信度提示，与 onboarding 卡 D 同源）
    let (total, conv): (u32, u32) = conn
        .query_row(
            "SELECT COUNT(*),
                    COALESCE(SUM(CASE WHEN commit_type IS NOT NULL THEN 1 ELSE 0 END), 0)
             FROM commit_metrics WHERE project_id = ?1",
            [project_id],
            |r| Ok((r.get(0)?, r.get(1)?)),
        )
        .map_err(|e| e.to_string())?;
    let dirty_history = total > 0 && (conv as f64 / total as f64) < 0.5;
    if dirty_history {
        notes.push(format!(
            "历史数据脏：conventional 率 {:.0}%（{}/{}），建议种 lefthook 后以新数据为准",
            conv as f64 / total as f64 * 100.0,
            conv,
            total
        ));
    }

    let sample_size = scope_stats
        .as_ref()
        .map(|s| s.commits)
        .unwrap_or_else(|| file_stats.iter().map(|f| f.commits).max().unwrap_or(0));

    // 有效返工率取 scope 与文件中的较高者（文件样本 ≥3 才参与，避免单次 fix 拉爆）
    let scope_rate = scope_stats.as_ref().map(|s| s.rework_rate).unwrap_or(0.0);
    let file_rate = file_stats
        .iter()
        .filter(|f| f.commits >= 3)
        .map(|f| f.rework_rate)
        .fold(0.0_f64, f64::max);
    let effective_rate = scope_rate.max(file_rate);

    let confidence = if sample_size < 5 || dirty_history {
        if sample_size < 5 {
            notes.push(format!("样本量 {sample_size} <5，结论仅供参考"));
        }
        "low"
    } else {
        "normal"
    };

    Ok(TaskRiskPrediction {
        project_id: project_id.to_string(),
        scope_stats,
        file_stats,
        sample_size,
        confidence: confidence.to_string(),
        risk_level: classify_risk(effective_rate).to_string(),
        notes,
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::commands::commit_metrics::{record_commit_entry, GitLogEntry};

    fn seed(conn: &Connection, pid: &str, sha: &str, subject: &str, files: &[&str]) {
        let entry = GitLogEntry {
            sha: sha.into(),
            subject: subject.into(),
            committed_at: "2026-07-04T00:00:00+09:00".into(),
            rules_trailer: String::new(),
            files: files.iter().map(|s| s.to_string()).collect(),
        };
        record_commit_entry(conn, pid, &entry).unwrap();
    }

    #[test]
    fn classify_thresholds() {
        assert_eq!(classify_risk(0.4), "high");
        assert_eq!(classify_risk(0.35), "high");
        assert_eq!(classify_risk(0.2), "medium");
        assert_eq!(classify_risk(0.1), "low");
    }

    #[test]
    fn high_rework_scope_flagged_high() {
        let conn = crate::db::conn_with_schema();
        // onboarding scope：6 commits 里 3 条 fix（含一个子 scope），返工率 0.5
        for (i, subj) in [
            "feat(onboarding): a",
            "fix(onboarding): b",
            "fix(onboarding/pack): c",
            "feat(onboarding): d",
            "fix(onboarding): e",
            "chore(onboarding): f",
        ]
        .iter()
        .enumerate()
        {
            seed(&conn, "p1", &format!("s{i}"), subj, &[]);
        }
        let r = predict_task_risk(&conn, "p1", Some("onboarding"), &[]).unwrap();
        let s = r.scope_stats.unwrap();
        assert_eq!(s.commits, 6, "前缀匹配应包含子 scope");
        assert_eq!(s.rework, 3);
        assert_eq!(r.risk_level, "high");
        assert_eq!(r.confidence, "normal");
    }

    #[test]
    fn small_sample_low_confidence() {
        let conn = crate::db::conn_with_schema();
        seed(&conn, "p1", "s1", "fix(auth): x", &[]);
        seed(&conn, "p1", "s2", "feat(auth): y", &[]);
        let r = predict_task_risk(&conn, "p1", Some("auth"), &[]).unwrap();
        assert_eq!(r.confidence, "low");
        assert!(r.notes.iter().any(|n| n.contains("样本量")), "应有小样本提示");
    }

    #[test]
    fn dirty_history_noted_and_low_confidence() {
        let conn = crate::db::conn_with_schema();
        // 10 条里仅 3 条 conventional → 脏历史
        for i in 0..7 {
            seed(&conn, "p1", &format!("junk{i}"), "update stuff", &[]);
        }
        seed(&conn, "p1", "c1", "feat(api): a", &[]);
        seed(&conn, "p1", "c2", "feat(api): b", &[]);
        seed(&conn, "p1", "c3", "fix(api): c", &[]);
        // api scope 样本 5 条以下也行，这里重点断言脏历史标记
        let r = predict_task_risk(&conn, "p1", Some("api"), &[]).unwrap();
        assert_eq!(r.confidence, "low");
        assert!(r.notes.iter().any(|n| n.contains("历史数据脏")));
    }

    #[test]
    fn file_level_rework_surfaces_hot_file() {
        let conn = crate::db::conn_with_schema();
        // hot.rs 被 4 个 commit 触碰，其中 2 个 fix；cold.rs 只有 feat
        seed(&conn, "p1", "s1", "feat(a): x", &["src/hot.rs", "src/cold.rs"]);
        seed(&conn, "p1", "s2", "fix(a): y", &["src/hot.rs"]);
        seed(&conn, "p1", "s3", "fix(a): z", &["src/hot.rs"]);
        seed(&conn, "p1", "s4", "feat(a): w", &["src/hot.rs"]);
        let r = predict_task_risk(
            &conn,
            "p1",
            None,
            &["src/hot.rs".into(), "src/cold.rs".into(), "src/ghost.rs".into()],
        )
        .unwrap();
        assert_eq!(r.file_stats.len(), 2, "无历史的文件不进结果");
        assert_eq!(r.file_stats[0].path, "src/hot.rs", "返工多的文件排前");
        assert_eq!(r.file_stats[0].rework, 2);
        assert_eq!(r.risk_level, "high", "hot.rs 返工率 0.5 应分级 high");
    }

    #[test]
    fn high_risk_scopes_orders_by_rate_with_min_sample() {
        let conn = crate::db::conn_with_schema();
        // proj-a/hot：4 commits 2 fix（rate 0.5）；proj-b/warm：5 commits 1 fix（rate 0.2）
        // proj-a/tiny：2 commits 1 fix——低于 min_commits=3 应被过滤
        for (sha, subj) in [
            ("a1", "fix(hot): x"), ("a2", "fix(hot): y"), ("a3", "feat(hot): z"), ("a4", "chore(hot): w"),
            ("b1", "fix(warm): x"), ("b2", "feat(warm): a"), ("b3", "feat(warm): b"),
            ("b4", "feat(warm): c"), ("b5", "feat(warm): d"),
            ("t1", "fix(tiny): x"), ("t2", "feat(tiny): y"),
        ] {
            let pid = if sha.starts_with('a') || sha.starts_with('t') { "proj-a" } else { "proj-b" };
            seed(&conn, pid, sha, subj, &[]);
        }
        let top = high_risk_scopes(&conn, 3, 10).unwrap();
        assert_eq!(top.len(), 2, "tiny 样本不足应被过滤");
        assert_eq!(top[0].scope, "hot", "返工率高的排前");
        assert_eq!(top[0].project_name, "proj-a", "无登记项目回退 project_id");
        assert_eq!(top[1].scope, "warm");
    }

    #[test]
    fn requires_scope_or_files() {
        let conn = crate::db::conn_with_schema();
        assert!(predict_task_risk(&conn, "p1", None, &[]).is_err());
    }

    #[test]
    fn ci_auto_fix_excluded_from_rework() {
        let conn = crate::db::conn_with_schema();
        seed(&conn, "p1", "s1", "fix(ci): auto-fix [DeepSeek-V3]", &[]);
        seed(&conn, "p1", "s2", "feat(ci): x", &[]);
        let r = predict_task_risk(&conn, "p1", Some("ci"), &[]).unwrap();
        assert_eq!(r.scope_stats.unwrap().rework, 0, "CI 自修不计返工");
    }
}
