name: Build Windows EXE Release

on:
  push:
    tags:
      - "v[0-9]+.[0-9]+.[0-9]+"
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: cicy-desktop-release-windows
  cancel-in-progress: true

jobs:
  build-windows-exe:
    runs-on: windows-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Sync runtime deps to latest (cicy-code + cicy-mihomo; drop msys2)
        run: node scripts/sync-runtime-deps.cjs

      - name: Install project dependencies
        run: npm install --no-audit

      - name: Install Electron build dependencies in project directory
        run: npm install --save-dev --no-audit electron@41.0.2 electron-builder@26.7.0

      # NOTE: the homepage SPA is rebuilt automatically by the prebuild:win npm
      # hook (scripts/build-homepage.cjs) right before `npm run build:win` below,
      # so the exe can never ship a stale homepage. No explicit step needed.

      - name: Prepare Windows icon
        shell: powershell
        continue-on-error: true
        run: |
          New-Item -ItemType Directory -Force -Path build | Out-Null
          try {
            Invoke-WebRequest -Uri "https://cicy-ai.com/logo.svg" -OutFile "build\logo.svg" -TimeoutSec 12
          } catch {
            Write-Host "::warning::logo.svg fetch failed ($_); building with default Electron icon"
            exit 0
          }
          try {
            magick -background none -density 768 "build\logo.svg" -alpha on -resize 256x256 PNG32:"build\logo-256.png"
            magick "build\logo-256.png" -define icon:auto-resize=256,128,64,48,32,16 "build\logo.ico"
          } catch {
            Write-Host "::warning::magick convert failed ($_); skipping icon"
            exit 0
          }

      - name: Resolve release version
        id: meta
        shell: bash
        run: |
          version="$(node -p "require('./package.json').version")"
          echo "version=${version}" >> "$GITHUB_OUTPUT"
          echo "tag=v${version}" >> "$GITHUB_OUTPUT"

      - name: Create GitHub release as draft (idempotent)
        # Draft so autoUpdater doesn't pick it up before .exe + latest.yml are
        # uploaded. Promoted to non-draft + latest in the final step.
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release create "${{ steps.meta.outputs.tag }}" \
            --repo "${{ github.repository }}" \
            --title "CiCy Desktop v${{ steps.meta.outputs.version }}" \
            --generate-notes \
            --draft \
            2>/dev/null || echo "release already exists, continuing"

      - name: Build Windows EXE and publish assets
        # --publish always uploads .exe + latest.yml to the release created above.
        shell: powershell
        env:
          GH_TOKEN: ${{ github.token }}
          PUBLISH_OWNER: ${{ github.repository_owner }}
          PUBLISH_REPO: ${{ github.event.repository.name }}
          CICY_OBFUSCATE: "1"   # 发版混淆 JS(prebuild 主进程 + Vite 渲染层),仅 release CI
        run: |
          if (Test-Path dist) { Remove-Item -Recurse -Force dist }
          if (Test-Path "build\logo.ico") {
            npm run build:win -- --config.win.icon=build/logo.ico --config.publish.owner="$env:PUBLISH_OWNER" --config.publish.repo="$env:PUBLISH_REPO" --publish always
          } else {
            Write-Host "::warning::no icon, using default"
            npm run build:win -- --config.publish.owner="$env:PUBLISH_OWNER" --config.publish.repo="$env:PUBLISH_REPO" --publish always
          }

      # 推 exe 到 R2。三件事(不再是"永远一个版本"):
      #   1) 带版本号的永久副本 releases/cicy-desktop-<version>.exe — 历史 / 指定版本 / 回滚
      #   2) latest 指针 releases/cicy-desktop-latest.exe — 只前进:本次版本 < 已发布版本(更旧的
      #      构建后完成)则不覆盖,避免并发构建竞态把 latest 退回旧版
      #   3) releases/latest-version.txt — 记录 latest 当前是哪个版本,curl 一下即可核对,无需猜 exe
      #
      # 原先发到 Aliyun OSS,该账号已停用(整桶 403 UserDisable),统一改走 scripts/r2.mjs。
      - name: Upload EXE to R2 (versioned + forward-only latest pointer)
        shell: pwsh
        env:
          R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
          R2_API_TOKEN:  ${{ secrets.R2_API_TOKEN }}
        run: |
          $ErrorActionPreference = "Continue"
          $version = "${{ steps.meta.outputs.version }}"
          if (-not $env:R2_ACCOUNT_ID -or -not $env:R2_API_TOKEN) {
            Write-Host "::warning::R2 creds missing — skip"; exit 0
          }
          $r2   = "scripts/r2.mjs"
          $base = "releases"
          $host_ = "https://r2.deepfetch.de5.net/releases"
          $exe = Get-ChildItem dist\*.exe | Where-Object { $_.Name -match 'Setup' } | Select-Object -First 1
          if (-not $exe) { $exe = Get-ChildItem dist\*.exe | Select-Object -First 1 }
          if (-not $exe) { Write-Error "no exe found in dist"; exit 1 }

          # 0) WSL2 内核裸文件(一次性,已存在就跳过):精简版 Windows 没有 msiexec 装不了
          #    wsl_update_x64.msi,桌面端改为直接下这个文件放到 System32\lxss\tools\kernel。
          $kernelKey = "$base/wsl-kernel/kernel"
          node $r2 exists $kernelKey
          if ($LASTEXITCODE -ne 0) {
            Write-Host "Publishing raw WSL2 kernel -> $kernelKey"
            Invoke-WebRequest -Uri "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi" -OutFile wsl_update_x64.msi
            $kdir = Join-Path $PWD "wsl-kernel-extract"
            Start-Process msiexec.exe -ArgumentList "/a","`"$PWD\wsl_update_x64.msi`"","/qn","TARGETDIR=$kdir" -Wait
            $kfile = Get-ChildItem $kdir -Recurse -File | Where-Object { $_.Name -eq "kernel" } | Select-Object -First 1
            if ($kfile) { node $r2 put $kernelKey "$($kfile.FullName)" } else { Write-Warning "kernel not found in msi extract" }
          } else { Write-Host "WSL2 kernel already on R2, skip" }

          # 0b) 新版 WSL 的 MSI(一次性,已存在就跳过):老 Win10 自带旧版 WSL 反复死锁,桌面端改为装新版。
          $wslKey = "$base/wsl/wsl.x64.msi"
          node $r2 exists $wslKey
          if ($LASTEXITCODE -ne 0) {
            $rel = Invoke-RestMethod -Uri "https://api.github.com/repos/microsoft/WSL/releases/latest" -Headers @{ "User-Agent" = "cicy-desktop-ci" }
            $asset = $rel.assets | Where-Object { $_.name -match 'x64\.msi$' } | Select-Object -First 1
            if ($asset) {
              Write-Host "Publishing modern WSL $($rel.tag_name) -> $wslKey"
              Invoke-WebRequest -Uri $asset.browser_download_url -OutFile wsl.x64.msi
              node $r2 put $wslKey "wsl.x64.msi"
              Set-Content -Path wsl-version.txt -Value $rel.tag_name
              node $r2 put "$base/wsl/version.txt" "wsl-version.txt"
            } else { Write-Warning "no x64 msi asset in microsoft/WSL latest" }
          } else { Write-Host "modern WSL msi already on R2, skip" }

          # 1) 永久版本化副本(unique key — 不会被后续发版覆盖)
          $verKey = "$base/cicy-desktop-$version.exe"
          Write-Host "Uploading versioned -> $verKey"
          node $r2 put $verKey "$($exe.FullName)"
          if ($LASTEXITCODE -ne 0) { Write-Error "versioned upload failed"; exit 1 }

          # 2) latest 指针 — 只在本次版本 >= 已发布版本时前进
          $cur = "0.0.0"
          try { $cur = (Invoke-WebRequest -Uri "$host_/latest-version.txt" -UseBasicParsing).Content.Trim() } catch { $cur = "0.0.0" }
          $advance = $true
          try { $advance = ([version]$version -ge [version]$cur) } catch { $advance = $true }
          if ($advance) {
            Write-Host "Advancing latest: $cur -> $version"
            # 关键:版本指针(latest-version.txt)必须在所有安装包传完之后才写 —— 否则
            # updater 先读到新版本号、用户点下载,但 exe 还没传完 → 404。所以先把所有 exe
            # 传齐(versioned 已在上面传过 + latest + win-latest),最后才更新版本指针。
            node $r2 put "$base/cicy-desktop-latest.exe" "$($exe.FullName)"
            if ($LASTEXITCODE -ne 0) { Write-Error "latest upload failed"; exit 1 }
            # Unified per-platform naming: cicy-desktop-<platform>-latest.<ext> so every OS
            # shares one URL shape (win + mac-x64 + mac-arm64). The bare cicy-desktop-latest.exe
            # / latest-version.txt are kept as backward-compat aliases for old links.
            node $r2 put "$base/cicy-desktop-win-latest.exe" "$($exe.FullName)"
            if ($LASTEXITCODE -ne 0) { Write-Error "win-latest upload failed"; exit 1 }
            # —— 安装包全部就位,最后写版本指针(version file 永远最后)——
            Set-Content -NoNewline -Path "current-latest.txt" -Value $version
            node $r2 put "$base/latest-version.txt" "current-latest.txt"
            if ($LASTEXITCODE -ne 0) { Write-Error "latest-version.txt upload failed"; exit 1 }
            node $r2 put "$base/win-latest-version.txt" "current-latest.txt"
            if ($LASTEXITCODE -ne 0) { Write-Error "win-latest-version.txt upload failed"; exit 1 }
          } else {
            Write-Host "Skip latest: building $version < published $cur (older build — not clobbering newer latest)"
          }

          # 3) 保留策略 — 只留最近 $KEEP 个版本化 exe(~130MB/个),旧的删掉,避免 bucket 无限膨胀。
          #    latest.exe / latest-version.txt 不在此列(名字不匹配 X.Y.Z),永远保留。
          $KEEP = 2
          $versions = @()
          foreach ($l in (node $r2 list "$base/cicy-desktop-")) {
            if ($l -match 'releases/cicy-desktop-(\d+\.\d+\.\d+)\.exe\s*$') { $versions += $matches[1] }
          }
          $sorted = @($versions | Sort-Object { [version]$_ } -Descending)
          if ($sorted.Count -gt $KEEP) {
            foreach ($v in ($sorted | Select-Object -Skip $KEEP)) {
              Write-Host "Pruning old versioned exe: cicy-desktop-$v.exe"
              node $r2 delete "$base/cicy-desktop-$v.exe"
            }
          } else {
            Write-Host "Retention: $($sorted.Count) versioned exe(s) <= keep $KEEP, nothing to prune"
          }

          Write-Host "Versioned : $host_/cicy-desktop-$version.exe"
          Write-Host "Latest    : $host_/cicy-desktop-latest.exe"
          Write-Host "Version-of: $host_/latest-version.txt"

      - name: Publish release (drop draft, mark latest)
        # Only promote AFTER all assets uploaded successfully. autoUpdater on
        # existing clients will pick up the new version on its next poll.
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          gh release edit "${{ steps.meta.outputs.tag }}" \
            --repo "${{ github.repository }}" \
            --draft=false \
            --latest
