name: Build App

on:
  push:
    branches: [main]
    paths:
      - "app/**"
      - "VERSION"
      - ".gitea/workflows/build-app.yml"
  pull_request:
    branches: [main]
    paths:
      - "app/**"
      - "VERSION"
      - ".gitea/workflows/build-app.yml"
  workflow_dispatch:

env:
  GITEA_URL: ${{ gitea.server_url }}
  REPO: ${{ gitea.repository }}

jobs:
  compute-version:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.VERSION }}
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Fetch all tags
        run: git fetch --tags

      - name: Compute version from VERSION file and tags
        id: version
        run: |
          MAJOR_MINOR=$(cat VERSION | tr -d '[:space:]')
          echo "Major.Minor: ${MAJOR_MINOR}"

          # Find the latest tag matching v{MAJOR_MINOR}.N (exclude -mac, -win suffixes)
          # `|| true` so an empty grep result doesn't fail the step under pipefail.
          LATEST_TAG=$(git tag -l "v${MAJOR_MINOR}.*" --sort=-v:refname | grep -E "^v${MAJOR_MINOR}\.[0-9]+$" | head -1 || true)

          if [ -n "$LATEST_TAG" ]; then
            echo "Latest matching tag: ${LATEST_TAG}"
            PATCH=$(git rev-list --count "${LATEST_TAG}..HEAD")
          else
            echo "No matching tag found for v${MAJOR_MINOR}.*, using total commit count"
            PATCH=$(git rev-list --count HEAD)
          fi

          VERSION="${MAJOR_MINOR}.${PATCH}"
          echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT
          echo "Computed version: ${VERSION}"

  build-linux:
    runs-on: ubuntu-latest
    needs: [compute-version]
    steps:
      - name: Install Node.js 22
        run: |
          NEED_INSTALL=false
          if command -v node >/dev/null 2>&1; then
            NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
            OLD_NODE_DIR=$(dirname "$(which node)")
            echo "Found Node.js $(node --version) at $(which node) (major: ${NODE_MAJOR})"
            if [ "$NODE_MAJOR" -lt 22 ]; then
              echo "Node.js ${NODE_MAJOR} is too old, removing before installing 22..."
              sudo rm -f "${OLD_NODE_DIR}/node" "${OLD_NODE_DIR}/npm" "${OLD_NODE_DIR}/npx" "${OLD_NODE_DIR}/corepack"
              hash -r
              NEED_INSTALL=true
            fi
          else
            echo "Node.js not found, installing 22..."
            NEED_INSTALL=true
          fi
          if [ "$NEED_INSTALL" = true ]; then
            curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
            sudo apt-get install -y nodejs
            hash -r
          fi
          echo "Node.js at: $(which node)"
          node --version
          npm --version

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set app version
        run: |
          VERSION="${{ needs.compute-version.outputs.version }}"
          sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/src-tauri/tauri.conf.json
          sed -i "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/package.json
          sed -i "s/^version = \".*\"/version = \"${VERSION}\"/" app/src-tauri/Cargo.toml
          echo "Patched version to ${VERSION}"

      - name: Install system dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libgtk-3-dev \
            libwebkit2gtk-4.1-dev \
            libayatana-appindicator3-dev \
            librsvg2-dev \
            libsoup-3.0-dev \
            libssl-dev \
            libxdo-dev \
            patchelf \
            pkg-config \
            build-essential \
            curl \
            wget \
            file \
            xdg-utils

      - name: Install Rust stable
        run: |
          if command -v rustup >/dev/null 2>&1; then
            echo "Rust already installed: $(rustc --version)"
            rustup update stable
            rustup default stable
          else
            curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
          fi
          export PATH="$HOME/.cargo/bin:$PATH"
          rustc --version
          cargo --version

      - name: Install frontend dependencies
        working-directory: ./app
        run: |
          rm -rf node_modules package-lock.json
          npm install

      - name: Install Tauri CLI
        working-directory: ./app
        run: |
          export PATH="$HOME/.cargo/bin:$PATH"
          npx tauri --version || npm install @tauri-apps/cli

      - name: Build Tauri app
        working-directory: ./app
        run: |
          export PATH="$HOME/.cargo/bin:$PATH"
          npx tauri build

      - name: Collect artifacts
        run: |
          mkdir -p artifacts
          cp app/src-tauri/target/release/bundle/appimage/*.AppImage artifacts/ 2>/dev/null || true
          cp app/src-tauri/target/release/bundle/deb/*.deb artifacts/ 2>/dev/null || true
          cp app/src-tauri/target/release/bundle/rpm/*.rpm artifacts/ 2>/dev/null || true
          ls -la artifacts/

      - name: Upload to Gitea release
        if: gitea.event_name == 'push'
        env:
          TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: |
          TAG="v${{ needs.compute-version.outputs.version }}"
          # Create release
          curl -s -X POST \
            -H "Authorization: token ${TOKEN}" \
            -H "Content-Type: application/json" \
            -d "{\"tag_name\": \"${TAG}\", \"name\": \"Triple-C ${TAG} (Linux)\", \"body\": \"Automated build from commit ${{ gitea.sha }}\"}" \
            "${GITEA_URL}/api/v1/repos/${REPO}/releases" > release.json
          RELEASE_ID=$(cat release.json | grep -o '"id":[0-9]*' | head -1 | grep -o '[0-9]*')
          echo "Release ID: ${RELEASE_ID}"
          # Upload each artifact
          for file in artifacts/*; do
            [ -f "$file" ] || continue
            filename=$(basename "$file")
            echo "Uploading ${filename}..."
            curl -s -X POST \
              -H "Authorization: token ${TOKEN}" \
              -H "Content-Type: application/octet-stream" \
              --data-binary "@${file}" \
              "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
          done

  build-macos:
    runs-on: macos-latest
    needs: [compute-version]
    steps:
      - name: Install Node.js 22
        run: |
          NEED_INSTALL=false
          if command -v node >/dev/null 2>&1; then
            NODE_MAJOR=$(node --version | sed 's/v\([0-9]*\).*/\1/')
            echo "Found Node.js $(node --version) (major: ${NODE_MAJOR})"
            if [ "$NODE_MAJOR" -lt 22 ]; then
              echo "Node.js ${NODE_MAJOR} is too old, upgrading to 22..."
              NEED_INSTALL=true
            fi
          else
            echo "Node.js not found, installing 22..."
            NEED_INSTALL=true
          fi
          if [ "$NEED_INSTALL" = true ]; then
            brew install node@22
            brew link --overwrite node@22
          fi
          node --version
          npm --version

      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set app version
        run: |
          VERSION="${{ needs.compute-version.outputs.version }}"
          sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/src-tauri/tauri.conf.json
          sed -i '' "s/\"version\": \".*\"/\"version\": \"${VERSION}\"/" app/package.json
          sed -i '' "s/^version = \".*\"/version = \"${VERSION}\"/" app/src-tauri/Cargo.toml
          echo "Patched version to ${VERSION}"

      - name: Install Rust stable
        run: |
          if command -v rustup >/dev/null 2>&1; then
            echo "Rust already installed: $(rustc --version)"
            rustup update stable
            rustup default stable
          else
            curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
          fi
          export PATH="$HOME/.cargo/bin:$PATH"
          rustup target add aarch64-apple-darwin x86_64-apple-darwin
          rustc --version
          cargo --version

      - name: Install frontend dependencies
        working-directory: ./app
        run: |
          rm -rf node_modules
          npm install

      - name: Install Tauri CLI
        working-directory: ./app
        run: |
          export PATH="$HOME/.cargo/bin:$PATH"
          npx tauri --version || npm install @tauri-apps/cli

      - name: Build Tauri app (universal)
        working-directory: ./app
        run: |
          export PATH="$HOME/.cargo/bin:$PATH"
          npx tauri build --target universal-apple-darwin

      - name: Collect artifacts
        run: |
          mkdir -p artifacts
          cp app/src-tauri/target/universal-apple-darwin/release/bundle/dmg/*.dmg artifacts/ 2>/dev/null || true
          cp app/src-tauri/target/universal-apple-darwin/release/bundle/macos/*.app.tar.gz artifacts/ 2>/dev/null || true
          ls -la artifacts/

      - name: Upload to Gitea release
        if: gitea.event_name == 'push'
        env:
          TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: |
          set -euo pipefail
          TAG="v${{ needs.compute-version.outputs.version }}-mac"

          # Idempotent get-or-create. macOS upload has historically failed
          # mid-stream (curl exit 92, exit 28), leaving the release record
          # with empty assets. A naive POST /releases on the next run hits
          # 409 from Gitea for the duplicate tag, the JSON parse below
          # then yields an empty RELEASE_ID, and pipefail aborts with an
          # opaque exit 1. Look the release up by tag first; create only
          # if it doesn't exist; reuse the existing id otherwise.
          HTTP_CODE=$(curl -sS -o release.json -w '%{http_code}' \
            -H "Authorization: token ${TOKEN}" \
            "${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}")
          case "${HTTP_CODE}" in
            200)
              echo "Release ${TAG} already exists, reusing"
              ;;
            404)
              echo "Release ${TAG} not found, creating"
              curl -fsS -X POST \
                -H "Authorization: token ${TOKEN}" \
                -H "Content-Type: application/json" \
                -d "{\"tag_name\": \"${TAG}\", \"name\": \"Triple-C v${{ needs.compute-version.outputs.version }} (macOS)\", \"body\": \"Automated build from commit ${{ gitea.sha }}\"}" \
                "${GITEA_URL}/api/v1/repos/${REPO}/releases" > release.json
              ;;
            *)
              echo "Unexpected HTTP ${HTTP_CODE} from get-release-by-tag" >&2
              cat release.json >&2 || true
              exit 1
              ;;
          esac

          RELEASE_ID=$(grep -o '"id":[0-9]*' release.json | head -1 | grep -o '[0-9]*' || true)
          if [ -z "${RELEASE_ID}" ]; then
            echo "Failed to parse release id; response was:" >&2
            cat release.json >&2
            exit 1
          fi
          echo "Release ID: ${RELEASE_ID}"

          # Upload each artifact. If an asset with the same name already
          # exists on the release (left over from a partial prior run),
          # delete it first so the upload is replace-not-conflict.
          # Network hardening: HTTP/1.1 to dodge HTTP/2 stream flakes
          # the macOS runner has hit, retries with backoff for transient
          # drops, and -f so HTTP errors stop being silently swallowed.
          for file in artifacts/*; do
            [ -f "$file" ] || continue
            filename=$(basename "$file")

            EXISTING_ID=$(curl -sS \
              -H "Authorization: token ${TOKEN}" \
              "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets" \
              | python3 -c "import json,sys; t=sys.argv[1]; print(next((a['id'] for a in json.load(sys.stdin) if a.get('name')==t), ''))" "${filename}" || true)
            if [ -n "${EXISTING_ID}" ]; then
              echo "Deleting existing asset ${filename} (id ${EXISTING_ID})"
              curl -fsS -X DELETE \
                -H "Authorization: token ${TOKEN}" \
                "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets/${EXISTING_ID}"
            fi

            echo "Uploading ${filename}..."
            curl -fsS --http1.1 \
              --retry 5 --retry-all-errors --retry-delay 5 \
              --max-time 600 \
              -X POST \
              -H "Authorization: token ${TOKEN}" \
              -H "Content-Type: application/octet-stream" \
              --data-binary "@${file}" \
              "${GITEA_URL}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${filename}"
          done

  build-windows:
    runs-on: windows-latest
    needs: [compute-version]
    defaults:
      run:
        shell: cmd
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Set app version
        shell: powershell
        run: |
          $version = "${{ needs.compute-version.outputs.version }}"
          (Get-Content app/src-tauri/tauri.conf.json) -replace '"version": ".*?"', "`"version`": `"$version`"" | Set-Content app/src-tauri/tauri.conf.json
          (Get-Content app/package.json) -replace '"version": ".*?"', "`"version`": `"$version`"" | Set-Content app/package.json
          (Get-Content app/src-tauri/Cargo.toml) -replace '^version = ".*?"', "version = `"$version`"" | Set-Content app/src-tauri/Cargo.toml
          Write-Host "Patched version to $version"

      - name: Install Rust stable
        run: |
          where rustup >nul 2>&1 && (
            rustup update stable
            rustup default stable
          ) || (
            curl -fSL -o rustup-init.exe https://win.rustup.rs/x86_64
            rustup-init.exe -y --default-toolchain stable
            del rustup-init.exe
          )

      - name: Install Node.js
        run: |
          where node >nul 2>&1 && (
            node --version
          ) || (
            curl -fSL -o node-install.msi "https://nodejs.org/dist/v22.14.0/node-v22.14.0-x64.msi"
            msiexec /i node-install.msi /quiet /norestart
            del node-install.msi
          )

      - name: Verify tools
        run: |
          set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
          rustc --version
          cargo --version
          node --version
          npm --version

      - name: Install Tauri CLI via cargo
        run: |
          set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
          cargo install tauri-cli --version "^2"

      - name: Fix npm platform detection
        run: |
          set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
          npm config set os win32
          npm config list

      - name: Install frontend dependencies
        working-directory: ./app
        run: |
          set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
          if exist node_modules rmdir /s /q node_modules
          npm ci

      - name: Build frontend
        working-directory: ./app
        run: |
          set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
          npm run build

      - name: Build Tauri app
        working-directory: ./app
        env:
          TAURI_CONFIG: "{\"build\":{\"beforeBuildCommand\":\"\"}}"
        run: |
          set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
          cargo tauri build

      - name: Collect artifacts
        run: |
          set "PATH=%USERPROFILE%\.cargo\bin;C:\Program Files\nodejs;%PATH%"
          mkdir artifacts
          copy app\src-tauri\target\release\bundle\msi\*.msi artifacts\ 2>nul
          copy app\src-tauri\target\release\bundle\nsis\*.exe artifacts\ 2>nul
          dir artifacts\

      - name: Upload to Gitea release
        if: gitea.event_name == 'push'
        shell: powershell
        env:
          TOKEN: ${{ secrets.REGISTRY_TOKEN }}
          COMMIT_SHA: ${{ gitea.sha }}
          VERSION: ${{ needs.compute-version.outputs.version }}
        run: |
          $ErrorActionPreference = "Stop"
          $tag = "v$env:VERSION-win"
          $headers = @{ Authorization = "token $env:TOKEN" }
          $api = "$env:GITEA_URL/api/v1/repos/$env:REPO"

          # Idempotent get-or-create. The old cmd-batch version swallowed
          # curl errors and parsed the release id with findstr, so a 409 on
          # a pre-existing tag yielded an empty RELEASE_ID and uploads went to
          # a malformed .../releases//assets URL while the step still reported
          # success. Look the release up by tag first; create only on 404.
          try {
            $release = Invoke-RestMethod -Method Get -Headers $headers -Uri "$api/releases/tags/$tag"
            Write-Host "Release $tag already exists, reusing"
          } catch {
            if ($_.Exception.Response.StatusCode.value__ -eq 404) {
              Write-Host "Release $tag not found, creating"
              $body = @{
                tag_name = $tag
                name     = "Triple-C v$env:VERSION (Windows)"
                body     = "Automated build from commit $env:COMMIT_SHA"
              } | ConvertTo-Json
              $release = Invoke-RestMethod -Method Post -Headers $headers `
                -ContentType "application/json" -Body $body -Uri "$api/releases"
            } else {
              throw
            }
          }

          $releaseId = $release.id
          if (-not $releaseId) { throw "Failed to resolve release id for $tag" }
          Write-Host "Release ID: $releaseId"

          # Upload each artifact. Delete any same-named asset left over from a
          # partial prior run first, so the upload replaces rather than 409s.
          $existing = Invoke-RestMethod -Method Get -Headers $headers -Uri "$api/releases/$releaseId/assets"
          foreach ($file in Get-ChildItem -File -Path artifacts\*) {
            $name = $file.Name
            $dupe = $existing | Where-Object { $_.name -eq $name }
            if ($dupe) {
              Write-Host "Deleting existing asset $name (id $($dupe.id))"
              Invoke-RestMethod -Method Delete -Headers $headers -Uri "$api/releases/$releaseId/assets/$($dupe.id)" | Out-Null
            }
            Write-Host "Uploading $name..."
            $uploadUri = "$api/releases/$releaseId/assets?name=$([uri]::EscapeDataString($name))"
            curl.exe -fsS --retry 5 --retry-all-errors --retry-delay 5 --max-time 600 `
              -X POST -H "Authorization: token $env:TOKEN" `
              -H "Content-Type: application/octet-stream" `
              --data-binary "@$($file.FullName)" $uploadUri
            if ($LASTEXITCODE -ne 0) { throw "Upload of $name failed (curl exit $LASTEXITCODE)" }
          }

  create-tag:
    runs-on: ubuntu-latest
    needs: [compute-version, build-linux, build-macos, build-windows]
    if: gitea.event_name == 'push'
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Create version tag
        env:
          TOKEN: ${{ secrets.REGISTRY_TOKEN }}
        run: |
          VERSION="${{ needs.compute-version.outputs.version }}"
          TAG="v${VERSION}"
          echo "Creating tag ${TAG}..."

          # Create annotated tag via Gitea API
          curl -s -X POST \
            -H "Authorization: token ${TOKEN}" \
            -H "Content-Type: application/json" \
            -d "{\"tag_name\": \"${TAG}\", \"target\": \"${{ gitea.sha }}\", \"message\": \"Release ${TAG}\"}" \
            "${GITEA_URL}/api/v1/repos/${REPO}/tags" || echo "Tag may already exist (created by release)"

          echo "Tag ${TAG} created successfully"

  sync-to-github:
    runs-on: ubuntu-latest
    needs: [compute-version, build-linux, build-macos, build-windows]
    if: gitea.event_name == 'push'
    env:
      GH_PAT: ${{ secrets.GH_PAT }}
      GITHUB_REPO: shadowdao/triple-c
    steps:
      - name: Download artifacts from Gitea releases
        env:
          TOKEN: ${{ secrets.REGISTRY_TOKEN }}
          VERSION: ${{ needs.compute-version.outputs.version }}
        run: |
          set -e
          mkdir -p artifacts

          # Download assets from all 3 platform releases
          for TAG_SUFFIX in "" "-mac" "-win"; do
            TAG="v${VERSION}${TAG_SUFFIX}"
            echo "==> Fetching assets for release ${TAG}..."

            RELEASE_JSON=$(curl -sf \
              -H "Authorization: token ${TOKEN}" \
              "${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}" 2>/dev/null || echo "{}")

            echo "$RELEASE_JSON" | jq -r '.assets[]? | "\(.name) \(.browser_download_url)"' | while read -r NAME URL; do
              [ -z "$NAME" ] && continue
              echo "    Downloading ${NAME}..."
              curl -sfL \
                -H "Authorization: token ${TOKEN}" \
                -o "artifacts/${NAME}" \
                "$URL"
            done
          done

          echo "==> All downloaded artifacts:"
          ls -la artifacts/

      - name: Create GitHub release and upload artifacts
        env:
          VERSION: ${{ needs.compute-version.outputs.version }}
          COMMIT_SHA: ${{ gitea.sha }}
        run: |
          set -e
          TAG="v${VERSION}"

          echo "==> Creating unified release ${TAG} on GitHub..."

          # Delete existing release if present (idempotent re-runs)
          EXISTING=$(curl -sf \
            -H "Authorization: Bearer ${GH_PAT}" \
            -H "Accept: application/vnd.github+json" \
            "https://api.github.com/repos/${GITHUB_REPO}/releases/tags/${TAG}" 2>/dev/null || echo "{}")
          EXISTING_ID=$(echo "$EXISTING" | jq -r '.id // empty')
          if [ -n "$EXISTING_ID" ]; then
            echo "    Deleting existing GitHub release ${TAG} (id: ${EXISTING_ID})..."
            curl -sf -X DELETE \
              -H "Authorization: Bearer ${GH_PAT}" \
              -H "Accept: application/vnd.github+json" \
              "https://api.github.com/repos/${GITHUB_REPO}/releases/${EXISTING_ID}"
          fi

          RESPONSE=$(curl -sf -X POST \
            -H "Authorization: Bearer ${GH_PAT}" \
            -H "Accept: application/vnd.github+json" \
            -H "Content-Type: application/json" \
            "https://api.github.com/repos/${GITHUB_REPO}/releases" \
            -d "{
              \"tag_name\": \"${TAG}\",
              \"name\": \"Triple-C ${TAG}\",
              \"body\": \"Automated build from commit ${COMMIT_SHA}\n\nIncludes Linux, macOS, and Windows artifacts.\",
              \"draft\": false,
              \"prerelease\": false
            }")

          UPLOAD_URL=$(echo "$RESPONSE" | jq -r '.upload_url' | sed 's/{?name,label}//')
          echo "==> Upload URL: ${UPLOAD_URL}"

          for file in artifacts/*; do
            [ -f "$file" ] || continue
            FILENAME=$(basename "$file")
            MIME="application/octet-stream"
            echo "==> Uploading ${FILENAME}..."
            curl -sf -X POST \
              -H "Authorization: Bearer ${GH_PAT}" \
              -H "Accept: application/vnd.github+json" \
              -H "Content-Type: ${MIME}" \
              --data-binary "@${file}" \
              "${UPLOAD_URL}?name=$(python3 -c "import urllib.parse, sys; print(urllib.parse.quote(sys.argv[1]))" "${FILENAME}")"
          done

          echo "==> GitHub release sync complete."
