#Requires -Version 5.1
<#
.SYNOPSIS
    Windows Event Log Collector for LLM Analysis (Datto RMM Component).

.DESCRIPTION
    Collects Windows event logs + system context, gzips the JSON, uploads to
    Backblaze B2, and POSTs metadata to a webhook for LLM analysis.

    Two transport modes — auto-selected by env vars supplied at job dispatch:

    1. Pulse-driven dispatch (preferred). Pulse passes these as Datto
       component variables (delivered as env vars):
         RunId         — correlation token; the webhook handler uses it to
                         find the dispatched execution row.
         WebhookUrl    — Pulse's /api/rmm/loglift/upload metadata endpoint.
         WebhookSecret — OPENCLAW_API_KEY; sent as x-openclaw-key.
         UploadUrl     — pre-presigned B2 PUT URL (30-min TTL).
         ObjectKey     — full B2 object key.
         ClientId      — Datto site uuid (informational; matches CS_PROFILE_UID).
       The script uploads to UploadUrl directly (no presign-fetch round-trip)
       and POSTs to WebhookUrl with x-openclaw-key.

    2. Legacy n8n flow. With none of the Pulse env vars set, the script falls
       back to the original behavior: POST to $WebhookUrl to fetch a
       presigned URL, upload, then POST to $NotifyWebhookUrl.

.NOTES
    Version: 3.0.1 (Pulse + n8n dual-mode)
    Author : Wulf Consulting / MSP Automation
    Requires: PowerShell 5.1+, Windows 10/11 or Server 2016+
    Platform: Datto RMM Component
#>

[CmdletBinding()]
param(
    [Parameter()]
    [string]$ClientId,

    # Legacy n8n presign-fetch URL. Ignored when $env:WebhookUrl is set.
    [Parameter()]
    [string]$WebhookUrl = "https://n8n.wulfnetwork.com/webhook/audit/get-upload-url/7uI1aKMcF",

    # Legacy n8n metadata-notify URL. Ignored when $env:WebhookUrl is set.
    [Parameter()]
    [string]$NotifyWebhookUrl = "https://n8n.wulfnetwork.com/webhook/dd9e1afc-a494-4688-9542-010d388c7bd9",

    [Parameter()]
    [int]$HoursBack = 24,

    [Parameter()]
    [string[]]$IncludeLogs = @('System', 'Application', 'Security'),

    [Parameter()]
    [ValidateSet('Critical', 'Error', 'Warning', 'Information', 'Verbose')]
    [string]$MinLevel = 'Warning',

    [Parameter()]
    [string]$IssueDescription = "",

    [Parameter()]
    [string]$TicketNumber = "",

    [Parameter()]
    [switch]$DryRun,

    [Parameter()]
    [switch]$IncludeInstalledSoftware,

    [Parameter()]
    [switch]$IncludeRunningProcesses,

    [Parameter()]
    [switch]$IncludeNetworkConfig,

    [Parameter()]
    [int]$MaxEventsPerLog = 1000,

    [Parameter()]
    [string]$OutputPath = "C:\wulf\results"
)

# ============================================================================
# LOGGING HELPER
# ============================================================================

function Write-Log {
    param(
        [string]$Message,
        [ValidateSet('Info', 'Warning', 'Error', 'Success')]
        [string]$Level = 'Info'
    )

    $colors = @{
        'Info'    = 'Cyan'
        'Warning' = 'Yellow'
        'Error'   = 'Red'
        'Success' = 'Green'
    }

    $timestamp = Get-Date -Format 'HH:mm:ss'
    Write-Host "[$timestamp] " -NoNewline -ForegroundColor Gray
    Write-Host $Message -ForegroundColor $colors[$Level]
}

# ============================================================================
# DATTO RMM INTEGRATION
# ============================================================================

function Get-DattoDeviceUid {
    <#
    .SYNOPSIS
        Retrieves the unique Device UID from Datto RMM agent. Falls back to
        hostname when the agent isn't detected.
    #>

    $regPaths = @(
        'HKLM:\SOFTWARE\CentraStage',
        'HKLM:\SOFTWARE\WOW6432Node\CentraStage'
    )

    foreach ($path in $regPaths) {
        if (Test-Path $path) {
            try {
                $deviceUid = (Get-ItemProperty -Path $path -Name 'DeviceID' -ErrorAction SilentlyContinue).DeviceID
                if ($deviceUid) {
                    Write-Log "Found Datto RMM Device UID: $deviceUid" -Level Success
                    return $deviceUid
                }
            } catch {
                # Continue to next path
            }
        }
    }

    if ($env:CS_DEVICE_UID) {
        Write-Log "Found Device UID from environment: $env:CS_DEVICE_UID" -Level Success
        return $env:CS_DEVICE_UID
    }

    Write-Log "Datto RMM agent not detected, using hostname: $env:COMPUTERNAME" -Level Warning
    return $env:COMPUTERNAME
}

function Get-DattoRmmContext {
    $rmmContext = @{
        AccountUid     = $env:CS_ACCOUNT_UID
        SiteUid        = $env:CS_PROFILE_UID
        SiteName       = $env:CS_PROFILE_NAME
        SiteDesc       = $env:CS_PROFILE_DESC
        Domain         = $env:CS_DOMAIN
        CcHost         = $env:CS_CC_HOST
        WsAddress      = $env:CS_WS_ADDRESS
        CsmAddress     = $env:CS_CSM_ADDRESS
        ProxyEnabled   = ($env:CS_PROFILE_PROXY_TYPE -eq '1')
        IsRmmManaged   = [bool]$env:CS_ACCOUNT_UID
    }

    $udfs = @{}
    for ($i = 1; $i -le 30; $i++) {
        $udfValue = [Environment]::GetEnvironmentVariable("UDF_$i")
        if ($udfValue) {
            $udfs["UDF_$i"] = $udfValue
        }
    }
    if ($udfs.Count -gt 0) {
        $rmmContext.UserDefinedFields = $udfs
    }

    return $rmmContext
}

# ============================================================================
# HELPERS
# ============================================================================

function Get-SystemContext {
    Write-Log "Collecting system context..."

    $os   = Get-CimInstance -ClassName Win32_OperatingSystem
    $cs   = Get-CimInstance -ClassName Win32_ComputerSystem
    $bios = Get-CimInstance -ClassName Win32_BIOS

    $disks = Get-CimInstance -ClassName Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
        @{
            Drive       = $_.DeviceID
            SizeGB      = [math]::Round($_.Size / 1GB, 2)
            FreeGB      = [math]::Round($_.FreeSpace / 1GB, 2)
            PercentFree = [math]::Round(($_.FreeSpace / $_.Size) * 100, 1)
        }
    }

    $memory = @{
        TotalGB     = [math]::Round($cs.TotalPhysicalMemory / 1GB, 2)
        AvailableGB = [math]::Round(($os.FreePhysicalMemory * 1KB) / 1GB, 2)
    }
    $memory.UsedPercent = [math]::Round((1 - ($memory.AvailableGB / $memory.TotalGB)) * 100, 1)

    $uptime = (Get-Date) - $os.LastBootUpTime

    $pendingReboot = $false
    $rebootReasons = @()
    if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired') {
        $pendingReboot = $true
        $rebootReasons += 'Windows Update'
    }
    if (Test-Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending') {
        $pendingReboot = $true
        $rebootReasons += 'Component Servicing'
    }
    if (Test-Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations') {
        $pendingReboot = $true
        $rebootReasons += 'File Rename Operations'
    }

    $recentUpdates = @()
    try {
        $session  = New-Object -ComObject Microsoft.Update.Session
        $searcher = $session.CreateUpdateSearcher()
        $history  = $searcher.GetTotalHistoryCount()
        if ($history -gt 0) {
            $recentUpdates = $searcher.QueryHistory(0, [Math]::Min(10, $history)) |
                Where-Object { $_.Date -gt (Get-Date).AddDays(-7) } |
                ForEach-Object {
                    @{
                        Title  = $_.Title
                        Date   = $_.Date.ToString('yyyy-MM-dd HH:mm:ss')
                        Result = switch ($_.ResultCode) {
                            1 { 'In Progress' }
                            2 { 'Succeeded' }
                            3 { 'Succeeded With Errors' }
                            4 { 'Failed' }
                            5 { 'Aborted' }
                            default { 'Unknown' }
                        }
                    }
                }
        }
    } catch {
        Write-Log "Could not retrieve Windows Update history: $_" -Level Warning
    }

    $context = @{
        ComputerName   = $env:COMPUTERNAME
        Domain         = $cs.Domain
        Workgroup      = if ($cs.PartOfDomain) { $null } else { $cs.Workgroup }
        IsDomainJoined = $cs.PartOfDomain
        OS             = @{
            Caption      = $os.Caption
            Version      = $os.Version
            Build        = $os.BuildNumber
            Architecture = $os.OSArchitecture
            InstallDate  = $os.InstallDate.ToString('yyyy-MM-dd')
        }
        Hardware       = @{
            Manufacturer = $cs.Manufacturer
            Model        = $cs.Model
            SerialNumber = $bios.SerialNumber
            BIOSVersion  = $bios.SMBIOSBIOSVersion
        }
        Memory         = $memory
        Disks          = $disks
        Uptime         = @{
            Days    = [math]::Floor($uptime.TotalDays)
            Hours   = $uptime.Hours
            Minutes = $uptime.Minutes
            Total   = $uptime.ToString('d\.hh\:mm\:ss')
        }
        LastBoot       = $os.LastBootUpTime.ToString('yyyy-MM-dd HH:mm:ss')
        PendingReboot  = $pendingReboot
        RebootReasons  = $rebootReasons
        RecentUpdates  = $recentUpdates
        CurrentUser    = $env:USERNAME
        TimeZone       = (Get-TimeZone).Id
        CollectedAt    = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
        CollectedAtUtc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ssZ')
    }

    if ($IncludeInstalledSoftware) {
        Write-Log "Collecting installed software list..."
        $software = @()
        $regPaths = @(
            'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
            'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
        )
        foreach ($path in $regPaths) {
            $software += Get-ItemProperty -Path $path -ErrorAction SilentlyContinue |
                Where-Object { $_.DisplayName -and $_.DisplayName -notmatch '^(Update|Hotfix|Security Update)' } |
                Select-Object DisplayName, DisplayVersion, Publisher, InstallDate |
                ForEach-Object {
                    @{
                        Name        = $_.DisplayName
                        Version     = $_.DisplayVersion
                        Publisher   = $_.Publisher
                        InstallDate = $_.InstallDate
                    }
                }
        }
        $context.InstalledSoftware = $software | Sort-Object { $_.Name } -Unique
    }

    if ($IncludeRunningProcesses) {
        Write-Log "Collecting running processes..."
        $context.RunningProcesses = Get-Process |
            Group-Object ProcessName |
            Where-Object { $_.Count -gt 0 } |
            ForEach-Object {
                @{
                    Name       = $_.Name
                    Count      = $_.Count
                    TotalMemMB = [math]::Round(($_.Group | Measure-Object WorkingSet64 -Sum).Sum / 1MB, 2)
                }
            } |
            Sort-Object { $_.TotalMemMB } -Descending |
            Select-Object -First 50
    }

    if ($IncludeNetworkConfig) {
        Write-Log "Collecting network configuration..."
        $context.NetworkAdapters = Get-NetAdapter | Where-Object { $_.Status -eq 'Up' } | ForEach-Object {
            $ipConfig = Get-NetIPAddress -InterfaceIndex $_.ifIndex -ErrorAction SilentlyContinue
            @{
                Name        = $_.Name
                Description = $_.InterfaceDescription
                MacAddress  = $_.MacAddress
                Speed       = "$([math]::Round($_.LinkSpeed / 1000000))Mbps"
                IPv4        = ($ipConfig | Where-Object { $_.AddressFamily -eq 'IPv4' }).IPAddress
                IPv6        = ($ipConfig | Where-Object { $_.AddressFamily -eq 'IPv6' }).IPAddress | Select-Object -First 1
            }
        }
        $context.DNSServers = (Get-DnsClientServerAddress -AddressFamily IPv4 |
            Where-Object { $_.ServerAddresses } |
            Select-Object -ExpandProperty ServerAddresses -Unique)
    }

    return $context
}

function Get-FilteredEvents {
    param(
        [string]$LogName,
        [datetime]$StartTime,
        [int]$MaxEvents
    )

    Write-Log "Collecting events from '$LogName' log..."

    $events = @()
    $criticalIds = $CriticalEventIds[$LogName]

    try {
        $filterHash = @{
            LogName   = $LogName
            StartTime = $StartTime
        }

        $rawEvents = Get-WinEvent -FilterHashtable $filterHash -MaxEvents $MaxEvents -ErrorAction SilentlyContinue

        if (-not $rawEvents) {
            Write-Log "No events found in '$LogName' log for the specified time period" -Level Warning
            return @()
        }

        Write-Log "Processing $($rawEvents.Count) raw events from '$LogName'..."

        foreach ($event in $rawEvents) {
            $isNoise = $false
            if ($NoiseFilter.ContainsKey($event.ProviderName)) {
                if ($NoiseFilter[$event.ProviderName] -contains $event.Id) {
                    $isNoise = $true
                }
            }
            if ($isNoise) { continue }

            $isCriticalEvent = $criticalIds -and ($criticalIds -contains $event.Id)
            $levelValue = switch ($event.LevelDisplayName) {
                'Critical'    { 1 }
                'Error'       { 2 }
                'Warning'     { 3 }
                'Information' { 4 }
                'Verbose'     { 5 }
                default       { 4 }
            }

            if ($isCriticalEvent -or $levelValue -le $MinLevelValue) {
                $events += @{
                    TimeCreated  = $event.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
                    LogName      = $LogName
                    EventId      = $event.Id
                    Level        = $event.LevelDisplayName
                    Source       = $event.ProviderName
                    Message      = $event.Message
                    TaskCategory = $event.TaskDisplayName
                    IsCritical   = $isCriticalEvent
                }
            }
        }

        Write-Log "Filtered to $($events.Count) relevant events from '$LogName'" -Level Success
    } catch {
        Write-Log "Error reading '$LogName' log: $_" -Level Error
    }

    return $events
}

function Get-EventSummary {
    param([array]$Events)

    $summary = @{
        TotalEvents    = $Events.Count
        ByLevel        = @{}
        ByLog          = @{}
        CriticalEvents = ($Events | Where-Object { $_.IsCritical }).Count
        TopEventIds    = @()
        TimeRange      = @{
            Earliest = $null
            Latest   = $null
        }
    }

    $Events | Group-Object Level | ForEach-Object {
        $summary.ByLevel[$_.Name] = $_.Count
    }

    $Events | Group-Object LogName | ForEach-Object {
        $summary.ByLog[$_.Name] = $_.Count
    }

    $summary.TopEventIds = $Events |
        Group-Object EventId, LogName, Level, Source |
        Sort-Object Count -Descending |
        Select-Object -First 10 |
        ForEach-Object {
            $parts  = $_.Name -split ', '
            $sample = $_.Group | Select-Object -First 1
            @{
                EventId = [int]$parts[0]
                LogName = $parts[1]
                Level   = $parts[2]
                Source  = $parts[3]
                Count   = $_.Count
                Sample  = if ($sample.Message.Length -gt 200) {
                    $sample.Message.Substring(0, 200) + '...'
                } else {
                    $sample.Message
                }
            }
        }

    $sortedTimes = $Events | Sort-Object TimeCreated
    $summary.TimeRange.Earliest = ($sortedTimes | Select-Object -First 1).TimeCreated
    $summary.TimeRange.Latest   = ($sortedTimes | Select-Object -Last 1).TimeCreated

    return $summary
}

function Get-PresignedUrl {
    <#
    .SYNOPSIS
        Legacy n8n flow only — fetches a B2 presigned PUT URL from the n8n
        webhook. Pulse mode skips this and uses $env:UploadUrl directly.
    #>
    param(
        [string]$WebhookUrl,
        [string]$ClientId,
        [string]$DeviceUid,
        [string]$RunId
    )

    Write-Log "Requesting presigned upload URL (legacy n8n flow)..."

    $body = @{
        clientId   = $ClientId
        deviceUid  = $DeviceUid
        runId      = $RunId
        pathFormat = "flat"
    } | ConvertTo-Json

    try {
        $response = Invoke-RestMethod -Uri $WebhookUrl -Method POST -Body $body `
            -ContentType 'application/json' -UseBasicParsing -TimeoutSec 30
        if ($response.presignedUrl) {
            Write-Log "Received presigned URL for: $($response.objectKey)" -Level Success
            return $response
        } else {
            throw "Response did not contain presignedUrl"
        }
    } catch {
        Write-Log "Failed to get presigned URL: $_" -Level Error
        throw
    }
}

function Upload-ToB2 {
    param(
        [string]$PresignedUrl,
        [byte[]]$Data,
        [string]$ContentType = 'application/gzip'
    )

    Write-Log "Uploading $([math]::Round($Data.Length / 1KB, 2)) KB to B2..."

    try {
        $headers = @{ "Content-Type" = $ContentType }
        $response = Invoke-WebRequest -Uri $PresignedUrl -Method PUT -Body $Data `
            -Headers $headers -UseBasicParsing -ErrorAction Stop
        if ($response.StatusCode -in 200, 201) {
            Write-Log "Upload successful!" -Level Success
            return $true
        } else {
            throw "Upload failed with status $($response.StatusCode)"
        }
    } catch {
        Write-Log "Upload failed: $_" -Level Error
        throw
    }
}

function Send-ProcessingNotification {
    <#
    .SYNOPSIS
        POST the upload's metadata to the receiving service. In Pulse mode
        the WebhookSecret is sent as the x-openclaw-key header.
    #>
    param(
        [string]$WebhookUrl,
        [string]$WebhookSecret,
        [string]$ObjectKey,
        [string]$ClientId,
        [string]$DeviceUid,
        [string]$ComputerName,
        [string]$RunId,
        [hashtable]$Summary,
        [hashtable]$RmmContext,
        [string]$IssueDescription,
        [string]$TicketNumber
    )

    Write-Log "Sending processing notification..."

    $body = @{
        objectKey        = $ObjectKey
        clientId         = $ClientId
        deviceUid        = $DeviceUid
        computerName     = $ComputerName
        runId            = $RunId
        collectedAt      = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ')
        summary          = @{
            totalEvents    = $Summary.TotalEvents
            criticalEvents = $Summary.CriticalEvents
            errorCount     = $Summary.ByLevel['Error']
            warningCount   = $Summary.ByLevel['Warning']
            timeRange      = "$($Summary.TimeRange.Earliest) to $($Summary.TimeRange.Latest)"
        }
        rmmContext       = @{
            siteName   = $RmmContext.SiteName
            siteUid    = $RmmContext.SiteUid
            accountUid = $RmmContext.AccountUid
        }
        issueDescription = $IssueDescription
        ticketNumber     = $TicketNumber
    } | ConvertTo-Json -Depth 10

    $headers = @{}
    if ($WebhookSecret) {
        $headers['x-openclaw-key'] = $WebhookSecret
    }

    try {
        $null = Invoke-RestMethod -Uri $WebhookUrl -Method POST -Body $body `
            -ContentType 'application/json' -Headers $headers `
            -UseBasicParsing -TimeoutSec 60
        Write-Log "Processing notification sent" -Level Success
        return $true
    } catch {
        Write-Log "Failed to send processing notification: $_" -Level Warning
        # Don't throw — upload succeeded; notification is secondary.
        return $false
    }
}

function Compress-Data {
    param([string]$JsonString)

    $bytes        = [System.Text.Encoding]::UTF8.GetBytes($JsonString)
    $memoryStream = New-Object System.IO.MemoryStream
    $gzipStream   = New-Object System.IO.Compression.GZipStream(
        $memoryStream,
        [System.IO.Compression.CompressionMode]::Compress
    )

    $gzipStream.Write($bytes, 0, $bytes.Length)
    $gzipStream.Close()

    $compressedData = $memoryStream.ToArray()
    $memoryStream.Close()

    $ratio = [math]::Round((1 - ($compressedData.Length / $bytes.Length)) * 100, 1)
    Write-Log "Compressed from $([math]::Round($bytes.Length / 1KB, 2)) KB to $([math]::Round($compressedData.Length / 1KB, 2)) KB ($ratio% reduction)"

    return $compressedData
}

# ============================================================================
# PULSE-MODE OVERRIDES (read Datto component variables)
# ============================================================================

# When the Quick Job is dispatched from Pulse, these arrive as env vars.
# Empty in the legacy n8n flow.
$PulseRunId         = $env:RunId
$PulseUploadUrl     = $env:UploadUrl
$PulseObjectKey     = $env:ObjectKey
$PulseWebhookUrl    = $env:WebhookUrl
$PulseWebhookSecret = $env:WebhookSecret
$PulseClientId      = $env:ClientId

$IsPulseMode = [bool]($PulseWebhookUrl -and $PulseUploadUrl -and $PulseObjectKey)

# ============================================================================
# RESOLVE PARAMETERS FROM DATTO RMM ENVIRONMENT
# ============================================================================

$DeviceUid = Get-DattoDeviceUid

# ClientId: explicit param > Pulse env > Datto site UID env > error
if (-not $ClientId) {
    if ($PulseClientId) {
        $ClientId = $PulseClientId
    } elseif ($env:CS_PROFILE_UID) {
        $ClientId = $env:CS_PROFILE_UID
    } else {
        Write-Error "ClientId not provided and CS_PROFILE_UID not available. Please provide a ClientId."
        exit 1
    }
}

# ============================================================================
# CONFIGURATION
# ============================================================================

$ErrorActionPreference = 'Continue'
$ProgressPreference = 'SilentlyContinue'

# Force TLS 1.2 (and 1.3 when available). Without this, Invoke-WebRequest on
# older Windows / PS 5.1 falls back to TLS 1.0/1.1, which Backblaze B2's S3
# endpoint rejects with "Could not create SSL/TLS secure channel."
try {
    [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
    if ([Net.SecurityProtocolType].GetEnumNames() -contains 'Tls13') {
        [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls13
    }
} catch {
    Write-Log "Could not set TLS 1.2/1.3 protocol — uploads may fail on older systems: $_" -Level Warning
}

$LevelMap = @{
    'Critical'    = 1
    'Error'       = 2
    'Warning'     = 3
    'Information' = 4
    'Verbose'     = 5
}
$MinLevelValue = $LevelMap[$MinLevel]

$NoiseFilter = @{
    'Microsoft-Windows-Security-SPP' = @(903, 904, 905)
    'Microsoft-Windows-Time-Service' = @(35, 37)
    'VSS'                            = @(8224)
}

$CriticalEventIds = @{
    'System' = @(
        1001,   # Bugcheck (BSOD)
        6008,   # Unexpected shutdown
        7031,   # Service crash
        7034,   # Service terminated unexpectedly
        41,     # Kernel power (unexpected restart)
        1074,   # Shutdown initiated
        6005,   # Event log started (boot)
        6006,   # Event log stopped (shutdown)
        7045,   # New service installed
        10016   # DCOM permission error
    )
    'Application' = @(
        1000,   # Application crash
        1001,   # Windows Error Reporting
        1002,   # Application hang
        1026,   # .NET Runtime error
        11707,  # Install completed
        11708,  # Install failed
        11724   # Uninstall completed
    )
    'Security' = @(
        4624,   # Successful logon
        4625,   # Failed logon
        4648,   # Explicit credential logon
        4672,   # Special privileges assigned
        4720,   # User account created
        4726,   # User account deleted
        4732,   # Member added to security group
        4756,   # Member added to universal group
        1102    # Audit log cleared
    )
}

# ============================================================================
# MAIN EXECUTION
# ============================================================================

$startTime = Get-Date
Write-Host ""
Write-Host "=================================================================" -ForegroundColor Cyan
Write-Host "       Windows Event Log Collector for LLM Analysis" -ForegroundColor Cyan
Write-Host "                    Datto RMM Component" -ForegroundColor Cyan
Write-Host "=================================================================" -ForegroundColor Cyan
Write-Host ""

if ($IsPulseMode) {
    Write-Log "Mode: Pulse-driven dispatch" -Level Success
} else {
    Write-Log "Mode: legacy (n8n / standalone) flow" -Level Info
}
Write-Log "Starting collection on $env:COMPUTERNAME"
Write-Log "Client ID: $ClientId"
Write-Log "Device UID: $DeviceUid"
Write-Log "Time Range: Last $HoursBack hours | Min Level: $MinLevel"

if ($IssueDescription) {
    Write-Log "Issue: $IssueDescription"
}

$rmmContext = Get-DattoRmmContext
if ($rmmContext.IsRmmManaged) {
    Write-Log "RMM Site: $($rmmContext.SiteName) ($($rmmContext.SiteUid))" -Level Info
}

# RunId: Pulse-supplied wins so the receiver can correlate to the dispatched
# execution row. Otherwise generate a timestamp-shaped one for n8n compat.
$runId = if ($PulseRunId) { $PulseRunId } else { "eventlogs_$(Get-Date -Format 'yyyyMMdd_HHmmss')" }
$eventStartTime = (Get-Date).AddHours(-$HoursBack)

$systemContext = Get-SystemContext

$allEvents = @()
foreach ($logName in $IncludeLogs) {
    $events = Get-FilteredEvents -LogName $logName -StartTime $eventStartTime -MaxEvents $MaxEventsPerLog
    $allEvents += $events
}
$allEvents = $allEvents | Sort-Object { [datetime]$_.TimeCreated } -Descending

$summary = Get-EventSummary -Events $allEvents

$payload = @{
    metadata = @{
        version          = "3.0.1"
        clientId         = $ClientId
        deviceUid        = $DeviceUid
        computerName     = $env:COMPUTERNAME
        runId            = $runId
        collectedAt      = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
        collectedAtUtc   = (Get-Date).ToUniversalTime().ToString('yyyy-MM-dd HH:mm:ssZ')
        collectionParams = @{
            hoursBack    = $HoursBack
            minLevel     = $MinLevel
            includedLogs = $IncludeLogs
            maxEvents    = $MaxEventsPerLog
        }
        issueDescription = $IssueDescription
        ticketNumber     = $TicketNumber
        transport        = if ($IsPulseMode) { 'pulse' } else { 'n8n_legacy' }
    }
    rmmContext    = $rmmContext
    systemContext = $systemContext
    summary       = $summary
    events        = $allEvents
    analysisHints = @{
        focus = @(
            "Look for patterns in Critical and Error events"
            "Check for service crashes (Event ID 7031, 7034)"
            "Look for unexpected shutdowns (Event ID 41, 6008)"
            "Check application crashes (Event ID 1000, 1001)"
            "Review any security anomalies (failed logins, privilege escalation)"
        )
        correlate = @(
            "Events occurring within seconds of each other may be related"
            "Service failures often trigger multiple cascading events"
            "Look for patterns before and after the issue timestamp"
        )
        context = @(
            "System uptime and last boot time"
            "Pending reboots may indicate incomplete updates"
            "Recent Windows Updates may correlate with new issues"
            "Low disk space or memory can cause various failures"
        )
    }
}

$jsonPayload = $payload | ConvertTo-Json -Depth 20 -Compress

# ----- Summary ---------------------------------------------------------------

Write-Host ""
Write-Host "-----------------------------------------------------------------" -ForegroundColor Yellow
Write-Host "                       COLLECTION SUMMARY" -ForegroundColor Yellow
Write-Host "-----------------------------------------------------------------" -ForegroundColor Yellow
Write-Host ""
Write-Host "  Device:       $env:COMPUTERNAME" -ForegroundColor White
Write-Host "  Device UID:   $DeviceUid" -ForegroundColor Gray
Write-Host "  Client ID:    $ClientId" -ForegroundColor Gray
Write-Host "  Run ID:       $runId" -ForegroundColor Gray
Write-Host ""
Write-Host "  Total Events Collected: $($summary.TotalEvents)" -ForegroundColor White
Write-Host ""

Write-Host "  By Level:" -ForegroundColor Gray
$summary.ByLevel.GetEnumerator() | Sort-Object {
    switch ($_.Key) { 'Critical' { 1 } 'Error' { 2 } 'Warning' { 3 } 'Information' { 4 } default { 5 } }
} | ForEach-Object {
    $color = switch ($_.Key) {
        'Critical'    { 'Magenta' }
        'Error'       { 'Red' }
        'Warning'     { 'Yellow' }
        'Information' { 'Cyan' }
        default       { 'Gray' }
    }
    Write-Host "    $($_.Key): $($_.Value)" -ForegroundColor $color
}

Write-Host ""
Write-Host "  By Log:" -ForegroundColor Gray
$summary.ByLog.GetEnumerator() | ForEach-Object {
    Write-Host "    $($_.Key): $($_.Value)" -ForegroundColor White
}

Write-Host ""
Write-Host "  Critical Events (always captured): $($summary.CriticalEvents)" -ForegroundColor $(if ($summary.CriticalEvents -gt 0) { 'Red' } else { 'Green' })
Write-Host "  Time Range: $($summary.TimeRange.Earliest) to $($summary.TimeRange.Latest)" -ForegroundColor Gray
Write-Host ""

if ($summary.TopEventIds.Count -gt 0) {
    Write-Host "  Top Event Types:" -ForegroundColor Gray
    $summary.TopEventIds | Select-Object -First 5 | ForEach-Object {
        $levelColor = switch ($_.Level) {
            'Critical' { 'Magenta' }
            'Error'    { 'Red' }
            'Warning'  { 'Yellow' }
            default    { 'White' }
        }
        Write-Host "    [$($_.LogName)] ID $($_.EventId): $($_.Count) occurrences" -ForegroundColor $levelColor -NoNewline
        Write-Host " ($($_.Source))" -ForegroundColor Gray
    }
    Write-Host ""
}

Write-Host "  System Health:" -ForegroundColor Gray
Write-Host "    Memory Used: $($systemContext.Memory.UsedPercent)%" -ForegroundColor $(if ($systemContext.Memory.UsedPercent -gt 90) { 'Red' } elseif ($systemContext.Memory.UsedPercent -gt 75) { 'Yellow' } else { 'Green' })
Write-Host "    Uptime: $($systemContext.Uptime.Total)" -ForegroundColor White
Write-Host "    Pending Reboot: $(if ($systemContext.PendingReboot) { 'Yes - ' + ($systemContext.RebootReasons -join ', ') } else { 'No' })" -ForegroundColor $(if ($systemContext.PendingReboot) { 'Yellow' } else { 'Green' })

foreach ($disk in $systemContext.Disks) {
    $diskColor = if ($disk.PercentFree -lt 10) { 'Red' } elseif ($disk.PercentFree -lt 20) { 'Yellow' } else { 'Green' }
    Write-Host "    Disk $($disk.Drive): $($disk.FreeGB) GB free ($($disk.PercentFree)%)" -ForegroundColor $diskColor
}

Write-Host ""
Write-Host "-----------------------------------------------------------------" -ForegroundColor Yellow

# ----- Upload ----------------------------------------------------------------

if ($DryRun) {
    Write-Host ""
    Write-Log "DRY RUN - Skipping upload" -Level Warning
    Write-Host ""
    Write-Host "Payload size: $([math]::Round($jsonPayload.Length / 1KB, 2)) KB (uncompressed)" -ForegroundColor Gray

    if (-not (Test-Path $OutputPath)) {
        New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
    }
    $localPath = Join-Path $OutputPath "EventLogExport_$runId.json"
    $jsonPayload | Out-File -FilePath $localPath -Encoding UTF8
    Write-Log "Saved to: $localPath" -Level Success

} else {
    Write-Host ""

    $compressedData = Compress-Data -JsonString $jsonPayload

    try {
        if ($IsPulseMode) {
            # Pulse mode: PUT URL + object key were pre-presigned at dispatch.
            Write-Log "Uploading to Pulse-supplied URL: $PulseObjectKey" -Level Success
            $urlResponse = @{
                presignedUrl = $PulseUploadUrl
                objectKey    = $PulseObjectKey
            }
        } else {
            # Legacy n8n flow: fetch a presigned URL from the n8n webhook.
            $urlResponse = Get-PresignedUrl -WebhookUrl $WebhookUrl `
                -ClientId $ClientId -DeviceUid $DeviceUid -RunId $runId
        }

        Upload-ToB2 -PresignedUrl $urlResponse.presignedUrl -Data $compressedData

        Write-Host ""
        Write-Host "=================================================================" -ForegroundColor Green
        Write-Host "                       UPLOAD COMPLETE" -ForegroundColor Green
        Write-Host "=================================================================" -ForegroundColor Green
        Write-Host ""
        Write-Host "  Object Key: $($urlResponse.objectKey)" -ForegroundColor White
        Write-Host "  Events:     $($summary.TotalEvents)" -ForegroundColor White
        Write-Host "  Size:       $([math]::Round($compressedData.Length / 1KB, 2)) KB (compressed)" -ForegroundColor White
        Write-Host ""

        # Pick the metadata endpoint + auth secret based on mode.
        $notifyUrl    = if ($IsPulseMode) { $PulseWebhookUrl }    else { $NotifyWebhookUrl }
        $notifySecret = if ($IsPulseMode) { $PulseWebhookSecret } else { '' }

        Send-ProcessingNotification `
            -WebhookUrl       $notifyUrl `
            -WebhookSecret    $notifySecret `
            -ObjectKey        $urlResponse.objectKey `
            -ClientId         $ClientId `
            -DeviceUid        $DeviceUid `
            -ComputerName     $env:COMPUTERNAME `
            -RunId            $runId `
            -Summary          $summary `
            -RmmContext       $rmmContext `
            -IssueDescription $IssueDescription `
            -TicketNumber     $TicketNumber

    } catch {
        Write-Host ""
        Write-Log "Failed to upload event logs: $_" -Level Error

        if (-not (Test-Path $OutputPath)) {
            New-Item -ItemType Directory -Path $OutputPath -Force | Out-Null
        }
        $localPath = Join-Path $OutputPath "EventLogExport_$runId.json.gz"
        [System.IO.File]::WriteAllBytes($localPath, $compressedData)
        Write-Log "Saved locally as fallback: $localPath" -Level Warning

        exit 1
    }
}

$duration = (Get-Date) - $startTime
Write-Log "Collection completed in $($duration.TotalSeconds.ToString('F1')) seconds"

return @{
    Success       = $true
    Mode          = if ($IsPulseMode) { 'pulse' } else { 'n8n_legacy' }
    RunId         = $runId
    DeviceUid     = $DeviceUid
    ClientId      = $ClientId
    EventCount    = $summary.TotalEvents
    CriticalCount = $summary.CriticalEvents
    ErrorCount    = $summary.ByLevel['Error']
    WarningCount  = $summary.ByLevel['Warning']
}
