[CmdletBinding()]
param(
    [string]$HostAddress = "0.0.0.0",
    [int]$Port = 8766,
    [switch]$ReplaceExisting,
    [switch]$NoBrowser,
    [switch]$ProbeOnly,
    [int]$HealthTimeoutSeconds = 60
)

$ErrorActionPreference = "Stop"
$appId = "d-group-inventory-hub"
$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$configuredData = [Environment]::GetEnvironmentVariable("D_GROUP_DATA_DIR")
if ([string]::IsNullOrWhiteSpace($configuredData)) {
    $runtimeDataRoot = Join-Path $env:LOCALAPPDATA "DGroupInventoryHub\data"
} else {
    $runtimeDataRoot = [System.IO.Path]::GetFullPath($configuredData)
}
$runtimeDir = Join-Path $runtimeDataRoot "runtime"
[System.IO.Directory]::CreateDirectory($runtimeDir) | Out-Null
$pidFile = Join-Path $runtimeDir "d-group-inventory-hub.pid.json"
$auditFile = Join-Path $runtimeDir "launcher-audit.jsonl"
$healthUrl = "http://127.0.0.1:$Port/api/health"
$openUrl = "http://127.0.0.1:$Port"

function Write-LauncherAudit {
    param([string]$Action, [string]$Result, [hashtable]$Detail = @{})
    $record = [ordered]@{
        timestamp = [DateTime]::UtcNow.ToString("o")
        actor = [Environment]::UserName
        action = $Action
        result = $Result
        appId = $appId
        port = $Port
        detail = $Detail
    }
    Add-Content -LiteralPath $auditFile -Value ($record | ConvertTo-Json -Compress -Depth 5) -Encoding utf8
}

function Get-AppHealth {
    try {
        return Invoke-RestMethod -Uri $healthUrl -Method Get -TimeoutSec 2
    } catch {
        return $null
    }
}

function Get-PortListeners {
    try {
        return @(Get-NetTCPConnection -State Listen -LocalPort $Port -ErrorAction Stop)
    } catch {
        return @()
    }
}

function Test-PortOccupied {
    $client = [System.Net.Sockets.TcpClient]::new()
    $occupied = $false
    try {
        $asyncResult = $client.BeginConnect("127.0.0.1", $Port, $null, $null)
        $completed = $asyncResult.AsyncWaitHandle.WaitOne(600)
        if ($completed) {
            $client.EndConnect($asyncResult)
            $occupied = $client.Connected
        }
    } catch {
        $occupied = $false
    } finally {
        $client.Dispose()
    }
    return [bool]$occupied
}

function Read-PidRecord {
    if (-not (Test-Path -LiteralPath $pidFile)) { return $null }
    try {
        return Get-Content -LiteralPath $pidFile -Raw -Encoding utf8 | ConvertFrom-Json
    } catch {
        return $null
    }
}

function Test-OwnedProcess {
    param($Record, $Health)
    if ($null -eq $Record -or $null -eq $Health) { return $false }
    if ($Record.appId -ne $appId -or [int]$Record.port -ne $Port) { return $false }
    if ($Health.appId -ne $appId -or $Health.buildToken -ne $Record.buildToken) { return $false }
    if ([int]$Health.processId -ne [int]$Record.pid) { return $false }
    try {
        $process = Get-Process -Id ([int]$Record.pid) -ErrorAction Stop
        $command = (Get-CimInstance Win32_Process -Filter "ProcessId = $($process.Id)" -ErrorAction Stop).CommandLine
        if ([string]::IsNullOrWhiteSpace($command)) { return $false }
        return ($command -match "DGroupInventoryHub" -or $command -match "d_group_inventory_hub" -or $command -match "run\.py")
    } catch {
        return $false
    }
}

function Find-ServerExecutable {
    $candidates = @(
        (Join-Path $projectRoot "DGroupInventoryHub.exe"),
        (Join-Path $projectRoot "app\DGroupInventoryHub.exe"),
        (Join-Path $projectRoot "dist\DGroupInventoryHub\DGroupInventoryHub.exe")
    )
    foreach ($candidate in $candidates) {
        if (Test-Path -LiteralPath $candidate -PathType Leaf) {
            return (Resolve-Path -LiteralPath $candidate).Path
        }
    }
    throw "DGroupInventoryHub.exe was not found. Start the tool from the complete release folder."
}

function Get-ExpectedBuildToken {
    $candidates = @(
        (Join-Path $projectRoot "BUILD_TOKEN"),
        (Join-Path $projectRoot "app\BUILD_TOKEN"),
        (Join-Path $projectRoot "dist\DGroupInventoryHub\BUILD_TOKEN")
    )
    foreach ($candidate in $candidates) {
        if (Test-Path -LiteralPath $candidate -PathType Leaf) {
            return (Get-Content -LiteralPath $candidate -Raw -Encoding utf8).Trim()
        }
    }
    throw "BUILD_TOKEN is missing. Startup was stopped to avoid connecting to a stale service."
}

$existingHealth = Get-AppHealth
$portOccupied = Test-PortOccupied
$listeners = if ($portOccupied) { Get-PortListeners } else { @() }
$sameAppRunning = $null -ne $existingHealth -and $existingHealth.appId -eq $appId

if ($ProbeOnly) {
    [ordered]@{
        appId = $appId
        port = $Port
        portOccupied = $portOccupied
        sameAppRunning = $sameAppRunning
        health = $existingHealth
    } | ConvertTo-Json -Depth 5
    exit 0
}

if ($sameAppRunning) {
    Write-LauncherAudit -Action "launcher.previous.detected" -Result "success" -Detail @{ buildToken = $existingHealth.buildToken }
    $shouldReplace = $ReplaceExisting.IsPresent
    if (-not $shouldReplace) {
        Add-Type -AssemblyName System.Windows.Forms
        $choice = [System.Windows.Forms.MessageBox]::Show(
            "D Group Inventory Hub is already running on port $Port. Stop the previous version and start this version? Selecting No cancels startup and does not open a browser.",
            "Inventory Hub startup protection",
            [System.Windows.Forms.MessageBoxButtons]::YesNo,
            [System.Windows.Forms.MessageBoxIcon]::Question
        )
        $shouldReplace = $choice -eq [System.Windows.Forms.DialogResult]::Yes
    }
    if (-not $shouldReplace) {
        Write-LauncherAudit -Action "launcher.previous.replace" -Result "cancelled" -Detail @{ reason = "user_cancelled" }
        exit 2
    }

    $pidRecord = Read-PidRecord
    if (-not (Test-OwnedProcess -Record $pidRecord -Health $existingHealth)) {
        Write-LauncherAudit -Action "launcher.previous.replace" -Result "blocked" -Detail @{ reason = "ownership_unverified" }
        throw "A matching service responded, but process ownership could not be verified. Startup was cancelled without stopping it. Close the previous Inventory Hub manually."
    }
    Stop-Process -Id ([int]$pidRecord.pid) -ErrorAction Stop
    $deadline = [DateTime]::UtcNow.AddSeconds(12)
    do {
        Start-Sleep -Milliseconds 300
        $portOccupied = Test-PortOccupied
    } while ($portOccupied -and [DateTime]::UtcNow -lt $deadline)
    if ($portOccupied) {
        Write-LauncherAudit -Action "launcher.previous.stop" -Result "failure" -Detail @{ pid = [int]$pidRecord.pid; reason = "port_still_busy" }
        throw "The previous version did not stop before the timeout. Startup was cancelled."
    }
    Write-LauncherAudit -Action "launcher.previous.stop" -Result "success" -Detail @{ pid = [int]$pidRecord.pid }
} elseif ($portOccupied) {
    $listenerPids = @($listeners | Select-Object -ExpandProperty OwningProcess -Unique)
    Write-LauncherAudit -Action "launcher.port.check" -Result "blocked" -Detail @{ reason = "unknown_process"; listenerPids = $listenerPids }
    throw "Port $Port belongs to another process. The process was not stopped and the browser will not open."
}

$serverExecutable = Find-ServerExecutable
$expectedBuildToken = Get-ExpectedBuildToken
$stdoutLog = Join-Path $runtimeDir "server-out.log"
$stderrLog = Join-Path $runtimeDir "server-error.log"
$argumentList = @("serve", "--host", $HostAddress, "--port", $Port)
$serverProcess = Start-Process -FilePath $serverExecutable -ArgumentList $argumentList -WorkingDirectory $projectRoot -WindowStyle Hidden -RedirectStandardOutput $stdoutLog -RedirectStandardError $stderrLog -PassThru
Write-LauncherAudit -Action "launcher.server.start" -Result "started" -Detail @{ pid = $serverProcess.Id; executable = $serverExecutable; expectedBuildToken = $expectedBuildToken }

$deadline = [DateTime]::UtcNow.AddSeconds($HealthTimeoutSeconds)
$verifiedHealth = $null
do {
    if ($serverProcess.HasExited) { break }
    Start-Sleep -Milliseconds 400
    $candidateHealth = Get-AppHealth
    if ($null -ne $candidateHealth -and $candidateHealth.appId -eq $appId -and $candidateHealth.buildToken -eq $expectedBuildToken -and $candidateHealth.databaseReady) {
        $verifiedHealth = $candidateHealth
        break
    }
} while ([DateTime]::UtcNow -lt $deadline)

if ($null -eq $verifiedHealth) {
    if (-not $serverProcess.HasExited) {
        Stop-Process -Id $serverProcess.Id -ErrorAction SilentlyContinue
    }
    Write-LauncherAudit -Action "launcher.server.verify" -Result "failure" -Detail @{ pid = $serverProcess.Id; expectedBuildToken = $expectedBuildToken }
    throw "The new service failed its version or health check. It was stopped and the browser will not open. Review $stderrLog"
}

$pidRecord = [ordered]@{
    appId = $appId
    pid = $serverProcess.Id
    port = $Port
    buildToken = $verifiedHealth.buildToken
    executable = $serverExecutable
    startedAt = [DateTime]::UtcNow.ToString("o")
}
$pidRecord | ConvertTo-Json | Set-Content -LiteralPath $pidFile -Encoding utf8
Write-LauncherAudit -Action "launcher.server.verify" -Result "success" -Detail @{ pid = $serverProcess.Id; buildToken = $verifiedHealth.buildToken }

if (-not $NoBrowser) {
    Start-Process $openUrl | Out-Null
    Write-LauncherAudit -Action "launcher.browser.open" -Result "success" -Detail @{ url = $openUrl }
}

Write-Output "Inventory Hub started: $openUrl"
Write-Output "LAN sharing addresses are available in Settings."
