use crate::db;
use rusqlite::params;
use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader};
use std::path::Path;
use std::process::Stdio;
use uuid::Uuid;

// ── 数据结构 ──────────────────────────────────────────────────────────────────

#[derive(Serialize, Deserialize, Clone)]
pub struct TemplateFile {
    pub id: String,
    pub template_id: String,
    pub path: String,
    pub content: String,
}

#[derive(Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ProjectTemplate {
    pub id: String,
    pub name: String,
    pub description: Option<String>,
    pub platform: String, // "win" | "wsl" | "both"
    pub tech_stack: Option<String>,
    pub init_commands: Option<String>,
    pub is_builtin: bool,
    pub created_at: Option<String>,
    #[serde(default)]
    pub files: Vec<TemplateFile>,
    /// JSON 数组，格式：[{label, description, initCommands}]
    /// 存在时前端显示下拉让用户选择初始化方式
    pub variants: Option<String>,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TemplateFileInput {
    pub path: String,
    pub content: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SaveTemplateInput {
    pub id: Option<String>,
    pub name: String,
    pub description: Option<String>,
    pub platform: String,
    pub tech_stack: Option<String>,
    pub init_commands: Option<String>,
    pub files: Vec<TemplateFileInput>,
}

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CreateProjectResult {
    pub project_id: String,
    pub logs: Vec<String>,
}

// ── Tauri 命令 ────────────────────────────────────────────────────────────────

#[tauri::command]
pub fn list_templates() -> Result<Vec<ProjectTemplate>, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let mut stmt = conn
        .prepare(
            "SELECT id, name, description, platform, tech_stack, init_commands, is_builtin, created_at, variants
             FROM project_templates ORDER BY is_builtin DESC, name",
        )
        .map_err(|e| e.to_string())?;

    let templates = stmt
        .query_map([], |row| {
            Ok(ProjectTemplate {
                id: row.get(0)?,
                name: row.get(1)?,
                description: row.get(2)?,
                platform: row.get(3)?,
                tech_stack: row.get(4)?,
                init_commands: row.get(5)?,
                is_builtin: row.get::<_, i64>(6)? == 1,
                created_at: row.get(7)?,
                files: vec![],
                variants: row.get(8)?,
            })
        })
        .map_err(|e| e.to_string())?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| e.to_string())?;

    Ok(templates)
}

#[tauri::command]
pub fn get_template(id: String) -> Result<ProjectTemplate, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;

    let mut tmpl = conn
        .query_row(
            "SELECT id, name, description, platform, tech_stack, init_commands, is_builtin, created_at, variants
             FROM project_templates WHERE id = ?1",
            params![id],
            |row| {
                Ok(ProjectTemplate {
                    id: row.get(0)?,
                    name: row.get(1)?,
                    description: row.get(2)?,
                    platform: row.get(3)?,
                    tech_stack: row.get(4)?,
                    init_commands: row.get(5)?,
                    is_builtin: row.get::<_, i64>(6)? == 1,
                    created_at: row.get(7)?,
                    files: vec![],
                    variants: row.get(8)?,
                })
            },
        )
        .map_err(|e| format!("模板不存在: {e}"))?;

    let mut file_stmt = conn
        .prepare(
            "SELECT id, template_id, path, content
             FROM project_template_files WHERE template_id = ?1 ORDER BY path",
        )
        .map_err(|e| e.to_string())?;

    tmpl.files = file_stmt
        .query_map(params![id], |row| {
            Ok(TemplateFile {
                id: row.get(0)?,
                template_id: row.get(1)?,
                path: row.get(2)?,
                content: row.get(3)?,
            })
        })
        .map_err(|e| e.to_string())?
        .collect::<Result<Vec<_>, _>>()
        .map_err(|e| e.to_string())?;

    Ok(tmpl)
}

#[tauri::command]
pub fn save_template(input: SaveTemplateInput) -> Result<String, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let id = input.id.unwrap_or_else(|| Uuid::new_v4().to_string());

    conn.execute(
        "INSERT INTO project_templates (id, name, description, platform, tech_stack, init_commands, is_builtin)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0)
         ON CONFLICT(id) DO UPDATE SET
             name          = excluded.name,
             description   = excluded.description,
             platform      = excluded.platform,
             tech_stack    = excluded.tech_stack,
             init_commands = excluded.init_commands",
        params![id, input.name, input.description, input.platform, input.tech_stack, input.init_commands],
    )
    .map_err(|e| e.to_string())?;

    // 全量替换文件列表
    conn.execute(
        "DELETE FROM project_template_files WHERE template_id = ?1",
        params![id],
    )
    .map_err(|e| e.to_string())?;

    for file in &input.files {
        let file_id = Uuid::new_v4().to_string();
        conn.execute(
            "INSERT INTO project_template_files (id, template_id, path, content) VALUES (?1, ?2, ?3, ?4)",
            params![file_id, id, file.path, file.content],
        )
        .map_err(|e| e.to_string())?;
    }

    Ok(id)
}

#[tauri::command]
pub fn delete_template(id: String) -> Result<(), String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    conn.execute(
        "DELETE FROM project_templates WHERE id = ?1 AND is_builtin = 0",
        params![id],
    )
    .map_err(|e| e.to_string())?;
    Ok(())
}

#[tauri::command]
pub fn create_project_from_template(
    app: tauri::AppHandle,
    template_id: String,
    parent_path: String,
    project_name: String,
    platform: String,
    space_id: Option<String>,
    variant_init_commands: Option<String>,
) -> Result<CreateProjectResult, String> {
    use tauri::Emitter;

    let mut tmpl = get_template(template_id)?;
    // 如果用户选择了 variant，用其 initCommands 覆盖模板默认值
    if let Some(v) = variant_init_commands {
        tmpl.init_commands = Some(v);
    }
    let mut logs: Vec<String> = Vec::new();

    // 辅助：同时 emit 事件 + 记录到 logs
    macro_rules! log_push {
        ($msg:expr) => {{
            let m: String = $msg;
            let _ = app.emit("template:progress", &m);
            logs.push(m);
        }};
    }

    // ── 1. 计算项目路径 ───────────────────────────────────────────────────────
    let win_parent = crate::commands::publish::wsl_to_win(&parent_path);
    let project_win_path = format!(
        "{}\\{}",
        win_parent.trim_end_matches('\\'),
        project_name
    );
    let project_root = Path::new(&project_win_path);

    if project_root.exists() {
        return Err(format!("目录已存在: {}", project_win_path));
    }

    // ── 2. 创建根目录 ─────────────────────────────────────────────────────────
    let _ = app.emit("template:progress", "__step:1");
    std::fs::create_dir_all(project_root)
        .map_err(|e| format!("创建目录失败: {e}"))?;
    log_push!(format!("📁 已创建目录: {}", project_win_path));

    // ── 3. 写入模板文件 ───────────────────────────────────────────────────────
    for file in &tmpl.files {
        if file.path.trim().is_empty() {
            continue;
        }
        let content = apply_vars(&file.content, &project_name);
        let file_path = project_root.join(&file.path);
        if let Some(parent) = file_path.parent() {
            std::fs::create_dir_all(parent)
                .map_err(|e| format!("创建子目录失败 {}: {e}", file.path))?;
        }
        std::fs::write(&file_path, content)
            .map_err(|e| format!("写入 {} 失败: {e}", file.path))?;
        log_push!(format!("📄 {}", file.path));
    }

    // ── 4. 执行初始化命令 ─────────────────────────────────────────────────────
    let _ = app.emit("template:progress", "__step:2");

    if let Some(cmds) = &tmpl.init_commands {
        let linux_project_path = if platform == "wsl" {
            crate::commands::publish::extract_linux_path(&parent_path, &win_parent)
                .map(|p| format!("{}/{}", p.trim_end_matches('/'), project_name))
        } else {
            None
        };

        for line in cmds.lines() {
            let cmd = line.trim();
            if cmd.is_empty() || cmd.starts_with('#') {
                continue;
            }

            let _ = app.emit("template:progress", &format!("__cmd:{}", cmd));
            log_push!(format!("⚙ 执行: {}", cmd));

            // 用 spawn 流式读 stdout，实时推送每行输出
            let spawn_result = if platform == "wsl" {
                let lp = linux_project_path.as_deref().unwrap_or(&project_win_path);
                crate::silent_cmd("wsl")
                    .args(["bash", "-c", &format!("cd '{}' && {}", lp, cmd)])
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped())
                    .spawn()
            } else {
                crate::silent_cmd("cmd")
                    .args(["/c", &format!("chcp 65001 > nul && {}", cmd)])
                    .current_dir(&project_win_path)
                    .stdout(Stdio::piped())
                    .stderr(Stdio::piped())
                    .spawn()
            };

            match spawn_result {
                Ok(mut child) => {
                    // 流式读 stdout，每行立即 emit
                    if let Some(stdout) = child.stdout.take() {
                        for line in BufReader::new(stdout).lines() {
                            if let Ok(line) = line {
                                let line = line.trim().to_string();
                                if !line.is_empty() {
                                    log_push!(line);
                                }
                            }
                        }
                    }
                    // 等进程结束，收集 stderr
                    let output = child.wait_with_output().map_err(|e| e.to_string())?;
                    let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
                    if !stderr.is_empty() {
                        log_push!(format!("⚠ {}", stderr));
                    }
                    if output.status.success() {
                        log_push!("  ✓".to_string());
                    } else {
                        log_push!(format!("  ✗ 退出码: {}", output.status));
                    }
                }
                Err(e) => {
                    log_push!(format!("  ✗ 启动失败: {e}"));
                }
            }
        }
    }

    // ── 5. 注册到数据库 ───────────────────────────────────────────────────────
    let _ = app.emit("template:progress", "__step:3");

    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let profile_id = Uuid::new_v4().to_string();
    let workspace_id = Uuid::new_v4().to_string();

    conn.execute(
        "INSERT INTO project_profiles (id, name, description, type, status, priority, tech_stack)
         VALUES (?1, ?2, ?3, 'app', 'active', 'mid', ?4)",
        params![profile_id, project_name, tmpl.description, tmpl.tech_stack],
    )
    .map_err(|e| e.to_string())?;

    let is_wsl = platform == "wsl";
    let (wsl_path, win_path_col): (Option<&str>, Option<&str>) = if is_wsl {
        (Some(&project_win_path), None)
    } else {
        (None, Some(&project_win_path))
    };
    let stored_platform = if is_wsl { "wsl" } else { "windows" };

    conn.execute(
        "INSERT INTO project_workspaces (id, profile_id, space_id, wsl_path, win_path, platform)
         VALUES (?1, ?2, ?3, ?4, ?5, ?6)",
        params![workspace_id, profile_id, space_id, wsl_path, win_path_col, stored_platform],
    )
    .map_err(|e| e.to_string())?;

    log_push!(format!("✅ 项目已注册，ID: {}", workspace_id));

    Ok(CreateProjectResult {
        project_id: workspace_id,
        logs,
    })
}

// ── 变量替换 ──────────────────────────────────────────────────────────────────

fn apply_vars(content: &str, project_name: &str) -> String {
    let kebab = project_name
        .to_lowercase()
        .replace(' ', "-")
        .replace('_', "-");
    let snake = project_name
        .to_lowercase()
        .replace(' ', "_")
        .replace('-', "_");
    content
        .replace("{{project_name}}", project_name)
        .replace("{{project_name_kebab}}", &kebab)
        .replace("{{project_name_snake}}", &snake)
}

// ── 内置模板种子数据 ──────────────────────────────────────────────────────────

pub fn seed_builtin_templates() {
    let conn = match db::pool().get() {
        Ok(c) => c,
        Err(_) => return,
    };

    // 不做全局跳过，直接用 INSERT OR IGNORE 保证幂等
    // 这样新增内置模板时，已有用户也能自动获得

    struct Tpl {
        id: &'static str,
        name: &'static str,
        description: &'static str,
        platform: &'static str,
        tech_stack: &'static str,
        init_commands: &'static str,
        files: Vec<(&'static str, &'static str)>,
        /// JSON 字符串，存在时前端显示下拉选择初始化方式
        variants: Option<&'static str>,
    }

    let templates = vec![
        Tpl {
            id: "builtin-pnpm-monorepo",
            name: "pnpm Monorepo",
            description: "多包 monorepo，含 apps/ 与 packages/ 目录",
            platform: "both",
            tech_stack: "Node,pnpm",
            init_commands: "pnpm install",
            variants: None,
            files: vec![
                ("package.json", r#"{
  "name": "{{project_name_kebab}}",
  "private": true,
  "scripts": {
    "dev": "pnpm -r --filter './apps/**' dev",
    "build": "pnpm -r --filter './apps/**' build",
    "lint": "pnpm -r lint"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  }
}
"#),
                ("pnpm-workspace.yaml", r#"packages:
  - 'apps/*'
  - 'packages/*'
"#),
                (".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"),
                ("apps/.gitkeep", ""),
                ("packages/.gitkeep", ""),
                (".gitignore", "node_modules/\ndist/\n.next/\n*.log\n.env\n.env.*\n!.env.example\n"),
            ],
        },
        Tpl {
            id: "builtin-pnpm-app",
            name: "pnpm App",
            description: "单包 Node.js 应用",
            platform: "both",
            tech_stack: "Node,pnpm",
            init_commands: "pnpm install",
            variants: None,
            files: vec![
                ("package.json", r#"{
  "name": "{{project_name_kebab}}",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js"
  }
}
"#),
                ("src/index.js", "console.log('Hello from {{project_name}}!')\n"),
                (".gitignore", "node_modules/\ndist/\n*.log\n.env\n"),
            ],
        },
        Tpl {
            id: "builtin-rust",
            name: "Rust",
            description: "Rust 二进制项目",
            platform: "both",
            tech_stack: "Rust",
            init_commands: "cargo build",
            variants: None,
            files: vec![
                ("Cargo.toml", r#"[package]
name = "{{project_name_kebab}}"
version = "0.1.0"
edition = "2021"

[dependencies]
"#),
                ("src/main.rs", "fn main() {\n    println!(\"Hello from {{project_name}}!\");\n}\n"),
                (".gitignore", "target/\n"),
            ],
        },
        Tpl {
            id: "builtin-empty",
            name: "空文件夹",
            description: "只创建目录，不添加任何文件",
            platform: "both",
            tech_stack: "",
            init_commands: "",
            variants: None,
            files: vec![],
        },
        Tpl {
            id: "builtin-tauri-react",
            name: "Tauri + React",
            description: "桌面应用，Tauri v2 + React 19 + TypeScript + Vite",
            platform: "both",
            tech_stack: "Rust,Node,pnpm,React,TypeScript,Tauri",
            init_commands: "pnpm install",
            variants: None,
            files: vec![
                ("package.json", r#"{
  "name": "{{project_name_kebab}}",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "tauri dev",
    "build": "tauri build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tauri-apps/api": "^2",
    "react": "^19",
    "react-dom": "^19"
  },
  "devDependencies": {
    "@tauri-apps/cli": "^2",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@vitejs/plugin-react": "^4",
    "typescript": "^5",
    "vite": "^6"
  }
}
"#),
                ("vite.config.ts", r#"import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";

export default defineConfig(async () => ({
  plugins: [react()],
  clearScreen: false,
  server: {
    port: 1420,
    strictPort: true,
    watch: {
      ignored: ["**/src-tauri/**"],
    },
  },
}));
"#),
                ("tsconfig.json", r#"{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true
  },
  "include": ["src"]
}
"#),
                ("index.html", r#"<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{project_name}}</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
"#),
                ("src/main.tsx", r#"import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
"#),
                ("src/App.tsx", r#"import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  return (
    <div style={{ padding: "2rem", fontFamily: "sans-serif" }}>
      <h1>{{project_name}}</h1>
      <p>Tauri v2 + React + TypeScript</p>
      <button onClick={() => setCount((c) => c + 1)}>count: {count}</button>
    </div>
  );
}

export default App;
"#),
                ("src-tauri/tauri.conf.json", r#"{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "{{project_name_kebab}}",
  "version": "0.1.0",
  "identifier": "com.example.{{project_name_snake}}",
  "build": {
    "beforeDevCommand": "pnpm vite",
    "devUrl": "http://localhost:1420",
    "beforeBuildCommand": "pnpm vite build",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [
      {
        "title": "{{project_name}}",
        "width": 1200,
        "height": 800
      }
    ],
    "security": {
      "csp": null
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": []
  }
}
"#),
                ("src-tauri/Cargo.toml", r#"[package]
name = "{{project_name_snake}}"
version = "0.1.0"
description = "A Tauri App"
authors = []
edition = "2021"

[lib]
name = "{{project_name_snake}}_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
"#),
                ("src-tauri/build.rs", "fn main() {\n    tauri_build::build()\n}\n"),
                ("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    {{project_name_snake}}_lib::run()
}
"#),
                ("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
"#),
                ("src-tauri/capabilities/default.json", r#"{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "opener:default"
  ]
}
"#),
                (".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"),
            ],
        },
        Tpl {
            id: "builtin-expo",
            name: "Expo",
            description: "React Native 移动端 App（Android / iOS）。TypeScript + Expo SDK，适合游戏、工具、内容类手机应用。选择下方模板变体来确定初始化方式。",
            platform: "both",
            tech_stack: "React Native,TypeScript,Expo",
            init_commands: "pnpm create expo-app@latest . --template blank-typescript",
            variants: Some(r#"[
  {"label":"blank-typescript（推荐）","description":"最小化 TypeScript 起步，适合游戏类、工具类、独立 App","initCommands":"pnpm create expo-app@latest . --template blank-typescript"},
  {"label":"tabs（底部标签导航）","description":"内置底部 Tab 栏 + Expo Router，适合多页面内容型 App","initCommands":"pnpm create expo-app@latest . --template tabs"},
  {"label":"bare（裸原生配置）","description":"完整原生 Expo 配置，可深度定制原生模块，适合进阶项目","initCommands":"pnpm create expo-app@latest . --template bare-minimum"},
  {"label":"默认（向导模式）","description":"运行官方交互式向导，在终端中选择模板类型","initCommands":"pnpm create expo-app@latest ."}
]"#),
            files: vec![],
        },
        Tpl {
            id: "builtin-pnpm-expo-monorepo",
            name: "pnpm Monorepo + Expo",
            description: "pnpm 多包 Monorepo：apps/ 内含 Expo Router (React Native) 应用，packages/ 内含共享 @repo/types 包。Metro 已配置 monorepo 路径解析（含 Windows 兼容修复），适合多 App 扩展场景。",
            platform: "both",
            tech_stack: "React Native,TypeScript,Expo,pnpm",
            init_commands: "pnpm install",
            variants: None,
            files: vec![
                // ── monorepo 根 ──────────────────────────────────────────────
                ("package.json", r#"{
  "name": "{{project_name_kebab}}",
  "private": true,
  "scripts": {
    "start": "pnpm --filter app start",
    "android": "pnpm --filter app android",
    "ios": "pnpm --filter app ios",
    "lint": "pnpm -r lint"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  }
}
"#),
                ("pnpm-workspace.yaml", "packages:\n  - 'apps/*'\n  - 'packages/*'\n"),
                ("tsconfig.base.json", r#"{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "exclude": ["node_modules", "dist", "build"]
}
"#),
                (".gitignore", "node_modules/\ndist/\n.expo/\nandroid/\nios/\n*.log\n.env\n.env.*\n!.env.example\n"),
                (".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"),

                // ── packages/types ───────────────────────────────────────────
                ("packages/types/package.json", r#"{
  "name": "@repo/types",
  "version": "0.0.1",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  },
  "files": ["src"],
  "scripts": {
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^5.4.5"
  }
}
"#),
                ("packages/types/src/index.ts", "// 在此导出跨 App 共享的类型\nexport type {}\n"),

                // ── apps/app ─────────────────────────────────────────────────
                ("apps/app/package.json", r#"{
  "name": "app",
  "version": "1.0.0",
  "private": true,
  "main": "expo-router/entry",
  "scripts": {
    "start": "expo start",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "lint": "eslint . --ext .ts,.tsx"
  },
  "dependencies": {
    "@repo/types": "workspace:*",
    "expo": "~52.0.0",
    "expo-router": "~4.0.0",
    "expo-status-bar": "~2.0.0",
    "react": "18.3.1",
    "react-native": "0.76.3",
    "react-native-gesture-handler": "~2.20.0",
    "react-native-reanimated": "~3.16.1",
    "react-native-safe-area-context": "4.12.0",
    "react-native-screens": "~4.1.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/react": "~18.3.12",
    "typescript": "^5.4.5"
  }
}
"#),
                ("apps/app/app.json", r##"{
  "expo": {
    "name": "{{project_name}}",
    "slug": "{{project_name_kebab}}",
    "version": "1.0.0",
    "orientation": "default",
    "userInterfaceStyle": "light",
    "scheme": "{{project_name_kebab}}",
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.example.{{project_name_snake}}"
    },
    "android": {
      "adaptiveIcon": {
        "backgroundColor": "#ffffff"
      },
      "package": "com.example.{{project_name_snake}}"
    },
    "plugins": ["expo-router"],
    "experiments": {
      "typedRoutes": true
    }
  }
}
"##),
                ("apps/app/tsconfig.json", r#"{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "paths": {
      "@repo/types": ["../../packages/types/src/index.ts"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}
"#),
                ("apps/app/babel.config.js", r#"module.exports = function (api) {
  api.cache(true)
  return {
    presets: ['babel-preset-expo'],
  }
}
"#),
                // Metro 配置：monorepo 路径解析 + Windows release 构建兼容修复
                ("apps/app/metro.config.js", r#"const { getDefaultConfig } = require('expo/metro-config')
const path = require('path')

const projectRoot = __dirname
const monorepoRoot = path.resolve(projectRoot, '../..')

const config = getDefaultConfig(projectRoot)

// Windows release 构建兼容：强制 unstable_serverRoot 为 projectRoot，
// 避免 Metro 与 RN Gradle 插件的路径基准不一致。
config.server = {
  ...config.server,
  unstable_serverRoot: projectRoot,
}

// 让 Metro 监听 monorepo 根目录，确保根 node_modules 可被解析
config.watchFolders = [monorepoRoot]

// 模块解析：优先查 app 自己的 node_modules，再查 monorepo 根
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(monorepoRoot, 'node_modules'),
]

module.exports = config
"#),
                ("apps/app/expo-env.d.ts", "/// <reference types=\"expo/types\" />\n\n// NOTE: This file should not be edited and should be in your git ignore\n"),
                ("apps/app/app/_layout.tsx", r#"import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'

export default function RootLayout() {
  return (
    <>
      <StatusBar style="auto" />
      <Stack>
        <Stack.Screen name="index" options={{ title: '{{project_name}}' }} />
      </Stack>
    </>
  )
}
"#),
                ("apps/app/app/index.tsx", r#"import { View, Text, StyleSheet } from 'react-native'
import { SafeAreaView } from 'react-native-safe-area-context'

export default function HomeScreen() {
  return (
    <SafeAreaView style={styles.container}>
      <View style={styles.content}>
        <Text style={styles.title}>{{project_name}}</Text>
        <Text style={styles.subtitle}>Expo Router + pnpm Monorepo</Text>
      </View>
    </SafeAreaView>
  )
}

const styles = StyleSheet.create({
  container: { flex: 1, backgroundColor: '#fff' },
  content: { flex: 1, alignItems: 'center', justifyContent: 'center', gap: 8 },
  title: { fontSize: 24, fontWeight: 'bold' },
  subtitle: { fontSize: 14, color: '#666' },
})
"#),
            ],
        },
        // ── P1: React + Vite SPA ─────────────────────────────────────────────
        Tpl {
            id: "builtin-vite-react",
            name: "React + Vite SPA",
            description: "纯前端 Web 应用，React 19 + TypeScript + TailwindCSS v4 + React Router v7",
            platform: "both",
            tech_stack: "React,TypeScript,Vite,TailwindCSS",
            init_commands: "pnpm install",
            variants: None,
            files: vec![
                ("package.json", r#"{
  "name": "{{project_name_kebab}}",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19",
    "react-dom": "^19",
    "react-router": "^7"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@vitejs/plugin-react": "^4",
    "tailwindcss": "^4",
    "typescript": "^5",
    "vite": "^6"
  }
}
"#),
                ("vite.config.ts", r#"import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});
"#),
                ("tsconfig.json", r#"{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true
  },
  "include": ["src"]
}
"#),
                ("index.html", r#"<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{project_name}}</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
"#),
                ("src/main.tsx", r#"import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
"#),
                ("src/App.tsx", r#"import { Routes, Route } from "react-router";
import Home from "./pages/Home";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
  );
}
"#),
                ("src/index.css", "@import \"tailwindcss\";\n"),
                ("src/pages/Home.tsx", r#"export default function Home() {
  return (
    <main className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="text-center">
        <h1 className="text-4xl font-bold text-gray-900">{{project_name}}</h1>
        <p className="mt-2 text-gray-500">React + Vite + TailwindCSS</p>
      </div>
    </main>
  );
}
"#),
                (".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\n"),
            ],
        },
        // ── P1: Tauri + React + TailwindCSS ──────────────────────────────────
        Tpl {
            id: "builtin-tauri-react-tailwind",
            name: "Tauri + React + Tailwind",
            description: "桌面应用，Tauri v2 + React 19 + TypeScript + TailwindCSS v4",
            platform: "both",
            tech_stack: "Rust,Node,pnpm,React,TypeScript,Tauri,TailwindCSS",
            init_commands: "pnpm install",
            variants: None,
            files: vec![
                ("package.json", r#"{
  "name": "{{project_name_kebab}}",
  "private": true,
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "tauri dev",
    "build": "tauri build",
    "preview": "vite preview"
  },
  "dependencies": {
    "@tauri-apps/api": "^2",
    "react": "^19",
    "react-dom": "^19"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4",
    "@tauri-apps/cli": "^2",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@vitejs/plugin-react": "^4",
    "tailwindcss": "^4",
    "typescript": "^5",
    "vite": "^6"
  }
}
"#),
                ("vite.config.ts", r#"import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig(async () => ({
  plugins: [react(), tailwindcss()],
  clearScreen: false,
  server: {
    port: 1420,
    strictPort: true,
    watch: {
      ignored: ["**/src-tauri/**"],
    },
  },
}));
"#),
                ("tsconfig.json", r#"{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx",
    "strict": true
  },
  "include": ["src"]
}
"#),
                ("index.html", r#"<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{project_name}}</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
"#),
                ("src/main.tsx", r#"import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);
"#),
                ("src/index.css", "@import \"tailwindcss\";\n"),
                ("src/App.tsx", r#"import { useState } from "react";

function App() {
  const [count, setCount] = useState(0);

  return (
    <main className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="text-center space-y-4">
        <h1 className="text-4xl font-bold text-gray-900">{{project_name}}</h1>
        <p className="text-gray-500">Tauri v2 + React + TailwindCSS</p>
        <button
          className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
          onClick={() => setCount((c) => c + 1)}
        >
          count: {count}
        </button>
      </div>
    </main>
  );
}

export default App;
"#),
                ("src-tauri/tauri.conf.json", r#"{
  "$schema": "https://schema.tauri.app/config/2",
  "productName": "{{project_name_kebab}}",
  "version": "0.1.0",
  "identifier": "com.example.{{project_name_snake}}",
  "build": {
    "beforeDevCommand": "pnpm vite",
    "devUrl": "http://localhost:1420",
    "beforeBuildCommand": "pnpm vite build",
    "frontendDist": "../dist"
  },
  "app": {
    "windows": [
      {
        "title": "{{project_name}}",
        "width": 1200,
        "height": 800
      }
    ],
    "security": {
      "csp": null
    }
  },
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": []
  }
}
"#),
                ("src-tauri/Cargo.toml", r#"[package]
name = "{{project_name_snake}}"
version = "0.1.0"
description = "A Tauri App"
authors = []
edition = "2021"

[lib]
name = "{{project_name_snake}}_lib"
crate-type = ["staticlib", "cdylib", "rlib"]

[build-dependencies]
tauri-build = { version = "2", features = [] }

[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
"#),
                ("src-tauri/build.rs", "fn main() {\n    tauri_build::build()\n}\n"),
                ("src-tauri/src/main.rs", r#"// Prevents additional console window on Windows in release, DO NOT REMOVE!!
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

fn main() {
    {{project_name_snake}}_lib::run()
}
"#),
                ("src-tauri/src/lib.rs", r#"#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
"#),
                ("src-tauri/capabilities/default.json", r#"{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "opener:default"
  ]
}
"#),
                (".gitignore", "node_modules/\ndist/\n.env\n.env.*\n!.env.example\nsrc-tauri/target/\nsrc-tauri/gen/\n"),
            ],
        },
        // ── P3: Web + Mobile 共享 Monorepo ────────────────────────────────────
        Tpl {
            id: "builtin-web-mobile-monorepo",
            name: "Web + Mobile Monorepo",
            description: "跨平台代码复用：packages/core 共享业务逻辑，apps/web（Vite + Tailwind）+ apps/mobile（Expo + NativeWind）",
            platform: "both",
            tech_stack: "React,React Native,TypeScript,Expo,Vite,TailwindCSS,NativeWind,pnpm",
            init_commands: "pnpm install",
            variants: None,
            files: vec![
                // ── 根目录 ──────────────────────────────────────────────────────
                ("package.json", r#"{
  "name": "{{project_name_kebab}}",
  "private": true,
  "scripts": {
    "dev": "pnpm --filter web dev",
    "build:web": "pnpm --filter web build",
    "start:mobile": "pnpm --filter mobile start",
    "android": "pnpm --filter mobile android",
    "ios": "pnpm --filter mobile ios",
    "typecheck": "pnpm -r typecheck"
  },
  "engines": {
    "node": ">=18",
    "pnpm": ">=8"
  }
}
"#),
                ("pnpm-workspace.yaml", "packages:\n  - 'apps/*'\n  - 'packages/*'\n"),
                ("tsconfig.base.json", r#"{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020"],
    "module": "ESNext",
    "moduleResolution": "bundler",
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strict": true,
    "skipLibCheck": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "exclude": ["node_modules", "dist", "build"]
}
"#),
                (".gitignore", "node_modules/\ndist/\nbuild/\n.expo/\nandroid/\nios/\n*.log\n.env\n.env.*\n!.env.example\n"),
                (".npmrc", "strict-peer-dependencies=false\nauto-install-peers=true\n"),

                // ── packages/types ──────────────────────────────────────────────
                ("packages/types/package.json", r#"{
  "name": "@repo/types",
  "version": "0.0.1",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  },
  "scripts": {
    "typecheck": "tsc --noEmit"
  },
  "devDependencies": {
    "typescript": "^5"
  }
}
"#),
                ("packages/types/tsconfig.json", r#"{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}
"#),
                ("packages/types/src/index.ts", r#"// 在此定义跨平台共享的类型
export interface User {
  id: string
  name: string
  email: string
}

export interface ApiResponse<T> {
  data: T
  error?: string
}
"#),

                // ── packages/core ───────────────────────────────────────────────
                ("packages/core/package.json", r#"{
  "name": "@repo/core",
  "version": "0.0.1",
  "private": true,
  "type": "module",
  "exports": {
    ".": {
      "types": "./src/index.ts",
      "import": "./src/index.ts"
    }
  },
  "scripts": {
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@repo/types": "workspace:*"
  },
  "devDependencies": {
    "typescript": "^5"
  }
}
"#),
                ("packages/core/tsconfig.json", r#"{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}
"#),
                ("packages/core/src/index.ts", r#"// 在此导出跨平台共享的业务逻辑（纯 TS，不依赖任何平台 API）
export * from "./api"
"#),
                ("packages/core/src/api.ts", r#"import type { ApiResponse, User } from "@repo/types"

const BASE_URL = ""

export async function getUser(id: string): Promise<ApiResponse<User>> {
  const res = await fetch(`${BASE_URL}/api/users/${id}`)
  if (!res.ok) return { data: null as unknown as User, error: res.statusText }
  const data = await res.json()
  return { data }
}
"#),

                // ── apps/web ────────────────────────────────────────────────────
                ("apps/web/package.json", r#"{
  "name": "web",
  "version": "0.0.1",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "preview": "vite preview",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@repo/core": "workspace:*",
    "@repo/types": "workspace:*",
    "react": "^19",
    "react-dom": "^19",
    "react-router": "^7"
  },
  "devDependencies": {
    "@tailwindcss/vite": "^4",
    "@types/react": "^19",
    "@types/react-dom": "^19",
    "@vitejs/plugin-react": "^4",
    "tailwindcss": "^4",
    "typescript": "^5",
    "vite": "^6"
  }
}
"#),
                ("apps/web/tsconfig.json", r#"{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "noEmit": true,
    "jsx": "react-jsx",
    "paths": {
      "@repo/core": ["../../packages/core/src/index.ts"],
      "@repo/types": ["../../packages/types/src/index.ts"]
    }
  },
  "include": ["src"]
}
"#),
                ("apps/web/vite.config.ts", r#"import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";

export default defineConfig({
  plugins: [react(), tailwindcss()],
});
"#),
                ("apps/web/index.html", r#"<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>{{project_name}}</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
"#),
                ("apps/web/src/main.tsx", r#"import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router";
import App from "./App";
import "./index.css";

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
  <React.StrictMode>
    <BrowserRouter>
      <App />
    </BrowserRouter>
  </React.StrictMode>
);
"#),
                ("apps/web/src/index.css", "@import \"tailwindcss\";\n"),
                ("apps/web/src/App.tsx", r#"import { Routes, Route } from "react-router";
import Home from "./pages/Home";

export default function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
    </Routes>
  );
}
"#),
                ("apps/web/src/pages/Home.tsx", r#"export default function Home() {
  return (
    <main className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="text-center">
        <h1 className="text-4xl font-bold text-gray-900">{{project_name}}</h1>
        <p className="mt-2 text-gray-500">Web + Mobile Monorepo</p>
      </div>
    </main>
  );
}
"#),

                // ── apps/mobile ─────────────────────────────────────────────────
                ("apps/mobile/package.json", r#"{
  "name": "mobile",
  "version": "1.0.0",
  "private": true,
  "main": "expo-router/entry",
  "scripts": {
    "start": "expo start",
    "android": "expo run:android",
    "ios": "expo run:ios",
    "typecheck": "tsc --noEmit"
  },
  "dependencies": {
    "@repo/core": "workspace:*",
    "@repo/types": "workspace:*",
    "expo": "~52.0.0",
    "expo-router": "~4.0.0",
    "expo-status-bar": "~2.0.0",
    "nativewind": "^4.0.1",
    "react": "18.3.1",
    "react-native": "0.76.3",
    "react-native-safe-area-context": "4.12.0",
    "react-native-screens": "~4.1.0",
    "tailwindcss": "^3.4.0"
  },
  "devDependencies": {
    "@babel/core": "^7.25.2",
    "@types/react": "~18.3.12",
    "typescript": "^5.4.5"
  }
}
"#),
                ("apps/mobile/app.json", r##"{
  "expo": {
    "name": "{{project_name}}",
    "slug": "{{project_name_kebab}}",
    "version": "1.0.0",
    "scheme": "{{project_name_kebab}}",
    "userInterfaceStyle": "automatic",
    "ios": {
      "supportsTablet": true,
      "bundleIdentifier": "com.example.{{project_name_snake}}"
    },
    "android": {
      "adaptiveIcon": {
        "backgroundColor": "#ffffff"
      },
      "package": "com.example.{{project_name_snake}}"
    },
    "plugins": ["expo-router"],
    "experiments": {
      "typedRoutes": true
    }
  }
}
"##),
                ("apps/mobile/tsconfig.json", r#"{
  "extends": "expo/tsconfig.base",
  "compilerOptions": {
    "strict": true,
    "paths": {
      "@repo/core": ["../../packages/core/src/index.ts"],
      "@repo/types": ["../../packages/types/src/index.ts"]
    }
  },
  "include": ["**/*.ts", "**/*.tsx", ".expo/types/**/*.ts", "expo-env.d.ts"]
}
"#),
                ("apps/mobile/tailwind.config.js", r#"/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./app/**/*.{ts,tsx}"],
  presets: [require("nativewind/preset")],
  theme: {
    extend: {},
  },
  plugins: [],
}
"#),
                ("apps/mobile/global.css", "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n"),
                ("apps/mobile/babel.config.js", r#"module.exports = function (api) {
  api.cache(true)
  return {
    presets: ['babel-preset-expo'],
    plugins: ['nativewind/babel'],
  }
}
"#),
                ("apps/mobile/metro.config.js", r#"const { getDefaultConfig } = require('expo/metro-config')
const { withNativeWind } = require('nativewind/metro')
const path = require('path')

const projectRoot = __dirname
const monorepoRoot = path.resolve(projectRoot, '../..')

const config = getDefaultConfig(projectRoot)

config.watchFolders = [monorepoRoot]
config.resolver.nodeModulesPaths = [
  path.resolve(projectRoot, 'node_modules'),
  path.resolve(monorepoRoot, 'node_modules'),
]

module.exports = withNativeWind(config, { input: './global.css' })
"#),
                ("apps/mobile/expo-env.d.ts", "/// <reference types=\"expo/types\" />\n"),
                ("apps/mobile/app/_layout.tsx", r#"import '../global.css'
import { Stack } from 'expo-router'
import { StatusBar } from 'expo-status-bar'

export default function RootLayout() {
  return (
    <>
      <StatusBar style="auto" />
      <Stack>
        <Stack.Screen name="index" options={{ title: '{{project_name}}' }} />
      </Stack>
    </>
  )
}
"#),
                ("apps/mobile/app/index.tsx", r#"import { View, Text } from 'react-native'

export default function HomeScreen() {
  return (
    <View className="flex-1 items-center justify-center bg-gray-50">
      <Text className="text-3xl font-bold text-gray-900">{{project_name}}</Text>
      <Text className="mt-2 text-gray-500">Web + Mobile Monorepo</Text>
    </View>
  )
}
"#),
            ],
        },
    ];

    for t in &templates {
        let _ = conn.execute(
            "INSERT OR IGNORE INTO project_templates
             (id, name, description, platform, tech_stack, init_commands, is_builtin, variants)
             VALUES (?1, ?2, ?3, ?4, ?5, ?6, 1, ?7)",
            params![
                t.id,
                t.name,
                if t.description.is_empty() { None } else { Some(t.description) },
                t.platform,
                if t.tech_stack.is_empty() { None } else { Some(t.tech_stack) },
                if t.init_commands.is_empty() { None } else { Some(t.init_commands) },
                t.variants,
            ],
        );
        // 已存在的模板也补充 variants（INSERT OR IGNORE 不更新已有行）
        let _ = conn.execute(
            "UPDATE project_templates SET variants = ?1 WHERE id = ?2 AND is_builtin = 1",
            params![t.variants, t.id],
        );

        for (path, content) in &t.files {
            let file_id = format!("{}-{}", t.id, path.replace('/', "-"));
            let _ = conn.execute(
                "INSERT OR IGNORE INTO project_template_files (id, template_id, path, content)
                 VALUES (?1, ?2, ?3, ?4)",
                params![file_id, t.id, path, content],
            );
        }
    }
}
