<#
.SYNOPSIS
  Panel one-line installer for Windows — controller + agent + Postgres on one box.

.DESCRIPTION
  PowerShell 5+ equivalent of install.sh. Checks Docker Desktop, starts a
  panel-postgres container, builds (or downloads) the panel binaries with the
  version baked in via ldflags, boots the controller once to bootstrap the CA
  and admin account, pairs the local agent, and registers both as logon-time
  Scheduled Tasks so they survive reboots.

  For a hardened always-on Windows service, wrap the same ExecStart lines with
  nssm (https://nssm.cc): `nssm install panel-controller <InstallDir>\bin\controller.exe ...`.

.EXAMPLE
  powershell -ExecutionPolicy Bypass -File install.ps1 -AdminPassword secret123
#>
[CmdletBinding()]
param(
    [string]$RepoUrl     = $(if ($env:PANEL_REPO_URL) { $env:PANEL_REPO_URL } else { 'https://git.pdxtechs.com/dbledeez/panel' }),
    # Defaults to the published v0.9.1 windows/amd64 prebuilt zip so the installer
    # needs no Go toolchain. Pass -PrebuiltUrl '' (empty) to force a source build.
    # A local checkout (running install.ps1 from inside the repo) wins over this.
    [string]$PrebuiltUrl = $(if ($null -ne $env:PANEL_PREBUILT_URL) { $env:PANEL_PREBUILT_URL } else { 'https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.1/panel-v0.9.1-windows-amd64.zip' }),
    [string]$Version     = $env:PANEL_VERSION,
    [string]$InstallDir  = $(if ($env:PANEL_DIR) { $env:PANEL_DIR } else { 'C:\Panel' }),
    [int]   $HttpPort    = $(if ($env:PANEL_HTTP_PORT) { [int]$env:PANEL_HTTP_PORT } else { 8080 }),
    [int]   $GrpcPort    = $(if ($env:PANEL_GRPC_PORT) { [int]$env:PANEL_GRPC_PORT } else { 8443 }),
    [string]$AdminEmail    = $env:PANEL_ADMIN_EMAIL,
    [string]$AdminPassword = $env:PANEL_ADMIN_PASSWORD,
    [string]$DbUrl       = $env:PANEL_DB_URL,
    [string]$DbPassword  = $env:PANEL_DB_PASSWORD,
    [string]$DbContainer = $(if ($env:PANEL_DB_CONTAINER) { $env:PANEL_DB_CONTAINER } else { 'panel-postgres' }),
    [int]   $DbPort      = $(if ($env:PANEL_DB_PORT) { [int]$env:PANEL_DB_PORT } else { 5432 }),
    [string]$DbVolume    = $(if ($env:PANEL_DB_VOLUME) { $env:PANEL_DB_VOLUME } else { 'panel_pgdata' }),
    [switch]$SkipAgent,
    # -InstallDocker (or PANEL_INSTALL_DOCKER=1) auto-installs Docker Desktop via
    # winget without prompting; -SkipDockerInstall keeps the old fail-if-missing.
    [switch]$InstallDocker,
    [switch]$SkipDockerInstall
)
$ErrorActionPreference = 'Stop'
# Run two independent panels on one host by giving each install distinct
# -InstallDir, -HttpPort, -GrpcPort, -DbContainer, -DbPort and -DbVolume.
$PgContainer = $DbContainer

function Log($msg)  { Write-Host "[panel] $msg" -ForegroundColor Green }
function Warn($msg) { Write-Host "[panel] $msg" -ForegroundColor Yellow }
function Fail($msg) { Write-Host "[panel] ERROR: $msg" -ForegroundColor Red; exit 1 }

# The literal one-liner to paste to run this installer again. Used by the
# "you must reboot / start Docker, then run this again" messages so the user
# is never left guessing what "re-run" means.
$RerunCommand = 'irm https://git.pdxtechs.com/dbledeez/panel/raw/branch/main/install.ps1 | iex'

# Show-RerunAndExit prints a loud, unmissable "not done yet — run this again"
# banner with the exact command, then exits. Windows Docker Desktop needs a
# reboot on first install, so a second run of the installer is expected and
# normal — this makes that crystal clear instead of a terse error.
function Show-RerunAndExit($why) {
    Write-Host ''
    Write-Host '===============================================================' -ForegroundColor Yellow
    Write-Host '  ACTION NEEDED — the panel is not installed yet' -ForegroundColor Yellow
    Write-Host '===============================================================' -ForegroundColor Yellow
    Write-Host "  $why" -ForegroundColor Yellow
    Write-Host ''
    Write-Host '  Do this, in order:' -ForegroundColor Yellow
    Write-Host '    1. REBOOT if Windows/winget asked you to (Docker Desktop needs' -ForegroundColor Yellow
    Write-Host '       WSL2 + virtualization, which require a restart).' -ForegroundColor Yellow
    Write-Host '    2. Start Docker Desktop and wait until the whale icon in the' -ForegroundColor Yellow
    Write-Host '       system tray stops animating (daemon ready).' -ForegroundColor Yellow
    Write-Host '    3. >>> RUN THIS INSTALLER AGAIN <<< in an Administrator' -ForegroundColor Green
    Write-Host '       PowerShell window. It will pick up from here and finish:' -ForegroundColor Green
    Write-Host ''
    Write-Host "          $RerunCommand" -ForegroundColor Cyan
    Write-Host ''
    Write-Host '  (Running it again is safe and expected — it is idempotent.)' -ForegroundColor Yellow
    Write-Host '===============================================================' -ForegroundColor Yellow
    exit 1
}

# ---- 0. Admin account: prompt when interactive -----------------------------
# A real person (interactive console) is asked to choose the admin login +
# password up front. When run non-interactively (irm | iex with no console,
# CI) the prompt is skipped: -AdminPassword / $env:PANEL_ADMIN_PASSWORD is used
# and a missing password is generated by the controller as before.
if (-not $AdminPassword -and [Environment]::UserInteractive -and $Host.UI.RawUI) {
    Write-Host ''
    Write-Host '-- Set up your panel admin login --' -ForegroundColor Cyan
    if (-not $AdminEmail) {
        $e = Read-Host 'Admin email [admin@panel.local]'
        if ($e) { $AdminEmail = $e }
    }
    while (-not $AdminPassword) {
        $s1 = Read-Host 'Admin password (min 8 chars, blank = auto-generate)' -AsSecureString
        $p1 = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [Runtime.InteropServices.Marshal]::SecureStringToBSTR($s1))
        if (-not $p1) { Warn 'no password entered - the controller will generate one and print it.'; break }
        if ($p1.Length -lt 8) { Write-Host '  password too short (need 8+); try again.'; continue }
        $s2 = Read-Host 'Confirm password' -AsSecureString
        $p2 = [Runtime.InteropServices.Marshal]::PtrToStringAuto(
                [Runtime.InteropServices.Marshal]::SecureStringToBSTR($s2))
        if ($p1 -ne $p2) { Write-Host '  passwords did not match; try again.'; continue }
        $AdminPassword = $p1
    }
}

# ---- 1. Docker Desktop -----------------------------------------------------
# The panel needs Docker for Postgres + every game server. Unlike Linux we
# can't fully script Docker Desktop (it's a GUI install + WSL2 + reboot), but
# we CAN kick it off via winget and guide the rest. -InstallDocker / env
# PANEL_INSTALL_DOCKER=1 answers "yes" non-interactively; -SkipDockerInstall
# forces the old fail-fast behavior.
if (-not (Get-Command docker -ErrorAction SilentlyContinue)) {
    $wantDocker = $InstallDocker -or ($env:PANEL_INSTALL_DOCKER -eq '1')
    if (-not $wantDocker -and -not $SkipDockerInstall -and [Environment]::UserInteractive -and $Host.UI.RawUI) {
        Write-Host ''
        Write-Host 'Docker Desktop is required (runs Postgres + your game servers) and is not installed.' -ForegroundColor Yellow
        $ans = Read-Host 'Install Docker Desktop now via winget? [Y/n]'
        $wantDocker = ($ans -eq '' -or $ans -match '^[Yy]')
    }
    if ($wantDocker) {
        if (-not (Get-Command winget -ErrorAction SilentlyContinue)) {
            Fail @"
winget isn't available to auto-install Docker. Install Docker Desktop manually:
  https://www.docker.com/products/docker-desktop/
Then start it (wait for the whale icon to settle) and re-run this installer.
"@
        }
        Log "installing Docker Desktop via winget (this is large; a reboot is usually required)"
        winget install --id Docker.DockerDesktop -e --accept-source-agreements --accept-package-agreements
        if ($LASTEXITCODE -ne 0) { Fail "winget install of Docker Desktop failed (exit $LASTEXITCODE). Install it manually, then re-run." }
        Show-RerunAndExit "Docker Desktop was just installed — the panel is NOT installed yet."
    } else {
        Fail "Docker not found. Install Docker Desktop (https://www.docker.com/products/docker-desktop/), start it, then re-run."
    }
}
docker info *> $null
if ($LASTEXITCODE -ne 0) {
    Show-RerunAndExit "Docker is installed but its daemon isn't running yet — the panel is NOT installed yet."
}
Log "docker present: $(docker --version)"

# ---- 2. Postgres -----------------------------------------------------------
New-Item -ItemType Directory -Force -Path "$InstallDir\bin", "$InstallDir\data" | Out-Null
$EnvFile = Join-Path $InstallDir 'panel.env'

if ($DbUrl) {
    Log "using external Postgres from -DbUrl"
} else {
    docker inspect $PgContainer *> $null
    if ($LASTEXITCODE -eq 0) {
        Log "postgres container '$PgContainer' already exists - starting"
        docker start $PgContainer | Out-Null
        if (-not $DbPassword -and (Test-Path $EnvFile)) {
            $DbPassword = (Get-Content $EnvFile | Where-Object { $_ -match '^PANEL_DB_PASSWORD=' } |
                           Select-Object -First 1) -replace '^PANEL_DB_PASSWORD=', ''
        }
        if (-not $DbPassword) { Fail "existing $PgContainer but no DbPassword known (pass -DbPassword, or 'docker rm -f $PgContainer' to start fresh)" }
    } else {
        if (-not $DbPassword) {
            $DbPassword = -join ((1..32) | ForEach-Object { '{0:x}' -f (Get-Random -Maximum 16) })
        }
        Log "starting postgres container '$PgContainer' on 127.0.0.1:$DbPort (volume $DbVolume)"
        docker run -d --name $PgContainer --restart unless-stopped `
            -e POSTGRES_USER=panel -e "POSTGRES_PASSWORD=$DbPassword" -e POSTGRES_DB=panel `
            -p "127.0.0.1:${DbPort}:5432" `
            -v "${DbVolume}:/var/lib/postgresql/data" `
            postgres:17-alpine | Out-Null
        if ($LASTEXITCODE -ne 0) { Fail "docker run for postgres failed" }
    }
    $DbUrl = "postgres://panel:$DbPassword@127.0.0.1:$DbPort/panel?sslmode=disable"
    Log "waiting for postgres"
    $ready = $false
    foreach ($i in 1..30) {
        docker exec $PgContainer pg_isready -U panel -d panel *> $null
        if ($LASTEXITCODE -eq 0) { $ready = $true; break }
        Start-Sleep -Seconds 1
    }
    if (-not $ready) { Fail "postgres did not become ready" }
}

# ---- 3. Source / binaries ---------------------------------------------------
$Src = $null
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
if ($ScriptDir -and (Test-Path (Join-Path $ScriptDir 'go.work')) -and (Test-Path (Join-Path $ScriptDir 'controller'))) {
    $Src = $ScriptDir
    Log "using local checkout at $Src"
    # Running from inside the repo means build THIS tree. Don't let the prebuilt
    # default override it. An explicit -PrebuiltUrl or $env:PANEL_PREBUILT_URL still
    # forces prebuilt (it won't equal the untouched default / the env is non-null).
    $prebuiltDefault = 'https://git.pdxtechs.com/dbledeez/panel/releases/download/v0.9.1/panel-v0.9.1-windows-amd64.zip'
    if (($null -eq $env:PANEL_PREBUILT_URL) -and ($PrebuiltUrl -eq $prebuiltDefault)) {
        $PrebuiltUrl = ''
    }
}

if ($PrebuiltUrl) {
    Log "downloading prebuilt binaries: $PrebuiltUrl"
    $zip = Join-Path $env:TEMP 'panel-prebuilt.zip'
    Invoke-WebRequest -Uri $PrebuiltUrl -OutFile $zip -UseBasicParsing
    Expand-Archive -Path $zip -DestinationPath $InstallDir -Force
    Remove-Item $zip -Force
    if (-not (Test-Path "$InstallDir\bin\controller.exe")) { Fail "prebuilt archive did not contain bin\controller.exe" }
} else {
    if (-not $Src) {
        if (-not (Get-Command git -ErrorAction SilentlyContinue)) { Fail "git required to fetch sources (or pass -PrebuiltUrl)" }
        $Src = Join-Path $InstallDir 'src'
        if (Test-Path (Join-Path $Src '.git')) {
            Log "updating source at $Src"
            git -C $Src pull --ff-only
        } else {
            Log "cloning $RepoUrl"
            git clone --depth 1 $RepoUrl $Src
        }
    }
    if (-not (Get-Command go -ErrorAction SilentlyContinue)) { Fail "Go toolchain not found. Install Go >= 1.26 (https://go.dev/dl/) or pass -PrebuiltUrl." }
    if (-not $Version) {
        $Version = (git -C $Src describe --tags --always --dirty 2>$null)
        if (-not $Version) { $Version = 'dev' }
    }
    $ldflags = "-s -w -X github.com/dbledeez/panel/pkg/version.Version=$Version"
    Log "building panel $Version"
    Push-Location $Src
    try {
        $env:CGO_ENABLED = '0'
        go build -trimpath -ldflags $ldflags -o "$InstallDir\bin\controller.exe" ./controller/cmd/controller
        if ($LASTEXITCODE -ne 0) { Fail 'controller build failed' }
        go build -trimpath -ldflags $ldflags -o "$InstallDir\bin\agent.exe" ./agent/cmd/agent
        if ($LASTEXITCODE -ne 0) { Fail 'agent build failed' }
        go build -trimpath -ldflags $ldflags -o "$InstallDir\bin\panelctl.exe" ./controller/cmd/panelctl
        if ($LASTEXITCODE -ne 0) { Fail 'panelctl build failed' }
    } finally { Pop-Location }
    Log "syncing game modules"
    if (Test-Path "$InstallDir\modules") { Remove-Item "$InstallDir\modules" -Recurse -Force }
    Copy-Item -Recurse "$Src\modules" "$InstallDir\modules"
}

# ---- 4. Env file ------------------------------------------------------------
$envLines = @("PANEL_DB_URL=$DbUrl")
if ($DbPassword)    { $envLines += "PANEL_DB_PASSWORD=$DbPassword" }
if ($AdminEmail)    { $envLines += "PANEL_ADMIN_EMAIL=$AdminEmail" }
if ($AdminPassword) { $envLines += "PANEL_ADMIN_PASSWORD=$AdminPassword" }
$envLines += "PANEL_HTTP_PORT=$HttpPort"
$envLines += "PANEL_GRPC_PORT=$GrpcPort"
Set-Content -Path $EnvFile -Value $envLines -Encoding ascii

# ---- 5. Start controller (background) + bootstrap ---------------------------
$ctrlLog = Join-Path $InstallDir 'controller.log'
$firstBoot = -not (Test-Path "$InstallDir\data\ca")

# Stop a previous run of this script's processes, if any.
Get-Process -Name controller, agent -ErrorAction SilentlyContinue |
    Where-Object { $_.Path -like "$InstallDir*" } | Stop-Process -Force -ErrorAction SilentlyContinue

$env:PANEL_DB_URL = $DbUrl
if ($AdminEmail)    { $env:PANEL_ADMIN_EMAIL = $AdminEmail }
if ($AdminPassword) { $env:PANEL_ADMIN_PASSWORD = $AdminPassword }
Log "starting controller"
Start-Process -FilePath "$InstallDir\bin\controller.exe" `
    -ArgumentList @('-http', ":$HttpPort", '-grpc', ":$GrpcPort", '-ca-dir', './data/ca') `
    -WorkingDirectory $InstallDir -WindowStyle Hidden `
    -RedirectStandardError $ctrlLog

$up = $false
foreach ($i in 1..60) {
    try {
        Invoke-WebRequest -Uri "http://127.0.0.1:$HttpPort/login" -UseBasicParsing -TimeoutSec 2 | Out-Null
        $up = $true; break
    } catch { Start-Sleep -Seconds 1 }
}
if (-not $up) { Fail "controller did not come up - see $ctrlLog" }

$adminEmail = if ($AdminEmail) { $AdminEmail } else { 'admin@panel.local' }
# BUG-5b: always give the operator a path to the password.
$adminShown = "(the one you set via -AdminPassword; also in $EnvFile)"
if (-not $AdminPassword) {
    if ($firstBoot) {
        $m = Select-String -Path $ctrlLog -Pattern 'password: ([a-f0-9]{16})' | Select-Object -First 1
        if ($m) {
            $AdminPassword = $m.Matches[0].Groups[1].Value
            $adminShown = $AdminPassword
        } else {
            $adminShown = "(see $ctrlLog, line 'password:')"
        }
    } else {
        $adminShown = '(unchanged from previous install)'
    }
}

# ---- 6. Pair token + local agent ---------------------------------------------
$pairCmd = "$InstallDir\bin\agent.exe --pair-url http://<controller-host>:$HttpPort --pair-token <token-from-dashboard> --cert-dir ./data/certs"
if (-not $SkipAgent) {
    $paired = Test-Path "$InstallDir\data\certs\agent.crt"
    if (-not $paired -and $AdminPassword) {
        try {
            $session = New-Object Microsoft.PowerShell.Commands.WebRequestSession
            $body = @{ email = $adminEmail; password = $AdminPassword } | ConvertTo-Json
            Invoke-RestMethod -Uri "http://127.0.0.1:$HttpPort/api/login" -Method Post `
                -ContentType 'application/json' -Body $body -WebSession $session | Out-Null
            $tokBody = @{ description = 'install.ps1 local agent'; expires_minutes = 60 } | ConvertTo-Json
            $tok = Invoke-RestMethod -Uri "http://127.0.0.1:$HttpPort/api/admin/pair-tokens" -Method Post `
                -ContentType 'application/json' -Body $tokBody -WebSession $session
            Log "pairing local agent"
            Push-Location $InstallDir
            try {
                & "$InstallDir\bin\agent.exe" --pair-url "http://127.0.0.1:$HttpPort" --pair-token $tok.token --cert-dir ./data/certs
                if ($LASTEXITCODE -eq 0) { $paired = $true } else { Warn 'agent pairing exited non-zero' }
            } finally { Pop-Location }
        } catch {
            Warn "auto-pairing failed: $($_.Exception.Message)"
        }
    }
    if ($paired) {
        Log "starting agent"
        Start-Process -FilePath "$InstallDir\bin\agent.exe" `
            -ArgumentList @('--controller', "localhost:$GrpcPort", '--modules-dir', './modules',
                            '--data-root', './data/instances', '--meta-dir', './data/instance-meta',
                            '--backup-dir', './data/backups', '--cert-dir', './data/certs') `
            -WorkingDirectory $InstallDir -WindowStyle Hidden `
            -RedirectStandardError (Join-Path $InstallDir 'agent.log')
    } else {
        Warn "agent not paired. Create a pair token in the dashboard (Admin -> Pair tokens) and run:"
        Warn "  cd $InstallDir; $pairCmd"
    }
}

# ---- 7. Scheduled tasks (survive reboots) -------------------------------------
Log "registering logon Scheduled Tasks (panel-controller / panel-agent)"
try {
    $ctrlAction = New-ScheduledTaskAction -Execute "$InstallDir\bin\controller.exe" `
        -Argument "-http :$HttpPort -grpc :$GrpcPort -ca-dir ./data/ca" -WorkingDirectory $InstallDir
    $agentAction = New-ScheduledTaskAction -Execute "$InstallDir\bin\agent.exe" `
        -Argument "--controller localhost:$GrpcPort --modules-dir ./modules --data-root ./data/instances --meta-dir ./data/instance-meta --backup-dir ./data/backups --cert-dir ./data/certs" `
        -WorkingDirectory $InstallDir
    $trigger  = New-ScheduledTaskTrigger -AtLogOn
    $settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) `
        -ExecutionTimeLimit (New-TimeSpan -Days 3650) -StartWhenAvailable
    Register-ScheduledTask -TaskName 'panel-controller' -Action $ctrlAction -Trigger $trigger -Settings $settings -Force | Out-Null
    Register-ScheduledTask -TaskName 'panel-agent' -Action $agentAction -Trigger $trigger -Settings $settings -Force | Out-Null
    Warn "NOTE: the scheduled tasks read PANEL_DB_URL etc. from the machine environment;"
    Warn "for a true always-on service (no logon needed), use nssm: https://nssm.cc"
    # Persist the DB URL machine-wide so the scheduled-task runs see it.
    [Environment]::SetEnvironmentVariable('PANEL_DB_URL', $DbUrl, 'Machine')
} catch {
    Warn "scheduled-task registration failed (not admin?): $($_.Exception.Message)"
    Warn "the controller/agent started by this script keep running until reboot."
}

# ---- 8. Summary ----------------------------------------------------------------
Write-Host ""
Log "------------------------------------------------------------"
Log " Panel is installed."
Log "   Dashboard : http://localhost:$HttpPort"
Log "   Login     : $adminEmail"
Log "   Password  : $adminShown"
Log "   (log in once and change the password in the UI)"
Log ""
Log " To pair an agent on ANOTHER machine, create a pair token in the"
Log " dashboard, copy the panel binaries + modules\ there, then run:"
Log "   $pairCmd"
Log " and start it: .\bin\agent.exe --controller <this-host>:$GrpcPort --cert-dir ./data/certs"
Log "------------------------------------------------------------"
