# Build the Windows installer on this machine, without GitHub Actions.
#
# CI is normally the only Windows box this project has, which makes it the
# only place the .iss can be compiled -- and a private repo gets 2,000 Actions
# minutes a month, with Windows billing at 2x. Running out of minutes should
# not mean running out of installers.
#
# Run from the repo root in PowerShell:
#
#     .\installer\build_local.ps1
#     .\installer\build_local.ps1 -Version 0.1.12
#     .\installer\build_local.ps1 -Iscc "C:\path\to\ISCC.exe"
#     .\installer\build_local.ps1 -Sign          (see docs/SIGNING.md)
#
# Needs: git-bash (ships with Git for Windows) for the payload script, and
# Inno Setup 6. If Inno is missing this offers to fetch it with winget.
#
# What it does NOT do is the second half of the CI job -- installing the
# result and checking it runs. Do that by hand: install the .exe, then open
# "Clipshelf (console)" from the Start menu and read the self-test.

[CmdletBinding()]
param(
    [string]$Version,
    [string]$Iscc,
    [switch]$SkipPayload,
    [switch]$Sign
)

$ErrorActionPreference = "Stop"
$repo = Split-Path -Parent $PSScriptRoot
Set-Location $repo

function Fail($msg) { Write-Host "`n  $msg`n" -ForegroundColor Red; exit 1 }
function Say($msg) { Write-Host "  $msg" -ForegroundColor DarkCyan }

# ---- version -------------------------------------------------------------
if (-not $Version) {
    $init = Get-Content "src\clipshelf\__init__.py" -Raw
    if ($init -match '__version__ = "([^"]+)"') { $Version = $Matches[1] }
    else { Fail "could not read __version__ -- pass -Version" }
}
Say "version $Version"

# ---- payload -------------------------------------------------------------
# build_payload.sh is bash: it fetches CPython's embeddable distro and lays
# the app out beside it. git-bash runs it fine.
if (-not $SkipPayload) {
    $bash = @(
        "$env:ProgramFiles\Git\bin\bash.exe",
        "${env:ProgramFiles(x86)}\Git\bin\bash.exe",
        "$env:LOCALAPPDATA\Programs\Git\bin\bash.exe"
    ) | Where-Object { Test-Path $_ } | Select-Object -First 1
    if (-not $bash) { Fail "git-bash not found -- install Git for Windows, or pass -SkipPayload" }

    Say "assembling payload (downloads ~11 MB of CPython)..."
    & $bash -lc "cd '$($repo -replace '\\','/')' && installer/build_payload.sh '$Version'"
    if ($LASTEXITCODE -ne 0) { Fail "payload assembly failed" }
} else {
    Say "reusing installer\payload"
}
if (-not (Test-Path "installer\payload\run.py")) { Fail "payload looks empty" }

# ---- Inno Setup ----------------------------------------------------------
# Looking in Program Files is not enough. winget run without elevation does a
# PER-USER install, which lands in %LOCALAPPDATA%\Programs -- so the install
# would succeed and then the script would say it could not find it, which is
# exactly what happened. Ask the registry, which is where Inno records itself
# whichever way it was installed.
$InnoSearched = @()
function Find-ISCC {
    $paths = @(
        "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe",
        "$env:ProgramFiles\Inno Setup 6\ISCC.exe",
        "$env:LOCALAPPDATA\Programs\Inno Setup 6\ISCC.exe"
    )
    foreach ($p in $paths) {
        if ($p) { $script:InnoSearched += $p }
        if ($p -and (Test-Path $p)) { return $p }
    }

    $keys = @(
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Inno Setup 6_is1"
    )
    foreach ($k in $keys) {
        $script:InnoSearched += $k
        $loc = (Get-ItemProperty -Path $k -Name InstallLocation -ErrorAction SilentlyContinue).InstallLocation
        if ($loc) {
            $exe = Join-Path $loc "ISCC.exe"
            if (Test-Path $exe) { return $exe }
        }
    }

    $script:InnoSearched += "PATH"
    $cmd = Get-Command ISCC.exe -ErrorAction SilentlyContinue
    if ($cmd) { return $cmd.Source }

    # Last resort, and slow, so it runs only after everything else missed.
    foreach ($root in @($env:ProgramFiles, ${env:ProgramFiles(x86)}, "$env:LOCALAPPDATA\Programs")) {
        if (-not $root -or -not (Test-Path $root)) { continue }
        $script:InnoSearched += "$root (recursive)"
        $hit = Get-ChildItem -Path $root -Filter ISCC.exe -Recurse -Depth 3 -ErrorAction SilentlyContinue |
               Select-Object -First 1
        if ($hit) { return $hit.FullName }
    }
    return $null
}

# NB: PowerShell variable names are case-insensitive, so a local named the
# same as the -Iscc parameter would BE that parameter. Hence $compiler.
if ($Iscc) {
    if (-not (Test-Path $Iscc)) { Fail "no ISCC.exe at $Iscc" }
    $compiler = $Iscc
} else {
    $compiler = Find-ISCC
}
if (-not $compiler) {
    Say "Inno Setup 6 not found."
    $yes = Read-Host "  install it now with winget? [y/N]"
    if ($yes -eq "y") {
        winget install --id JRSoftware.InnoSetup -e --accept-source-agreements --accept-package-agreements
        $InnoSearched = @()
        $compiler = Find-ISCC
    }
    if (-not $compiler) {
        Write-Host ""
        Write-Host "  looked in:" -ForegroundColor DarkGray
        $InnoSearched | ForEach-Object { Write-Host "    $_" -ForegroundColor DarkGray }
        Write-Host ""
        Write-Host "  If it IS installed, pass the compiler directly:" -ForegroundColor DarkGray
        Write-Host "    .\installer\build_local.ps1 -Iscc 'C:\path\to\ISCC.exe'" -ForegroundColor DarkGray
        Fail "need Inno Setup 6: https://jrsoftware.org/isdl.php"
    }
}
Say "compiler $compiler"

# ---- compile -------------------------------------------------------------
Say "compiling..."
& $compiler "/DAppVersion=$Version" "installer\clipshelf.iss"
if ($LASTEXITCODE -ne 0) { Fail "ISCC failed -- the error is above" }

$exe = Get-ChildItem "dist\clipshelf-setup-$Version.exe" -ErrorAction SilentlyContinue
if (-not $exe) { Fail "no installer produced" }

# ---- sign ----------------------------------------------------------------
# Optional, and deliberately not the default: an unsigned build is still a
# usable build, and the signing account is not something every machine has.
# What is NOT optional is checking afterwards -- signtool can report success
# on a file that then fails to verify, and shipping that is worse than
# shipping unsigned, because it looks tampered with rather than merely new.
if ($Sign) {
    $cfg = Join-Path $repo "installer\signing.json"
    if (-not (Test-Path $cfg)) {
        Fail "no installer\signing.json -- copy signing.example.json and fill it in (docs/SIGNING.md)"
    }

    $signtool = @(
        Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin\*\x64\signtool.exe" -ErrorAction SilentlyContinue |
            Sort-Object FullName -Descending | Select-Object -First 1 -ExpandProperty FullName
        "$env:ProgramFiles\Microsoft SDKs\ClickOnce\SignTool\signtool.exe"
    ) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
    if (-not $signtool) {
        Fail "signtool not found -- winget install -e --id Microsoft.Azure.ArtifactSigningClientTools"
    }

    $dlib = @(
        "$env:ProgramFiles\Microsoft\Azure Artifact Signing Client Tools\bin\x64\Azure.CodeSigning.Dlib.dll",
        "$env:ProgramFiles\Microsoft\Trusted Signing Client Tools\bin\x64\Azure.CodeSigning.Dlib.dll"
    ) + (Get-ChildItem "$env:ProgramFiles\Microsoft\*Signing Client Tools*\bin\x64\Azure.CodeSigning.Dlib.dll" `
            -ErrorAction SilentlyContinue | Select-Object -ExpandProperty FullName)
    $dlib = $dlib | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
    if (-not $dlib) {
        Fail "the signing dlib was not found -- winget install -e --id Microsoft.Azure.ArtifactSigningClientTools"
    }

    Say "signing with $(Split-Path -Leaf $signtool)"
    # The timestamp is not decoration. These certificates are valid for about
    # three days; the signature outlives them only because it is timestamped.
    # Without /tr every installer stops verifying three days after it was
    # built, which reads to a user as a corrupted download.
    & $signtool sign /v /debug /fd SHA256 `
        /tr "http://timestamp.acs.microsoft.com" /td SHA256 `
        /dlib $dlib /dmdf $cfg $exe.FullName
    if ($LASTEXITCODE -ne 0) { Fail "signing failed -- the error is above" }

    & $signtool verify /pa /v $exe.FullName
    if ($LASTEXITCODE -ne 0) { Fail "SIGNED BUT DOES NOT VERIFY -- do not publish this" }
    Say "signature verified"
} else {
    Say "not signed (pass -Sign; see docs/SIGNING.md)"
}

Write-Host ""
Write-Host ("  built {0}  ({1:N1} MB)" -f $exe.Name, ($exe.Length / 1MB)) -ForegroundColor Green
Write-Host "  $($exe.FullName)"
Write-Host ""
Write-Host "  Not yet verified. CI would now install it and run the app's" -ForegroundColor DarkYellow
Write-Host "  self-test; do that by hand before publishing this." -ForegroundColor DarkYellow
Write-Host ""
