<#
.SYNOPSIS
    Windows Remote Desktop Services Prometheus Metrics Exporter
.DESCRIPTION
    Prometheus exporter for Windows RDS. Monitors active/disconnected sessions,
    per-user session details, license availability, session host resources,
    Connection Broker status, and RD Gateway metrics.
.PARAMETER TextFile
    Write to windows_exporter textfile directory
.PARAMETER OutFile
    Write metrics to a specific file path
.PARAMETER Install
    Create a scheduled task for automatic collection
.PARAMETER Listen
    Start HTTP listener on specified address:port
.PARAMETER Interval
    Collection interval in seconds for scheduled task (default: 120)
.NOTES
    Author:  Phil Connor
    Contact: contact@mylinux.work
    Website: https://mylinux.work
    License: MIT
    Version: 1.0
#>

param(
    [switch]$TextFile,
    [string]$OutFile = "",
    [switch]$Install,
    [string]$Listen = "",
    [int]$Interval = 120
)

$ErrorActionPreference = "SilentlyContinue"
$Version = "1.0"
$TextfileDir = "C:\ProgramData\node_exporter"
$MetricPrefix = "rds"

# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

function Get-PrometheusEscape {
    param([string]$Value)
    $Value -replace '\\', '\\' -replace '"', '\"' -replace "`n", ''
}

function Write-MetricHeader {
    param([string]$Name, [string]$Help, [string]$Type)
    "# HELP $Name $Help"
    "# TYPE $Name $Type"
}

# ============================================================================
# METRIC GENERATION
# ============================================================================

function Get-RdsMetrics {
    $startTime = Get-Date
    $metrics = [System.Collections.ArrayList]::new()

    # Check if RDS module is available
    $rdsAvailable = $false
    if (Get-Module -ListAvailable -Name "RemoteDesktop" -ErrorAction SilentlyContinue) {
        Import-Module RemoteDesktop -ErrorAction SilentlyContinue
        $rdsAvailable = $true
    }

    $up = if ($rdsAvailable) { 1 } else { 0 }

    [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_up" "RDS exporter status (1=up, 0=down)" "gauge"))
    [void]$metrics.Add("${MetricPrefix}_up $up")
    [void]$metrics.Add("")

    [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_info" "Exporter version information" "gauge"))
    [void]$metrics.Add("${MetricPrefix}_exporter_info{version=`"$Version`"} 1")
    [void]$metrics.Add("")

    if ($up -eq 0) {
        $endTime = Get-Date
        $duration = [math]::Round(($endTime - $startTime).TotalSeconds, 2)
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_duration_seconds" "Script execution time" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_exporter_duration_seconds $duration")
        return ($metrics -join "`n")
    }

    # ========================================================================
    # SESSION METRICS
    # ========================================================================

    try {
        $sessions = Get-RDUserSession -ErrorAction Stop
        $sessionHosts = $sessions | Group-Object -Property HostServer

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_sessions_active" "Active RDS sessions per host" "gauge"))
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_sessions_disconnected" "Disconnected RDS sessions per host" "gauge"))
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_sessions_total" "Total RDS sessions per host" "gauge"))

        $totalActive = 0
        $totalDisconnected = 0

        foreach ($hostGroup in $sessionHosts) {
            $host = Get-PrometheusEscape $hostGroup.Name
            $active = @($hostGroup.Group | Where-Object { $_.SessionState -eq "STATE_ACTIVE" }).Count
            $disconnected = @($hostGroup.Group | Where-Object { $_.SessionState -eq "STATE_DISCONNECTED" }).Count
            $total = $hostGroup.Count

            [void]$metrics.Add("${MetricPrefix}_sessions_active{host=`"$host`"} $active")
            [void]$metrics.Add("${MetricPrefix}_sessions_disconnected{host=`"$host`"} $disconnected")
            [void]$metrics.Add("${MetricPrefix}_sessions_total{host=`"$host`"} $total")

            $totalActive += $active
            $totalDisconnected += $disconnected
        }
        [void]$metrics.Add("")

        # Per-user session details
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_session_state" "Per-user session state (1=current)" "gauge"))
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_session_idle_seconds" "Session idle time in seconds" "gauge"))

        foreach ($session in $sessions) {
            $host = Get-PrometheusEscape $session.HostServer
            $user = Get-PrometheusEscape $session.UserName
            $state = $session.SessionState -replace "STATE_", ""

            [void]$metrics.Add("${MetricPrefix}_session_state{host=`"$host`",user=`"$user`",state=`"$state`"} 1")

            if ($session.IdleTime) {
                $idleSeconds = [math]::Round($session.IdleTime.TotalSeconds)
                [void]$metrics.Add("${MetricPrefix}_session_idle_seconds{host=`"$host`",user=`"$user`"} $idleSeconds")
            }
        }
        [void]$metrics.Add("")

    } catch {
        [void]$metrics.Add("# INFO: Could not query RDS sessions: $_")
        [void]$metrics.Add("")
    }

    # ========================================================================
    # LICENSE METRICS
    # ========================================================================

    try {
        $licenseServers = Get-RDLicenseConfiguration -ErrorAction Stop

        if ($licenseServers) {
            $licenses = Get-WmiObject -Class Win32_TSLicenseKeyPack -ErrorAction Stop

            [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_license_total" "Total RDS licenses by type" "gauge"))
            [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_license_available" "Available RDS licenses by type" "gauge"))
            [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_license_used" "Used RDS licenses by type" "gauge"))

            foreach ($lic in $licenses) {
                $type = switch ($lic.ProductVersionID) {
                    0 { "PerDevice" }
                    1 { "PerUser" }
                    2 { "PerDevice" }
                    default { "Unknown" }
                }

                $total = $lic.TotalLicenses
                $available = $lic.AvailableLicenses
                $used = $total - $available

                [void]$metrics.Add("${MetricPrefix}_license_total{type=`"$type`"} $total")
                [void]$metrics.Add("${MetricPrefix}_license_available{type=`"$type`"} $available")
                [void]$metrics.Add("${MetricPrefix}_license_used{type=`"$type`"} $used")
            }
            [void]$metrics.Add("")
        }
    } catch {
        # License server may not be on this host
    }

    # ========================================================================
    # SESSION HOST RESOURCE METRICS
    # ========================================================================

    try {
        $rdServers = Get-RDServer -Role "RDS-RD-SERVER" -ErrorAction Stop

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_host_cpu_percent" "Session host CPU usage" "gauge"))
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_host_memory_percent" "Session host memory usage" "gauge"))
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_host_disk_free_bytes" "Session host free disk space" "gauge"))

        foreach ($server in $rdServers) {
            $hostName = Get-PrometheusEscape $server.Server

            $cpu = Get-Counter -ComputerName $server.Server -Counter "\Processor(_Total)\% Processor Time" -ErrorAction SilentlyContinue
            if ($cpu) {
                $cpuVal = [math]::Round($cpu.CounterSamples[0].CookedValue, 1)
                [void]$metrics.Add("${MetricPrefix}_host_cpu_percent{host=`"$hostName`"} $cpuVal")
            }

            $mem = Get-WmiObject -ComputerName $server.Server -Class Win32_OperatingSystem -ErrorAction SilentlyContinue
            if ($mem) {
                $memPercent = [math]::Round((($mem.TotalVisibleMemorySize - $mem.FreePhysicalMemory) / $mem.TotalVisibleMemorySize) * 100, 1)
                [void]$metrics.Add("${MetricPrefix}_host_memory_percent{host=`"$hostName`"} $memPercent")
            }

            $disks = Get-WmiObject -ComputerName $server.Server -Class Win32_LogicalDisk -Filter "DriveType=3" -ErrorAction SilentlyContinue
            foreach ($disk in $disks) {
                $drive = $disk.DeviceID -replace ":", ""
                [void]$metrics.Add("${MetricPrefix}_host_disk_free_bytes{host=`"$hostName`",drive=`"$drive`"} $($disk.FreeSpace)")
            }
        }
        [void]$metrics.Add("")
    } catch {
        # May not have access to remote servers
    }

    # ========================================================================
    # CONNECTION BROKER
    # ========================================================================

    $brokerSvc = Get-Service -Name "Tssdis" -ErrorAction SilentlyContinue
    $brokerUp = if ($brokerSvc -and $brokerSvc.Status -eq "Running") { 1 } else { 0 }

    [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_connection_broker_up" "Connection Broker service status" "gauge"))
    [void]$metrics.Add("${MetricPrefix}_connection_broker_up $brokerUp")
    [void]$metrics.Add("")

    # Collection health
    try {
        $collections = Get-RDSessionCollection -ErrorAction Stop

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_collection_health" "Collection health (1=healthy)" "gauge"))
        foreach ($col in $collections) {
            $colName = Get-PrometheusEscape $col.CollectionName
            $health = 1
            [void]$metrics.Add("${MetricPrefix}_collection_health{collection=`"$colName`"} $health")
        }
        [void]$metrics.Add("")
    } catch {
        # Collections may not be configured
    }

    # ========================================================================
    # RD GATEWAY METRICS
    # ========================================================================

    try {
        $gwCounter = Get-Counter -Counter "\TS Gateway\Current Connections" -ErrorAction Stop
        if ($gwCounter) {
            $gwActive = [math]::Round($gwCounter.CounterSamples[0].CookedValue)
            [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_gateway_connections_active" "Active RD Gateway connections" "gauge"))
            [void]$metrics.Add("${MetricPrefix}_gateway_connections_active $gwActive")
            [void]$metrics.Add("")
        }

        $gwRate = Get-Counter -Counter "\TS Gateway\Connections/Sec" -ErrorAction SilentlyContinue
        if ($gwRate) {
            $gwPerSec = [math]::Round($gwRate.CounterSamples[0].CookedValue, 2)
            [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_gateway_connections_per_second" "RD Gateway connection rate" "gauge"))
            [void]$metrics.Add("${MetricPrefix}_gateway_connections_per_second $gwPerSec")
            [void]$metrics.Add("")
        }

        $gwAuth = Get-Counter -Counter "\TS Gateway\Failed Authentications" -ErrorAction SilentlyContinue
        if ($gwAuth) {
            $gwFailed = [math]::Round($gwAuth.CounterSamples[0].CookedValue)
            [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_gateway_auth_failures_total" "RD Gateway authentication failures" "counter"))
            [void]$metrics.Add("${MetricPrefix}_gateway_auth_failures_total $gwFailed")
            [void]$metrics.Add("")
        }
    } catch {
        # Gateway may not be installed
    }

    # ========================================================================
    # EXPORTER RUNTIME
    # ========================================================================

    $endTime = Get-Date
    $duration = [math]::Round(($endTime - $startTime).TotalSeconds, 2)
    $timestamp = [math]::Round((Get-Date -UFormat %s), 0)

    [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_duration_seconds" "Script execution time" "gauge"))
    [void]$metrics.Add("${MetricPrefix}_exporter_duration_seconds $duration")
    [void]$metrics.Add("")

    [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_exporter_last_run_timestamp" "Unix timestamp of last run" "gauge"))
    [void]$metrics.Add("${MetricPrefix}_exporter_last_run_timestamp $timestamp")

    return ($metrics -join "`n")
}

# ============================================================================
# OUTPUT MODES
# ============================================================================

if ($Install) {
    $scriptPath = $MyInvocation.MyCommand.Path
    $action = New-ScheduledTaskAction -Execute "powershell.exe" `
        -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -TextFile"
    $trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Seconds $Interval) -Once -At (Get-Date)
    $principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest
    $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
    Register-ScheduledTask -TaskName "RDS Metrics Exporter" -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force
    Write-Host "Scheduled task 'RDS Metrics Exporter' created (interval: ${Interval}s)"
    exit 0
}

if ($Listen -ne "") {
    $port = $Listen -replace '.*:', ''
    if (-not $port) { $port = "9185" }
    $listener = [System.Net.HttpListener]::new()
    $listener.Prefixes.Add("http://+:$port/")
    $listener.Start()
    Write-Host "RDS exporter listening on port $port..."

    while ($listener.IsListening) {
        $context = $listener.GetContext()
        $response = $context.Response
        $output = Get-RdsMetrics
        $buffer = [System.Text.Encoding]::UTF8.GetBytes($output)
        $response.ContentType = "text/plain; version=0.0.4"
        $response.ContentLength64 = $buffer.Length
        $response.OutputStream.Write($buffer, 0, $buffer.Length)
        $response.Close()
    }
} elseif ($TextFile -or $OutFile -ne "") {
    $outputPath = if ($OutFile -ne "") { $OutFile } else { Join-Path $TextfileDir "rds.prom" }
    $outputDir = Split-Path $outputPath -Parent
    if (-not (Test-Path $outputDir)) { New-Item -ItemType Directory -Path $outputDir -Force | Out-Null }
    $tempFile = Join-Path $outputDir ".rds-metrics.tmp"
    $metricsOutput = Get-RdsMetrics
    $metricsOutput | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline
    Move-Item -Path $tempFile -Destination $outputPath -Force
    Write-Host "Metrics written to $outputPath"
} else {
    Get-RdsMetrics
}
