# Install Siraya Printer Service from a USB/release folder (Windows 10/11 x64).
param(
  [Parameter(Mandatory = $true)]
  [string]$BundleRoot,
  [switch]$NoBrowser
)

$ErrorActionPreference = "Stop"
$LogFile = Join-Path $env:TEMP "SirayaPrinter-Install.log"

function Write-Log {
  param([string]$Message)
  $line = "[$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')] $Message"
  Add-Content -Path $LogFile -Value $line
  Write-Host $Message
}

function Test-IsWindows10OrLater {
  $os = Get-CimInstance Win32_OperatingSystem
  return [int]$os.BuildNumber -ge 10240
}

function Resolve-ServiceInstallerPath {
  param([string]$Root, [string]$AppSource)
  $candidates = @(
    (Join-Path $Root "install-service.js"),
    (Join-Path $AppSource "installer\windows\install-service.js")
  )
  foreach ($path in $candidates) {
    if (Test-Path -LiteralPath $path) {
      return $path
    }
  }
  return $null
}

function Test-BundleComplete {
  param(
    [string]$AppSource,
    [string]$ServiceInstallerPath,
    [switch]$RequireNodeModules
  )
  $required = @(
    (Join-Path $AppSource "index.js")
  )
  if ($RequireNodeModules) {
    $required += (Join-Path $AppSource "node_modules\node-windows\package.json")
  }
  foreach ($path in $required) {
    if (-not (Test-Path -LiteralPath $path)) {
      throw "Bundle incomplete - missing $path. Delete the extracted folder, re-download the zip, extract fully, then run Install.cmd as administrator."
    }
  }
  if (-not $ServiceInstallerPath -or -not (Test-Path -LiteralPath $ServiceInstallerPath)) {
    throw "Bundle incomplete - missing install-service.js at zip root or payload\win-x64\app\installer\windows\. Re-download the zip and extract fully."
  }
}

function Copy-AppPayload {
  param([string]$Source, [string]$Destination)
  if (-not (Test-Path -LiteralPath $Source)) {
    throw "Missing source path: $Source"
  }
  New-Item -ItemType Directory -Path $Destination -Force | Out-Null
  & robocopy $Source $Destination /E /COPY:DAT /R:2 /W:2 /NFL /NDL /NJH /NJS | Out-Null
  if ($LASTEXITCODE -ge 8) {
    throw "File copy failed (robocopy exit code $LASTEXITCODE)"
  }
}

function Ensure-InstallerScripts {
  param(
    [string]$ServiceInstallerSource,
    [string]$UninstallInstallerSource,
    [string]$InstallDir,
    [string]$BundleRoot
  )
  $destDir = Join-Path $InstallDir "installer\windows"
  New-Item -ItemType Directory -Path $destDir -Force | Out-Null
  $destService = Join-Path $destDir "install-service.js"
  Copy-Item -LiteralPath $ServiceInstallerSource -Destination $destService -Force
  if ($UninstallInstallerSource -and (Test-Path -LiteralPath $UninstallInstallerSource)) {
    Copy-Item -LiteralPath $UninstallInstallerSource -Destination (Join-Path $destDir "uninstall-service.js") -Force
  }
  foreach ($name in @("health-probe.js", "sync-apply.js", "stop-agent.ps1")) {
    $src = Join-Path $BundleRoot $name
    if (-not (Test-Path -LiteralPath $src)) {
      $src = Join-Path $BundleRoot "installer\windows\$name"
    }
    if (Test-Path -LiteralPath $src) {
      Copy-Item -LiteralPath $src -Destination (Join-Path $destDir $name) -Force
    }
  }
  return $destService
}

function Stop-ServiceIfPresent {
  $stopScript = Join-Path $PSScriptRoot "stop-agent.ps1"
  if (Test-Path -LiteralPath $stopScript) {
    . $stopScript
    Stop-SirayaPrinterAgent -Log { param($Message) Write-Log $Message }
    return
  }
  try {
    $svc = Get-Service -Name SirayaPrinterService -ErrorAction SilentlyContinue
    if ($svc -and $svc.Status -eq 'Running') {
      Write-Log "Stopping existing SirayaPrinterService..."
      & net.exe stop SirayaPrinterService 2>$null | Out-Null
      Start-Sleep -Seconds 2
    }
  } catch {
    Write-Log "WARN: Could not stop existing service: $($_.Exception.Message)"
  }
}

function Remove-InstallDirSafe {
  param([string]$InstallDir)

  if (-not (Test-Path -LiteralPath $InstallDir)) {
    return
  }

  for ($attempt = 1; $attempt -le 5; $attempt++) {
    try {
      Write-Log "Removing previous install files (attempt $attempt)..."
      Remove-Item -LiteralPath $InstallDir -Recurse -Force -ErrorAction Stop
      return
    } catch {
      Write-Log "WARN: Could not remove install folder: $($_.Exception.Message)"
      Stop-ServiceIfPresent
      Start-Sleep -Seconds 3
    }
  }

  throw "Could not remove $InstallDir. Run STOP-AGENT.cmd as administrator, reboot if needed, then run Install.cmd again."
}

function Add-FirewallRuleIfNeeded {
  $ruleName = "Siraya Printer (localhost)"
  $existing = netsh advfirewall firewall show rule name="$ruleName" 2>$null
  if ($LASTEXITCODE -eq 0) {
    Write-Log "Firewall rule already exists: $ruleName"
    return
  }
  Write-Log "Adding firewall rule for TCP port 5000..."
  netsh advfirewall firewall add rule name="$ruleName" dir=in action=allow protocol=TCP localport=5000 | Out-Null
}

function Get-HealthCheckPorts {
  return @(5000)
}

function Get-PidsOnPort {
  param([int]$Port)
  $pids = New-Object System.Collections.Generic.List[int]
  try {
    $conns = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction SilentlyContinue
    foreach ($c in $conns) {
      if ($c.OwningProcess -gt 0) {
        [void]$pids.Add([int]$c.OwningProcess)
      }
    }
  } catch {
    $lines = netstat -ano | Select-String ":$Port\s+.*LISTENING"
    foreach ($line in $lines) {
      if ($line -match '\s+(\d+)\s*$') {
        [void]$pids.Add([int]$Matches[1])
      }
    }
  }
  return @($pids | Select-Object -Unique)
}

function Ensure-Port5000Free {
  param([string]$InstallDir)

  $installDirEsc = [regex]::Escape($InstallDir)
  foreach ($pid in (Get-PidsOnPort -Port 5000)) {
    try {
      $proc = Get-CimInstance Win32_Process -Filter "ProcessId=$pid" -ErrorAction SilentlyContinue
      $cmd = $proc.CommandLine
      if (-not $cmd) { continue }
      $isSiraya = ($cmd -match 'Siraya[\\/]Printer') -or ($cmd -match "$installDirEsc")
      $isNode = $cmd -match 'node(\.exe)?'
      if ($isSiraya -or ($isNode -and $cmd -match 'index\.js')) {
        Write-Log "Freeing port 5000 — stopping PID $pid"
        Stop-Process -Id $pid -Force -ErrorAction SilentlyContinue
      } else {
        Write-Log "WARN: Port 5000 in use by PID $pid (not Siraya) — $($proc.Name)"
      }
    } catch {
      Write-Log "WARN: Could not inspect PID $pid on port 5000: $($_.Exception.Message)"
    }
  }
  Start-Sleep -Seconds 1
}

function Get-BundleVersion {
  param([string]$BundleRoot)
  $manifestFile = Join-Path $BundleRoot "manifest.json"
  if (Test-Path -LiteralPath $manifestFile) {
    try {
      $manifest = Get-Content -LiteralPath $manifestFile -Raw | ConvertFrom-Json
      if ($manifest.version) {
        return [string]$manifest.version
      }
    } catch {
      Write-Log "WARN: Could not read manifest.json version: $($_.Exception.Message)"
    }
  }
  $pkgFile = Join-Path $BundleRoot "payload\win-x64\app\package.json"
  if (Test-Path -LiteralPath $pkgFile) {
    try {
      $pkg = Get-Content -LiteralPath $pkgFile -Raw | ConvertFrom-Json
      if ($pkg.version) {
        return [string]$pkg.version
      }
    } catch { }
  }
  return "unknown"
}

function Pin-Port5000Config {
  param(
    [string]$DataDir,
    [string]$ServiceVersion = "unknown"
  )

  New-Item -ItemType Directory -Force -Path $DataDir | Out-Null
  $envFile = Join-Path $DataDir ".env"
  Set-EnvVarLine -File $envFile -Key "PORT" -Value "5000"
  Set-EnvVarLine -File $envFile -Key "HOST" -Value "127.0.0.1"
  if ($ServiceVersion -and $ServiceVersion -ne "unknown") {
    Set-EnvVarLine -File $envFile -Key "SERVICE_VERSION" -Value $ServiceVersion
  }

  $settingsFile = Join-Path $DataDir "settings.json"
  $settings = @{}
  if (Test-Path -LiteralPath $settingsFile) {
    try {
      $parsed = Get-Content -LiteralPath $settingsFile -Raw | ConvertFrom-Json
      $parsed.PSObject.Properties | ForEach-Object { $settings[$_.Name] = $_.Value }
    } catch {
      Write-Log "WARN: Could not read settings.json for port pin: $($_.Exception.Message)"
    }
  }
  $settings["port"] = 5000
  if (-not $settings.ContainsKey("host")) {
    $settings["host"] = "127.0.0.1"
  }
  ($settings | ConvertTo-Json -Depth 10) | Set-Content -LiteralPath $settingsFile -Encoding UTF8
  Write-Log "Pinned agent port to 5000 and SERVICE_VERSION=$ServiceVersion in .env"
}

function Write-ServiceDiagnostics {
  $dataDir = Join-Path $env:ProgramData "Siraya\Printer"
  $logDir = Join-Path $dataDir "logs"
  $since = $script:InstallStartedAt
  Write-Log "Service diagnostics:"
  try {
    $status = (& sc.exe query SirayaPrinterService 2>&1 | Out-String).Trim()
    if ($status) {
      Write-Log $status
    }
  } catch {
    Write-Log "WARN: sc query failed: $($_.Exception.Message)"
  }
  foreach ($name in @("error.log", "combined.log")) {
    $file = Join-Path $logDir $name
    if (-not (Test-Path -LiteralPath $file)) { continue }
    Write-Log "--- tail $name ---"
    $lines = @(Get-Content -LiteralPath $file -Tail 80)
    foreach ($line in $lines) {
      if ($since -and $line -match '"timestamp":"([^"]+)"') {
        try {
          $ts = [DateTime]::Parse($Matches[1]).ToUniversalTime()
          if ($ts -lt $since.ToUniversalTime()) { continue }
        } catch { }
      }
      Write-Log $line
    }
  }
  $daemonDir = Join-Path $env:ProgramFiles "Siraya\Printer\daemon"
  if (Test-Path -LiteralPath $daemonDir) {
    $wrapperLog = Get-ChildItem -LiteralPath $daemonDir -Filter "*.wrapper.log" -ErrorAction SilentlyContinue |
      Sort-Object LastWriteTime -Descending |
      Select-Object -First 1
    if ($wrapperLog) {
      Write-Log "--- tail $($wrapperLog.Name) ---"
      Get-Content -LiteralPath $wrapperLog.FullName -Tail 25 | ForEach-Object { Write-Log $_ }
    }
  }
}

function Remove-ServiceRegistration {
  Write-Log "Removing previous Windows service registration (if any)..."
  Stop-ServiceIfPresent
  try {
    & sc.exe delete SirayaPrinterService 2>$null | Out-Null
    Start-Sleep -Seconds 2
  } catch { }
  $daemonDir = Join-Path $env:ProgramFiles "Siraya\Printer\daemon"
  if (Test-Path -LiteralPath $daemonDir) {
    Remove-Item -LiteralPath $daemonDir -Recurse -Force -ErrorAction SilentlyContinue
  }
}

function Wait-ForHealth {
  param(
    [int]$TimeoutSec = 150,
    [switch]$AllowTaskRestart,
    [string]$StartBat
  )
  $started = Get-Date
  $deadline = $started.AddSeconds($TimeoutSec)
  $restarts = 0
  Write-Log "Waiting for print agent health on http://127.0.0.1:5000/health (up to ${TimeoutSec}s)..."

  do {
    $elapsed = ((Get-Date) - $started).TotalSeconds
    if ($AllowTaskRestart -and $StartBat -and $restarts -lt 2) {
      if (($restarts -eq 0 -and $elapsed -ge 30) -or ($restarts -eq 1 -and $elapsed -ge 60)) {
        $restarts++
        Write-Log "No health yet ($([int]$elapsed)s) - restarting agent in user context (attempt $restarts)..."
        try {
          schtasks /End /TN "SirayaPrinterAgent" 2>$null | Out-Null
          Start-Sleep -Seconds 2
          Start-Process -FilePath $StartBat -WindowStyle Hidden
          Start-Sleep -Seconds 5
        } catch {
          Write-Log "WARN: Agent restart during health wait: $($_.Exception.Message)"
        }
      }
    }

    foreach ($port in Get-HealthCheckPorts) {
      $url = "http://127.0.0.1:$port/health"
      try {
        $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 3
        if ($r.StatusCode -eq 200) {
          Write-Log "Health check OK at $url"
          return $port
        }
      } catch {
        # retry
      }
    }
    Start-Sleep -Seconds 2
  } while ((Get-Date) -lt $deadline)

  return $null
}

function Write-PersistenceMode {
  param(
    [string]$DataDir,
    [string]$Mode
  )
  $file = Join-Path $DataDir "persistence.json"
  $payload = @{
    mode      = $Mode
    port      = 5000
    updatedAt = (Get-Date).ToUniversalTime().ToString("o")
  } | ConvertTo-Json -Compress
  Set-Content -LiteralPath $file -Value $payload -Encoding UTF8
  Write-Log "Wrote persistence mode: $Mode (port 5000)"
}

function Invoke-AgentSmokeTest {
  param(
    [string]$NodeExe,
    [string]$InstallDir,
    [string]$DataDir
  )

  Write-Log "Running direct node smoke test on port 5000..."
  $env:SIRAYA_INNO_INSTALL = "1"
  $env:SIRAYA_INSTALL_DIR = $InstallDir
  $env:SIRAYA_DATA_DIR = $DataDir
  $env:SIRAYA_NODE_EXE = $NodeExe
  $env:PORT = "5000"
  $env:HOST = "127.0.0.1"

  $proc = $null
  try {
    $proc = Start-Process -FilePath $NodeExe -ArgumentList "`"$InstallDir\index.js`"" -WorkingDirectory $InstallDir -WindowStyle Hidden -PassThru
    $port = Wait-ForHealth -TimeoutSec 45
    if ($port) {
      Write-Log "Smoke test passed on port $port"
    } else {
      Write-Log "WARN: Smoke test did not get /health on port 5000"
      Write-ServiceDiagnostics
    }
    return $port
  } finally {
    if ($proc -and -not $proc.HasExited) {
      try {
        Stop-Process -Id $proc.Id -Force -ErrorAction SilentlyContinue
        Write-Log "Stopped smoke-test node PID $($proc.Id)"
      } catch { }
    }
    Start-Sleep -Seconds 2
    Ensure-Port5000Free -InstallDir $InstallDir
  }
}

function Remove-WindowsServiceCompletely {
  Write-Log "Removing Windows service registration for task-only mode..."
  try {
    & net.exe stop SirayaPrinterService 2>$null | Out-Null
    Start-Sleep -Seconds 2
  } catch { }
  try {
    & sc.exe delete SirayaPrinterService 2>$null | Out-Null
    Start-Sleep -Seconds 2
  } catch { }
  $daemonDir = Join-Path $env:ProgramFiles "Siraya\Printer\daemon"
  if (Test-Path -LiteralPath $daemonDir) {
    Remove-Item -LiteralPath $daemonDir -Recurse -Force -ErrorAction SilentlyContinue
  }
}

function Set-EnvVarLine {
  param(
    [string]$File,
    [string]$Key,
    [string]$Value
  )
  $lines = @()
  if (Test-Path -LiteralPath $File) {
    $lines = @(Get-Content -LiteralPath $File)
  }
  $found = $false
  $out = New-Object System.Collections.Generic.List[string]
  foreach ($line in $lines) {
    if ($line -match "^\s*$([regex]::Escape($Key))=") {
      [void]$out.Add("${Key}=${Value}")
      $found = $true
    } else {
      [void]$out.Add($line)
    }
  }
  if (-not $found) {
    [void]$out.Add("${Key}=${Value}")
  }
  Set-Content -LiteralPath $File -Value $out.ToArray() -Encoding UTF8
}

function Set-PrinterEnvConfig {
  param([string]$DataDir)

  New-Item -ItemType Directory -Force -Path $DataDir | Out-Null
  $envFile = Join-Path $DataDir ".env"
  $queue = $null

  try {
    $printers = @(Get-Printer -ErrorAction SilentlyContinue)
    foreach ($p in $printers) {
      if ($p.Name -like '*BC-95AC*' -or $p.Name -match 'BC-?95|receipt|thermal|pos|escpos|80mm|xprinter|tm-|t88') {
        $queue = $p.Name
        break
      }
    }
  } catch {
    Write-Log "WARN: Get-Printer failed: $($_.Exception.Message)"
  }

  if ($queue) {
    Write-Log "Configuring printer queue in .env: $queue"
    Set-EnvVarLine -File $envFile -Key "PRINTER_SPOOLER_NAME" -Value $queue
    Set-EnvVarLine -File $envFile -Key "PRINTER_MODE" -Value "spooler"
  } else {
    Write-Log "WARN: No thermal printer queue found in Windows - configure BC-95AC in setup UI"
  }

  $hasPort = $false
  if (Test-Path -LiteralPath $envFile) {
    foreach ($line in Get-Content -LiteralPath $envFile) {
      if ($line -match '^\s*PORT=\d+\s*$') {
        $hasPort = $true
        break
      }
    }
  }
  if (-not $hasPort) {
    Write-Log "Setting default PORT=5000 in .env"
    Set-EnvVarLine -File $envFile -Key "PORT" -Value "5000"
  } else {
    Set-EnvVarLine -File $envFile -Key "PORT" -Value "5000"
  }
  Set-EnvVarLine -File $envFile -Key "HOST" -Value "127.0.0.1"
}

function Install-StartAgentBat {
  param(
    [string]$InstallDir,
    [string]$BundleRoot
  )
  $src = Join-Path $BundleRoot "start-agent.bat"
  $dest = Join-Path $InstallDir "start-agent.bat"
  if (Test-Path -LiteralPath $src) {
    Copy-Item -LiteralPath $src -Destination $dest -Force
    return $dest
  }
  $content = @(
    '@echo off',
    'cd /d "%~dp0"',
    '"%~dp0runtime\node.exe" "%~dp0index.js"'
  ) -join "`r`n"
  Set-Content -LiteralPath $dest -Value $content -Encoding ASCII
  return $dest
}

function Get-TaskRunAsUser {
  if ($env:USERDOMAIN -and $env:USERNAME) {
    return "$env:USERDOMAIN\$env:USERNAME"
  }
  return $env:USERNAME
}

function Register-AgentScheduledTask {
  param([string]$StartBat)

  $runAs = Get-TaskRunAsUser
  Write-Log "Registering scheduled task SirayaPrinterAgent (ONLOGON as $runAs)..."
  schtasks /Delete /TN "SirayaPrinterAgent" /F 2>$null | Out-Null
  $tr = "`"$StartBat`""
  schtasks /Create /TN "SirayaPrinterAgent" /TR $tr /SC ONLOGON /RU $runAs /RL HIGHEST /F | Out-Null
  if ($LASTEXITCODE -ne 0) {
    throw "schtasks create failed with exit code $LASTEXITCODE"
  }
  Write-Log "Scheduled task SirayaPrinterAgent registered"
}

function Start-AgentInUserContext {
  param([string]$StartBat)

  Start-Process -FilePath $StartBat -WindowStyle Hidden
  Write-Log "Started agent via start-agent.bat (current user)"
}

function Confirm-HealthJson {
  param([int]$Port)

  $url = "http://127.0.0.1:$Port/health"
  $r = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 5
  if ($r.StatusCode -ne 200) {
    throw "Health endpoint returned HTTP $($r.StatusCode)"
  }
  $json = $r.Content | ConvertFrom-Json
  Write-Log "Health JSON OK service=$($json.service) printerConnected=$($json.printerConnected)"
  return $json
}

try {
  if (-not [Environment]::Is64BitOperatingSystem) {
    throw "64-bit Windows is required."
  }
  if (-not (Test-IsWindows10OrLater)) {
    throw "Windows 10 or newer is required."
  }

  $BundleRoot = $BundleRoot.Trim().TrimEnd('\', '/')
  if (-not $BundleRoot) {
    throw "BundleRoot path is empty."
  }
  $BundleRoot = (Resolve-Path -LiteralPath $BundleRoot).Path
  $script:InstallStartedAt = Get-Date
  Write-Log "Bundle root: $BundleRoot"
  $bundleVersion = Get-BundleVersion -BundleRoot $BundleRoot
  Write-Log "Bundle version: $bundleVersion"

  $payloadRoot = Join-Path $BundleRoot "payload\win-x64"
  $appSrc = Join-Path $payloadRoot "app"
  $runtimeRoot = Join-Path $payloadRoot "runtime"
  $nodeSrc = Join-Path $runtimeRoot "node.exe"
  $npmCmd = Join-Path $runtimeRoot "npm.cmd"

  if (-not (Test-Path -LiteralPath $nodeSrc)) {
    throw "Bundled Node not found at $nodeSrc"
  }

  $serviceInstallerSrc = Resolve-ServiceInstallerPath -Root $BundleRoot -AppSource $appSrc
  Test-BundleComplete -AppSource $appSrc -ServiceInstallerPath $serviceInstallerSrc

  if (Test-Path -LiteralPath (Join-Path $appSrc "node_modules")) {
    Write-Log "Using bundled dependencies (offline install)"
  } elseif (-not (Test-Path -LiteralPath (Join-Path $appSrc "node_modules"))) {
    if (-not (Test-Path -LiteralPath $npmCmd)) {
      throw "node_modules missing and npm.cmd not found at $runtimeRoot"
    }
    Write-Log "Installing dependencies (first run - internet required)..."
    Push-Location $appSrc
    try {
      & $npmCmd ci --omit=dev
      if ($LASTEXITCODE -ne 0) {
        throw "npm ci failed with exit code $LASTEXITCODE"
      }
    } finally {
      Pop-Location
    }
  }

  Test-BundleComplete -AppSource $appSrc -ServiceInstallerPath $serviceInstallerSrc -RequireNodeModules

  $installDir = Join-Path $env:ProgramFiles "Siraya\Printer"
  $dataDir = Join-Path $env:ProgramData "Siraya\Printer"
  $runtimeDir = Join-Path $installDir "runtime"

  Write-Log "Installing to $installDir"

  Write-Log "Stopping any existing print agent before file operations..."
  Stop-ServiceIfPresent
  Remove-ServiceRegistration
  Ensure-Port5000Free -InstallDir $installDir

  Remove-InstallDirSafe -InstallDir $installDir

  Copy-AppPayload -Source $appSrc -Destination $installDir
  New-Item -ItemType Directory -Path $runtimeDir -Force | Out-Null
  Copy-Item -LiteralPath $nodeSrc -Destination (Join-Path $runtimeDir "node.exe") -Force

  $uninstallSrc = Join-Path $BundleRoot "uninstall-service.js"
  if (-not (Test-Path -LiteralPath $uninstallSrc)) {
    $uninstallSrc = Join-Path $appSrc "installer\windows\uninstall-service.js"
  }
  $installScript = Ensure-InstallerScripts `
    -ServiceInstallerSource $serviceInstallerSrc `
    -UninstallInstallerSource $uninstallSrc `
    -InstallDir $installDir `
    -BundleRoot $BundleRoot

  $nodeExe = Join-Path $runtimeDir "node.exe"

  Add-FirewallRuleIfNeeded

  New-Item -ItemType Directory -Force -Path $dataDir | Out-Null
  $logDir = Join-Path $dataDir "logs"
  New-Item -ItemType Directory -Force -Path $logDir | Out-Null
  foreach ($logName in @("error.log", "combined.log")) {
    $logPath = Join-Path $logDir $logName
    if (Test-Path -LiteralPath $logPath) {
      try {
        Clear-Content -LiteralPath $logPath -ErrorAction Stop
        Write-Log "Cleared stale $logName from previous install"
      } catch {
        Write-Log "WARN: Could not clear ${logName}: $($_.Exception.Message)"
      }
    }
  }
  Set-PrinterEnvConfig -DataDir $dataDir
  Pin-Port5000Config -DataDir $dataDir -ServiceVersion $bundleVersion

  $startBat = Install-StartAgentBat -InstallDir $installDir -BundleRoot $BundleRoot
  $persistenceMode = "scheduled-task"

  $env:SIRAYA_INNO_INSTALL = "1"
  $env:SIRAYA_INSTALL_DIR = $installDir
  $env:SIRAYA_DATA_DIR = $dataDir
  $env:SIRAYA_NODE_EXE = $nodeExe
  $env:PORT = "5000"
  $env:HOST = "127.0.0.1"

  $null = Invoke-AgentSmokeTest -NodeExe $nodeExe -InstallDir $installDir -DataDir $dataDir

  Remove-WindowsServiceCompletely

  Write-Log "Starting print agent in current user context..."
  Ensure-Port5000Free -InstallDir $installDir
  Start-Sleep -Seconds 2
  Start-AgentInUserContext -StartBat $startBat
  $healthPort = Wait-ForHealth -TimeoutSec 150 -AllowTaskRestart -StartBat $startBat

  if (-not $healthPort) {
    Write-Log "Health failed after user-context start — retrying once..."
    Stop-ServiceIfPresent
    Ensure-Port5000Free -InstallDir $installDir
    Start-Sleep -Seconds 2
    Start-AgentInUserContext -StartBat $startBat
    $healthPort = Wait-ForHealth -TimeoutSec 90 -AllowTaskRestart -StartBat $startBat
  }

  if ($healthPort) {
    Write-Log "Registering scheduled-task persistence for future logons..."
    Register-AgentScheduledTask -StartBat $startBat
  }

  if (-not $healthPort) {
    Write-ServiceDiagnostics
    Write-Host ""
    Write-Host "Install failed: print agent did not respond on http://127.0.0.1:5000/health"
    Write-Host "Try: reboot this PC, then run Install.cmd again."
    Write-Host "Log: $LogFile"
    Write-Host "Service log: $env:ProgramData\Siraya\Printer\logs\error.log"
    throw "Print agent did not respond on http://127.0.0.1:5000/health. Reboot and run Install.cmd again. See $LogFile"
  }

  Write-PersistenceMode -DataDir $dataDir -Mode $persistenceMode

  try {
    $null = Confirm-HealthJson -Port $healthPort
  } catch {
    Write-Log "WARN: Health JSON check: $($_.Exception.Message)"
  }

  $setupUrl = "http://127.0.0.1:5000"
  Write-Log "Persistence mode: $persistenceMode"

  $agentExe = Join-Path $BundleRoot "SirayaPrinter.exe"
  $shortcutScript = Join-Path $BundleRoot "create-desktop-shortcut.ps1"
  if ((Test-Path -LiteralPath $agentExe) -and (Test-Path -LiteralPath $shortcutScript)) {
    Write-Log "Creating desktop shortcuts..."
    try {
      & powershell -ExecutionPolicy Bypass -File $shortcutScript -TargetPath $agentExe
    } catch {
      Write-Log "WARN: Desktop shortcut creation failed: $($_.Exception.Message)"
    }
  }

  if (-not $NoBrowser) {
    Write-Log "Opening setup UI in browser..."
    Start-Process $setupUrl
  }
  Write-Host ""
  Write-Host "============================================"
  Write-Host "  INSTALL OK - open $setupUrl"
  Write-Host "============================================"
  Write-Log "INSTALL OK - Setup UI: $setupUrl"
  exit 0
} catch {
  Write-Log "ERROR: $($_.Exception.Message)"
  exit 1
}
