#Requires -Version 7.0

<#
.SYNOPSIS
    Builds, signs and publishes the Android release channel.

.DESCRIPTION
    The phone's counterpart of release-windows.ps1, and the same two-phase shape for the same reason: the
    thing that is uploaded must be the thing that was installed and checked, so nothing is rebuilt between
    the two phases.

    Phase 1 (no token, no upload) builds and signs the APK with the project's release keystore and stops,
    printing where it is. Phase 2 (-Upload) attaches it to the tag's release on the project's own forge.

    ── WHY THIS IS A SCRIPT AND NOT A CI JOB ──────────────────────────────────────────────────────────
    docs/adr/0011-android-distribution.md rule 1 puts the release key on a machine that is not a runner,
    because a key a workflow can reach is a key held by everyone who can change a workflow file. That is
    the whole of it, and docs/adr/0014-android-updates.md explains why the convenience of a CI release
    went to a separate nightly channel with a deliberately public key instead of coming here.

    The forge token is the second half of the same capability and is treated the same way: prompted for,
    never stored, never a workflow secret. Whoever can write a release can publish an update every phone
    on this channel will install, which is the signing key reached through a different door.

.PARAMETER Upload
    Runs phase 2 against the package phase 1 produced. Prompts for a forge token.

.PARAMETER KeystorePath
    The release keystore. Defaults to the DODOSSH_ANDROID_KEYSTORE environment variable.

.PARAMETER KeyAlias
    The key inside it. Defaults to DODOSSH_ANDROID_ALIAS, then to 'dodossh'.

.EXAMPLE
    ./scripts/release-android.ps1
    ./scripts/release-android.ps1 -Upload
#>

[CmdletBinding()]
param(
    [switch] $Upload,
    [string] $KeystorePath = $env:DODOSSH_ANDROID_KEYSTORE,
    [string] $KeyAlias = $(if ($env:DODOSSH_ANDROID_ALIAS) { $env:DODOSSH_ANDROID_ALIAS } else { 'dodossh' })
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$RepoRoot = Split-Path -Parent $PSScriptRoot
$Project = Join-Path $RepoRoot 'src/DodoSSH.Client.Android/DodoSSH.Client.Android.csproj'
$Staging = Join-Path $RepoRoot 'artifacts/android'
$Forge = 'https://git.dodotech.cloud'
$Repo = 'DodoTech-Public/DodoSSH'
$Api = "$Forge/api/v1/repos/$Repo"

function Write-Step([string] $Message) {
    Write-Host ''
    Write-Host "==> $Message" -ForegroundColor Cyan
}

function Stop-With([string] $Message) {
    Write-Host ''
    Write-Host $Message -ForegroundColor Red
    exit 1
}

# ============ what the tag says, which is the version of record ============
function Get-TagVersion {
    $tag = & git -C $RepoRoot describe --exact-match --tags HEAD 2>$null
    if ($LASTEXITCODE -ne 0 -or -not $tag) {
        Stop-With @'
HEAD is not on a tag, so there is no version to release.

The tag is the version of record here — MinVer derives every assembly version from it, and a build
from an untagged commit answers 0.0.0-alpha.0.N rather than failing. Tag first:

    git tag v0.1.0
'@
    }

    if ($tag -notmatch '^v\d+\.\d+\.\d+') {
        Stop-With "HEAD is on '$tag', which is not a v* release tag."
    }

    return $tag.Substring(1)
}

# ============ what is already published, so a versionCode cannot go backwards ============
function Get-PublishedVersionCode {
    # Android refuses an install whose versionCode is not higher than the installed one, and
    # ApplicationVersion in the csproj is a hand-bumped literal on this channel by decision — see the
    # comment there. This is the guard that comment promises: forgetting the bump fails here rather than
    # on somebody's phone, where it presents as an install that simply will not go on.
    try {
        $latest = Invoke-RestMethod -Uri "$Api/releases/latest" -Method Get -ErrorAction Stop
    }
    catch {
        Write-Host '    no published release yet, so any versionCode will do' -ForegroundColor DarkGray
        return 0
    }

    $manifest = $latest.assets | Where-Object { $_.name -eq 'android-release.json' } | Select-Object -First 1
    if (-not $manifest) {
        Write-Host '    the latest release carries no android manifest' -ForegroundColor DarkGray
        return 0
    }

    $published = Invoke-RestMethod -Uri $manifest.browser_download_url -Method Get
    return [int] $published.versionCode
}

if (-not $Upload) {
    # ================================ phase 1 ================================
    Write-Step 'Checking the working tree'

    if (& git -C $RepoRoot status --porcelain) {
        Stop-With 'The working tree has changes. A release is built from a commit, not from a desk.'
    }

    $version = Get-TagVersion
    Write-Host "    v$version"

    if (-not $KeystorePath) {
        Stop-With @'
No keystore. Pass -KeystorePath, or set DODOSSH_ANDROID_KEYSTORE.

This is the key ADR 0011 rule 1 keeps off runners and out of this repository. It is the application's
identity for its whole life: losing it means no installed copy can ever be updated again.
'@
    }

    if (-not (Test-Path $KeystorePath)) {
        Stop-With "No keystore at $KeystorePath."
    }

    Write-Step 'Reading what is already published'
    $publishedCode = Get-PublishedVersionCode

    $declaredCode = [int] (& dotnet msbuild $Project -getProperty:ApplicationVersion -nologo `
        | ForEach-Object { $_.Trim() } | Where-Object { $_ })

    Write-Host "    published versionCode $publishedCode, this build declares $declaredCode"

    if ($declaredCode -le $publishedCode) {
        Stop-With @"
ApplicationVersion in DodoSSH.Client.Android.csproj is $declaredCode and the published channel is
already at $publishedCode. Android refuses an install that does not increase it, so this package
could be uploaded and would not install over anything.

Bump ApplicationVersion, commit, move the tag, and run this again.
"@
    }

    Write-Step 'Restoring'
    & dotnet restore $Project --locked-mode
    if ($LASTEXITCODE -ne 0) { Stop-With 'Restore failed.' }

    Write-Step 'Building and signing'

    # The keystore's passwords are prompted for and held only for this call. They are not parameters,
    # because a parameter is a thing that ends up in shell history.
    $storePass = Read-Host -Prompt 'Keystore password' -AsSecureString
    $keyPass = Read-Host -Prompt "Password for key '$KeyAlias' (blank to reuse)" -AsSecureString

    $storePlain = [System.Net.NetworkCredential]::new('', $storePass).Password
    $keyPlain = [System.Net.NetworkCredential]::new('', $keyPass).Password
    if (-not $keyPlain) { $keyPlain = $storePlain }

    if (Test-Path $Staging) { Remove-Item -Recurse -Force $Staging }
    New-Item -ItemType Directory -Path $Staging | Out-Null

    # No RuntimeIdentifier, so every supported ABI is packaged. An arm64-only APK will not install on a
    # 32-bit handset or an x86_64 emulator, and this is the package strangers are pointed at.
    & dotnet build $Project --no-restore --configuration Release `
        -t:SignAndroidPackage `
        -p:DodoChannel=release `
        -p:AndroidKeyStore=true `
        -p:AndroidSigningKeyStore=$KeystorePath `
        -p:AndroidSigningKeyAlias=$KeyAlias `
        -p:AndroidSigningStorePass=$storePlain `
        -p:AndroidSigningKeyPass=$keyPlain

    if ($LASTEXITCODE -ne 0) { Stop-With 'The build failed.' }

    $signed = Get-ChildItem -Path (Join-Path $RepoRoot 'src/DodoSSH.Client.Android/bin/Release') `
        -Recurse -Filter '*-Signed.apk' | Select-Object -First 1

    if (-not $signed) { Stop-With 'The build produced no signed APK.' }

    $apkName = "DodoSSH-$version.apk"
    Copy-Item $signed.FullName (Join-Path $Staging $apkName)

    # The manifest the client reads. versionCode is the comparison and versionName is what a person sees;
    # see docs/adr/0014-android-updates.md for why it is not the other way round.
    $manifest = [ordered] @{
        versionCode = $declaredCode
        versionName = $version
        apk         = $apkName
    }

    $manifest | ConvertTo-Json -Compress `
        | Set-Content -Path (Join-Path $Staging 'android-release.json') -Encoding utf8 -NoNewline

    Write-Step 'Built'
    Get-ChildItem $Staging | Format-Table Name, Length

    Write-Host @"
Install this on a phone and walk docs/manual-checks.md phase 17 before uploading anything. The APK in
artifacts/android is what phase 2 uploads — nothing is rebuilt — so what you check is what ships.

    ./scripts/release-android.ps1 -Upload

"@ -ForegroundColor Yellow

    exit 0
}

# ================================ phase 2 ================================
Write-Step 'Uploading'

$version = Get-TagVersion
$apkPath = Join-Path $Staging "DodoSSH-$version.apk"
$manifestPath = Join-Path $Staging 'android-release.json'

if (-not (Test-Path $apkPath) -or -not (Test-Path $manifestPath)) {
    Stop-With "No package for v$version in $Staging. Run phase 1 first."
}

$token = Read-Host -Prompt 'Forge token with release write' -AsSecureString
$tokenPlain = [System.Net.NetworkCredential]::new('', $token).Password

if (-not $tokenPlain) { Stop-With 'No token, so nothing was uploaded.' }

$headers = @{ Authorization = "token $tokenPlain" }

# --merge in spirit: the tag push may already have created a release entry, and creating a second one for
# the same tag fails. Reused where it exists.
try {
    $release = Invoke-RestMethod -Uri "$Api/releases/tags/v$version" -Headers $headers -Method Get
    Write-Host "    reusing the existing release for v$version"
}
catch {
    $body = @{
        tag_name   = "v$version"
        name       = "DodoSSH $version"
        prerelease = $version -match '-'
    } | ConvertTo-Json

    $release = Invoke-RestMethod -Uri "$Api/releases" -Headers $headers -Method Post `
        -ContentType 'application/json' -Body $body

    Write-Host "    created the release for v$version"
}

# The APK first and the manifest last, which is the order the client depends on: it reads the manifest
# and then fetches what the manifest names, so a manifest published before its APK is a window in which
# every phone is told to download something that is not there.
foreach ($file in @($apkPath, $manifestPath)) {
    $name = Split-Path -Leaf $file

    # Replaced rather than added beside. Gitea will happily hold two assets with one name, and the client
    # takes the first — which after a re-upload is whichever the API happens to list first.
    $existing = $release.assets | Where-Object { $_.name -eq $name } | Select-Object -First 1
    if ($existing) {
        Invoke-RestMethod -Uri "$Api/releases/$($release.id)/assets/$($existing.id)" `
            -Headers $headers -Method Delete | Out-Null
    }

    Write-Host "    $name"
    Invoke-RestMethod -Uri "$Api/releases/$($release.id)/assets?name=$name" `
        -Headers $headers -Method Post -Form @{ attachment = Get-Item $file } | Out-Null
}

Write-Step "Published v$version"
Write-Host 'Phones on the release channel will see it within six hours, or on the next CHECK NOW.'
