name: release

on:
  workflow_dispatch:
    inputs:
      version:
        description: "Release version, for example 1.2.24 or v1.2.24. Leave empty to bump automatically."
        required: false
        type: string
      bump:
        description: "Version bump to use when version is empty."
        required: true
        default: patch
        type: choice
        options:
          - patch
          - minor
          - major
      draft:
        description: "Create the GitHub Release as a draft."
        required: true
        default: false
        type: boolean
      prerelease:
        description: "Mark the GitHub Release as a prerelease."
        required: true
        default: false
        type: boolean

permissions:
  contents: write

concurrency:
  group: release
  cancel-in-progress: false

jobs:
  windows:
    name: Prepare and publish Windows release
    runs-on: windows-latest

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

      - name: Setup .NET
        uses: actions/setup-dotnet@v5
        with:
          dotnet-version: "8.0.x"

      - name: Prepare release metadata
        id: meta
        shell: pwsh
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

          [xml]$project = Get-Content "AppTunnel/AppTunnel.csproj"
          $currentVersion = [version]$project.Project.PropertyGroup.Version
          $requestedVersion = "${{ inputs.version }}".Trim()

          if ($requestedVersion) {
            $requestedVersion = $requestedVersion.TrimStart("v")
            if ($requestedVersion -notmatch '^\d+\.\d+\.\d+$') {
              throw "Version must use MAJOR.MINOR.PATCH format. Received: $requestedVersion"
            }
            $nextVersion = [version]$requestedVersion
          }
          else {
            $bump = "${{ inputs.bump }}"
            if ($bump -eq "major") {
              $nextVersion = [version]::new($currentVersion.Major + 1, 0, 0)
            }
            elseif ($bump -eq "minor") {
              $nextVersion = [version]::new($currentVersion.Major, $currentVersion.Minor + 1, 0)
            }
            else {
              $nextVersion = [version]::new($currentVersion.Major, $currentVersion.Minor, $currentVersion.Build + 1)
            }
          }

          if ($nextVersion -le $currentVersion) {
            throw "Release version ($nextVersion) must be greater than current project version ($currentVersion)."
          }

          $version = $nextVersion.ToString(3)
          $tag = "v$version"

          git fetch --tags origin
          $existingTag = git tag --list $tag
          if ($existingTag) {
            throw "Tag $tag already exists."
          }

          $artifactName = "TunnelX-$tag-standalone-compressed.exe"

          "version=$version" >> $env:GITHUB_OUTPUT
          "tag=$tag" >> $env:GITHUB_OUTPUT
          "artifact_name=$artifactName" >> $env:GITHUB_OUTPUT

      - name: Update version and changelog
        id: release_notes
        shell: pwsh
        run: |
          $version = "${{ steps.meta.outputs.version }}"
          $tag = "${{ steps.meta.outputs.tag }}"
          $date = (Get-Date).ToUniversalTime().ToString("yyyy-MM-dd")

          [xml]$project = Get-Content "AppTunnel/AppTunnel.csproj"
          $project.Project.PropertyGroup.Version = $version
          $project.Project.PropertyGroup.AssemblyVersion = "$version.0"
          $project.Project.PropertyGroup.FileVersion = "$version.0"
          $project.Save((Resolve-Path "AppTunnel/AppTunnel.csproj"))

          $changelogPath = "CHANGELOG.md"
          $changelog = Get-Content -Raw -LiteralPath $changelogPath
          if ($changelog -notmatch '(?m)^## Unreleased\s*$') {
            throw "CHANGELOG.md must contain a '## Unreleased' section."
          }

          $unreleasedMatch = [regex]::Match($changelog, '(?ms)^## Unreleased\s*(?<notes>.*?)(?=^##\s|\z)')
          $notes = $unreleasedMatch.Groups["notes"].Value.Trim()

          if (-not $notes) {
            $previousTag = git describe --tags --abbrev=0 2>$null
            if ($LASTEXITCODE -eq 0 -and $previousTag) {
              $subjects = git log "$previousTag..HEAD" --pretty=format:"- %s"
            }
            else {
              $subjects = git log --pretty=format:"- %s"
            }

            $notes = ($subjects | Where-Object { $_ -and ($_ -notmatch '^- Prepare release v') }) -join "`n"
            if (-not $notes) {
              $notes = "- Maintenance release."
            }
          }

          $releaseHeading = "## $version - $date"
          $replacement = "## Unreleased`n`n$releaseHeading`n`n$notes`n`n"
          $changelog = [regex]::Replace($changelog, '(?ms)^## Unreleased\s*(?<notes>.*?)(?=^##\s|\z)', $replacement, 1)
          Set-Content -Encoding UTF8 -LiteralPath $changelogPath -Value $changelog

          $notesFile = Join-Path $env:RUNNER_TEMP "release-notes.md"
          $notes | Set-Content -Encoding UTF8 -LiteralPath $notesFile

          "notes_file=$notesFile" >> $env:GITHUB_OUTPUT

      - name: Commit release changes
        id: release_commit
        shell: pwsh
        run: |
          $tag = "${{ steps.meta.outputs.tag }}"

          git add AppTunnel/AppTunnel.csproj CHANGELOG.md
          git commit -m "Prepare release $tag"
          $releaseSha = git rev-parse HEAD
          git tag $tag
          git push origin HEAD:${{ github.ref_name }}
          git push origin $tag

          "sha=$releaseSha" >> $env:GITHUB_OUTPUT

      - name: Restore
        run: dotnet restore AppTunnel.sln

      - name: Build
        run: dotnet build AppTunnel.sln -c Release --no-restore

      - name: Publish standalone executable
        run: >
          dotnet publish AppTunnel\AppTunnel.csproj
          -c Release
          -r win-x64
          --self-contained true
          -p:PublishSingleFile=true
          -p:EnableCompressionInSingleFile=true
          -p:IncludeNativeLibrariesForSelfExtract=true
          -p:DebugType=None
          -p:DebugSymbols=false
          -o publish\TunnelX

      - name: Package release asset
        id: package
        shell: pwsh
        run: |
          $source = "publish/TunnelX/TunnelX.exe"
          $asset = "publish/${{ steps.meta.outputs.artifact_name }}"
          $checksum = "$asset.sha256"

          if (-not (Test-Path $source)) {
            throw "Published executable was not found at $source"
          }

          Move-Item -LiteralPath $source -Destination $asset
          $hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $asset).Hash.ToLowerInvariant()
          "$hash  ${{ steps.meta.outputs.artifact_name }}" | Set-Content -Encoding ASCII -LiteralPath $checksum

          "asset=$asset" >> $env:GITHUB_OUTPUT
          "checksum=$checksum" >> $env:GITHUB_OUTPUT
          "sha256=$hash" >> $env:GITHUB_OUTPUT

      - name: Upload workflow artifact
        uses: actions/upload-artifact@v6
        with:
          name: TunnelX-${{ steps.meta.outputs.tag }}-win-x64
          path: |
            ${{ steps.package.outputs.asset }}
            ${{ steps.package.outputs.checksum }}
          if-no-files-found: error

      - name: Create GitHub release
        env:
          GH_TOKEN: ${{ github.token }}
        shell: pwsh
        run: |
          $tag = "${{ steps.meta.outputs.tag }}"
          $title = "TunnelX $tag"
          $runUrl = "https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
          $sha256 = "${{ steps.package.outputs.sha256 }}".ToUpperInvariant()
          $artifactName = "${{ steps.meta.outputs.artifact_name }}"

          $notes = Get-Content -Raw -LiteralPath "${{ steps.release_notes.outputs.notes_file }}"
          $provenanceLines = @(
            "<!-- release-provenance:start -->",
            "## Build provenance",
            "",
            "- Built and uploaded by GitHub Actions.",
            "- Workflow: ``release``",
            "- Run: $runUrl",
            "- Commit: ``${{ steps.release_commit.outputs.sha }}``",
            "- SHA256: ``$sha256  $artifactName``",
            "<!-- release-provenance:end -->"
          )
          $notes = "$($notes.Trim())`n`n$($provenanceLines -join "`n")"

          $notesFile = Join-Path $env:RUNNER_TEMP "final-release-notes.md"
          $notes | Set-Content -Encoding UTF8 -LiteralPath $notesFile

          $releaseArgs = @(
            "release", "create", $tag,
            "${{ steps.package.outputs.asset }}",
            "${{ steps.package.outputs.checksum }}",
            "--title", $title,
            "--notes-file", $notesFile,
            "--latest"
          )

          if ("${{ inputs.draft }}" -eq "true") {
            $releaseArgs += "--draft"
          }
          if ("${{ inputs.prerelease }}" -eq "true") {
            $releaseArgs += "--prerelease"
          }

          gh @releaseArgs
