<#
.SYNOPSIS
    Monitors RDP user sessions and exports metrics for Prometheus windows_exporter.

.DESCRIPTION
    This script monitors the number of active RDP user sessions and creates Prometheus-formatted metrics.
    The metrics are written to a text file that can be consumed by the windows_exporter.
    It can also run periodically.

.PARAMETER MetricsPath
    The path where the Prometheus metrics file will be written.

.PARAMETER IntervalSeconds
    The interval in seconds for the scheduled task. Default is 60 seconds.

.Parameter RunOnce
    Switch to run the script once and exit instead of creating a scheduled task.

.PARAMETER Debug
    Switch to run the script in debug mode.

.PARAMETER RunOnce
    Switch to run the script once and exit instead of creating a scheduled task.

.PARAMETER DryRun
    Switch to output metrics to console instead of writing to file.

.PARAMETER Verbose
    Switch to enable verbose debug output.

.PARAMETER Quiet
    Switch to suppress non-error output.

.PARAMETER NoSchedule
    Switch to skip scheduled task creation.

.PARAMETER Version
    Switch to display script version and exit.

.NOTES
    Version: 1.1.2-20251002
    Author: Phil Connor contact@mylinux.work
    
    Features:
    - Monitors active RDP user sessions using quser command
    - Captures username, session name, session ID, state (Active/Disconnected), idle time, and logon time
    - Attempts to correlate session IDs with client IP addresses using qwinsta
    - Writes metrics to a text file for consumption by windows_exporter.    
    - Reads last 10 PowerShell commands from each user's PSReadline history file.  
#>

param(
    [ValidateScript({
            if ($_ -and -not (Test-Path (Split-Path $_ -Parent))) {
                throw "Directory for metrics path does not exist: $(Split-Path $_ -Parent)"
            }
            return $true
        })]
    [string]$MetricsPath = "C:\Program Files\windows_exporter\textfile_inputs\users_logged_in.prom",
    [int]$IntervalSeconds = 60,
    [switch]$RunOnce,
    [switch]$Debug,
    [switch]$DryRun,
    [switch]$Verbose,
    [switch]$Quiet,
    [switch]$NoSchedule,
    [switch]$Version
)

# Handle version display
if ($Version) {
    Write-Host "Windows RDP User Monitor PowerShell Script"
    Write-Host "Version: 1.1.0-20250915"
    Write-Host "Author: Phil Connor contact@mylinux.work"
    exit 0
}

# Set up logging preferences based on Verbose/Quiet flags
if ($Verbose) {
    $VerbosePreference = 'Continue'
    $InformationPreference = 'Continue'
}
if ($Quiet) {
    $VerbosePreference = 'SilentlyContinue'
    $InformationPreference = 'SilentlyContinue'
    $WarningPreference = 'SilentlyContinue'
}

# Enhanced logging functions
function Write-InfoLog {
    param([string]$Message)
    if (-not $Quiet) {
        Write-Host "[INFO] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Green
    }
}

function Write-VerboseLog {
    param([string]$Message)
    if ($Verbose) {
        Write-Host "[VERBOSE] $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') $Message" -ForegroundColor Cyan
    }
}

# Configuration constants for the script
$script:Config = @{
    RDP_SESSION_PATTERN = "rdp-tcp#\d+|console"           # Regex pattern to match RDP session names
    METRIC_NAME         = "windows_rdp_users_logged_in"   # Primary Prometheus metric name
    QWINSTA_IP_REGEX    = '^\s*(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+\.\d+\.\d+\.\d+)'  # Pattern for IP extraction
    QUSER_HEADER_REGEX  = "USERNAME.*SESSIONNAME.*ID.*STATE"  # Expected quser output header format
    COLUMNS             = @{                              # Column positions in quser output
        USERNAME = 0; SESSION = 1; ID = 2; STATE = 3; IDLE = 4; LOGON_START = 5
    }
}

# Sanitize string values for use as Prometheus metric labels
# Removes or replaces characters that would break Prometheus metric format
function ConvertTo-MetricLabel {
    param([AllowEmptyString()][string]$Value)
    if ([string]::IsNullOrEmpty($Value)) { return "" }
    
    # Replace problematic characters with underscores to prevent metric parsing issues
    $sanitized = $Value -replace '["\\\n\r\t>]', '_'
    
    # Limit length to prevent overly long metric labels (Prometheus best practice)
    if ($sanitized.Length -gt 200) {
        $sanitized = $sanitized.Substring(0, 200) + "..."
    }
    return $sanitized
}

# Format metric data into Prometheus text format
function Write-PrometheusMetric {
    param(
        [ValidateNotNullOrEmpty()][string]$Name,
        [ValidateNotNullOrEmpty()][string]$Help,
        [ValidateNotNullOrEmpty()][string]$Type,
        [ValidateNotNull()][array]$Metrics
    )

    try {
        @(
            # Write Prometheus metric header with help text and type
            "# HELP $Name $Help"
            "# TYPE $Name $Type"
            # Format each metric with its labels and value
            $Metrics | ForEach-Object {
                if ($null -eq $_ -or $null -eq $_.Labels -or $null -eq $_.Value) {
                    throw "Invalid metric data"
                }
                "$Name$($_.Labels) $($_.Value)"
            }
        )
    }
    catch {
        Write-Error "Failed to write metric: $($_.Exception.Message)"
    }
}

# Execute quser command and validate output format
# Returns raw quser command output after basic validation
function Get-QUserData {
    try {
        # Run quser command and suppress stderr to avoid noise
        $output = quser 2>$null
        
        # Validate that we got some output
        if (-not $output -or $output.Count -eq 0) {
            throw "No user sessions found or quser command failed"
        }
        
        # Ensure output has expected header format
        if ($output.Count -lt 2 -or $output[0] -notmatch $script:Config.QUSER_HEADER_REGEX) {
            throw "Unexpected quser output format"
        }
        return $output
    }
    catch [System.Management.Automation.CommandNotFoundException] {
        throw "quser command not found. This script requires Windows with Terminal Services."
    }
}

# Get IP addresses for RDP sessions using qwinsta command
# Attempts to correlate session IDs with client IP addresses for remote sessions
function Get-SessionIPAddresses {
    try {
        $sessionIPs = @{}
        # Run qwinsta to get session information including IP addresses
        $qwinstaOutput = qwinsta 2>$null
        
        if ($qwinstaOutput) {
            Write-Verbose "Raw qwinsta output:"
            $qwinstaOutput | ForEach-Object { Write-Verbose "  $_" }
            
            foreach ($line in $qwinstaOutput) {
                # Skip header lines and empty lines
                if ([string]::IsNullOrWhiteSpace($line) -or $line -match '^\s*SESSIONNAME') { 
                    continue 
                }
                
                Write-Verbose "Processing qwinsta line: '$line'"
                
                # Look for any IP address in the line and try to correlate with session ID
                if ($line -match '(\d+\.\d+\.\d+\.\d+)') {
                    $ipAddress = $matches[1]
                    
                    # Try different patterns to find session ID that corresponds to this IP
                    $sessionId = $null
                    
                    # Pattern 1: Standard format with session ID as 3rd column
                    if ($line -match '^\s*(\S+)\s+(\S+)?\s+(\d+)\s+') {
                        $sessionId = $matches[3]
                    }
                    # Pattern 2: RDP session format
                    elseif ($line -match 'rdp-tcp#\d+.*?\s(\d+)\s+') {
                        $sessionId = $matches[1]
                    }
                    # Pattern 3: Any number that looks like a session ID (between spaces)
                    elseif ($line -match '\s(\d+)\s+\w+') {
                        $sessionId = $matches[1]
                    }
                    
                    # Store the mapping if we found a valid session ID
                    if ($sessionId) {
                        $sessionIPs[$sessionId] = $ipAddress
                        Write-Verbose "Mapped session ID $sessionId to IP $ipAddress"
                    }
                    else {
                        Write-Verbose "Found IP $ipAddress but could not determine session ID"
                    }
                }
            }
        }
        
        Write-Verbose "Final session IP mapping: $($sessionIPs | ConvertTo-Json -Compress)"
        return $sessionIPs
    }
    catch {
        # Don't fail the entire script if IP detection fails
        Write-Warning "Failed to get session IP addresses: $($_.Exception.Message)"
        return @{}
    }
}

# Parses a single line of quser output into a structured object
# Converts space-separated quser output into a PowerShell object with named properties
function ConvertFrom-QUserLine {
    param(
        [ValidateNotNullOrEmpty()][string]$Line, 
        [hashtable]$SessionIPs = @{}
    )
    
    # Split the line into fields, normalizing whitespace
    $fields = $Line.Trim() -Replace '\s+', ' ' -Split '\s'
    
    # Validate minimum expected field count
    if ($fields.Length -lt 6) { return $null }
    
    $cols = $script:Config.COLUMNS
    $sessionId = $fields[$cols.ID]
    
    # Look up IP address for this session if available
    $ipAddress = if ($SessionIPs.ContainsKey($sessionId)) { $SessionIPs[$sessionId] } else { "unknown" }
    
    # Extract logon time from remaining fields (may span multiple columns)
    $logonTime = if ($fields.Length -gt $cols.LOGON_START) {
        $endIndex = if ($fields.Length -gt 6) { $fields.Length - 2 } else { $fields.Length - 1 }
        $fields[$cols.LOGON_START..$endIndex] -join ' '
    }
    else { "Unknown" }
    
    # Clean username by removing leading ">" character if present (indicates active session)
    $cleanUserName = $fields[$cols.USERNAME] -replace '^>', ''
    
    # Create structured object with all session information
    return [PSCustomObject]@{
        UserName       = $cleanUserName
        SessionName    = $fields[$cols.SESSION]
        ID             = $sessionId
        State          = $fields[$cols.STATE]
        IdleTime       = $fields[$cols.IDLE]
        LogonTime      = $logonTime
        ClientLocation = if ($fields.Length -gt 6) { $fields[-1] } else { "local" }
        IPAddress      = $ipAddress
    }
}

# Get command history for a specific user session
# Retrieves recent PowerShell commands from the user's PSReadline history file
function Get-UserCommandHistory {
    param(
        [string]$UserName,
        [string]$SessionId,
        [int]$MaxCommands = 10
    )
    
    try {
        # Sanitize username to remove invalid file path characters
        $sanitizedUserName = $UserName -replace '[<>:"|?*]', '_'
        
        # Try to get PowerShell history from the user's profile
        $historyPath = "C:\Users\$sanitizedUserName\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadline\ConsoleHost_history.txt"
        $commands = @()
        
        # Check if PowerShell history file exists
        if (Test-Path $historyPath) {
            # Read the last N commands from the history file
            $historyContent = Get-Content $historyPath -Tail $MaxCommands -ErrorAction SilentlyContinue
            if ($historyContent) {
                # Clean up the commands by trimming whitespace and removing empty lines
                $commands = $historyContent | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
            }
        }
        
        # If no PowerShell history, try to get CMD history using doskey
        if ($commands.Count -eq 0) {
            try {
                # Use query session to check if user is active, then try to get command history
                $sessionInfo = query session $SessionId 2>$null
                if ($sessionInfo) {
                    # This is a simplified approach - in practice, CMD history is harder to access remotely
                    $commands = @("No recent command history available")
                }
            }
            catch {
                $commands = @("Unable to retrieve command history")
            }
        }
        
        # Return the most recent commands up to the specified limit
        return $commands | Select-Object -First $MaxCommands
    }
    catch {
        Write-Verbose "Failed to get command history for user $UserName (Session $SessionId): $($_.Exception.Message)"
        return @("Command history unavailable")
    }
}

# Get all active RDP user sessions with detailed information
# Combines quser and qwinsta data to create comprehensive user session objects
function Get-RDPUsers {
    try {
        # Get raw user session data and IP address mappings
        $qUserOutput = Get-QUserData
        $sessionIPs = Get-SessionIPAddresses
        Write-Verbose "Found $($qUserOutput.Count) total user sessions"
        Write-Verbose "Found $($sessionIPs.Count) session IP addresses"
        
        # Process each user session line (skip header line)
        $allUsers = $qUserOutput | Select-Object -Skip 1 | ForEach-Object {
            # Parse the quser output line into a structured object
            $user = ConvertFrom-QUserLine $_ $sessionIPs
            if ($null -eq $user) {
                Write-Warning "Skipping malformed quser output: $_"
                return
            }
            
            # Add command history to user object
            $commandHistory = Get-UserCommandHistory -UserName $user.UserName -SessionId $user.ID
            $user | Add-Member -NotePropertyName "CommandHistory" -NotePropertyValue $commandHistory
            
            $user
        } | Where-Object { $_ }
        
        # Filter to only RDP sessions (excluding services and other non-user sessions)
        $rdpUsers = $allUsers | Where-Object {
            $_.SessionName -match $script:Config.RDP_SESSION_PATTERN -and
            ![string]::IsNullOrEmpty($_.UserName) -and
            ![string]::IsNullOrEmpty($_.SessionName) -and
            ![string]::IsNullOrEmpty($_.State)
        }
        
        Write-Verbose "Processed $($allUsers.Count) valid user sessions"
        Write-Verbose "Filtered to $($rdpUsers.Count) RDP sessions"
        
        return $rdpUsers
    }
    catch {
        throw "Failed to collect user data: $($_.Exception.Message)"
    }
}

# Creates Prometheus metrics from user session data
# Transforms user session objects into Prometheus-formatted metric data
function New-UserMetrics {
    param([array]$Users)
    
    if (-not $Users) { return @() }
    
    # Initialize counters and collections for metric generation
    $stateCount = @{ Active = 0; Disc = 0 }
    $usernames = @()
    $userMetrics = @()
    $commandMetrics = @()
    
    # Process each user to create individual metrics
    foreach ($user in $Users) {
        if ($null -eq $user) {
            Write-Warning "Found null user in collection"
            continue
        }
        
        # Track state counts for summary metrics
        $stateCount[$user.State]++
        $usernames += $user.UserName
        
        # Create individual user session metric
        $userMetrics += @{
            Labels = "{username=`"$(ConvertTo-MetricLabel $user.UserName)`",session=`"$(ConvertTo-MetricLabel $user.SessionName)`",state=`"$($user.State)`",location=`"$(ConvertTo-MetricLabel $user.ClientLocation)`",ip=`"$(ConvertTo-MetricLabel $user.IPAddress)`"}"
            Value  = 1
        }
        
        # Add command history metrics for each user
        if ($user.CommandHistory -and $user.CommandHistory.Count -gt 0) {
            for ($i = 0; $i -lt $user.CommandHistory.Count; $i++) {
                $command = ConvertTo-MetricLabel $user.CommandHistory[$i]
                $commandMetrics += @{
                    Labels = "{username=`"$(ConvertTo-MetricLabel $user.UserName)`",session=`"$(ConvertTo-MetricLabel $user.SessionName)`",command_index=`"$($i + 1)`",command=`"$command`"}"
                    Value  = 1
                }
            }
        }
    }
    
    # Create summary metrics with totals and user list
    $summaryMetrics = @(
        @{ Labels = '{metric="total"}'; Value = $Users.Count }
        @{ Labels = '{metric="active"}'; Value = $stateCount.Active }
        @{ Labels = '{metric="disconnected"}'; Value = $stateCount.Disc }
        @{ Labels = '{metric="users_list",users="' + $(ConvertTo-MetricLabel (($usernames | Sort-Object) -join ',')) + '"}'; Value = 1 }
    )
    
    # Combine all metric types into a single collection
    return $summaryMetrics + $userMetrics + $commandMetrics
}

# Write metrics content to file using atomic write operation
function Write-MetricsFile {
    param(
        [ValidateNotNull()]$Content, 
        [string]$Path
    )
    
    if (-not $Path) {
        return $Content
    }
    
    # Ensure the directory exists
    $directory = Split-Path $Path -Parent
    if ($directory -and -not (Test-Path $directory)) {
        try {
            New-Item -Path $directory -ItemType Directory -Force | Out-Null
            Write-Verbose "Created directory: $directory"
        }
        catch {
            Write-Error "Failed to create directory '$directory': $($_.Exception.Message)"
            return
        }
    }
    
    $tempPath = "$Path.tmp"
    try {
        if ($Content -is [array]) {
            $Content -join "`n" | Out-File -FilePath $tempPath -Encoding UTF8
        } else {
            $Content | Out-File -FilePath $tempPath -Encoding UTF8
        }
        Move-Item -Path $tempPath -Destination $Path -Force -ErrorAction Stop
    }
    catch {
        Write-Error "Failed to write metrics file: $($_.Exception.Message)"
        if (Test-Path $tempPath) { Remove-Item $tempPath -Force }
    }
}

# Main function that orchestrates the complete metrics collection process
# Coordinates all data collection, processing, and output generation
function Invoke-MetricsCollection {
    $startTime = Get-Date
    
    # Add dry-run header if applicable
    if ($DryRun) {
        Write-Host "=== DRY RUN MODE - Metrics that would be written to $MetricsPath ===" -ForegroundColor Yellow
    }
    
    try {
        # Collect RDP user session data
        Write-VerboseLog "Collecting RDP user session data..."
        $rdpUsers = Get-RDPUsers
        if ($null -eq $rdpUsers) {
            throw "Get-RDPUsers returned null"
        }
        
        Write-VerboseLog "Found $($rdpUsers.Count) RDP users"
        
        # Convert user data to Prometheus metrics
        $metrics = New-UserMetrics -Users $rdpUsers
        if ($null -eq $metrics) {
            throw "New-UserMetrics returned null"
        }
        
        # Collect failed login attempts
        Write-VerboseLog "Collecting failed login data..."
        $failedLoginMetrics = Get-FailedLogins
        
        # Calculate script execution time for performance monitoring
        $endTime = Get-Date
        $executionTimeMs = [math]::Round(($endTime - $startTime).TotalMilliseconds, 2)
        
        # Add execution time metric for monitoring script performance
        $executionMetric = @{
            Labels = '{metric="execution_time_ms"}'
            Value = $executionTimeMs
        }
        $metrics += $executionMetric
        
        # Split metrics into different types
        $userMetrics = $metrics | Where-Object { $_.Labels -notmatch 'command=' }
        $commandMetrics = $metrics | Where-Object { $_.Labels -match 'command=' }
        
        # Generate Prometheus-formatted output
        $output = @()
        $output += Write-PrometheusMetric -Name $script:Config.METRIC_NAME -Help "Number of RDP users currently logged in" -Type "gauge" -Metrics $userMetrics
        
        # Add command history metrics as a separate metric family
        if ($commandMetrics.Count -gt 0) {
            $output += Write-PrometheusMetric -Name "windows_rdp_user_command_history" -Help "Recent command history for RDP users" -Type "gauge" -Metrics $commandMetrics
        }
        
        # Add failed login metrics
        if ($failedLoginMetrics.Count -gt 0) {
            $output += Write-PrometheusMetric -Name "windows_user_failed_logins" -Help "Failed login attempts from Windows Event Log" -Type "counter" -Metrics $failedLoginMetrics
        }
        
        if ($null -eq $output) {
            throw "Write-PrometheusMetric returned null"
        }
        
        Write-VerboseLog "Metrics collection completed (execution time: ${executionTimeMs}ms)"
        
        # Output to console and/or file based on mode
        if ($DryRun) {
            Write-Host $output
            Write-Host "=== END DRY RUN OUTPUT ===" -ForegroundColor Yellow
        } else {
            Write-Output $output
            Write-MetricsFile -Content $output -Path $MetricsPath
        }
    }
    catch {
        Write-Error "Failed to collect metrics: $($_.Exception.Message)"
        # Attempt to write partial results if available
        if ($MetricsPath -and $output -and -not $DryRun) {
            $output | Out-File -FilePath $MetricsPath -Encoding UTF8
        }
    }
}

# Register cleanup handler for graceful shutdown
Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action {
    Write-Host "Shutting down gracefully..."
}

# Create scheduled task for periodic execution
function New-MetricsScheduledTask {
    param(
        [int]$IntervalSeconds = 60,
        [string]$TaskName = "PrometheusRDPMetrics"
    )
    
    try {
        # Check if scheduled task already exists
        if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) {
            Write-InfoLog "Scheduled task '$TaskName' already exists. Skipping creation."
            return
        }
        
        $principal =  New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
        $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -MetricsPath `"$MetricsPath`" -RunOnce"
        $trigger = New-ScheduledTaskTrigger -Once -At (Get-Date) -RepetitionInterval (New-TimeSpan -Seconds $IntervalSeconds)
        $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
        
        Register-ScheduledTask -TaskName $TaskName -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
        Write-InfoLog "Scheduled task '$TaskName' created successfully with $IntervalSeconds second interval"
    }
    catch {
        Write-Error "Failed to create scheduled task: $($_.Exception.Message)"
    }
}

# Debug function to test qwinsta parsing
function Test-QwinstaOutput {
    Write-Host "=== Testing qwinsta output parsing ===" -ForegroundColor Cyan
    
    try {
        $qwinstaOutput = qwinsta 2>$null
        Write-Host "Raw qwinsta output:" -ForegroundColor Yellow
        $qwinstaOutput | ForEach-Object { Write-Host "  $_" }
        
        Write-Host "`nTesting IP address extraction:" -ForegroundColor Yellow
        $sessionIPs = Get-SessionIPAddresses
        $sessionIPs.GetEnumerator() | ForEach-Object {
            Write-Host "  Session ID $($_.Key) -> IP $($_.Value)" -ForegroundColor Green
        }
        
        Write-Host "`nTesting quser output:" -ForegroundColor Yellow
        $quserOutput = quser 2>$null
        $quserOutput | ForEach-Object { Write-Host "  $_" }
        
    }
    catch {
        Write-Error "Test failed: $($_.Exception.Message)"
    }
}

# Get failed login attempts from Windows Event Log
function Get-FailedLogins {
    try {
        $failedLogins = @()
        $24HoursAgo = (Get-Date).AddHours(-24)
        
        # Query Windows Security Event Log for failed logon attempts (Event ID 4625)
        $failedLogonEvents = Get-WinEvent -FilterHashtable @{
            LogName = 'Security'
            Id = 4625  # Failed logon attempts
            StartTime = $24HoursAgo
        } -ErrorAction SilentlyContinue | Select-Object -First 50
        
        if ($failedLogonEvents) {
            foreach ($event in $failedLogonEvents) {
                try {
                    $eventXml = [xml]$event.ToXml()
                    $eventData = $eventXml.Event.EventData.Data
                    
                    # Extract relevant information from event data
                    $targetUserName = ($eventData | Where-Object {$_.Name -eq 'TargetUserName'}).'#text'
                    $workstationName = ($eventData | Where-Object {$_.Name -eq 'WorkstationName'}).'#text'
                    $sourceNetworkAddress = ($eventData | Where-Object {$_.Name -eq 'IpAddress'}).'#text'
                    $failureReason = ($eventData | Where-Object {$_.Name -eq 'SubStatus'}).'#text'
                    
                    # Clean up values
                    if ([string]::IsNullOrWhiteSpace($targetUserName)) { $targetUserName = "unknown" }
                    if ([string]::IsNullOrWhiteSpace($sourceNetworkAddress) -or $sourceNetworkAddress -eq '-') { $sourceNetworkAddress = "local" }
                    if ([string]::IsNullOrWhiteSpace($workstationName)) { $workstationName = "unknown" }
                    
                    # Determine failure type based on sub status
                    $failureType = switch ($failureReason) {
                        "0xC0000064" { "invalid_user" }
                        "0xC000006A" { "wrong_password" }
                        "0xC0000234" { "account_locked" }
                        "0xC0000072" { "account_disabled" }
                        "0xC000006F" { "logon_time_restriction" }
                        "0xC0000070" { "workstation_restriction" }
                        default { "other_failure" }
                    }
                    
                    $failedLogins += @{
                        Labels = "{username=`"$targetUserName`",source_ip=`"$sourceNetworkAddress`",workstation=`"$workstationName`",failure_type=`"$failureType`"}"
                        Value = 1
                    }
                } catch {
                    Write-VerboseLog "Failed to parse event: $($_.Exception.Message)"
                }
            }
        }
        
        return $failedLogins
    } catch {
        Write-Warning "Failed to get failed login events: $($_.Exception.Message)"
        return @()
    }
}

# Main execution logic - determines script behavior based on parameters
if ($Debug) {
    # Debug mode: test qwinsta and quser output parsing
    Test-QwinstaOutput
}
elseif ($RunOnce -or $DryRun) {
    # Single execution mode: collect metrics once and exit
    Invoke-MetricsCollection
}
else {
    # Scheduled mode: create scheduled task (unless NoSchedule) and run immediately
    if (-not $NoSchedule) {
        New-MetricsScheduledTask -IntervalSeconds $IntervalSeconds
    } else {
        Write-InfoLog "Skipping scheduled task creation (-NoSchedule specified)"
    }
    
    # Run metrics collection immediately
    Invoke-MetricsCollection
}
