#Requires -Version 5.1
<#
.SYNOPSIS
    Install sdl-service.exe as a Windows service (mirrors install.sh for Linux/macOS).

.DESCRIPTION
    sdl-service.exe already implements full Windows SCM service support, so this
    script only needs to lay out binaries + env files and register the service.

    Layout after install (default InstallDir = C:\Program Files\SDL):
        $InstallDir\sdl.exe
        $InstallDir\sdl-service.exe
        $InstallDir\wintun.dll
        $InstallDir\env\config.json
        $InstallDir\env\device-id
        $InstallDir\env\device.key
        $InstallDir\env\service-state.json
        $InstallDir\profiles\<user-id>.json
        device-id and device.key are generated only when missing. Existing
        installed identity files are never overwritten. If the target identity
        files are missing, legacy files from SourceDir\env are migrated first.

    app_home() in sdl-cli resolves to <dir of sdl-service.exe>\env, so the
    command socket (namespaced pipe sdl-command-<hash(app_home)>) is shared by
    sdl.exe and sdl-service.exe only when both live in the same directory.

    The service name is hardcoded to "sdl-service" inside sdl-service.exe
    (service_dispatcher::start), so it CANNOT be overridden here.

.PARAMETER SourceDir
    Directory containing sdl.exe, sdl-service.exe and wintun.dll (plus an optional
    env\ subtree). Defaults to the script's own directory.

.PARAMETER InstallDir
    Install root. Default: C:\Program Files\SDL

.PARAMETER OverwriteConfig
    Replace an existing env\config.json with the installer copy.

.PARAMETER PreserveConfig
    Keep an existing env\config.json (default).

.PARAMETER Uninstall
    Stop and remove the service, then delete the install dir and PATH entry.

.PARAMETER NoStart
    Register the service but do not start it. Without this flag, the installer
    attempts to start the service as a best-effort post-install check. A start
    failure is reported as a warning and does not fail installation.

.EXAMPLE
    # Elevated PowerShell, from the unzipped release folder:
    .\install.ps1

.EXAMPLE
    .\install.ps1 -OverwriteConfig

.EXAMPLE
    .\install.ps1 -NoStart

.EXAMPLE
    .\install.ps1 -Uninstall
#>
[CmdletBinding()]
param(
    [string]$SourceDir = $null,
    [string]$InstallDir = "C:\Program Files\SDL",
    [switch]$OverwriteConfig,
    [switch]$PreserveConfig,
    [switch]$NoStart,
    [switch]$Uninstall
)

$ErrorActionPreference = "Stop"
$ServiceName = "sdl-service"   # MUST match the const in sdl-cli/src/windows_service.rs

function Log-Step([string]$msg) { Write-Host "[install] $msg" }
function Log-Warn([string]$msg) { Write-Warning "[install] $msg" }
function Die([string]$msg) { Write-Error "[install] $msg"; exit 1 }

# --- admin check -----------------------------------------------------------
$principal = New-Object Security.Principal.WindowsPrincipal(
    [Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
    Die "install.ps1 must run as Administrator (start an elevated PowerShell)."
}

if (-not $SourceDir) { $SourceDir = $PSScriptRoot }
if (-not (Test-Path -LiteralPath $SourceDir)) { Die "SourceDir not found: $SourceDir" }

# --- uninstall path --------------------------------------------------------
if ($Uninstall) {
    Log-Step "Uninstalling SDL"
    $svc = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
    if ($svc) {
        if ($svc.Status -ne "Stopped") {
            Log-Step "Stopping service $ServiceName"
            Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
            $svc.WaitForStatus("Stopped", (New-TimeSpan -Seconds 15)) | Out-Null
        }
        Log-Step "Deleting service $ServiceName"
        sc.exe delete $ServiceName | Out-Null
    }
    else {
        Log-Step "Service $ServiceName not present"
    }

    if (Test-Path -LiteralPath $InstallDir) {
        Log-Step "Removing install dir $InstallDir"
        Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction SilentlyContinue
    }

    # drop install dir from machine PATH
    $machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
    if ($machinePath) {
        $entries = $machinePath -split ';' | Where-Object { $_ -and ($_ -ne $InstallDir) }
        $newPath = ($entries -join ';')
        if ($newPath -ne $machinePath) {
            Log-Step "Removing $InstallDir from machine PATH"
            [Environment]::SetEnvironmentVariable("Path", $newPath, "Machine")
        }
    }
    Log-Step "Uninstall complete"
    exit 0
}

# --- validate source binaries ---------------------------------------------
$sdlSrc        = Join-Path $SourceDir "sdl.exe"
$sdlServiceSrc = Join-Path $SourceDir "sdl-service.exe"
$wintunSrc     = Join-Path $SourceDir "wintun.dll"
foreach ($p in @($sdlSrc, $sdlServiceSrc, $wintunSrc)) {
    if (-not (Test-Path -LiteralPath $p)) { Die "missing file: $p" }
}

# config mode
if ($OverwriteConfig -and $PreserveConfig) { Die "-OverwriteConfig and -PreserveConfig are mutually exclusive" }
$ConfigMode = if ($OverwriteConfig) { "overwrite" } else { "preserve" }

# --- helpers mirroring install.sh -----------------------------------------
function Get-ConfigVersion([string]$path) {
    if (-not (Test-Path -LiteralPath $path)) { return $null }
    try {
        $j = Get-Content -LiteralPath $path -Raw | ConvertFrom-Json
        if ($null -ne $j.config_version) { return [string]$j.config_version }
    } catch { }
    return "1"  # CONFIG_VERSION_FALLBACK in install.sh
}

function Backup-File([string]$path) {
    $stamp = Get-Date -Format "yyyyMMddHHmmss"
    $bak = "$path.bak.$stamp"
    Copy-Item -LiteralPath $path -Destination $bak -Force
    Log-Step "Backed up $path to $bak"
}

function Install-ConfigFile {
    $src = Join-Path $SourceDir "env\config.json"
    $dst = Join-Path $InstallDir "env\config.json"
    if (-not (Test-Path -LiteralPath $src)) {
        Log-Step "No installer env\config.json found; keeping any existing config.json"
        return
    }
    if (-not (Test-Path -LiteralPath $dst)) {
        Log-Step "Installing initial config.json"
        Copy-Item -LiteralPath $src -Destination $dst -Force
        return
    }
    if ((Get-FileHash -LiteralPath $src).Hash -eq (Get-FileHash -LiteralPath $dst).Hash) {
        Log-Step "Existing config.json already matches installer config"
        return
    }
    $srcVer = Get-ConfigVersion $src
    $dstVer = Get-ConfigVersion $dst
    if ($srcVer -ne $dstVer) {
        $mismatch = "Existing config.json uses config_version=$dstVer, installer config uses config_version=$srcVer. Keeping the old file may be incompatible with this build."
        if ($ConfigMode -eq "overwrite") {
            Log-Step $mismatch
            Backup-File $dst
            Copy-Item -LiteralPath $src -Destination $dst -Force
            return
        }
        Die ($mismatch + " Re-run with -OverwriteConfig to replace it.")
    }
    if ($ConfigMode -eq "overwrite") {
        Log-Step "Replacing existing config.json (-OverwriteConfig)"
        Backup-File $dst
        Copy-Item -LiteralPath $src -Destination $dst -Force
        return
    }
    Log-Step "Keeping existing config.json (default preserve behavior)"
}

function Copy-EnvFileIfExists([string]$name) {
    $src = Join-Path $SourceDir "env\$name"
    $dst = Join-Path $InstallDir "env\$name"
    if (Test-Path -LiteralPath $src) { Copy-Item -LiteralPath $src -Destination $dst -Force }
}

function Add-DirectoryAccessRule(
    [Security.AccessControl.DirectorySecurity]$Acl,
    [string]$Identity,
    [Security.AccessControl.FileSystemRights]$Rights
) {
    $rule = New-Object Security.AccessControl.FileSystemAccessRule(
        $Identity, $Rights, "ContainerInherit,ObjectInherit", "None", "Allow")
    $Acl.AddAccessRule($rule) | Out-Null
}

function Add-FileAccessRule(
    [Security.AccessControl.FileSecurity]$Acl,
    [string]$Identity,
    [Security.AccessControl.FileSystemRights]$Rights
) {
    $rule = New-Object Security.AccessControl.FileSystemAccessRule(
        $Identity, $Rights, "Allow")
    $Acl.AddAccessRule($rule) | Out-Null
}

function Set-SharedEnvAcl([string]$path) {
    # The CLI and service intentionally share app_home() under <InstallDir>\env.
    # Non-admin users need read/traverse access to compute the IPC pipe name and
    # read local status. Writes to shared config/state are performed by the
    # elevated service through IPC, so ordinary users do not get Modify here.
    $acl = Get-Acl -LiteralPath $path
    $acl.SetAccessRuleProtection($true, $false) | Out-Null
    Add-DirectoryAccessRule $acl "NT AUTHORITY\SYSTEM" "FullControl"
    Add-DirectoryAccessRule $acl "BUILTIN\Administrators" "FullControl"
    Add-DirectoryAccessRule $acl "BUILTIN\Users" "ReadAndExecute"
    $owner = New-Object Security.Principal.NTAccount("BUILTIN\Administrators")
    $acl.SetOwner($owner)
    Set-Acl -LiteralPath $path -AclObject $acl
}

function Set-ServicePrivateDirAcl([string]$path) {
    $acl = Get-Acl -LiteralPath $path
    $acl.SetAccessRuleProtection($true, $false) | Out-Null
    Add-DirectoryAccessRule $acl "NT AUTHORITY\SYSTEM" "FullControl"
    Add-DirectoryAccessRule $acl "BUILTIN\Administrators" "FullControl"
    $owner = New-Object Security.Principal.NTAccount("BUILTIN\Administrators")
    $acl.SetOwner($owner)
    Set-Acl -LiteralPath $path -AclObject $acl
}

function Protect-PrivateEnvFile([string]$path) {
    if (-not (Test-Path -LiteralPath $path)) { return }
    $acl = Get-Acl -LiteralPath $path
    $acl.SetAccessRuleProtection($true, $false) | Out-Null
    Add-FileAccessRule $acl "NT AUTHORITY\SYSTEM" "FullControl"
    Add-FileAccessRule $acl "BUILTIN\Administrators" "FullControl"
    $owner = New-Object Security.Principal.NTAccount("BUILTIN\Administrators")
    $acl.SetOwner($owner)
    Set-Acl -LiteralPath $path -AclObject $acl
}

function Ensure-DeviceKeyFile([string]$path) {
    if (Test-Path -LiteralPath $path) {
        $len = (Get-Item -LiteralPath $path).Length
        if ($len -ne 32) {
            Die "invalid device key length at $path : expected 32 bytes, got $len"
        }
        return
    }

    $legacyPath = Join-Path $SourceDir "env\device.key"
    if (Test-Path -LiteralPath $legacyPath) {
        $legacyLen = (Get-Item -LiteralPath $legacyPath).Length
        if ($legacyLen -ne 32) {
            Die "invalid legacy device key length at $legacyPath : expected 32 bytes, got $legacyLen"
        }
        Log-Step "Migrating legacy device key from $legacyPath"
        Copy-Item -LiteralPath $legacyPath -Destination $path -Force
        return
    }

    Log-Step "Generating persistent service device key"
    $bytes = New-Object byte[] 32
    $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create()
    try {
        $rng.GetBytes($bytes)
        [System.IO.File]::WriteAllBytes($path, $bytes)
    } finally {
        $rng.Dispose()
    }
}

function Ensure-DeviceIdFile([string]$path) {
    if (Test-Path -LiteralPath $path) {
        $existing = (Get-Content -LiteralPath $path -Raw -ErrorAction SilentlyContinue).Trim()
        if ($existing) {
            Log-Step "Keeping existing device-id"
            return
        }
        Die "invalid empty device-id at $path; remove it to generate a new identity"
    }

    $legacyPath = Join-Path $SourceDir "env\device-id"
    if (Test-Path -LiteralPath $legacyPath) {
        $existing = (Get-Content -LiteralPath $legacyPath -Raw -ErrorAction SilentlyContinue).Trim()
        if (-not $existing) {
            Die "invalid empty legacy device-id at $legacyPath"
        }
        Log-Step "Migrating legacy device-id from $legacyPath"
        Copy-Item -LiteralPath $legacyPath -Destination $path -Force
        return
    }

    Log-Step "Generating persistent device-id"
    [System.IO.File]::WriteAllText($path, [Guid]::NewGuid().ToString())
}

function Set-ServiceEnvironment([string]$name, [string]$deviceKeyPath) {
    $serviceRegPath = "HKLM:\SYSTEM\CurrentControlSet\Services\$name"
    $envValue = @("SDL_DEVICE_KEY_PATH=$deviceKeyPath")
    New-ItemProperty `
        -LiteralPath $serviceRegPath `
        -Name "Environment" `
        -PropertyType MultiString `
        -Value $envValue `
        -Force | Out-Null
}

function Try-StartServiceBestEffort([string]$name, [string]$logPath) {
    Log-Step "Starting $name"
    try {
        Start-Service -Name $name -ErrorAction Stop
    } catch {
        Log-Warn "Service $name was registered but did not start: $_"
        Log-Warn "Check Windows Event Viewer and $logPath after fixing runtime prerequisites."
        return "StartFailed"
    }

    $deadline = (Get-Date).AddSeconds(20)
    do {
        Start-Sleep -Seconds 1
        $svc = Get-Service -Name $name -ErrorAction SilentlyContinue
        if ($svc -and $svc.Status -eq "Running") {
            Log-Step "Service $name is running"
            return "Running"
        }
    } while ((Get-Date) -lt $deadline)

    $status = if ($svc) { [string]$svc.Status } else { "Missing" }
    Log-Warn "Service $name was registered but did not reach Running state within 20s (status=$status)."
    Log-Warn "Installation is complete; inspect Windows Event Viewer and $logPath for runtime startup details."
    return $status
}

# --- stop & remove any pre-existing service -------------------------------
$existing = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
if ($existing) {
    Log-Step "Stopping existing service if present: $ServiceName"
    if ($existing.Status -ne "Stopped") {
        Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
        $existing.WaitForStatus("Stopped", (New-TimeSpan -Seconds 15)) | Out-Null
    }
    Log-Step "Deleting existing service $ServiceName"
    sc.exe delete $ServiceName | Out-Null
    Start-Sleep -Seconds 1
}

# --- lay out install tree --------------------------------------------------
Log-Step "Preparing install directories under $InstallDir"
$envDir = Join-Path $InstallDir "env"
$profilesDir = Join-Path $InstallDir "profiles"
$logDir = Join-Path $InstallDir "log"
New-Item -ItemType Directory -Force -Path $InstallDir | Out-Null
New-Item -ItemType Directory -Force -Path $envDir | Out-Null
New-Item -ItemType Directory -Force -Path $profilesDir | Out-Null
New-Item -ItemType Directory -Force -Path $logDir | Out-Null

Log-Step "Installing sdl.exe, sdl-service.exe, wintun.dll"
Copy-Item -LiteralPath $sdlSrc        -Destination (Join-Path $InstallDir "sdl.exe")        -Force
Copy-Item -LiteralPath $sdlServiceSrc -Destination (Join-Path $InstallDir "sdl-service.exe") -Force
Copy-Item -LiteralPath $wintunSrc     -Destination (Join-Path $InstallDir "wintun.dll")     -Force
$wintunLicense = Join-Path $SourceDir "Wintun-LICENSE.txt"
if (Test-Path -LiteralPath $wintunLicense) {
    Copy-Item -LiteralPath $wintunLicense -Destination (Join-Path $InstallDir "Wintun-LICENSE.txt") -Force
}

Log-Step "Copying persisted env files (if present)"
foreach ($name in "service-state.json") { Copy-EnvFileIfExists $name }
Install-ConfigFile
$deviceIdPath = Join-Path $envDir "device-id"
$deviceKeyPath = Join-Path $envDir "device.key"
Ensure-DeviceIdFile $deviceIdPath
Ensure-DeviceKeyFile $deviceKeyPath

# Allow non-admin users to run `sdl` while keeping private env files restricted.
try {
    Set-SharedEnvAcl $envDir
    Set-ServicePrivateDirAcl $profilesDir
    Protect-PrivateEnvFile $deviceKeyPath
} catch {
    Log-Warn "Could not update ACL on $envDir or $profilesDir : $_"
}

# --- register service ------------------------------------------------------
$binPath = '"' + (Join-Path $InstallDir "sdl-service.exe") + '"'
Log-Step "Creating service $ServiceName -> $binPath"
New-Service -Name $ServiceName `
    -BinaryPathName $binPath `
    -DisplayName "SDL Service" `
    -Description "SDL (Software Defined LAN) local service: TUN device, control connection, P2P and relay." `
    -StartupType Automatic | Out-Null
Set-ServiceEnvironment $ServiceName $deviceKeyPath

# Delayed autostart (survives boot before network is fully up) and restart-on-failure
# mirrors systemd Restart=on-failure / launchd KeepAlive.
sc.exe config $ServiceName start= delayed-auto | Out-Null
sc.exe failure $ServiceName reset= 0 actions= restart/3000/restart/3000/restart/3000 | Out-Null

# --- add install dir to machine PATH (mirrors /usr/local/bin symlinks) ----
$machinePath = [Environment]::GetEnvironmentVariable("Path", "Machine")
$entries = if ($machinePath) { $machinePath -split ';' } else { @() }
if ($entries -notcontains $InstallDir) {
    Log-Step "Adding $InstallDir to machine PATH"
    $newPath = (($entries + $InstallDir) | Where-Object { $_ }) -join ';'
    [Environment]::SetEnvironmentVariable("Path", $newPath, "Machine")
    # refresh current session copy
    $env:Path = "$env:Path;$InstallDir"
}

# --- start service ---------------------------------------------------------
$serviceStatus = "NotStarted"
if ($NoStart) {
    Log-Step "Skipping service start (-NoStart)"
} else {
    $serviceStatus = Try-StartServiceBestEffort $ServiceName (Join-Path $logDir "sdl-service.log")
}

Write-Host ""
Write-Host "Installed SDL to $InstallDir"
Write-Host "Service: $ServiceName (Automatic, delayed start, restart on failure, current status: $serviceStatus)"
Write-Host "CLI 'sdl' is on the machine PATH (open a new shell to use it)"
Write-Host "env dir: $envDir (service-owned state; non-admin CLI users have read/traverse access)"
Write-Host "profiles dir: $profilesDir (saved per-user profiles)"
Write-Host "device id: $deviceIdPath (generated only when missing)"
Write-Host "device key: $deviceKeyPath (used by service via SDL_DEVICE_KEY_PATH)"
Write-Host "Logs: $logDir\sdl-service.log (rotates at 10MB, keeps 5 archives); level via RUST_LOG (default info)"
Write-Host ""
Write-Host "Manage:"
Write-Host "  Get-Service $ServiceName"
Write-Host "  Start-Service $ServiceName ; Stop-Service $ServiceName"
Write-Host "  Restart-Service $ServiceName"
Write-Host "  sc.exe failure $ServiceName reset= 0 actions= restart/3000/restart/3000/restart/3000"
Write-Host "  .\install.ps1 -Uninstall"
