name: Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Release tag'
        required: true
        type: string
      is_prerelease:
        description: 'Pre-release'
        required: false
        type: boolean
        default: false

env:
  DOTNET_VERSION: '10.0.x'
  Solution_Name: LanMountainDesktop.slnx
  DOTNET_gcServer: 1

jobs:
  prepare:
    runs-on: ubuntu-latest
    outputs:
      version: ${{ steps.version.outputs.version }}
      assembly_version: ${{ steps.version.outputs.assembly_version }}
      informational_version: ${{ steps.version.outputs.informational_version }}
      tag: ${{ steps.version.outputs.tag }}
      checkout_ref: ${{ steps.version.outputs.checkout_ref }}
      is_prerelease: ${{ steps.version.outputs.is_prerelease }}
      release_channel: ${{ steps.version.outputs.release_channel }}

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

      - name: Get release info
        id: version
        run: |
          if [[ "${{ github.event_name }}" == "push" ]]; then
            TAG="${GITHUB_REF#refs/tags/}"
            CHECKOUT_REF="${GITHUB_REF}"
            IS_PRERELEASE="false"
          else
            RAW_TAG="${{ github.event.inputs.tag }}"
            if [[ "${RAW_TAG}" == refs/tags/* ]]; then
              TAG="${RAW_TAG#refs/tags/}"
            elif [[ "${RAW_TAG}" == v* ]]; then
              TAG="${RAW_TAG}"
            else
              TAG="v${RAW_TAG}"
            fi

            if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
              CHECKOUT_REF="refs/tags/${TAG}"
            else
              CHECKOUT_REF="${GITHUB_SHA}"
            fi

            if [[ "${{ github.event.inputs.is_prerelease }}" == "true" ]]; then
              IS_PRERELEASE="true"
            else
              IS_PRERELEASE="false"
            fi
          fi

          VERSION="${TAG#v}"
          IFS='.' read -r -a VERSION_PARTS <<< "${VERSION}"
          while [ "${#VERSION_PARTS[@]}" -lt 4 ]; do
            VERSION_PARTS+=("0")
          done
          ASSEMBLY_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}.${VERSION_PARTS[3]}"

          if [[ "${IS_PRERELEASE}" == "true" ]]; then
            RELEASE_CHANNEL="preview"
          else
            RELEASE_CHANNEL="stable"
          fi

          echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
          echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "assembly_version=${ASSEMBLY_VERSION}" >> "$GITHUB_OUTPUT"
          echo "informational_version=${VERSION}" >> "$GITHUB_OUTPUT"
          echo "checkout_ref=${CHECKOUT_REF}" >> "$GITHUB_OUTPUT"
          echo "is_prerelease=${IS_PRERELEASE}" >> "$GITHUB_OUTPUT"
          echo "release_channel=${RELEASE_CHANNEL}" >> "$GITHUB_OUTPUT"

  build-windows:
    needs: prepare
    runs-on: windows-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - arch: x64
            self_contained: true
            suffix: ''
          - arch: x86
            self_contained: true
            suffix: ''
    name: Build_Windows_${{ matrix.arch }}${{ matrix.suffix }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          submodules: recursive
          ref: ${{ needs.prepare.outputs.checkout_ref }}

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}
          dotnet-quality: 'preview'

      - name: Restore
        run: dotnet restore ${{ env.Solution_Name }}

      - name: Build
        run: >
          dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
          -p:Version=${{ needs.prepare.outputs.version }}
          -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }}
          -p:FileVersion=${{ needs.prepare.outputs.assembly_version }}
          -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

      - name: Publish Launcher (AOT)
        run: |
          $version = "${{ needs.prepare.outputs.version }}"
          $arch = "${{ matrix.arch }}"
          $launcherPublishDir = "publish/launcher-win-$arch"

          dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj `
            -c Release `
            -o ./$launcherPublishDir `
            --self-contained `
            -r win-$arch `
            -p:PublishAot=true `
            -p:PublishSingleFile=true `
            -p:IncludeNativeLibrariesForSelfExtract=true `
            -p:EnableCompressionInSingleFile=true `
            -p:DebugType=none `
            -p:DebugSymbols=false `
            -p:Version=${{ needs.prepare.outputs.version }} `
            -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
            -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
            -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

          if ($LASTEXITCODE -ne 0) {
            Write-Error "Launcher AOT publish failed"
            exit 1
          }
        shell: pwsh

      - name: Publish Main App
        run: |
          $selfContained = "${{ matrix.self_contained }}" -eq "true"
          $publishDir = if ($selfContained) { "publish/windows-${{ matrix.arch }}" } else { "publish/windows-${{ matrix.arch }}-lite" }

          if ($selfContained) {
            dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
              -c Release `
              -o ./$publishDir `
              --self-contained `
              -r win-${{ matrix.arch }} `
              -p:PublishSingleFile=false `
              -p:DebugType=none `
              -p:DebugSymbols=false `
              -p:PublishTrimmed=false `
              -p:PublishReadyToRun=false `
              -p:Version=${{ needs.prepare.outputs.version }} `
              -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
              -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
              -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
          } else {
            dotnet publish LanMountainDesktop/LanMountainDesktop.csproj `
              -c Release `
              -o ./$publishDir `
              --self-contained:false `
              -p:PublishSingleFile=false `
              -p:DebugType=none `
              -p:DebugSymbols=false `
              -p:PublishTrimmed=false `
              -p:PublishReadyToRun=false `
              -p:Version=${{ needs.prepare.outputs.version }} `
              -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
              -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
              -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
          }
        shell: pwsh

      - name: Restructure for Launcher
        run: |
          $version = "${{ needs.prepare.outputs.version }}"
          $arch = "${{ matrix.arch }}"
          $selfContained = "${{ matrix.self_contained }}" -eq "true"
          $publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" }
          $launcherPublishDir = "publish/launcher-win-$arch"
          $appDir = "app-$version"
          $newStructure = "publish-launcher/windows-$arch"

          New-Item -ItemType Directory -Path $newStructure -Force | Out-Null
          $appPath = Join-Path $newStructure $appDir
          Move-Item -Path $publishDir -Destination $appPath -Force

          if (Test-Path $launcherPublishDir) {
            Copy-Item -Path "$launcherPublishDir\*" -Destination $newStructure -Recurse -Force
          }

          New-Item -ItemType File -Path (Join-Path $appPath ".current") -Force | Out-Null

          Remove-Item -Path $publishDir -Recurse -Force -ErrorAction SilentlyContinue
          Remove-Item -Path $launcherPublishDir -Recurse -Force -ErrorAction SilentlyContinue
          Move-Item -Path $newStructure -Destination $publishDir -Force
        shell: pwsh

      - name: Install Inno Setup and 7z
        run: |
          choco install innosetup -y --no-progress
          choco install 7zip -y --no-progress
        shell: pwsh

      - name: Build Installer
        run: |
          $version = "${{ needs.prepare.outputs.version }}"
          $arch = "${{ matrix.arch }}"
          $suffix = "${{ matrix.suffix }}"
          $selfContained = "${{ matrix.self_contained }}" -eq "true"
          $publishDir = if ($selfContained) { "publish/windows-$arch" } else { "publish/windows-$arch-lite" }
          $outputDir = "build-installer"
          $installerScript = "LanMountainDesktop/installer/LanMountainDesktop.iss"

          New-Item -ItemType Directory -Path $outputDir -Force | Out-Null

          $candidatePaths = @(
            (Get-Command iscc.exe -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source -ErrorAction SilentlyContinue),
            "$env:ProgramFiles(x86)\Inno Setup 6\ISCC.exe",
            "$env:ProgramFiles\Inno Setup 6\ISCC.exe",
            "$env:ChocolateyInstall\lib\innosetup\tools\ISCC.exe"
          ) | Where-Object { $_ -and (Test-Path $_) }

          $isccPath = $candidatePaths | Select-Object -First 1
          if (-not $isccPath) {
            Write-Error "Inno Setup compiler not found."
            exit 1
          }

          if (-not (Test-Path $installerScript)) {
            Write-Error "Installer script not found: $(Join-Path $PWD $installerScript)"
            exit 1
          }

          $publishDir = (Resolve-Path $publishDir).Path
          $outputDir = (Resolve-Path $outputDir).Path
          $installerScript = (Resolve-Path $installerScript).Path

          $compileArgs = @(
            "/DMyAppVersion=$version",
            "/DPublishDir=$publishDir",
            "/DMyOutputDir=$outputDir",
            "/DMyAppArch=$arch",
            "/DMyAppSuffix=$suffix",
            "/DIsSelfContained=$selfContained",
            $installerScript
          )

          & $isccPath @compileArgs
          if ($LASTEXITCODE -ne 0) {
            Write-Error "Inno Setup compiler exited with code $LASTEXITCODE"
            exit 1
          }

          $installerFile = Get-ChildItem -Path $outputDir -Filter "*.exe" -ErrorAction SilentlyContinue | Select-Object -First 1
          if (-not $installerFile) {
            Write-Error "Failed to create installer"
            exit 1
          }
        shell: pwsh

      - name: Package Payload Zip
        run: |
          $version = "${{ needs.prepare.outputs.version }}"
          $arch = "${{ matrix.arch }}"
          $payloadRoot = Join-Path (Join-Path $PWD "publish/windows-$arch") "app-$version"
          if (-not (Test-Path $payloadRoot)) {
            Write-Error "Payload root not found: $payloadRoot"
            exit 1
          }

          $stageDir = Join-Path $PWD "payload-stage/windows-$arch"
          $releaseDir = Join-Path $PWD "release-assets"
          Remove-Item -Path $stageDir -Recurse -Force -ErrorAction SilentlyContinue
          New-Item -ItemType Directory -Path $stageDir -Force | Out-Null
          New-Item -ItemType Directory -Path $releaseDir -Force | Out-Null

          Get-ChildItem -Path $payloadRoot -Recurse -File | ForEach-Object {
            $relative = [System.IO.Path]::GetRelativePath($payloadRoot, $_.FullName).Replace('\', '/')
            if ($relative -eq '.current' -or $relative -eq '.partial' -or $relative -eq '.destroy' -or $relative.StartsWith('.current/') -or $relative.StartsWith('.partial/') -or $relative.StartsWith('.destroy/')) {
              return
            }

            $destination = Join-Path $stageDir ($relative -replace '/', [System.IO.Path]::DirectorySeparatorChar)
            $destinationDir = Split-Path -Path $destination -Parent
            if (-not [string]::IsNullOrWhiteSpace($destinationDir)) {
              New-Item -ItemType Directory -Path $destinationDir -Force | Out-Null
            }

            Copy-Item -Path $_.FullName -Destination $destination -Force
          }

          $payloadZip = Join-Path $releaseDir "files-windows-$arch.zip"
          if (Test-Path $payloadZip) {
            Remove-Item $payloadZip -Force
          }
          Compress-Archive -Path (Join-Path $stageDir '*') -DestinationPath $payloadZip -Force
        shell: pwsh

      - name: Upload Release Assets
        uses: actions/upload-artifact@v4
        with:
          name: release-windows-${{ matrix.arch }}
          path: |
            release-assets/files-windows-${{ matrix.arch }}.zip
            build-installer/*.exe
          if-no-files-found: error
          retention-days: 30

  build-linux:
    needs: prepare
    runs-on: ubuntu-latest
    name: Build_Linux

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          submodules: recursive
          ref: ${{ needs.prepare.outputs.checkout_ref }}

      - name: Install dependencies
        run: |
          sudo apt-get update
          sudo apt-get install -y \
            libfontconfig1 libfreetype6 \
            libx11-6 libxrandr2 libxinerama1 \
            libxi6 libxcursor1 libxext6 \
            libxrender1 libxkbcommon-x11-0 \
            clang zlib1g-dev zip rsync

          sudo apt-get install -y libasound2t64 || sudo apt-get install -y libasound2
          sudo apt-get install -y libportaudio2t64 || sudo apt-get install -y libportaudio2
          sudo apt-get install -y libwebkit2gtk-4.1-dev || sudo apt-get install -y libwebkit2gtk-4.0-dev

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}
          dotnet-quality: 'preview'

      - name: Restore
        run: dotnet restore ${{ env.Solution_Name }}

      - name: Build
        run: >
          dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
          -p:Version=${{ needs.prepare.outputs.version }}
          -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }}
          -p:FileVersion=${{ needs.prepare.outputs.assembly_version }}
          -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

      - name: Publish Launcher (AOT)
        run: |
          dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj \
            -c Release \
            -o ./publish/launcher-linux-x64 \
            --self-contained \
            -r linux-x64 \
            -p:PublishAot=true \
            -p:PublishSingleFile=true \
            -p:IncludeNativeLibrariesForSelfExtract=true \
            -p:EnableCompressionInSingleFile=true \
            -p:DebugType=none \
            -p:DebugSymbols=false \
            -p:Version=${{ needs.prepare.outputs.version }} \
            -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

      - name: Publish Main App
        run: |
          dotnet publish LanMountainDesktop/LanMountainDesktop.csproj \
            -c Release \
            -o ./publish/linux-x64-app \
            --self-contained \
            -r linux-x64 \
            -p:PublishSingleFile=false \
            -p:SelfContained=true \
            -p:DebugType=none \
            -p:DebugSymbols=false \
            -p:PublishTrimmed=false \
            -p:PublishReadyToRun=false \
            -p:Version=${{ needs.prepare.outputs.version }} \
            -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

      - name: Restructure for Launcher
        run: |
          version="${{ needs.prepare.outputs.version }}"
          publishDir="publish/linux-x64"
          appDir="app-$version"
          launcherDir="publish/launcher-linux-x64"

          mkdir -p "$publishDir"
          mv "publish/linux-x64-app" "$publishDir/$appDir"

          if [ -d "$launcherDir" ]; then
            cp -r "$launcherDir"/* "$publishDir/"
            chmod +x "$publishDir/LanMountainDesktop.Launcher" 2>/dev/null || true
          fi

          touch "$publishDir/$appDir/.current"
          rm -rf "$launcherDir"

      - name: Package as DEB
        run: |
          version="${{ needs.prepare.outputs.version }}"
          source="publish/linux-x64"
          package_name="LanMountainDesktop"
          package_version="${version}"
          arch="amd64"
          desktop_template="LanMountainDesktop/packaging/linux/LanMountainDesktop.desktop"
          icon_source="LanMountainDesktop/packaging/linux/lanmountaindesktop.png"

          mkdir -p "build-deb/DEBIAN"
          mkdir -p "build-deb/usr/local/bin"
          mkdir -p "build-deb/usr/share/applications"
          mkdir -p "build-deb/usr/share/pixmaps"
          mkdir -p "build-deb/usr/share/icons/hicolor/256x256/apps"

          cp -r "$source"/* "build-deb/usr/local/bin/"

          sed \
            -e "s|@@EXEC@@|/usr/local/bin/LanMountainDesktop.Launcher|g" \
            -e "s|@@ICON@@|lanmountaindesktop|g" \
            "$desktop_template" > "build-deb/usr/share/applications/LanMountainDesktop.desktop"

          cp "$icon_source" "build-deb/usr/share/pixmaps/lanmountaindesktop.png"
          cp "$icon_source" "build-deb/usr/share/icons/hicolor/256x256/apps/lanmountaindesktop.png"

          {
            printf '%s\n' '#!/bin/sh'
            printf '%s\n' 'set -e'
            printf '%s\n' 'if command -v update-desktop-database >/dev/null 2>&1; then'
            printf '%s\n' '  update-desktop-database /usr/share/applications >/dev/null 2>&1 || true'
            printf '%s\n' 'fi'
            printf '%s\n' 'if command -v gtk-update-icon-cache >/dev/null 2>&1; then'
            printf '%s\n' '  gtk-update-icon-cache /usr/share/icons/hicolor >/dev/null 2>&1 || true'
            printf '%s\n' 'fi'
          } > "build-deb/DEBIAN/postinst"

          {
            printf '%s\n' "Package: $package_name"
            printf '%s\n' "Version: $package_version"
            printf '%s\n' "Architecture: $arch"
            printf '%s\n' 'Maintainer: LanMountain Team <dev@example.com>'
            printf '%s\n' 'Description: LanMountain Desktop Application'
            printf '%s\n' ' A desktop application for LanMountain.'
          } > "build-deb/DEBIAN/control"

          chmod 755 "build-deb/usr/local/bin/LanMountainDesktop.Launcher" 2>/dev/null || chmod 755 "build-deb/usr/local/bin"/*
          chmod 644 "build-deb/usr/share/applications/LanMountainDesktop.desktop"
          chmod 644 "build-deb/usr/share/pixmaps/lanmountaindesktop.png"
          chmod 644 "build-deb/usr/share/icons/hicolor/256x256/apps/lanmountaindesktop.png"
          chmod 755 "build-deb/DEBIAN/postinst"

          dpkg-deb --build "build-deb" "${package_name}_${package_version}_${arch}.deb"

      - name: Package Payload Zip
        run: |
          version="${{ needs.prepare.outputs.version }}"
          payload_root="publish/linux-x64/app-$version"
          release_dir="$PWD/release-assets"
          stage_dir="$PWD/payload-stage/linux-x64"

          if [ ! -d "$payload_root" ]; then
            echo "Payload root not found: $payload_root"
            exit 1
          fi

          rm -rf "$stage_dir"
          mkdir -p "$stage_dir" "$release_dir"
          rsync -a \
            --exclude '.current' \
            --exclude '.partial' \
            --exclude '.destroy' \
            "$payload_root/" "$stage_dir/"

          (
            cd "$stage_dir"
            zip -qr "$release_dir/files-linux-x64.zip" .
          )

      - name: Upload Release Assets
        uses: actions/upload-artifact@v4
        with:
          name: release-linux-x64
          path: |
            release-assets/files-linux-x64.zip
            *.deb
          if-no-files-found: error
          retention-days: 30

  build-macos:
    needs: prepare
    runs-on: macos-latest
    continue-on-error: true
    strategy:
      fail-fast: false
      matrix:
        arch: [x64, arm64]
    name: Build_macOS_${{ matrix.arch }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          submodules: recursive
          ref: ${{ needs.prepare.outputs.checkout_ref }}

      - name: Install dependencies
        run: brew install portaudio

      - name: Setup .NET
        uses: actions/setup-dotnet@v4
        with:
          dotnet-version: ${{ env.DOTNET_VERSION }}
          dotnet-quality: 'preview'

      - name: Restore
        run: dotnet restore ${{ env.Solution_Name }}

      - name: Build
        run: >
          dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
          -p:Version=${{ needs.prepare.outputs.version }}
          -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }}
          -p:FileVersion=${{ needs.prepare.outputs.assembly_version }}
          -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

      - name: Publish Launcher (AOT)
        run: |
          dotnet publish LanMountainDesktop.Launcher/LanMountainDesktop.Launcher.csproj \
            -c Release \
            -o ./publish/launcher-macos-${{ matrix.arch }} \
            --self-contained \
            -r osx-${{ matrix.arch }} \
            -p:PublishAot=true \
            -p:PublishSingleFile=true \
            -p:IncludeNativeLibrariesForSelfExtract=true \
            -p:EnableCompressionInSingleFile=true \
            -p:DebugType=none \
            -p:DebugSymbols=false \
            -p:Version=${{ needs.prepare.outputs.version }} \
            -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

      - name: Publish Main App
        run: |
          dotnet publish LanMountainDesktop/LanMountainDesktop.csproj \
            -c Release \
            -o ./publish/macos-${{ matrix.arch }}-app \
            --self-contained \
            -r osx-${{ matrix.arch }} \
            -p:PublishSingleFile=false \
            -p:SelfContained=true \
            -p:DebugType=none \
            -p:DebugSymbols=false \
            -p:PublishTrimmed=false \
            -p:PublishReadyToRun=false \
            -p:Version=${{ needs.prepare.outputs.version }} \
            -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \
            -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}

      - name: Package Payload Zip
        run: |
          release_dir="$PWD/release-assets"
          stage_dir="$PWD/payload-stage/macos-${{ matrix.arch }}"
          payload_root="publish/macos-${{ matrix.arch }}-app"

          rm -rf "$stage_dir"
          mkdir -p "$stage_dir" "$release_dir"
          rsync -a "$payload_root/" "$stage_dir/"
          (
            cd "$stage_dir"
            zip -qr "$release_dir/files-macos-${{ matrix.arch }}.zip" .
          )

      - name: Restructure and Package as DMG
        continue-on-error: true
        run: |
          version="${{ needs.prepare.outputs.version }}"
          arch="${{ matrix.arch }}"
          app_name="LanMountainDesktop"
          package_name="${app_name}-${version}-macos-${arch}"
          launcherDir="publish/launcher-macos-$arch"
          appSourceDir="publish/macos-$arch-app"

          mkdir -p "${app_name}.app/Contents/MacOS"
          appDir="app-$version"
          mkdir -p "${app_name}.app/Contents/MacOS/$appDir"

          cp -r "$appSourceDir"/* "${app_name}.app/Contents/MacOS/$appDir/"
          if [ -d "$launcherDir" ]; then
            cp -r "$launcherDir"/* "${app_name}.app/Contents/MacOS/"
            chmod +x "${app_name}.app/Contents/MacOS/LanMountainDesktop.Launcher" 2>/dev/null || true
          fi

          touch "${app_name}.app/Contents/MacOS/$appDir/.current"
          mkdir -p "${app_name}.app/Contents/Resources"

          {
            printf '%s\n' '<?xml version="1.0" encoding="UTF-8"?>'
            printf '%s\n' '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">'
            printf '%s\n' '<plist version="1.0">'
            printf '%s\n' '<dict>'
            printf '%s\n' '  <key>CFBundleExecutable</key>'
            printf '%s\n' '  <string>LanMountainDesktop.Launcher</string>'
            printf '%s\n' '  <key>CFBundleName</key>'
            printf '%s\n' '  <string>LanMountain Desktop</string>'
            printf '%s\n' '  <key>CFBundleVersion</key>'
            printf '%s\n' "  <string>$version</string>"
            printf '%s\n' '  <key>CFBundleShortVersionString</key>'
            printf '%s\n' "  <string>$version</string>"
            printf '%s\n' '  <key>CFBundleIdentifier</key>'
            printf '%s\n' '  <string>com.lanmountain.desktop</string>'
            printf '%s\n' '  <key>CFBundlePackageType</key>'
            printf '%s\n' '  <string>APPL</string>'
            printf '%s\n' '</dict>'
            printf '%s\n' '</plist>'
          } > "${app_name}.app/Contents/Info.plist"

          mkdir -p dmg-temp
          cp -r "${app_name}.app" dmg-temp/
          hdiutil create -volname "${app_name}" -srcfolder dmg-temp -ov -format UDZO "${package_name}.dmg"

      - name: Upload Release Assets
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: release-macos-${{ matrix.arch }}
          path: |
            release-assets/files-macos-${{ matrix.arch }}.zip
            *.dmg
          if-no-files-found: ignore
          retention-days: 30

  github-release:
    needs: [prepare, build-windows, build-linux]
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Download release artifacts
        uses: actions/download-artifact@v4
        with:
          path: release-files
          pattern: release-*
          merge-multiple: true

      - name: Normalize release files
        run: |
          mkdir -p release-bundle

          mapfile -t downloaded_files < <(find release-files -type f)
          if [ "${#downloaded_files[@]}" -eq 0 ]; then
            echo "No downloaded release artifacts were found."
            exit 1
          fi

          for file in "${downloaded_files[@]}"; do
            base_name="$(basename "$file")"
            target_path="release-bundle/$base_name"

            if [ -e "$target_path" ]; then
              echo "Duplicate release asset name detected: $base_name"
              echo "Conflicting file: $file"
              exit 1
            fi

            cp "$file" "$target_path"
          done

      - name: Validate release files
        run: |
          echo "Release files:"
          find release-bundle -maxdepth 1 -type f -exec ls -lh {} \;

          if [ ! -f release-bundle/files-windows-x64.zip ] || [ ! -f release-bundle/files-windows-x86.zip ] || [ ! -f release-bundle/files-linux-x64.zip ]; then
            echo "Required payload zips are missing."
            exit 1
          fi

          file_count=$(find release-bundle -maxdepth 1 -type f | wc -l)
          if [ "$file_count" -eq 0 ]; then
            echo "No release files were produced."
            exit 1
          fi

      - name: Create or Update Release
        uses: ncipollo/release-action@v1
        with:
          tag: ${{ needs.prepare.outputs.tag }}
          name: ${{ needs.prepare.outputs.tag }}
          allowUpdates: true
          draft: false
          prerelease: ${{ needs.prepare.outputs.is_prerelease == 'true' }}
          artifacts: 'release-bundle/*'
          body: |
            ## Release ${{ needs.prepare.outputs.version }}

            ### Installers
            - `LanMountainDesktop-Setup-${{ needs.prepare.outputs.version }}-x64.exe`
            - `LanMountainDesktop-Setup-${{ needs.prepare.outputs.version }}-x86.exe`
            - `LanMountainDesktop_${{ needs.prepare.outputs.version }}_amd64.deb`

            ### Payload Archives
            - `files-windows-x64.zip`
            - `files-windows-x86.zip`
            - `files-linux-x64.zip`

            ### macOS
            - macOS assets are best-effort and will not block the release.

            Release keeps only the stable installer and payload outputs. PLONDS delta assets and external mirror metadata are generated by follow-up workflows.
          token: ${{ secrets.GITHUB_TOKEN }}
