[CmdletBinding()]
param(
    [string]$ConfigPath = 'C:\ProgramData\AWatch-rus\deployment-config.json'
)

Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'

$modulePath = Join-Path $PSScriptRoot 'ActivityWatch.Windows.Common.psm1'
Import-Module $modulePath -Force

$config = Read-ActivityWatchDeploymentConfig -Path $ConfigPath
$installRoot = [string]$config.paths.installRoot
$stateRoot = [string]$config.paths.stateRoot
$collectorScript = [string]$config.paths.collectorScript
$endpointCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'endpointCollectorScript') { [string]$config.paths.endpointCollectorScript } else { Join-Path $stateRoot 'dlp-endpoint-signals-collector.ps1' }
$fileCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'fileCollectorScript') { [string]$config.paths.fileCollectorScript } else { Join-Path $stateRoot 'file-operations-collector.ps1' }
$sessionCollectorScript = if ($config.paths.PSObject.Properties.Name -contains 'sessionCollectorScript') { [string]$config.paths.sessionCollectorScript } else { Join-Path $stateRoot 'worktime-session-collector.ps1' }
$telemetryExecutable = if ($config.paths.PSObject.Properties.Name -contains 'file1cTelemetryExecutable' -and -not [string]::IsNullOrWhiteSpace([string]$config.paths.file1cTelemetryExecutable)) { [string]$config.paths.file1cTelemetryExecutable } else { Join-Path (Join-Path ([System.Environment]::GetFolderPath('ProgramFiles')) 'AWatch-rus\windows') 'aw-windows-telemetry.exe' }
$evtxExportScript = if ($config.paths.PSObject.Properties.Name -contains 'evtxExportScript') { [string]$config.paths.evtxExportScript } else { Join-Path $stateRoot 'export-evtx-for-hayabusa.ps1' }
$rulesPath = [string]$config.paths.rulesPath
$policyPath = if ($config.paths.PSObject.Properties.Name -contains 'policyPath') { [string]$config.paths.policyPath } else { Join-Path $stateRoot 'dlp-policy.json' }
$policyClientScript = if ($config.paths.PSObject.Properties.Name -contains 'policyClientScript') { [string]$config.paths.policyClientScript } else { Join-Path $stateRoot 'dlp-policy-client.ps1' }
$launchScript = [string]$config.paths.launchScript
$recoveryScript = [string]$config.paths.recoveryScript
$awHostname = if ($config.PSObject.Properties.Name -contains 'awHostname' -and -not [string]::IsNullOrWhiteSpace([string]$config.awHostname)) { [string]$config.awHostname } else { [string]$env:COMPUTERNAME }
$serverUrl = '{0}://{1}:{2}' -f [string]$config.server.scheme, [string]$config.server.host, [int]$config.server.port
$apiBase = "$serverUrl/api/0"
$pollSeconds = if ($config.PSObject.Properties.Name -contains 'collector' -and $config.collector.PSObject.Properties.Name -contains 'pollSeconds') { [int]$config.collector.pollSeconds } else { 5 }
$pulseSeconds = if ($config.PSObject.Properties.Name -contains 'collector' -and $config.collector.PSObject.Properties.Name -contains 'pulseSeconds') { [int]$config.collector.pulseSeconds } else { [Math]::Max($pollSeconds * 3, 30) }
$freshnessSeconds = [Math]::Max($pollSeconds * 3, 30)
$sessionFreshnessSeconds = [Math]::Max($pollSeconds * 4, 45)
$transportStaleSeconds = [Math]::Max($pollSeconds * 12, 180)
$endpointFreshnessSeconds = [Math]::Max($transportStaleSeconds, 300)
$queueMaxDepth = 1000

$afkExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'afkEnabled') { [bool]$config.collectors.afkEnabled } else { $true }
$windowExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'windowEnabled') { [bool]$config.collectors.windowEnabled } else { $true }
$fileOpsExpected = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsEnabled') { [bool]$config.collectors.fileOpsEnabled } else { $true }
$browserCollectorMode = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'browserCollectorMode') { [string]$config.collectors.browserCollectorMode } else { 'rust_primary' }
$dlpEndpointMode = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'dlpEndpointMode') { [string]$config.collectors.dlpEndpointMode } else { 'rust_primary' }
$fileOpsMode = if ($config.PSObject.Properties.Name -contains 'collectors' -and $config.collectors.PSObject.Properties.Name -contains 'fileOpsMode') { [string]$config.collectors.fileOpsMode } else { 'rust_primary' }
$sessionEventsConfig = if ($config.PSObject.Properties.Name -contains 'sessionEvents') { $config.sessionEvents } else { $null }
$sessionLogonEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'logonEnabled') { [bool]$sessionEventsConfig.logonEnabled } else { $false }
$sessionProcessEventsEnabled = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'processEventsEnabled') { [bool]$sessionEventsConfig.processEventsEnabled } else { $false }
$sessionEventsBucketId = if ($sessionEventsConfig -and $sessionEventsConfig.PSObject.Properties.Name -contains 'bucketPrefix' -and -not [string]::IsNullOrWhiteSpace([string]$sessionEventsConfig.bucketPrefix)) {
    ('{0}_{1}' -f [string]$sessionEventsConfig.bucketPrefix, $awHostname)
}
else {
    'aw-session-events_' + $awHostname
}

function Get-LoggedOnUsers {
    param(
        [bool]$IncludeDisconnected = $false
    )

    $users = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    $activeStates = @('Active', 'Активно')
    $inactiveStates = @('Disc', 'Disconnected', 'Idle', 'Listen', 'Диск', 'Откл', 'Отключен')
    try {
        $lines = & quser.exe 2>$null
        foreach ($line in @($lines)) {
            $normalized = [string]$line
            if ([string]::IsNullOrWhiteSpace($normalized)) { continue }
            $normalized = $normalized.TrimStart(' ', '>')
            if ([string]::IsNullOrWhiteSpace($normalized)) { continue }
            if ($normalized -match '^(USERNAME|ПОЛЬЗОВАТЕЛЬ)\s+') { continue }
            $parts = $normalized -split '\s+'
            if ($parts.Count -lt 1) { continue }
            $user = [string]$parts[0]
            if ([string]::IsNullOrWhiteSpace($user)) { continue }
            $state = $null
            foreach ($part in @($parts | Select-Object -Skip 1)) {
                $token = [string]$part
                if ([string]::IsNullOrWhiteSpace($token)) { continue }
                if ($activeStates -contains $token -or $inactiveStates -contains $token) {
                    $state = $token
                    break
                }
            }
            if ($null -ne $state -and $activeStates -notcontains $state) {
                if (-not $IncludeDisconnected -or $inactiveStates -notcontains $state) {
                    continue
                }
            }
            [void]$users.Add($user)
            [void]$users.Add(('{0}\{1}' -f $env:COMPUTERNAME, $user))
            if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
                [void]$users.Add(('{0}\{1}' -f $env:USERDOMAIN, $user))
            }
        }
    }
    catch {
    }
    return @($users)
}

function Test-UserHasSession {
    param(
        [string]$UserId,
        [string[]]$LoggedOnUsers
    )

    if ([string]::IsNullOrWhiteSpace($UserId)) { return $false }
    $candidateIds = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
    [void]$candidateIds.Add($UserId)
    $leafUser = $UserId
    if ($leafUser -match '^[^\\]+\\(.+)$') {
        $leafUser = $Matches[1]
        [void]$candidateIds.Add($leafUser)
    }
    [void]$candidateIds.Add(('{0}\{1}' -f $env:COMPUTERNAME, $leafUser))
    if (-not [string]::IsNullOrWhiteSpace($env:USERDOMAIN)) {
        [void]$candidateIds.Add(('{0}\{1}' -f $env:USERDOMAIN, $leafUser))
    }
    foreach ($candidate in @($candidateIds)) {
        if ($LoggedOnUsers -contains $candidate) { return $true }
    }
    return $false
}

function Get-CollectorProcesses {
    param(
        [Parameter(Mandatory = $true)]
        [AllowEmptyString()]
        [string]$ScriptPath
    )

    if ([string]::IsNullOrWhiteSpace($ScriptPath)) {
        return @()
    }

    return @(
        Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
            Where-Object {
                ($_.Name -ieq 'powershell.exe' -or $_.Name -ieq 'pwsh.exe') -and
                $_.CommandLine -and
                $_.CommandLine -match [Regex]::Escape($ScriptPath)
            } |
            Select-Object @{ Name = 'Name'; Expression = { $_.Name } }, @{ Name = 'Id'; Expression = { [int]$_.ProcessId } }, @{ Name = 'SessionId'; Expression = { [int]$_.SessionId } }, @{ Name = 'CommandLine'; Expression = { [string]$_.CommandLine } }
    )
}

function Get-DuplicateProcessGroups {
    param(
        [object[]]$Processes,
        [bool]$PerSession = $true
    )

    if (-not $Processes -or @($Processes).Count -eq 0) { return @() }
    $groups = if ($PerSession) {
        $Processes | Group-Object -Property Name, SessionId
    }
    else {
        $Processes | Group-Object -Property Name
    }

    return @(
        $groups |
            Where-Object { $_.Count -gt 1 } |
            ForEach-Object {
                [pscustomobject]@{
                    name = [string]$_.Name
                    count = [int]$_.Count
                    members = @($_.Group | Select-Object Name, Id, SessionId, CommandLine)
                }
            }
    )
}

function Convert-ToUtcDate {
    param($Value)

    if ($null -eq $Value) { return $null }
    try {
        return ([DateTimeOffset]::Parse([string]$Value)).UtcDateTime
    }
    catch {
        return $null
    }
}

function Get-BucketHealth {
    param(
        [Parameter(Mandatory = $true)]
        [string]$BucketId,
        [Parameter(Mandatory = $true)]
        [int]$MaxAgeSeconds,
        [bool]$Required = $true,
        [bool]$RequireFreshEvent = $true
    )

    $events = @()
    $queryOk = $false
    $errorMessage = $null
    try {
        $response = Invoke-RestMethod -Method Get -Uri "$apiBase/buckets/$BucketId/events?limit=25" -TimeoutSec 15 -DisableKeepAlive -ErrorAction Stop
        $events = @($response)
        $queryOk = $true
    }
    catch {
        $errorMessage = $_.Exception.Message
    }

    $latestTimestampUtc = $null
    $ageSeconds = $null
    if ($events.Count -gt 0) {
        $latestTimestampUtc = @(
            $events |
                ForEach-Object { Convert-ToUtcDate $_.timestamp } |
                Where-Object { $null -ne $_ } |
                Sort-Object -Descending
        ) | Select-Object -First 1
        if ($null -ne $latestTimestampUtc) {
            $ageSeconds = [int][Math]::Floor(((Get-Date).ToUniversalTime() - $latestTimestampUtc).TotalSeconds)
        }
    }

    $hasFreshEvent = ($null -ne $ageSeconds -and $ageSeconds -le $MaxAgeSeconds)
    $hasAnyEvent = ($events.Count -gt 0)
    $ok = if (-not $Required) { $true } elseif ($RequireFreshEvent) { $queryOk -and $hasFreshEvent } else { $queryOk -and $hasAnyEvent }

    return [pscustomobject]@{
        bucketId = $BucketId
        required = [bool]$Required
        requireFreshEvent = [bool]$RequireFreshEvent
        maxAgeSeconds = [int]$MaxAgeSeconds
        queryOk = [bool]$queryOk
        latestTimestampUtc = if ($null -ne $latestTimestampUtc) { $latestTimestampUtc.ToString('o') } else { $null }
        ageSeconds = if ($null -ne $ageSeconds) { [int]$ageSeconds } else { $null }
        count = [int]$events.Count
        ok = [bool]$ok
        error = $errorMessage
    }
}

function Get-TransportQueueHealth {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [Parameter(Mandatory = $true)]
        [string]$QueuePath,
        [Parameter(Mandatory = $true)]
        [string]$LockPath,
        [Parameter(Mandatory = $true)]
        [int]$StaleAfterSeconds,
        [Parameter(Mandatory = $true)]
        [int]$MaxDepth,
        [int]$ActiveProcessCount = 0,
        [bool]$Required = $true
    )

    $queueExists = Test-Path -LiteralPath $QueuePath
    $depth = 0
    $sizeBytes = 0
    $ageSeconds = $null
    $lastWriteUtc = $null
    if ($queueExists) {
        $item = Get-Item -LiteralPath $QueuePath -ErrorAction SilentlyContinue
        if ($item) {
            $sizeBytes = [int64]$item.Length
            $lastWriteUtc = $item.LastWriteTimeUtc
            $ageSeconds = [int][Math]::Floor(((Get-Date).ToUniversalTime() - $lastWriteUtc).TotalSeconds)
        }
        try {
            $depth = [int]((Get-Content -LiteralPath $QueuePath -ErrorAction SilentlyContinue | Measure-Object).Count)
        }
        catch {
            $depth = 0
        }
    }

    $lockExists = Test-Path -LiteralPath $LockPath
    $lockHeld = $false
    if ($lockExists) {
        try {
            $lockHandle = [System.IO.File]::Open($LockPath, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
            $lockHandle.Dispose()
        }
        catch {
            $lockHeld = $true
        }
    }

    $staleQueue = ($depth -gt 0 -and $null -ne $ageSeconds -and $ageSeconds -gt $StaleAfterSeconds -and -not $lockHeld)
    $orphanedQueue = ($depth -gt 0 -and $ActiveProcessCount -le 0 -and $null -ne $ageSeconds -and $ageSeconds -gt $StaleAfterSeconds)
    $oversizedQueue = ($depth -gt $MaxDepth)
    $ok = if (-not $Required) { $true } else { -not ($staleQueue -or $orphanedQueue -or $oversizedQueue) }

    return [pscustomobject]@{
        name = $Name
        required = [bool]$Required
        queuePath = $QueuePath
        queueExists = [bool]$queueExists
        depth = [int]$depth
        sizeBytes = [int64]$sizeBytes
        lastWriteUtc = if ($null -ne $lastWriteUtc) { $lastWriteUtc.ToString('o') } else { $null }
        ageSeconds = if ($null -ne $ageSeconds) { [int]$ageSeconds } else { $null }
        lockPath = $LockPath
        lockExists = [bool]$lockExists
        lockHeld = [bool]$lockHeld
        activeProcessCount = [int]$ActiveProcessCount
        staleAfterSeconds = [int]$StaleAfterSeconds
        maxDepth = [int]$MaxDepth
        staleQueue = [bool]$staleQueue
        orphanedQueue = [bool]$orphanedQueue
        oversizedQueue = [bool]$oversizedQueue
        ok = [bool]$ok
    }
}

function Get-TransportQueueGroupHealth {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,
        [Parameter(Mandatory = $true)]
        [string]$StateRoot,
        [Parameter(Mandatory = $true)]
        [string]$QueuePattern,
        [Parameter(Mandatory = $true)]
        [int]$StaleAfterSeconds,
        [Parameter(Mandatory = $true)]
        [int]$MaxDepth,
        [int]$ActiveProcessCount = 0,
        [bool]$Required = $true
    )

    $queues = @(Get-ChildItem -LiteralPath $StateRoot -Filter $QueuePattern -ErrorAction SilentlyContinue | Sort-Object Name)
    if ($queues.Count -eq 0) {
        return [pscustomobject]@{
            name = $Name
            required = [bool]$Required
            queuePattern = $QueuePattern
            queueCount = 0
            queues = @()
            depth = 0
            sizeBytes = 0
            activeProcessCount = [int]$ActiveProcessCount
            staleAfterSeconds = [int]$StaleAfterSeconds
            maxDepth = [int]$MaxDepth
            ok = [bool](-not $Required)
        }
    }

    $items = @()
    foreach ($queue in $queues) {
        $lockPath = [System.IO.Path]::ChangeExtension($queue.FullName, '.lock')
        $items += Get-TransportQueueHealth -Name $queue.BaseName -QueuePath $queue.FullName -LockPath $lockPath -StaleAfterSeconds $StaleAfterSeconds -MaxDepth $MaxDepth -ActiveProcessCount $ActiveProcessCount -Required $Required
    }

    return [pscustomobject]@{
        name = $Name
        required = [bool]$Required
        queuePattern = $QueuePattern
        queueCount = [int]$items.Count
        queues = @($items)
        depth = [int](($items | Measure-Object -Property depth -Sum).Sum)
        sizeBytes = [int64](($items | Measure-Object -Property sizeBytes -Sum).Sum)
        activeProcessCount = [int]$ActiveProcessCount
        staleAfterSeconds = [int]$StaleAfterSeconds
        maxDepth = [int]$MaxDepth
        ok = [bool](-not ($items | Where-Object { -not $_.ok }))
    }
}

function Resolve-ActivityWatchLaunchTaskName {
    param(
        [Parameter(Mandatory = $true)]
        [string]$TaskName
    )

    if ($TaskName -notmatch '\[[^\]]+_Administrator\]') {
        return $TaskName
    }

    $localizedCandidate = 'ActivityWatch Launch [{0}_Администратор]' -f $awHostname
    $localizedTask = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $localizedCandidate } | Select-Object -First 1
    if ($localizedTask) {
        return $localizedCandidate
    }

    try {
        $builtinAdmin = Get-LocalUser -ErrorAction Stop |
            Where-Object { [string]$_.SID -match '-500$' } |
            Select-Object -First 1
        if ($builtinAdmin -and -not [string]::IsNullOrWhiteSpace([string]$builtinAdmin.Name)) {
            $candidate = 'ActivityWatch Launch [{0}_{1}]' -f $awHostname, [string]$builtinAdmin.Name
            $existing = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $candidate } | Select-Object -First 1
            if ($existing) {
                return $candidate
            }
        }
    }
    catch {
    }

    return $TaskName
}

function Get-TaskSnapshot {
    param(
        [Parameter(Mandatory = $true)]
        [string[]]$TaskNames
    )

    return @(
        foreach ($taskName in @($TaskNames | Sort-Object -Unique)) {
            $task = Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.TaskName -eq $taskName } | Select-Object -First 1
            if ($null -eq $task) {
                [pscustomobject]@{
                    taskName = $taskName
                    present = $false
                    enabled = $false
                    state = 'Отсутствует'
                    lastResult = $null
                    ok = $false
                }
                continue
            }

            $taskInfo = $null
            try {
                $taskInfo = Get-ScheduledTaskInfo -TaskName $task.TaskName -TaskPath $task.TaskPath -ErrorAction Stop
            }
            catch {
            }

            $enabled = $true
            try {
                if ($task.Settings.PSObject.Properties.Name -contains 'Enabled') {
                    $enabled = [bool]$task.Settings.Enabled
                }
            }
            catch {
            }
            [pscustomobject]@{
                taskName = [string]$task.TaskName
                present = $true
                enabled = [bool]$enabled
                state = [string]$task.State
                lastResult = if ($taskInfo) { [int64]$taskInfo.LastTaskResult } else { $null }
                ok = [bool]$enabled
            }
        }
    )
}

$requiredFiles = @(
    $sessionCollectorScript,
    $evtxExportScript,
    $rulesPath,
    $policyPath,
    $policyClientScript,
    $launchScript,
    $recoveryScript,
    $ConfigPath
)
if ($browserCollectorMode -ieq 'rust_primary' -or $dlpEndpointMode -ieq 'rust_primary' -or ($fileOpsExpected -and $fileOpsMode -ieq 'rust_primary')) {
    $requiredFiles += $telemetryExecutable
}
if ($browserCollectorMode -ine 'rust_primary') {
    $requiredFiles += $collectorScript
}
if ($dlpEndpointMode -ine 'rust_primary') {
    $requiredFiles += $endpointCollectorScript
}
if ($fileOpsExpected -and $fileOpsMode -ine 'rust_primary') {
    $requiredFiles += $fileCollectorScript
}
if ($afkExpected) {
    $requiredFiles += (Join-Path $installRoot 'aw-watcher-afk\aw-watcher-afk.exe')
}
if ($windowExpected) {
    $requiredFiles += (Join-Path $installRoot 'aw-watcher-window\aw-watcher-window.exe')
}

$missingFiles = @(
    $requiredFiles |
        Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) } |
        Where-Object { -not (Test-Path -LiteralPath $_) }
)

$runningWatchers = @()
$expectedWatcherNames = @()
if ($afkExpected) { $expectedWatcherNames += 'aw-watcher-afk' }
if ($windowExpected) { $expectedWatcherNames += 'aw-watcher-window' }
if ($expectedWatcherNames.Count -gt 0) {
    $runningWatchers = @(Get-Process -Name $expectedWatcherNames -ErrorAction SilentlyContinue | Select-Object Name, Id, SessionId)
}

$sessionCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $sessionCollectorScript)
$endpointCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $endpointCollectorScript)
$fileCollectorProcesses = if ($fileOpsExpected) { @(Get-CollectorProcesses -ScriptPath $fileCollectorScript) } else { @() }
$browserCollectorProcesses = @(Get-CollectorProcesses -ScriptPath $collectorScript)
$rustCollectorProcesses = @(
    Get-CimInstance Win32_Process -ErrorAction SilentlyContinue |
        Where-Object {
            $_.Name -ieq 'aw-windows-telemetry.exe' -and
            $_.CommandLine -and
            ($_.CommandLine -match 'browser-domains-collector' -or $_.CommandLine -match 'dlp-endpoint-collector' -or $_.CommandLine -match 'file-operations-collector')
        } |
        Select-Object @{ Name = 'Name'; Expression = { $_.Name } }, @{ Name = 'Id'; Expression = { [int]$_.ProcessId } }, @{ Name = 'SessionId'; Expression = { [int]$_.SessionId } }, @{ Name = 'CommandLine'; Expression = { [string]$_.CommandLine } }
)

$liveLoggedOnUsers = Get-LoggedOnUsers
$interactiveUsers = Get-LoggedOnUsers -IncludeDisconnected $true
$liveSessionBoundUsers = @(
    @($config.userTasks) |
        Where-Object { Test-UserHasSession -UserId ([string]$_.userId) -LoggedOnUsers $liveLoggedOnUsers } |
        ForEach-Object { [string]$_.userId }
)
$interactiveSessionBoundUsers = @(
    @($config.userTasks) |
        Where-Object { Test-UserHasSession -UserId ([string]$_.userId) -LoggedOnUsers $interactiveUsers } |
        ForEach-Object { [string]$_.userId }
)
$sessionScopedExpectedCount = if ($liveSessionBoundUsers.Count -gt 0) {
    [int]$liveSessionBoundUsers.Count
}
elseif ($interactiveSessionBoundUsers.Count -gt 0) {
    1
}
else {
    0
}
$sessionScopedCollectorsRequired = ($sessionScopedExpectedCount -gt 0)
$liveSessionScopedCollectorsRequired = ($liveSessionBoundUsers.Count -gt 0)

$collectorGuardService = Get-Service -Name 'AWatchRusCollectorGuard' -ErrorAction SilentlyContinue
$collectorGuardActive = [bool]($collectorGuardService -and $collectorGuardService.Status -eq 'Running')

$taskNames = @()
if ($config.userTasks) {
    $taskNames += @($config.userTasks | ForEach-Object { Resolve-ActivityWatchLaunchTaskName -TaskName ([string]$_.launchTaskName) })
}
$taskNames += [string]$config.recovery.taskName
$tasks = @(Get-TaskSnapshot -TaskNames $taskNames)

$watcherDuplicates = @(Get-DuplicateProcessGroups -Processes $runningWatchers -PerSession $true)
$sessionCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $sessionCollectorProcesses -PerSession $false)
$endpointCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $endpointCollectorProcesses -PerSession $true)
$fileCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $fileCollectorProcesses -PerSession $true)
$browserCollectorDuplicates = @(Get-DuplicateProcessGroups -Processes $browserCollectorProcesses -PerSession $true)

$watcherByName = @{}
foreach ($watcher in $runningWatchers) {
    if (-not $watcherByName.ContainsKey([string]$watcher.Name)) {
        $watcherByName[[string]$watcher.Name] = 0
    }
    $watcherByName[[string]$watcher.Name]++
}

$bucketChecks = @(
    Get-BucketHealth -BucketId ('aw-worktime-sessions_' + $awHostname) -MaxAgeSeconds $sessionFreshnessSeconds -Required $true -RequireFreshEvent $true
)
if ($liveSessionScopedCollectorsRequired -and $afkExpected) {
    $bucketChecks += Get-BucketHealth -BucketId ('aw-watcher-afk_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $false
}
if ($liveSessionScopedCollectorsRequired -and $windowExpected) {
    $bucketChecks += Get-BucketHealth -BucketId ('aw-watcher-window_' + $awHostname) -MaxAgeSeconds $freshnessSeconds -Required $true -RequireFreshEvent $false
}
if ($liveSessionScopedCollectorsRequired) {
    $bucketChecks += Get-BucketHealth -BucketId ('aw-dlp-endpoint-signals_' + $awHostname) -MaxAgeSeconds $endpointFreshnessSeconds -Required $true -RequireFreshEvent $true
}
if ($liveSessionScopedCollectorsRequired -and $fileOpsExpected) {
    $bucketChecks += Get-BucketHealth -BucketId ('aw-file-operations_' + $awHostname) -MaxAgeSeconds $transportStaleSeconds -Required $false -RequireFreshEvent $true
}

$queueChecks = @(
    Get-TransportQueueGroupHealth -Name 'endpoint' -StateRoot $stateRoot -QueuePattern 'dlp-endpoint-signals-queue*.jsonl' -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($endpointCollectorProcesses).Count -Required $liveSessionScopedCollectorsRequired
)
if ($fileOpsExpected) {
    $queueChecks += Get-TransportQueueGroupHealth -Name 'fileops' -StateRoot $stateRoot -QueuePattern 'file-operations-queue*.jsonl' -StaleAfterSeconds $transportStaleSeconds -MaxDepth $queueMaxDepth -ActiveProcessCount @($fileCollectorProcesses).Count -Required $liveSessionScopedCollectorsRequired
}

$printServiceOperationalEnabled = $false
try {
    $printServiceLog = Get-WinEvent -ListLog 'Microsoft-Windows-PrintService/Operational' -ErrorAction Stop
    $printServiceOperationalEnabled = [bool]$printServiceLog.IsEnabled
}
catch {
}

$printJobTitlePolicyEnabled = $false
try {
    $printPolicy = Get-ItemProperty -LiteralPath 'HKLM:\Software\Policies\Microsoft\Windows NT\Printers' -Name 'ShowJobTitleInEventLogs' -ErrorAction Stop
    $printJobTitlePolicyEnabled = ([int]$printPolicy.ShowJobTitleInEventLogs -eq 1)
}
catch {
}

$watcherCountsOk = $true
$endpointProcessOk = $true
$fileProcessOk = $true
$browserProcessOk = $true
$sessionCollectorOk = (@($sessionCollectorProcesses).Count -eq 1)

$result = [ordered]@{
    generatedAtUtc = (Get-Date).ToUniversalTime().ToString('o')
    configPath = $ConfigPath
    serverUrl = $serverUrl
    apiBase = $apiBase
    awHostname = $awHostname
    installRoot = $installRoot
    stateRoot = $stateRoot
    timing = [ordered]@{
        pollSeconds = [int]$pollSeconds
        pulseSeconds = [int]$pulseSeconds
        freshnessSeconds = [int]$freshnessSeconds
        sessionFreshnessSeconds = [int]$sessionFreshnessSeconds
        transportStaleSeconds = [int]$transportStaleSeconds
    }
    files = [ordered]@{
        required = $requiredFiles
        missing = $missingFiles
        ok = ($missingFiles.Count -eq 0)
    }
    tasks = [ordered]@{
        list = $tasks
        ok = [bool]($tasks.Count -gt 0 -and -not ($tasks | Where-Object { -not $_.ok }))
    }
    processes = [ordered]@{
        liveSessionBoundUsers = $liveSessionBoundUsers
        sessionBoundUsers = $interactiveSessionBoundUsers
        sessionScopedExpectedCount = [int]$sessionScopedExpectedCount
        liveSessionScopedCollectorsRequired = [bool]$liveSessionScopedCollectorsRequired
        collectorGuardServiceActive = [bool]$collectorGuardActive
        watchers = @($runningWatchers)
        watcherDuplicates = @($watcherDuplicates)
        sessionCollectors = @($sessionCollectorProcesses)
        sessionCollectorDuplicates = @($sessionCollectorDuplicates)
        browserCollectors = @($browserCollectorProcesses)
        browserCollectorDuplicates = @($browserCollectorDuplicates)
        endpointCollectors = @($endpointCollectorProcesses)
        endpointCollectorDuplicates = @($endpointCollectorDuplicates)
        fileCollectors = @($fileCollectorProcesses)
        fileCollectorDuplicates = @($fileCollectorDuplicates)
        rustCollectors = @($rustCollectorProcesses)
        collectorModes = [ordered]@{
            browser = $browserCollectorMode
            endpoint = $dlpEndpointMode
            fileOps = $fileOpsMode
        }
        ok = [bool](
            $watcherCountsOk -and
            $sessionCollectorOk -and
            $browserProcessOk -and
            $endpointProcessOk -and
            $fileProcessOk -and
            ($watcherDuplicates.Count -eq 0) -and
            ($sessionCollectorDuplicates.Count -eq 0) -and
            ($browserCollectorDuplicates.Count -eq 0) -and
            ($endpointCollectorDuplicates.Count -eq 0) -and
            ($fileCollectorDuplicates.Count -eq 0)
        )
    }
    buckets = [ordered]@{
        list = @($bucketChecks)
        ok = [bool](-not ($bucketChecks | Where-Object { -not $_.ok }))
    }
    queues = [ordered]@{
        list = @($queueChecks)
        ok = [bool](-not ($queueChecks | Where-Object { -not $_.ok }))
    }
    printTelemetry = [ordered]@{
        operationalLogEnabled = $printServiceOperationalEnabled
        jobTitlePolicyEnabled = $printJobTitlePolicyEnabled
        ok = [bool]($printServiceOperationalEnabled -and $printJobTitlePolicyEnabled)
    }
    sessionEvents = [ordered]@{
        bucketId = $sessionEventsBucketId
        logonEnabled = [bool]$sessionLogonEnabled
        processEventsEnabled = [bool]$sessionProcessEventsEnabled
        ok = $true
    }
    forensics = [ordered]@{
        evtxExportRoot = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') { [string]$config.forensics.evtxExportRoot } else { $null }
        retentionDays = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'retentionDays') { [int]$config.forensics.retentionDays } else { $null }
        evtxChannels = if ($config.PSObject.Properties.Name -contains 'forensics' -and $config.forensics.PSObject.Properties.Name -contains 'evtxChannels') { @($config.forensics.evtxChannels) } else { @() }
        ok = [bool](
            ($config.PSObject.Properties.Name -contains 'forensics') -and
            ($config.forensics.PSObject.Properties.Name -contains 'evtxExportRoot') -and
            ($config.forensics.PSObject.Properties.Name -contains 'retentionDays') -and
            ($config.forensics.PSObject.Properties.Name -contains 'evtxChannels') -and
            (@($config.forensics.evtxChannels).Count -gt 0)
        )
    }
}

$result.summary = [ordered]@{
    failedSections = @(
        'files', 'tasks', 'processes', 'buckets', 'queues', 'printTelemetry', 'forensics' |
            Where-Object { -not [bool]$result.$_.ok }
    )
}

$result.overallOk = [bool](
    $result.files.ok -and
    $result.tasks.ok -and
    $result.processes.ok -and
    $result.buckets.ok -and
    $result.queues.ok -and
    $result.printTelemetry.ok -and
    $result.forensics.ok
)

$result
