//! 接入包模板分发（基线档）。
//!
//! 版本化分发（T1，2026-07-03）：AGENTS.md / git-workflow.md 的炼境管理内容
//! 包在 ONBOARDING_MANAGED 标记块中——文件不存在写全新；已有标记原位升级块；
//! 无标记则末尾追加块。块外用户内容永不触碰。
//! lefthook.yml 因用户常深度定制（monorepo filter 等），保持"存在即跳过"。
//! 模板蓝本取自 enterprise-system，已通用化。

use serde::{Deserialize, Serialize};
use std::path::Path;

const MANAGED_START: &str = "<!-- ONBOARDING_MANAGED_START";
const MANAGED_END: &str = "<!-- ONBOARDING_MANAGED_END -->";

// 标准模板（编译期内嵌）
const TMPL_AGENTS: &str = include_str!("../../resources/onboarding/AGENTS.md.tmpl");
const TMPL_LEFTHOOK: &str = include_str!("../../resources/onboarding/lefthook.yml.tmpl");
const TMPL_GIT_WORKFLOW: &str = include_str!("../../resources/onboarding/git-workflow.md.tmpl");

/// 单个模板的分发结果。
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
pub struct TemplateResult {
    /// 相对项目根的目标路径
    pub name: String,
    /// "written" | "skipped" | "failed"
    pub action: String,
    pub detail: String,
}

/// 分发单个模板：存在则跳过（绝不覆盖），不存在则写入。
fn dispatch_one(root: &Path, relative: &str, content: &str) -> TemplateResult {
    let target = root.join(relative);
    if target.exists() {
        return TemplateResult {
            name: relative.to_string(),
            action: "skipped".into(),
            detail: "已存在，未覆盖".into(),
        };
    }
    if let Some(parent) = target.parent() {
        if let Err(e) = std::fs::create_dir_all(parent) {
            return TemplateResult {
                name: relative.to_string(),
                action: "failed".into(),
                detail: format!("建目录失败: {e}"),
            };
        }
    }
    match std::fs::write(&target, content) {
        Ok(_) => TemplateResult {
            name: relative.to_string(),
            action: "written".into(),
            detail: "已写入".into(),
        },
        Err(e) => TemplateResult {
            name: relative.to_string(),
            action: "failed".into(),
            detail: format!("写入失败: {e}"),
        },
    }
}

/// 从模板中提取 ONBOARDING_MANAGED 块（含首尾标记行）。
fn extract_managed_block(template: &str) -> Option<&str> {
    let start = template.find(MANAGED_START)?;
    let end = template.find(MANAGED_END)? + MANAGED_END.len();
    template.get(start..end)
}

/// 从标记行提取版本号（`version="x.y.z"`）。
fn block_version(text: &str) -> Option<&str> {
    let start = text.find(MANAGED_START)?;
    let rest = &text[start..];
    let v = rest.find("version=\"")? + "version=\"".len();
    let tail = &rest[v..];
    tail.split('"').next()
}

/// 版本化分发（三态）：不存在→写全新；有标记→原位升级块；无标记→末尾追加块。
fn dispatch_versioned(root: &Path, relative: &str, template: &str) -> TemplateResult {
    let target = root.join(relative);
    let tpl_block = match extract_managed_block(template) {
        Some(b) => b,
        None => return dispatch_one(root, relative, template), // 模板无标记，退化为旧行为
    };
    let tpl_ver = block_version(tpl_block).unwrap_or("?");

    if !target.exists() {
        return dispatch_one(root, relative, template);
    }
    let content = match std::fs::read_to_string(&target) {
        Ok(c) => c,
        Err(e) => {
            return TemplateResult {
                name: relative.to_string(),
                action: "failed".into(),
                detail: format!("读取失败: {e}"),
            }
        }
    };

    let (new_content, action, detail) = if let (Some(s), Some(e)) =
        (content.find(MANAGED_START), content.find(MANAGED_END))
    {
        let old_ver = block_version(&content).unwrap_or("?").to_string();
        if old_ver == tpl_ver {
            return TemplateResult {
                name: relative.to_string(),
                action: "skipped".into(),
                detail: format!("管理块已是 v{tpl_ver}"),
            };
        }
        let mut nc = String::new();
        nc.push_str(&content[..s]);
        nc.push_str(tpl_block);
        nc.push_str(&content[e + MANAGED_END.len()..]);
        (nc, "updated", format!("管理块 v{old_ver} → v{tpl_ver}，块外内容未动"))
    } else {
        (
            format!("{}\n\n{}\n", content.trim_end(), tpl_block),
            "appended",
            format!("追加管理块 v{tpl_ver}（存量文件无标记，原内容保留在上方）"),
        )
    };

    match std::fs::write(&target, new_content) {
        Ok(_) => TemplateResult {
            name: relative.to_string(),
            action: action.into(),
            detail,
        },
        Err(e) => TemplateResult {
            name: relative.to_string(),
            action: "failed".into(),
            detail: format!("写入失败: {e}"),
        },
    }
}

/// 向项目分发全部基线模板，返回每个的处理结果。
/// project_path 为项目根（WSL 项目为 \\wsl.localhost\... UNC 路径，可直接写）。
pub fn dispatch_templates(project_path: &str) -> Vec<TemplateResult> {
    let root = Path::new(project_path.trim_end_matches(['/', '\\']));
    vec![
        dispatch_versioned(root, "AGENTS.md", TMPL_AGENTS),
        dispatch_one(root, "lefthook.yml", TMPL_LEFTHOOK),
        dispatch_versioned(root, "docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW),
    ]
}

// ── V1：lefthook 激活（分发 ≠ 生效，文件写入后必须 lefthook install 挂上 hook）──

/// lefthook 激活结果。status: "activated" | "missing_binary" | "failed"
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct HookActivation {
    pub status: String,
    pub detail: String,
}

/// 执行 lefthook install：优先项目本地 node_modules/.bin，回退全局 PATH。
/// Windows 的 PATH 兜底须额外尝试 "lefthook.cmd"——pnpm 全局安装的是 .cmd shim，
/// 而 CreateProcess 对裸名 "lefthook" 只解析 .exe（2026-07-04 批量接入实测踩坑）。
fn run_lefthook_install(root: &Path) -> HookActivation {
    let mut candidates: Vec<std::path::PathBuf> = Vec::new();
    #[cfg(target_os = "windows")]
    {
        candidates.push(root.join("node_modules/.bin/lefthook.exe"));
        candidates.push(root.join("node_modules/.bin/lefthook.cmd"));
    }
    #[cfg(not(target_os = "windows"))]
    candidates.push(root.join("node_modules/.bin/lefthook"));

    let local = candidates.into_iter().find(|p| p.exists());
    let mut attempts: Vec<(String, &str)> = Vec::new();
    match &local {
        Some(p) => attempts.push((p.to_string_lossy().to_string(), "项目本地")),
        None => {
            attempts.push(("lefthook".to_string(), "全局 PATH"));
            #[cfg(target_os = "windows")]
            attempts.push(("lefthook.cmd".to_string(), "全局 PATH(.cmd shim)"));
        }
    }

    for (program, source) in &attempts {
        match crate::silent_cmd(program).arg("install").current_dir(root).output() {
            Ok(out) if out.status.success() => {
                return HookActivation {
                    status: "activated".into(),
                    detail: format!("lefthook install 完成（{source}）"),
                }
            }
            Ok(out) => {
                return HookActivation {
                    status: "failed".into(),
                    detail: format!(
                        "lefthook install 退出码 {:?}：{}",
                        out.status.code(),
                        String::from_utf8_lossy(&out.stderr).trim()
                    ),
                }
            }
            Err(e) if e.kind() == std::io::ErrorKind::NotFound => continue,
            Err(e) => {
                return HookActivation {
                    status: "failed".into(),
                    detail: format!("执行失败: {e}"),
                }
            }
        }
    }
    HookActivation {
        status: "missing_binary".into(),
        detail: "未找到 lefthook（node_modules/.bin 与 PATH 均无）。安装：pnpm add -g lefthook 或项目内 pnpm add -D lefthook 后重跑接入包".into(),
    }
}

/// 接入包应用报告：模板分发结果 + hook 激活 + buff 状态。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct OnboardingReport {
    pub templates: Vec<TemplateResult>,
    pub hook: HookActivation,
    pub buff: crate::commands::buff::BuffStatus,
}

/// 应用接入包（基线档）：分发标准模板 + 激活 lefthook + 挂 buff（炼境登记 + git 观测/飞轮采集）。
#[tauri::command]
pub fn apply_onboarding_pack(
    project_path: String,
    project_id: String,
) -> Result<OnboardingReport, String> {
    let templates = dispatch_templates(&project_path);
    let hook = run_lefthook_install(Path::new(project_path.trim_end_matches(['/', '\\'])));
    let buff = crate::commands::buff::apply_blueprint_buff(project_path, project_id)?;
    Ok(OnboardingReport { templates, hook, buff })
}

// ── V1：接入生效验收（verify_onboarding 检查链，判定逻辑纯函数化可单测）──────

/// 单项验收结果。
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct VerifyCheck {
    pub name: String,
    pub pass: bool,
    pub detail: String,
}

/// 纯函数：pre-commit hook 内容是否由 lefthook 接管。
fn hook_is_lefthook(content: &str) -> bool {
    content.contains("lefthook")
}

/// 纯函数：从 .git/config 文本提取 origin remote 的 url。
fn parse_origin_url(git_config: &str) -> Option<String> {
    let mut in_origin = false;
    for line in git_config.lines() {
        let t = line.trim();
        if t.starts_with('[') {
            in_origin = t.replace(' ', "").eq_ignore_ascii_case("[remote\"origin\"]");
            continue;
        }
        if in_origin {
            if let Some(rest) = t.strip_prefix("url") {
                if let Some(v) = rest.trim_start().strip_prefix('=') {
                    return Some(v.trim().to_string());
                }
            }
        }
    }
    None
}

/// 纯函数：提取 URL 的 host（支持 https:// 与 git@host: 两种形式，忽略端口与用户名）。
fn url_host(url: &str) -> Option<String> {
    let u = url.trim();
    if let Some(rest) = u.split("://").nth(1) {
        let authority = rest.split('/').next()?;
        let host = authority.rsplit('@').next()?.split(':').next()?;
        return Some(host.to_lowercase());
    }
    // ssh 形式 git@host:owner/repo.git
    let after_at = u.rsplit('@').next()?;
    let host = after_at.split([':', '/']).next()?;
    if host.contains('.') {
        Some(host.to_lowercase())
    } else {
        None
    }
}

/// 纯函数：管理块版本是否与最新模板一致。
fn check_managed_version(content: Option<&str>, expected: &str) -> (bool, String) {
    match content.and_then(block_version) {
        Some(v) if v == expected => (true, format!("管理块 v{v}（最新）")),
        Some(v) => (false, format!("管理块 v{v}，最新模板 v{expected}，重跑接入包可升级")),
        None => (false, "文件缺失或无管理块标记".into()),
    }
}

/// 纯函数：origin host 是否命中已登记的 Gitea 实例。
fn check_origin(origin_host: Option<&str>, instance_hosts: &[String]) -> (bool, String) {
    match origin_host {
        None => (false, "未配置 origin remote（或无 .git）".into()),
        Some(h) if instance_hosts.is_empty() => {
            (false, format!("origin={h}，但炼境未登记任何 Gitea 实例"))
        }
        Some(h) if instance_hosts.iter().any(|i| i == h) => {
            (true, format!("origin → {h}（已登记 Gitea 实例）"))
        }
        Some(h) => (
            false,
            format!("origin={h}，不在已登记 Gitea 实例（{}）中", instance_hosts.join(", ")),
        ),
    }
}

/// 接入生效验收：五项检查链。分发成功 ≠ 功能生效，本函数验证"真的用起来了"。
/// conn 由调用方注入（R06）。
pub fn verify_onboarding(
    conn: &rusqlite::Connection,
    project_id: &str,
    root: &Path,
) -> Vec<VerifyCheck> {
    let read = |rel: &str| std::fs::read_to_string(root.join(rel)).ok();
    let mut checks = Vec::new();

    // 1/2 模板管理块版本到位（期望版本从内嵌模板自省，不硬编码）
    for (rel, tmpl, label) in [
        ("AGENTS.md", TMPL_AGENTS, "AGENTS.md 管理块版本"),
        ("docs/ai-context/git-workflow.md", TMPL_GIT_WORKFLOW, "git-workflow 管理块版本"),
    ] {
        let expected = extract_managed_block(tmpl).and_then(block_version).unwrap_or("?");
        let (pass, detail) = check_managed_version(read(rel).as_deref(), expected);
        checks.push(VerifyCheck { name: label.into(), pass, detail });
    }

    // 3 lefthook hook 真挂在 .git/hooks
    let (pass, detail) = match read(".git/hooks/pre-commit") {
        Some(c) if hook_is_lefthook(&c) => (true, "pre-commit 由 lefthook 接管".to_string()),
        Some(_) => (false, "pre-commit 存在但非 lefthook（被其他工具占用？）".to_string()),
        None => (false, "无 pre-commit hook——lefthook install 未执行".to_string()),
    };
    checks.push(VerifyCheck { name: "lefthook hook 挂载".into(), pass, detail });

    // 4 origin 指向已登记 Gitea 实例
    let origin_host = read(".git/config")
        .as_deref()
        .and_then(parse_origin_url)
        .as_deref()
        .and_then(url_host);
    let mut instance_hosts: Vec<String> = Vec::new();
    if let Ok(mut stmt) = conn.prepare("SELECT url FROM gitea_instances") {
        if let Ok(rows) = stmt.query_map([], |r| r.get::<_, String>(0)) {
            for u in rows.flatten() {
                if let Some(h) = url_host(&u) {
                    instance_hosts.push(h);
                }
            }
        }
    }
    let (pass, detail) = check_origin(origin_host.as_deref(), &instance_hosts);
    checks.push(VerifyCheck { name: "origin 指向 Gitea".into(), pass, detail });

    // 5 CI workflow 存在
    let yml_count = std::fs::read_dir(root.join(".gitea/workflows"))
        .map(|rd| {
            rd.flatten()
                .filter(|e| {
                    e.path()
                        .extension()
                        .map(|x| x == "yml" || x == "yaml")
                        .unwrap_or(false)
                })
                .count()
        })
        .unwrap_or(0);
    checks.push(VerifyCheck {
        name: "CI workflow 存在".into(),
        pass: yml_count > 0,
        detail: if yml_count > 0 {
            format!(".gitea/workflows/ 有 {yml_count} 个 workflow")
        } else {
            "无 .gitea/workflows/*.yml——可调 distribute_cicd 分发".into()
        },
    });

    // 6 飞轮数据在流入（近 30 天 commit_metrics）
    let recent: i64 = conn
        .query_row(
            "SELECT COUNT(*) FROM commit_metrics WHERE project_id = ?1 AND committed_at >= datetime('now','-30 days')",
            rusqlite::params![project_id],
            |r| r.get(0),
        )
        .unwrap_or(0);
    checks.push(VerifyCheck {
        name: "飞轮 commit 数据流入".into(),
        pass: recent > 0,
        detail: if recent > 0 {
            format!("近 30 天 {recent} 条 commit 已采集")
        } else {
            "近 30 天无数据——确认 buff 已挂且有 commit 活动（或调 poll_now）".into()
        },
    });

    checks
}

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

    #[test]
    fn writes_then_skips_never_overwrites() {
        let base = std::env::temp_dir()
            .join(format!("lianjing_onboarding_test_{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&base);
        std::fs::create_dir_all(&base).unwrap();
        let pp = base.to_str().unwrap();

        // 首次：全部写入
        let r1 = dispatch_templates(pp);
        assert!(r1.iter().all(|t| t.action == "written"), "首次应全部写入");
        assert!(base.join("AGENTS.md").exists());
        assert!(base.join("lefthook.yml").exists());
        assert!(base.join("docs/ai-context/git-workflow.md").exists());

        // 改一个文件（无管理块标记），验证用户内容不被覆盖：
        // T1 后的契约——AGENTS/git-workflow 追加管理块（原内容保留），lefthook 仍跳过
        std::fs::write(base.join("AGENTS.md"), "用户自己的内容").unwrap();
        let r2 = dispatch_templates(pp);
        let agents_r = r2.iter().find(|t| t.name == "AGENTS.md").unwrap();
        assert_eq!(agents_r.action, "appended", "无标记存量文件应追加管理块");
        let kept = std::fs::read_to_string(base.join("AGENTS.md")).unwrap();
        assert!(kept.starts_with("用户自己的内容"), "用户内容必须保留在原位");
        let lefthook_r = r2.iter().find(|t| t.name == "lefthook.yml").unwrap();
        assert_eq!(lefthook_r.action, "skipped", "lefthook 保持存在即跳过");

        let _ = std::fs::remove_dir_all(&base);
    }

    // ── T1：版本化三态分发 ────────────────────────────────────────────────

    #[test]
    fn versioned_dispatch_three_states() {
        let base = std::env::temp_dir()
            .join(format!("lianjing_onb_ver_test_{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&base);
        std::fs::create_dir_all(&base).unwrap();
        let pp = base.to_str().unwrap();

        // 态一：不存在 → 写全新（含管理块标记）
        let r1 = dispatch_templates(pp);
        assert!(r1.iter().all(|t| t.action == "written"));
        let agents = std::fs::read_to_string(base.join("AGENTS.md")).unwrap();
        assert!(agents.contains(MANAGED_START) && agents.contains(MANAGED_END));

        // 态二（同版本）：有标记且版本一致 → skipped，文件不动
        let r2 = dispatch_templates(pp);
        let agents_r = r2.iter().find(|t| t.name == "AGENTS.md").unwrap();
        assert_eq!(agents_r.action, "skipped");

        // 态二（旧版本）：把版本改旧 + 块外加用户内容 → updated，块升级、用户内容保留
        let old = agents
            .replace("version=\"1.2.0\"", "version=\"1.0.0\"")
            .replace("## 项目概况", "## 项目概况

我的项目说明")
            + "
## 用户自加的章节
内容
";
        std::fs::write(base.join("AGENTS.md"), &old).unwrap();
        let r3 = dispatch_templates(pp);
        let agents_r = r3.iter().find(|t| t.name == "AGENTS.md").unwrap();
        assert_eq!(agents_r.action, "updated");
        let upgraded = std::fs::read_to_string(base.join("AGENTS.md")).unwrap();
        assert!(upgraded.contains("version=\"1.2.0\""), "块应升级到模板版本");
        assert!(upgraded.contains("我的项目说明"), "块外用户内容（前）须保留");
        assert!(upgraded.contains("用户自加的章节"), "块外用户内容（后）须保留");
        assert_eq!(upgraded.matches(MANAGED_START).count(), 1, "只有一个管理块");

        // 态三：存量文件无标记 → appended，原内容保留在上方
        std::fs::write(base.join("AGENTS.md"), "# 手写的 AGENTS

自定义内容").unwrap();
        let r4 = dispatch_templates(pp);
        let agents_r = r4.iter().find(|t| t.name == "AGENTS.md").unwrap();
        assert_eq!(agents_r.action, "appended");
        let appended = std::fs::read_to_string(base.join("AGENTS.md")).unwrap();
        assert!(appended.starts_with("# 手写的 AGENTS"));
        assert!(appended.contains(MANAGED_START));

        // lefthook 始终保持"存在即跳过"
        let lefthook_r = r4.iter().find(|t| t.name == "lefthook.yml").unwrap();
        assert_eq!(lefthook_r.action, "skipped");

        let _ = std::fs::remove_dir_all(&base);
    }

    // ── V1：接入生效验收纯函数 ────────────────────────────────────────────

    #[test]
    fn hook_detection_covers_mounted_and_foreign() {
        assert!(hook_is_lefthook("#!/bin/sh\ncall lefthook run pre-commit"), "lefthook 接管应判 true");
        assert!(!hook_is_lefthook("#!/bin/sh\nnpx husky run"), "非 lefthook hook 应判 false");
    }

    #[test]
    fn origin_parsing_and_host_extraction() {
        let cfg = "[core]\n\trepositoryformatversion = 0\n[remote \"origin\"]\n\turl = https://git.example.com/o/r.git\n\tfetch = +refs/heads/*:refs/remotes/origin/*\n[remote \"mirror\"]\n\turl = https://gitee.com/x/y.git\n";
        assert_eq!(
            parse_origin_url(cfg).as_deref(),
            Some("https://git.example.com/o/r.git"),
            "应取 origin 而非其他 remote"
        );
        assert_eq!(parse_origin_url("[core]\n\tbare = false"), None, "无 origin 应返回 None");

        assert_eq!(url_host("https://git.example.com/o/r.git").as_deref(), Some("git.example.com"));
        assert_eq!(url_host("https://user@git.example.com:3000/o/r.git").as_deref(), Some("git.example.com"), "应忽略用户名与端口");
        assert_eq!(url_host("git@git.example.com:o/r.git").as_deref(), Some("git.example.com"), "ssh 形式");
    }

    #[test]
    fn origin_check_match_and_mismatch() {
        let hosts = vec!["git.example.com".to_string()];
        assert!(check_origin(Some("git.example.com"), &hosts).0, "命中实例应 pass");
        assert!(!check_origin(Some("github.com"), &hosts).0, "未命中应 fail");
        assert!(!check_origin(None, &hosts).0, "无 origin 应 fail");
        assert!(!check_origin(Some("git.example.com"), &[]).0, "无登记实例应 fail");
    }

    #[test]
    fn verify_onboarding_full_chain() {
        let conn = crate::db::conn_with_schema();
        let base = std::env::temp_dir()
            .join(format!("lianjing_verify_test_{}", std::process::id()));
        let _ = std::fs::remove_dir_all(&base);
        std::fs::create_dir_all(&base).unwrap();

        // 空目录：六项全不通过（真实反映"什么都没接"）
        let checks = verify_onboarding(&conn, "p1", &base);
        assert_eq!(checks.len(), 6, "检查链应为六项");
        assert!(checks.iter().all(|c| !c.pass), "空项目应全部不通过");

        // 铺齐全部生效条件后：六项全通过
        dispatch_templates(base.to_str().unwrap());
        std::fs::create_dir_all(base.join(".git/hooks")).unwrap();
        std::fs::write(base.join(".git/hooks/pre-commit"), "call lefthook run pre-commit").unwrap();
        std::fs::write(
            base.join(".git/config"),
            "[remote \"origin\"]\n\turl = https://git.example.com/o/r.git\n",
        )
        .unwrap();
        std::fs::create_dir_all(base.join(".gitea/workflows")).unwrap();
        std::fs::write(base.join(".gitea/workflows/pr-check.yml"), "name: x").unwrap();
        conn.execute(
            "INSERT INTO gitea_instances (id, name, url, access_token) VALUES ('i1','t','https://git.example.com','tok')",
            [],
        )
        .unwrap();
        conn.execute(
            "INSERT INTO commit_metrics (sha, project_id, commit_type, is_rework, is_ci_auto, committed_at, parsed_at)
             VALUES ('abc','p1','feat',0,0,datetime('now'),datetime('now'))",
            [],
        )
        .unwrap();

        let checks = verify_onboarding(&conn, "p1", &base);
        for c in &checks {
            assert!(c.pass, "「{}」应通过，实际: {}", c.name, c.detail);
        }

        let _ = std::fs::remove_dir_all(&base);
    }

    #[test]
    fn managed_version_check_states() {
        let expected = extract_managed_block(TMPL_AGENTS).and_then(block_version).unwrap();
        assert!(check_managed_version(Some(TMPL_AGENTS), expected).0, "同版本应 pass");

        let old = TMPL_AGENTS.replacen(
            &format!("version=\"{expected}\""),
            "version=\"0.0.1\"",
            1,
        );
        let (pass, detail) = check_managed_version(Some(&old), expected);
        assert!(!pass && detail.contains("0.0.1"), "旧版本应 fail 且报出版本号");

        assert!(!check_managed_version(None, expected).0, "文件缺失应 fail");
        assert!(!check_managed_version(Some("手写内容，无标记"), expected).0, "无标记应 fail");
    }
}

