<#
.SYNOPSIS
    Moneymoor installer (Windows).

.DESCRIPTION
    Generated by ariza from App::Moneymoor's ariza.toml. Do not edit this
    file: edit ariza's resources/templates/install-windows.ps1.j2 and
    re-run `ariza installers --app=<this repository>`.

    Downloads the prebuilt Moneymoor bundle for this machine from the
    m-doughty/App-Moneymoor releases, verifies its sha256, and unpacks it into
    %LOCALAPPDATA%\Moneymoor\versions\<version>\. A junction named
    `current` points at the version in use, and
    %LOCALAPPDATA%\Moneymoor\current\bin goes on your user PATH once.

    Nothing is compiled, nothing needs administrator rights, and nothing
    outside %LOCALAPPDATA%\Moneymoor and your user PATH is written.

    The installed application is then run once, so that whatever a first
    launch has to do is done here rather than the first time you want the
    program.

.PARAMETER Version
    Install a specific release tag (e.g. -Version v1.2.3) rather than the
    latest one.

.PARAMETER Url
    Install this archive instead, bypassing GitHub entirely. A local file
    path works as well as a URL. Also settable as $env:MONEYMOOR_BUNDLE_URL.

.PARAMETER InsecureNoVerify
    With -Url only: proceed even when the source has no sibling .sha256.

.EXAMPLE
    irm https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/install.ps1 | iex

.EXAMPLE
    # With arguments, a piped script needs to become a script block first:
    & ([scriptblock]::Create((irm https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/install.ps1))) -Version v1.2.3
#>
[CmdletBinding()]
param(
    [string] $Version,
    [string] $Url,
    [switch] $InsecureNoVerify
)

$AppDisplay = 'Moneymoor'
$AppExec    = 'moneymoor'
$AppRepo    = 'm-doughty/App-Moneymoor'

# The platforms App::Moneymoor publishes bundles for. Detection produces one
# of these strings or nothing at all; there is deliberately no "closest
# match", because a bundle for the wrong architecture does not run.
$ArizaSlugs = @('windows-x86_64')

$ArizaRoot     = Join-Path $env:LOCALAPPDATA $AppDisplay
$ArizaVersions = Join-Path $ArizaRoot 'versions'
$ArizaBinDir   = Join-Path (Join-Path $ArizaRoot 'current') 'bin'

############################# common runtime ##################################
###############################################################################
# ariza installer runtime (Windows / PowerShell)
#
# Inlined verbatim into install.ps1 and uninstall.ps1 at render time, so a
# generated installer is one self-contained file.
#
# Adapted from the pre-bundle installers' template/lib/common-windows.ps1:
# the registry, the package managers and the shortcut helpers are gone (a
# bundle installs none of that), the user-PATH machinery is kept, because
# an unguarded PATH write is how a user ends up with the same directory in
# it eleven times.
#
# Callers must have set: $AppDisplay, $AppExec, $ArizaRoot.
###############################################################################

$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

function Ariza-Log  { param([string]$Message) Write-Host "==> $Message" -ForegroundColor Blue }
function Ariza-Ok   { param([string]$Message) Write-Host "ok  $Message" -ForegroundColor Green }
function Ariza-Warn { param([string]$Message) Write-Host "!!  $Message" -ForegroundColor Yellow }

function Ariza-Err {
    param([string]$Message)
    Write-Host "error $Message" -ForegroundColor Red
    exit 1
}

# ---- downloading -------------------------------------------------------------

function Ariza-Download {
    param([string]$Url, [string]$Dest)
    # -UseBasicParsing keeps this working on a Server Core box with no
    # Internet Explorer engine, which is where Invoke-WebRequest's default
    # parser falls over.
    Invoke-WebRequest -Uri $Url -OutFile $Dest -UseBasicParsing
}

# ---- checksums ---------------------------------------------------------------

function Ariza-Sha256 {
    param([string]$Path)
    (Get-FileHash -LiteralPath $Path -Algorithm SHA256).Hash.ToLower()
}

function Ariza-FirstWord {
    param([string]$Path)
    # A `<hex>  <filename>` checksum file, as shasum -c reads.
    $line = (Get-Content -LiteralPath $Path -TotalCount 1)
    if (-not $line) { return '' }
    return ($line -split '\s+')[0].ToLower()
}

function Ariza-VerifySha256 {
    param([string]$Path, [string]$Expected)
    $actual = Ariza-Sha256 $Path
    if ($actual -ne $Expected.ToLower()) {
        Ariza-Err "checksum mismatch for $(Split-Path -Leaf $Path): expected $Expected, got $actual -- not installing"
    }
}

# ---- unpacking ---------------------------------------------------------------

function Ariza-Extract {
    param([string]$Archive, [string]$Into)
    New-Item -ItemType Directory -Path $Into -Force | Out-Null
    if ($Archive.ToLower().EndsWith('.zip')) {
        Expand-Archive -LiteralPath $Archive -DestinationPath $Into -Force
    }
    else {
        # bsdtar has shipped in Windows since 10 1803 and reads .tar.gz,
        # which is what ariza packages every platform's bundle as.
        $tar = Get-Command tar.exe -ErrorAction SilentlyContinue
        if (-not $tar) {
            Ariza-Err "need tar.exe (Windows 10 1803 or later) to unpack $(Split-Path -Leaf $Archive)"
        }
        & $tar.Path -x -z -f $Archive -C $Into
        if ($LASTEXITCODE -ne 0) {
            Ariza-Err "tar could not unpack $(Split-Path -Leaf $Archive) (exit $LASTEXITCODE)"
        }
    }
}

# ---- user PATH ---------------------------------------------------------------
#
# The user environment in the registry (HKCU\Environment), never the
# machine one: nothing here needs administrator rights, and nothing here
# should be visible to other accounts.

function Ariza-PersistPath {
    param([string]$Dir)
    $current = [Environment]::GetEnvironmentVariable('PATH', 'User')
    if (-not $current) { $current = '' }
    $parts = $current -split ';' | Where-Object { $_ -ne '' }
    if ($parts -notcontains $Dir) {
        [Environment]::SetEnvironmentVariable('PATH', ((@($Dir) + $parts) -join ';'), 'User')
        Ariza-Ok "added $Dir to your PATH"
    }
    if (($env:PATH -split ';') -notcontains $Dir) {
        $env:PATH = "$Dir;$env:PATH"
    }
}

function Ariza-UnpersistPath {
    param([string]$Dir)
    $current = [Environment]::GetEnvironmentVariable('PATH', 'User')
    if (-not $current) { return }
    $parts = $current -split ';' | Where-Object { $_ -ne '' }
    if ($parts -notcontains $Dir) { return }
    $kept = $parts | Where-Object { $_ -ne $Dir }
    [Environment]::SetEnvironmentVariable('PATH', ($kept -join ';'), 'User')
    Ariza-Ok "removed $Dir from your PATH"
}

# ---- the `current` junction --------------------------------------------------
#
# A junction rather than a symbolic link: creating a symlink on Windows
# needs either administrator rights or Developer Mode, and an installer
# that demands either for a per-user install is one nobody runs.

function Ariza-RemoveLink {
    param([string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) { return }
    $item = Get-Item -LiteralPath $Path -Force
    if (-not $item.LinkType) {
        Ariza-Err "$Path exists and is not a junction -- move it aside and re-run"
    }
    # .Delete() removes the junction itself; Remove-Item -Recurse would
    # walk through it and delete the target's contents.
    $item.Delete()
}

function Ariza-PointCurrent {
    param([string]$Target)
    $link = Join-Path $ArizaRoot 'current'
    Ariza-RemoveLink $link
    New-Item -ItemType Junction -Path $link -Target $Target | Out-Null
}

###############################################################################

function Ariza-DetectSlug {
    $arch = $env:PROCESSOR_ARCHITECTURE
    if (-not $arch) { $arch = '' }
    switch ($arch.ToUpper()) {
        'AMD64' { return 'windows-x86_64' }
        'ARM64' { return 'windows-arm64' }
        'X86'   {
            # A 32-bit PowerShell on a 64-bit machine reports x86; ask
            # what the hardware actually is before giving up on it.
            if ($env:PROCESSOR_ARCHITEW6432 -eq 'AMD64') { return 'windows-x86_64' }
            if ($env:PROCESSOR_ARCHITEW6432 -eq 'ARM64') { return 'windows-arm64' }
            return ''
        }
    }
    return ''
}

function Ariza-VersionFromName {
    param([string]$Name)
    # ariza names a bundle <exec>-<version>-<slug>. Stripping a known slug
    # off the end rather than splitting on dashes is what makes a version
    # containing one (1.0-rc1) parse correctly.
    $n = $Name
    foreach ($ext in @('.tar.gz', '.tgz', '.zip')) {
        if ($n.ToLower().EndsWith($ext)) { $n = $n.Substring(0, $n.Length - $ext.Length) }
    }
    if (-not $n.StartsWith("$AppExec-")) { return '' }
    $n = $n.Substring($AppExec.Length + 1)
    foreach ($slug in $ArizaSlugs) {
        if ($n.EndsWith("-$slug")) { return $n.Substring(0, $n.Length - $slug.Length - 1) }
    }
    return ''
}

function Ariza-LatestTag {
    # The releases/latest redirect names the tag, so this needs no JSON
    # parsing and no API token.
    $url = "https://github.com/$AppRepo/releases/latest"
    try {
        $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -MaximumRedirection 5
    }
    catch {
        return ''
    }
    $final = ''
    if ($resp.BaseResponse.PSObject.Properties.Name -contains 'ResponseUri') {
        $final = $resp.BaseResponse.ResponseUri.AbsoluteUri      # PowerShell 5.1
    }
    elseif ($resp.BaseResponse.PSObject.Properties.Name -contains 'RequestMessage') {
        $final = $resp.BaseResponse.RequestMessage.RequestUri.AbsoluteUri   # PowerShell 7
    }
    if (-not $final) { return '' }
    return ($final -split '/')[-1]
}

function Ariza-LinkBin {
    Ariza-PersistPath $ArizaBinDir
}

function Ariza-HasEntryPoint {
    param([string]$Dir)
    # A bundle launches from bin\<exec>.exe where one was staged, and
    # from bin\<exec>.cmd otherwise. Either is a complete install: the
    # directory goes on PATH, and PATHEXT already prefers the executable
    # over the batch file where both are there.
    foreach ($leaf in @("bin\$AppExec.exe", "bin\$AppExec.cmd")) {
        if (Test-Path -LiteralPath (Join-Path $Dir $leaf)) { return $true }
    }
    return $false
}

function Ariza-Prune {
    param([string]$Keep)
    # Exactly one older version is kept, so a bad release can be rolled
    # back to the one before it without hoarding every bundle ever
    # downloaded.
    $old = 0
    $dirs = Get-ChildItem -LiteralPath $ArizaVersions -Directory -ErrorAction SilentlyContinue |
            Sort-Object LastWriteTime -Descending
    foreach ($dir in $dirs) {
        if ($dir.Name -eq $Keep) { continue }
        $old++
        if ($old -gt 1) {
            Remove-Item -LiteralPath $dir.FullName -Recurse -Force
            Ariza-Log "removed superseded version $($dir.Name)"
        }
    }
}

function Ariza-CheckExisting {
    param([string]$InstalledVersion)
    # Returns $true when that version is already installed and usable, in
    # which case the junction and PATH are repaired on the way past: a
    # re-run is the obvious thing to try when something has gone missing,
    # and it should fix it.
    $dir = Join-Path $ArizaVersions $InstalledVersion
    if (-not (Test-Path -LiteralPath $dir)) { return $false }
    if (-not (Ariza-HasEntryPoint $dir)) {
        Ariza-Warn "$dir is incomplete -- installing it again"
        Remove-Item -LiteralPath $dir -Recurse -Force
        return $false
    }
    Ariza-PointCurrent $dir
    Ariza-LinkBin
    Ariza-Ok "$AppDisplay $InstalledVersion is already installed"
    return $true
}

function Ariza-Warmup {
    # Run the app once, now, through the same junction the user's shell
    # will take. A first launch pages a few hundred megabytes off a cold
    # disk and builds whatever per-user state the app keeps; doing it
    # here means it happens while an installer is on screen saying so,
    # instead of the first time somebody actually wants the program.
    #
    # Never fatal. The bundle is installed and its sha256 was checked
    # before anything was moved into place, so a warm-up that fails on
    # one machine is far more likely to be that machine -- no console, a
    # policy, an over-eager scanner -- than a broken release. Refusing to
    # finish the install over it would take a working program away from
    # a user who has one.
    $bin = ''
    foreach ($leaf in @("$AppExec.exe", "$AppExec.cmd")) {
        $candidate = Join-Path $ArizaBinDir $leaf
        if (Test-Path -LiteralPath $candidate) { $bin = $candidate; break }
    }
    if (-not $bin) { return }

    # The arguments come from the app's ariza.toml and are written out
    # already quoted, once as the argument list and once as the single
    # string the message names. `@warmArgs` is splatting -- an array
    # written inline at the call site would be passed as one argument
    # with spaces in it, which is a different command entirely.
    $warmArgs = @('--version')
    $what = '--version'

    Ariza-Log 'warming up -- the first launch does the work the rest never repeat'
    $failed = ''
    try {
        & $bin @warmArgs *> $null
        # A native command's exit code is not an error to PowerShell, so
        # it has to be looked at on purpose.
        if ($LASTEXITCODE -ne 0) { $failed = "exit code $LASTEXITCODE" }
    }
    catch {
        $failed = $_.Exception.Message
    }

    if ($failed) {
        Ariza-Warn "warm-up failed: $AppExec $what did not complete ($failed)"
        Ariza-Warn "$AppDisplay is installed and its download was verified -- try running it; the first launch may just take longer"
    }
    else {
        Ariza-Ok 'ready'
    }
}

function Ariza-Report {
    param([string]$InstalledVersion)
    Write-Host ''
    Write-Host "    run it:        $AppExec"
    Write-Host "    installed in:  $(Join-Path $ArizaVersions $InstalledVersion)"
    Write-Host "    uninstall:     irm https://raw.githubusercontent.com/m-doughty/App-Moneymoor/HEAD/uninstall.ps1 | iex"
    Write-Host ''
    Write-Host "    Open a new terminal to pick up the PATH change."
}

function Ariza-Main {
    $src        = if ($Url) { $Url } elseif ($env:MONEYMOOR_BUNDLE_URL) { $env:MONEYMOOR_BUNDLE_URL } else { '' }
    $override   = [bool]$src
    $installVer = ''

    if ($override) {
        # An explicit source bypasses GitHub completely. It is how a
        # release candidate, an air-gapped copy or an offline test gets
        # installed, so it accepts a plain file path as readily as a URL.
        $installVer = Ariza-VersionFromName (Split-Path -Leaf $src)
        Ariza-Log "$AppDisplay from $src"
    }
    else {
        $slug = Ariza-DetectSlug
        if (-not $slug -or $ArizaSlugs -notcontains $slug) {
            Ariza-Err "no prebuilt $AppDisplay bundle for Windows $env:PROCESSOR_ARCHITECTURE yet -- see https://github.com/$AppRepo/releases for what is published"
        }
        $tag = if ($Version) { $Version } else { Ariza-LatestTag }
        if (-not $tag) {
            Ariza-Err "could not read the latest release tag from https://github.com/$AppRepo/releases/latest -- pass -Version to name one"
        }
        $installVer = $tag -replace '^v', ''
        $src = "https://github.com/$AppRepo/releases/download/$tag/$AppExec-$installVer-$slug.tar.gz"
        Ariza-Log "$AppDisplay $installVer for $slug"
    }

    # Every path that reaches the parting message warms up first,
    # including the one where nothing was downloaded -- a re-run is what
    # somebody tries when the last one did not take.
    if ($installVer -and (Ariza-CheckExisting $installVer)) {
        Ariza-Warmup
        Ariza-Report $installVer
        return
    }

    $tmp = Join-Path ([IO.Path]::GetTempPath()) "$AppExec-install-$PID"
    New-Item -ItemType Directory -Path $tmp -Force | Out-Null
    $staging = Join-Path $ArizaRoot ".staging-$PID"

    try {
        $archive = Join-Path $tmp (Split-Path -Leaf $src)
        if (Test-Path -LiteralPath $src) {
            Copy-Item -LiteralPath $src -Destination $archive -Force
        }
        else {
            Ariza-Log "downloading $src"
            Ariza-Download $src $archive
        }

        $expected = ''
        $digestSrc = "$src.sha256"
        if (Test-Path -LiteralPath $digestSrc) {
            $expected = Ariza-FirstWord $digestSrc
        }
        elseif (-not (Test-Path -LiteralPath $src)) {
            try {
                Ariza-Download $digestSrc "$archive.sha256"
                $expected = Ariza-FirstWord "$archive.sha256"
            }
            catch {
                $expected = ''
            }
        }

        if ($expected) {
            Ariza-VerifySha256 $archive $expected
            Ariza-Ok 'sha256 verified'
        }
        elseif ($override -and $InsecureNoVerify) {
            # Only reachable for a source the user named themselves, and
            # only when they asked for it in as many words. A published
            # release always has a checksum beside it, so its absence
            # there is a tampered or half-uploaded release and stays fatal.
            Ariza-Warn "NOT VERIFIED: no $(Split-Path -Leaf $digestSrc) beside the source you gave, and -InsecureNoVerify was passed"
        }
        else {
            Ariza-Err "no checksum published at $digestSrc -- refusing to install a bundle that cannot be verified"
        }

        # Unpacked inside the data directory rather than in TEMP, so the
        # move into place is a rename on the same volume: the new version
        # appears complete or not at all, and `current` is repointed only
        # after it is there.
        New-Item -ItemType Directory -Path $ArizaVersions -Force | Out-Null
        if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force }
        Ariza-Extract $archive $staging

        $tops = @(Get-ChildItem -LiteralPath $staging -Directory)
        if ($tops.Count -ne 1) {
            Ariza-Err "that archive does not look like a $AppDisplay bundle (expected one directory inside it)"
        }
        $top = $tops[0]

        # What the bundle says it is beats what the archive was called: a
        # versions\<x> directory that does not contain <x> would make
        # every later "already installed" check a lie.
        $actual = Ariza-VersionFromName $top.Name
        if (-not $actual) {
            if (-not $installVer) {
                Ariza-Err "cannot tell which version $($top.Name) is -- expected $AppExec-<version>-<platform>"
            }
        }
        else {
            if ($installVer -and $actual -ne $installVer) {
                Ariza-Warn "that archive is named $installVer but contains $actual -- installing it as $actual"
            }
            $installVer = $actual
        }

        if (Ariza-CheckExisting $installVer) {
            Ariza-Warmup
            Ariza-Report $installVer
            return
        }

        $dest = Join-Path $ArizaVersions $installVer
        if (Test-Path -LiteralPath $dest) { Remove-Item -LiteralPath $dest -Recurse -Force }
        Move-Item -LiteralPath $top.FullName -Destination $dest
        # Stamped with the install time, not the build time: the archive
        # carries the directory's original timestamp, which is the same
        # for every bundle cut from one source tree, and pruning by age
        # has to mean "the one I installed before this one".
        (Get-Item -LiteralPath $dest).LastWriteTime = Get-Date

        if (-not (Ariza-HasEntryPoint $dest)) {
            Ariza-Err "the unpacked bundle has neither bin\$AppExec.exe nor bin\$AppExec.cmd -- not touching your existing install"
        }

        Ariza-PointCurrent $dest
        Ariza-LinkBin
        Ariza-Prune $installVer
        Ariza-Ok "$AppDisplay $installVer installed"
        Ariza-Warmup
        Ariza-Report $installVer
    }
    finally {
        if (Test-Path -LiteralPath $tmp)     { Remove-Item -LiteralPath $tmp -Recurse -Force }
        if (Test-Path -LiteralPath $staging) { Remove-Item -LiteralPath $staging -Recurse -Force }
    }
}

Ariza-Main
