# Pulse Host Agent Installation Script for Windows
#
# Usage:
#   iwr -useb http://pulse-server:7656/install-host-agent.ps1 | iex
#   OR with parameters:
#   $url = "http://pulse-server:7656"; $token = "your-token"; iwr -useb "$url/install-host-agent.ps1" | iex
#
# Parameters can be passed via environment variables or script parameters

param(
    [string]$PulseUrl = $env:PULSE_URL,
    [string]$Token = $env:PULSE_TOKEN,
    [string]$Interval = $env:PULSE_INTERVAL,
    [string]$InstallPath = "C:\Program Files\Pulse",
    [string]$Arch = $env:PULSE_ARCH,
    [switch]$NoService
)

$ErrorActionPreference = "Stop"

function Write-PulseMessage {
    param(
        [string]$Label,
        [string]$Message,
        [ConsoleColor]$Color
    )

    if ($Label) {
        Write-Host ("[{0}] {1}" -f $Label, $Message) -ForegroundColor $Color
    } else {
        Write-Host $Message -ForegroundColor $Color
    }
}

function PulseSuccess { param([string]$msg) Write-PulseMessage -Label 'OK' -Message $msg -Color 'Green' }
function PulseError { param([string]$msg) Write-PulseMessage -Label 'FAIL' -Message $msg -Color 'Red' }
function PulseInfo { param([string]$msg) Write-PulseMessage -Label 'INFO' -Message $msg -Color 'Cyan' }
function PulseWarn { param([string]$msg) Write-PulseMessage -Label 'WARN' -Message $msg -Color 'Yellow' }

function Write-InstallerEvent {
    param(
        [string]$SourceName,
        [string]$Message,
        [ValidateSet('Information', 'Warning', 'Error')] [string]$EntryType = 'Information',
        [int]$EventId = 1000
    )

    if (-not $SourceName) { return }

    try {
        Write-EventLog -LogName Application -Source $SourceName -EventId $EventId -EntryType $EntryType -Message $Message
    } catch {
        PulseWarn "Unable to write installer event log entry: $_"
    }
}

try {
    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
} catch {
    # Ignore if platform does not expose TLS 1.3
}

function Get-RecentAgentEvents {
    param(
        [string]$ProviderName,
        [int]$Max = 5
    )
    try {
        return Get-WinEvent -FilterHashtable @{ LogName = 'Application'; ProviderName = $ProviderName } -MaxEvents $Max -ErrorAction Stop
    } catch {
        return Get-EventLog -LogName Application -Source $ProviderName -Newest $Max -ErrorAction SilentlyContinue
    }
}

function Test-AgentRegistration {
    param(
        [string]$PulseUrl,
        [string]$Hostname,
        [string]$Token
    )

    if (-not $Token) {
        return $null
    }

    try {
        $encodedHostname = [System.Uri]::EscapeDataString($Hostname)
        $lookupUri = "$PulseUrl/api/agents/host/lookup?hostname=$encodedHostname"
        $headers = @{ Authorization = "Bearer $Token" }
        $response = Invoke-RestMethod -Uri $lookupUri -Headers $headers -Method Get -ErrorAction Stop
        return $response.host
    } catch {
        return $null
    }
}

function Resolve-PulseArchitectureCandidate {
    param(
        [string]$CandidateValue,
        [string]$Source
    )

    if ([string]::IsNullOrWhiteSpace($CandidateValue)) {
        return $null
    }

    $normalized = $CandidateValue.Trim()
    $upper = $normalized.ToUpperInvariant()

    $mapping = $null
    if ($upper -match 'ARM64|AARCH64') {
        $mapping = @{ OsLabel = 'Arm64'; DownloadArch = 'arm64' }
    } elseif ($upper -match 'AMD64|X64|64-BIT') {
        $mapping = @{ OsLabel = 'X64'; DownloadArch = 'amd64' }
    } elseif ($upper -match 'X86|I386|IA32|32-BIT') {
        $mapping = @{ OsLabel = 'X86'; DownloadArch = '386' }
    }

    if (-not $mapping) {
        return $null
    }

    return [pscustomobject]@{
        OsLabel       = $mapping.OsLabel
        DownloadArch  = $mapping.DownloadArch
        RawValue      = $normalized
        Source        = $Source
        UsedHeuristic = $false
        ObservedValues = @()
    }
}

function Get-PulseArchitecture {
    param(
        [string]$OverrideArch
    )

    $observations = @()

    $candidateSources = @()
    if ($OverrideArch) {
        $candidateSources += @{ Name = 'Override'; Getter = { $OverrideArch } }
    }

    $candidateSources += @(
        @{ Name = 'RuntimeInformation.OSArchitecture'; Getter = { [System.Runtime.InteropServices.RuntimeInformation]::OSArchitecture } },
        @{ Name = 'mscorlib.RuntimeInformation.OSArchitecture'; Getter = { [System.Runtime.InteropServices.RuntimeInformation,mscorlib]::OSArchitecture } },
        @{ Name = 'Win32_OperatingSystem (CIM)'; Getter = { (Get-CimInstance -ClassName Win32_OperatingSystem -Property OSArchitecture -ErrorAction Stop).OSArchitecture } },
        @{ Name = 'Win32_OperatingSystem (WMI)'; Getter = { (Get-WmiObject -Class Win32_OperatingSystem -ErrorAction Stop).OSArchitecture } },
        @{ Name = 'PROCESSOR_ARCHITEW6432'; Getter = { $env:PROCESSOR_ARCHITEW6432 } },
        @{ Name = 'PROCESSOR_ARCHITECTURE'; Getter = { $env:PROCESSOR_ARCHITECTURE } },
        @{ Name = 'PROCESSOR_IDENTIFIER'; Getter = { $env:PROCESSOR_IDENTIFIER } }
    )

    foreach ($source in $candidateSources) {
        try {
            $value = & $source.Getter
        } catch {
            continue
        }

        if ([string]::IsNullOrWhiteSpace($value)) {
            continue
        }

        $valueString = $value.ToString().Trim()
        $observations += [pscustomobject]@{ Source = $source.Name; Value = $valueString }

        $resolved = Resolve-PulseArchitectureCandidate -CandidateValue $valueString -Source $source.Name
        if ($resolved) {
            $resolved.ObservedValues = $observations
            return $resolved
        }
    }

    $armHint = $observations | Where-Object { $_.Value -match 'ARM' } | Select-Object -First 1
    if ($armHint) {
        return [pscustomobject]@{
            OsLabel       = 'Arm64'
            DownloadArch  = 'arm64'
            RawValue      = $armHint.Value
            Source        = "$($armHint.Source) heuristic"
            UsedHeuristic = $true
            ObservedValues = $observations
        }
    }

    $is64BitOS = $null
    try { $is64BitOS = [Environment]::Is64BitOperatingSystem } catch { }
    if ($is64BitOS -eq $null) {
        $is64BitOS = [System.IntPtr]::Size -ge 8
    }

    if ($is64BitOS -eq $true) {
        return [pscustomobject]@{
            OsLabel        = 'X64'
            DownloadArch   = 'amd64'
            RawValue       = 'Environment.Is64BitOperatingSystem=True'
            Source         = 'Environment heuristic'
            UsedHeuristic  = $true
            ObservedValues = $observations
        }
    }

    if ($is64BitOS -eq $false) {
        return [pscustomobject]@{
            OsLabel        = 'X86'
            DownloadArch   = '386'
            RawValue       = 'Environment.Is64BitOperatingSystem=False'
            Source         = 'Environment heuristic'
            UsedHeuristic  = $true
            ObservedValues = $observations
        }
    }

    return $null
}

Write-Host ""
$banner = "=" * 59
Write-Host $banner -ForegroundColor Cyan
Write-Host "  Pulse Host Agent - Windows Installation" -ForegroundColor Cyan
Write-Host $banner -ForegroundColor Cyan
Write-Host ""

# Check if running as Administrator
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
    PulseError "This script must be run as Administrator"
    PulseInfo "Right-click PowerShell and select 'Run as Administrator'"
    exit 1
}

# Interactive prompts if parameters not provided
if (-not $PulseUrl) {
    $PulseUrl = Read-Host "Enter Pulse server URL (e.g., http://pulse.example.com:7656)"
}
$PulseUrl = $PulseUrl.TrimEnd('/')

if (-not $Token) {
    PulseWarn "No API token provided - agent will attempt to connect without authentication"
    $response = Read-Host "Continue without token? (y/N)"
    if ($response -ne 'y' -and $response -ne 'Y') {
        $Token = Read-Host "Enter API token"
    }
}

if (-not $Interval) {
    $Interval = "30s"
}

PulseInfo "Configuration:"
Write-Host "  Pulse URL: $PulseUrl"
Write-Host "  Token: $(if ($Token) { '***' + $Token.Substring([Math]::Max(0, $Token.Length - 4)) } else { 'none' })"
Write-Host "  Interval: $Interval"
Write-Host "  Install Path: $InstallPath"
if ($Arch) {
    Write-Host "  Architecture Override: $Arch"
}
Write-Host ""

# Determine architecture (support both legacy Windows PowerShell and pwsh)
if ($Arch) {
    if (-not (Resolve-PulseArchitectureCandidate -CandidateValue $Arch -Source "Override")) {
        PulseError "Invalid architecture override '$Arch'. Supported values: amd64, arm64, 386"
        exit 1
    }
}

$archInfo = Get-PulseArchitecture -OverrideArch $Arch
$observedSummary = $null
if ($archInfo -and $archInfo.ObservedValues) {
    $observedSummary = ($archInfo.ObservedValues | ForEach-Object { "$($_.Source)='$($_.Value)'" }) -join "; "
}

if (-not $archInfo -or -not $archInfo.DownloadArch) {
    PulseError "Unable to determine operating system architecture"
    if ($observedSummary) {
        PulseInfo "Observed architecture values: $observedSummary"
    }
    PulseInfo "Specify -Arch amd64|arm64|386 or set PULSE_ARCH to override detection."
    exit 1
}

$osArch = $archInfo.OsLabel
$arch = $archInfo.DownloadArch
$rawArchValue = $archInfo.RawValue
$archSource = if ($archInfo.Source) { $archInfo.Source } else { "auto-detected" }
$downloadUrl = "$PulseUrl/download/pulse-host-agent?platform=windows&arch=$arch"

PulseInfo "System Information:"
if ($rawArchValue -and $rawArchValue -ne $osArch) {
    Write-Host "  OS Architecture: $osArch (reported as '$rawArchValue')"
} else {
    Write-Host "  OS Architecture: $osArch"
}
Write-Host "  Detection Source: $archSource"
Write-Host "  Download Architecture: $arch"
Write-Host "  Download URL: $downloadUrl"
Write-Host ""

if ($archInfo.UsedHeuristic) {
    PulseWarn "Architecture detected via fallback ($archSource). If this looks wrong, rerun with -Arch amd64|arm64|386 or set PULSE_ARCH."
    if ($observedSummary) {
        PulseInfo "Observed architecture values: $observedSummary"
    }
}

PulseInfo "Downloading agent binary from $downloadUrl..."
try {
    # Create install directory
    if (-not (Test-Path $InstallPath)) {
        New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null
    }

    $agentPath = Join-Path $InstallPath "pulse-host-agent.exe"

    # Download binary
    Invoke-WebRequest -Uri $downloadUrl -OutFile $agentPath -UseBasicParsing
    PulseSuccess "Downloaded agent to $agentPath"

    # Validate PE header
    $fileBytes = [System.IO.File]::ReadAllBytes($agentPath)
    $fileSizeMB = [math]::Round($fileBytes.Length / 1MB, 2)
    PulseInfo "File size: $fileSizeMB MB ($($fileBytes.Length) bytes)"

    if ($fileBytes.Length -lt 64) {
        throw "Downloaded file is too small ($($fileBytes.Length) bytes) - expected Windows PE executable"
    }

    # Check for MZ signature (PE header)
    if ($fileBytes[0] -ne 0x4D -or $fileBytes[1] -ne 0x5A) {
        $firstBytes = ($fileBytes[0..15] | ForEach-Object { $_.ToString("X2") }) -join " "
        throw "Downloaded file is not a valid Windows executable (missing MZ signature). First bytes: $firstBytes"
    }

    # Get PE header offset (at 0x3C)
    $peOffset = [BitConverter]::ToUInt32($fileBytes, 0x3C)
    if ($peOffset -ge $fileBytes.Length - 6) {
        throw "Invalid PE header offset in downloaded file"
    }

    # Check PE signature
    if ($fileBytes[$peOffset] -ne 0x50 -or $fileBytes[$peOffset+1] -ne 0x45) {
        throw "Downloaded file has invalid PE signature"
    }

    # Check machine type (should be 0x8664 for x64, 0xAA64 for ARM64)
    $machineType = [BitConverter]::ToUInt16($fileBytes, $peOffset + 4)
    $expectedMachine = switch ($arch) {
        'amd64' { 0x8664 }
        'arm64' { 0xAA64 }
        '386'   { 0x014C }
        default { 0x0000 }
    }

    if ($machineType -ne $expectedMachine) {
        $machineStr = "0x" + $machineType.ToString("X4")
        $expectedStr = "0x" + $expectedMachine.ToString("X4")
        throw "Downloaded binary is for wrong architecture (got $machineStr, expected $expectedStr for $arch)"
    }

    PulseSuccess "Verified PE executable for $osArch architecture"

    # Verify checksum
    PulseInfo "Verifying checksum..."
    $checksumUrl = "$PulseUrl/download/pulse-host-agent.sha256?platform=windows&arch=$arch"
    try {
        $expectedChecksum = (Invoke-WebRequest -Uri $checksumUrl -UseBasicParsing).Content.Trim().Split()[0]
        $actualChecksum = (Get-FileHash -Path $agentPath -Algorithm SHA256).Hash.ToLower()

        if ($actualChecksum -ne $expectedChecksum.ToLower()) {
            throw "Checksum mismatch! Expected: $expectedChecksum, Got: $actualChecksum"
        }
        PulseSuccess "Checksum verified: $actualChecksum"
    } catch {
        PulseWarn "Could not verify checksum: $_"
        PulseInfo "Continuing anyway (PE header was validated)"
    }

    $agentArgs = @("--url", "`"$PulseUrl`"", "--interval", $Interval)
    if ($Token) {
        $agentArgs += @("--token", "`"$Token`"")
    }
    $serviceBinaryPath = "`"$agentPath`" $($agentArgs -join ' ')"
    $manualCommand = "& `"$agentPath`" $($agentArgs -join ' ')"
} catch {
    PulseError "Failed to download agent: $_"
    exit 1
}

# Create configuration
$configPath = Join-Path $InstallPath "config.json"
$config = @{
    url = $PulseUrl
    interval = $Interval
}
if ($Token) {
    $config.token = $Token
}

$config | ConvertTo-Json | Set-Content $configPath
PulseSuccess "Created configuration at $configPath"

# Stop existing service if running
$serviceName = "PulseHostAgent"
$existingService = Get-Service -Name $serviceName -ErrorAction SilentlyContinue
if ($existingService) {
    PulseInfo "Stopping existing service..."
    Stop-Service -Name $serviceName -Force
    PulseSuccess "Stopped existing service"
}

if (-not $NoService) {
    PulseInfo "Installing native Windows service with built-in service support..."

    try {
        if ($existingService) {
            PulseInfo "Removing existing service..."
            sc.exe delete $serviceName | Out-Null
            Start-Sleep -Seconds 2
        }

        # Create the service using New-Service
        New-Service -Name $serviceName `
                    -BinaryPathName $serviceBinaryPath `
                    -DisplayName "Pulse Host Agent" `
                    -Description "Monitors system metrics and reports to Pulse monitoring server" `
                    -StartupType Automatic | Out-Null

        PulseSuccess "Created Windows service '$serviceName'"

        # Register Windows Event Log source
        try {
            if (-not ([System.Diagnostics.EventLog]::SourceExists($serviceName))) {
                New-EventLog -LogName Application -Source $serviceName
                PulseSuccess "Registered Event Log source"
            }
        } catch {
            PulseWarn "Could not register Event Log source (not critical): $_"
        }

        Write-InstallerEvent -SourceName $serviceName -Message "Pulse Host Agent installer registered service version $(Get-Item $agentPath).VersionInfo.FileVersion" -EventId 1000

        # Configure service recovery options (restart on failure)
        sc.exe failure $serviceName reset= 86400 actions= restart/60000/restart/60000/restart/60000 | Out-Null
        PulseSuccess "Configured automatic restart on failure"

        # Start the service
        PulseInfo "Starting service..."
        Start-Service -Name $serviceName
        Start-Sleep -Seconds 3

        $status = (Get-Service -Name $serviceName).Status
        if ($status -eq 'Running') {
            PulseSuccess "Service started successfully!"

            PulseInfo "Waiting for agent to register with Pulse (up to 30s)..."
            
            $maxRetries = 15
            $retryDelay = 2
            $lookupHost = $null
            
            for ($i = 1; $i -le $maxRetries; $i++) {
                Start-Sleep -Seconds $retryDelay
                $hostname = $env:COMPUTERNAME
                $lookupHost = Test-AgentRegistration -PulseUrl $PulseUrl -Hostname $hostname -Token $Token
                
                if ($lookupHost) {
                    break
                }
                Write-Host "." -NoNewline
            }
            Write-Host "" # Newline after dots

            if ($lookupHost) {
                PulseSuccess "Agent successfully registered with Pulse (host '$hostname')."
                if ($lookupHost.status) {
                    $lastSeen = $lookupHost.lastSeen
                    if ($lastSeen -is [DateTime]) {
                        $lastSeen = $lastSeen.ToString("u")
                    }
                    PulseInfo ("Pulse reports status: {0} (last seen {1})" -f $lookupHost.status, $lastSeen)
                }
                PulseInfo "Check your Pulse dashboard - this host should appear shortly."
                $statusForLog = if ($lookupHost.status) { $lookupHost.status } else { 'unknown' }
                Write-InstallerEvent -SourceName $serviceName -Message "Installer verified host '$hostname' reporting to Pulse (status: $statusForLog)." -EventId 1010
            } elseif ($Token) {
                PulseWarn "Agent is running but the lookup endpoint has not confirmed registration yet."
                PulseInfo "It may take another moment for metrics to appear in the dashboard."
                Write-InstallerEvent -SourceName $serviceName -Message "Installer could not yet confirm host '$hostname' registration with Pulse." -EntryType Warning -EventId 1011
            } else {
                PulseInfo "Registration check skipped (no API token available)."
                Write-InstallerEvent -SourceName $serviceName -Message "Installer skipped registration lookup (no API token provided)." -EventId 1012
            }

            $recentLogs = Get-RecentAgentEvents -ProviderName $serviceName -Max 5
            if ($recentLogs) {
                PulseInfo "Recent service events:"
                $recentLogs | Select-Object -First 3 | ForEach-Object {
                    $time = $_.TimeCreated
                    if (-not $time) { $time = $_.TimeGenerated }
                    Write-Host ("    [{0}] {1}" -f $time.ToString("u"), $_.Message)
                }
            } else {
                PulseWarn "No recent Application log entries were found for $serviceName."
            }
        } else {
            PulseWarn "Service status: $status"
            PulseInfo "Checking service logs..."
            $recentLogs = Get-RecentAgentEvents -ProviderName $serviceName -Max 5
            if ($recentLogs) {
                $recentLogs | ForEach-Object {
                    $time = $_.TimeCreated
                    if (-not $time) { $time = $_.TimeGenerated }
                    Write-Host ("    [{0}] {1}" -f $time.ToString("u"), $_.Message)
                }
            } else {
                PulseWarn "No Application log entries were found for $serviceName."
            }
        }

    } catch {
        PulseError "Failed to create/start service: $_"
        PulseInfo "You can start the agent manually with:"
        Write-Host "  $manualCommand"
        Write-Host ""
        PulseInfo "Or check Windows Event Viewer (Application log) for error details."
        exit 1
    }
} else {
    PulseInfo "Skipping service installation (--NoService flag)"
    Write-Host ""
    PulseInfo "To start the agent manually:"
    Write-Host "  $manualCommand"
}

Write-Host ""
$successBanner = "=" * 59
Write-Host $successBanner -ForegroundColor Green
PulseSuccess "Installation complete!"
Write-Host $successBanner -ForegroundColor Green
Write-Host ""

PulseInfo "Service Management Commands:"
Write-Host "  Start:   Start-Service -Name PulseHostAgent"
Write-Host "  Stop:    Stop-Service -Name PulseHostAgent"
Write-Host "  Restart: Restart-Service -Name PulseHostAgent"
Write-Host "  Status:  Get-Service -Name PulseHostAgent | Select Status, StartType"
Write-Host "  Remove:  sc.exe delete PulseHostAgent"
Write-Host "  Logs:    Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='PulseHostAgent'} -MaxEvents 50"
Write-Host ""

PulseInfo "Files installed:"
Write-Host "  Binary: $agentPath"
Write-Host "  Config: $configPath"
Write-Host ""

PulseInfo "The agent is now reporting to: $PulseUrl"
Write-Host ""
