// CI/CD 配置管理与 Workflow 生成（Gitea Actions，语法兼容 GitHub Actions）
// 历史部署教程与踩坑指南见 docs/tauri-cicd-guide.md（原 GitHub Actions 方案）
use crate::db;
use crate::models::{CicdConfig, WorkflowResult};
use rusqlite::params;
use std::path::Path;
use uuid::Uuid;

#[tauri::command]
pub fn get_cicd_config(project_id: String) -> Result<Option<CicdConfig>, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let result = conn.query_row(
        "SELECT id, project_id, config_type, trigger_branch, build_command, node_version,
                server_host, server_user, deploy_path, start_command, oss_endpoint, created_at
         FROM cicd_configs WHERE project_id = ?1",
        params![project_id],
        CicdConfig::from_row,
    );
    match result {
        Ok(cfg) => Ok(Some(cfg)),
        Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
        Err(e) => Err(e.to_string()),
    }
}

#[tauri::command]
pub fn save_cicd_config(input: CicdConfig) -> Result<CicdConfig, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let id = if input.id.is_empty() {
        Uuid::new_v4().to_string()
    } else {
        input.id.clone()
    };
    conn.execute(
        "INSERT INTO cicd_configs
             (id, project_id, config_type, trigger_branch, build_command, node_version,
              server_host, server_user, deploy_path, start_command, oss_endpoint)
         VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11)
         ON CONFLICT(project_id) DO UPDATE SET
             config_type    = excluded.config_type,
             trigger_branch = excluded.trigger_branch,
             build_command  = excluded.build_command,
             node_version   = excluded.node_version,
             server_host    = excluded.server_host,
             server_user    = excluded.server_user,
             deploy_path    = excluded.deploy_path,
             start_command  = excluded.start_command,
             oss_endpoint   = excluded.oss_endpoint",
        params![
            id,
            input.project_id,
            input.config_type,
            input.trigger_branch,
            input.build_command,
            input.node_version,
            input.server_host,
            input.server_user,
            input.deploy_path,
            input.start_command,
            input.oss_endpoint,
        ],
    )
    .map_err(|e| e.to_string())?;

    get_cicd_config(input.project_id)?.ok_or_else(|| "save failed".to_string())
}

/// 根据项目的 cicd_configs 生成 Gitea Actions workflow 文件，写入 {project_path}/.gitea/workflows/
/// project_path 必须是 Windows 可访问的路径（WSL 项目请传 UNC 路径 \\wsl.localhost\...）
#[tauri::command]
pub fn generate_workflow(project_id: String, project_path: String) -> Result<WorkflowResult, String> {
    let conn = db::pool().get().map_err(|e| e.to_string())?;
    let config = conn
        .query_row(
            "SELECT id, project_id, config_type, trigger_branch, build_command, node_version,
                    server_host, server_user, deploy_path, start_command, oss_endpoint, created_at
             FROM cicd_configs WHERE project_id = ?1",
            params![project_id],
            CicdConfig::from_row,
        )
        .map_err(|e| e.to_string())?;

    let (content, filename, secrets) = match config.config_type.as_str() {
        "tauri_oss" => build_tauri_oss_workflow(&config),
        _ => build_web_ssh_workflow(&config),
    };

    let workflows_dir = Path::new(&project_path).join(".gitea").join("workflows");
    std::fs::create_dir_all(&workflows_dir).map_err(|e| e.to_string())?;
    let file_path = workflows_dir.join(&filename);
    std::fs::write(&file_path, &content).map_err(|e| e.to_string())?;

    Ok(WorkflowResult {
        content,
        path: file_path.to_string_lossy().to_string(),
        secrets,
    })
}

// ── workflow 模板生成 ──────────────────────────────────────────────────────────

fn build_web_ssh_workflow(cfg: &CicdConfig) -> (String, String, Vec<String>) {
    let branch = &cfg.trigger_branch;
    let node = cfg.node_version.as_deref().unwrap_or("20");
    let build = cfg.build_command.as_deref().unwrap_or("pnpm build");
    let path = cfg.deploy_path.as_deref().unwrap_or("/app/project");
    let start = cfg.start_command.as_deref().unwrap_or("pm2 restart app");

    let content = format!(
        r#"# Gitea Actions（语法兼容 GitHub Actions，action 由 runner 从镜像源拉取）
name: Deploy

on:
  push:
    branches: [{branch}]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '{node}'
          cache: 'pnpm'

      - run: pnpm install --frozen-lockfile
      - run: {build}

      - name: Deploy to server
        uses: appleboy/ssh-action@v1
        with:
          host: ${{{{ secrets.SERVER_HOST }}}}
          username: ${{{{ secrets.SERVER_USER }}}}
          key: ${{{{ secrets.SSH_KEY }}}}
          script: |
            cd {path}
            git pull
            pnpm install --frozen-lockfile
            {build}
            {start}
"#
    );

    let secrets = vec![
        "SERVER_HOST".to_string(),
        "SERVER_USER".to_string(),
        "SSH_KEY".to_string(),
    ];
    (content, "deploy.yml".to_string(), secrets)
}

fn build_tauri_oss_workflow(cfg: &CicdConfig) -> (String, String, Vec<String>) {
    let node = cfg.node_version.as_deref().unwrap_or("20");
    let endpoint = cfg
        .oss_endpoint
        .as_deref()
        .unwrap_or("oss-cn-hangzhou.aliyuncs.com");

    let content = format!(
        r#"# Tauri v2 Windows 自动构建 + 签名 + 阿里云 OSS 发布（Gitea Actions）
# 需要一台带 'windows' 标签的自建 Windows runner（act_runner）
# 发布产物走 OSS 分发（tauri-plugin-updater 读 latest.json），不依赖 Release 页面
name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: windows
    steps:
      - uses: actions/checkout@v4

      - uses: pnpm/action-setup@v4
        with:
          version: 9

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '{node}'
          cache: 'pnpm'

      - name: Install Rust
        uses: dtolnay/rust-toolchain@stable

      - name: Rust cache
        uses: swatinem/rust-cache@v2
        with:
          workspaces: './src-tauri -> target'

      - run: pnpm install --frozen-lockfile

      - name: Verify signing key
        shell: pwsh
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY }}}}
        run: |
          if ([string]::IsNullOrEmpty($env:TAURI_SIGNING_PRIVATE_KEY)) {{
            Write-Error "TAURI_SIGNING_PRIVATE_KEY is EMPTY - update bundles will not be generated!"
            exit 1
          }}
          $lines = $env:TAURI_SIGNING_PRIVATE_KEY.Split("`n").Count
          Write-Host "TAURI_SIGNING_PRIVATE_KEY is set ($lines lines)"

      - name: Build（签名随环境变量注入）
        shell: pwsh
        env:
          TAURI_SIGNING_PRIVATE_KEY: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY }}}}
          TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{{{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}}}
        run: pnpm tauri build --verbose

      - name: Upload to OSS & generate latest.json
        shell: pwsh
        env:
          OSS_KEY_ID: ${{{{ secrets.OSS_KEY_ID }}}}
          OSS_KEY_SECRET: ${{{{ secrets.OSS_KEY_SECRET }}}}
          OSS_BUCKET: ${{{{ secrets.OSS_BUCKET }}}}
          OSS_ENDPOINT: ${{{{ secrets.OSS_ENDPOINT }}}}
        run: |
          # 下载 ossutil
          $oss = "$env:GITHUB_WORKSPACE\ossutil64.exe"
          Invoke-WebRequest -Uri "https://gosspublic.alicdn.com/ossutil/1.7.17/ossutil-v1.7.17-windows-amd64.zip" -OutFile oss.zip
          Expand-Archive oss.zip -DestinationPath oss_tmp
          $exeFound = Get-ChildItem oss_tmp -Filter "ossutil*.exe" -Recurse | Select-Object -First 1
          Copy-Item $exeFound.FullName $oss

          $tag     = "${{{{ github.ref_name }}}}"
          $version = $tag.TrimStart('v')
          $nsisDir = "$env:GITHUB_WORKSPACE\src-tauri\target\release\bundle\nsis"

          Write-Host "=== bundle dir ==="
          Get-ChildItem "$env:GITHUB_WORKSPACE\src-tauri\target\release\bundle" -Recurse | ForEach-Object {{ Write-Host $_.FullName }}

          $all       = Get-ChildItem $nsisDir
          $installer = $all | Where-Object {{ $_.Name.EndsWith('-setup.exe') }} | Select-Object -First 1
          $sigFile   = $all | Where-Object {{ $_.Name.EndsWith('-setup.exe.sig') }} | Select-Object -First 1

          if (-not $installer) {{ Write-Error "installer not found!"; exit 1 }}

          $bucket   = "oss://$env:OSS_BUCKET"
          $endpoint = "https://$env:OSS_ENDPOINT"

          & $oss config -e $endpoint -i $env:OSS_KEY_ID -k $env:OSS_KEY_SECRET -L CH
          & $oss cp $installer.FullName "$bucket/releases/$tag/$($installer.Name)" -f

          if ($sigFile) {{
            & $oss cp $sigFile.FullName "$bucket/releases/$tag/$($sigFile.Name)" -f

            $date    = (Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')
            $baseUrl = "https://$env:OSS_BUCKET.{endpoint}/releases/$tag"
            $sig     = (Get-Content $sigFile.FullName -Raw).Trim()

            $json = [ordered]@{{
              version  = $version
              pub_date = $date
              notes    = "版本 $tag 已发布"
              platforms = [ordered]@{{
                'windows-x86_64' = [ordered]@{{
                  signature = $sig
                  url       = "$baseUrl/$($installer.Name)"
                }}
              }}
            }} | ConvertTo-Json -Depth 5

            $json | Out-File -FilePath latest.json -Encoding utf8NoBOM
            Write-Host "=== latest.json ==="
            Get-Content latest.json

            & $oss cp latest.json "$bucket/latest.json" -f
            Write-Host "latest.json uploaded"
          }} else {{
            Write-Warning "sigFile (.exe.sig) not found - skipping updater upload"
          }}
"#
    );

    let secrets = vec![
        "TAURI_SIGNING_PRIVATE_KEY".to_string(),
        "TAURI_SIGNING_PRIVATE_KEY_PASSWORD".to_string(),
        "OSS_KEY_ID".to_string(),
        "OSS_KEY_SECRET".to_string(),
        "OSS_BUCKET".to_string(),
        "OSS_ENDPOINT".to_string(),
    ];
    (content, "release.yml".to_string(), secrets)
}

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

    fn cfg(config_type: &str) -> CicdConfig {
        CicdConfig {
            id: "t".into(),
            project_id: "p".into(),
            config_type: config_type.into(),
            trigger_branch: "main".into(),
            build_command: Some("pnpm build".into()),
            node_version: Some("20".into()),
            server_host: Some("host".into()),
            server_user: Some("ubuntu".into()),
            deploy_path: Some("/app".into()),
            start_command: Some("pm2 restart app".into()),
            oss_endpoint: Some("oss-cn-hangzhou.aliyuncs.com".into()),
            created_at: None,
        }
    }

    #[test]
    fn web_ssh_template_has_no_github_residue() {
        let (content, filename, secrets) = build_web_ssh_workflow(&cfg("web_ssh"));
        assert!(!content.contains("github.com"), "模板残留 github.com 引用");
        assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN");
        assert_eq!(filename, "deploy.yml");
        assert!(secrets.contains(&"SSH_KEY".to_string()));
    }

    #[test]
    fn tauri_oss_template_has_no_github_residue() {
        let (content, filename, secrets) = build_tauri_oss_workflow(&cfg("tauri_oss"));
        assert!(!content.contains("github.com"), "模板残留 github.com 引用");
        assert!(!content.contains("GITHUB_TOKEN"), "模板残留 GITHUB_TOKEN");
        assert!(!content.contains("gh-release"), "模板残留 GitHub Release 步骤");
        assert!(content.contains("pnpm tauri build"), "缺少构建步骤");
        assert!(content.contains("latest.json"), "缺少 updater 产物上传");
        assert_eq!(filename, "release.yml");
        assert!(secrets.contains(&"TAURI_SIGNING_PRIVATE_KEY".to_string()));
        assert!(!secrets.iter().any(|s| s.contains("GITHUB")), "secrets 残留 GitHub 项");
    }
}
