<#
.SYNOPSIS
    IIS Prometheus Metrics Exporter
.DESCRIPTION
    Prometheus exporter for IIS (Internet Information Services). Collects metrics
    via PowerShell WebAdministration module, Windows performance counters, and
    IIS log analysis. Outputs Prometheus-compatible text format for consumption
    by windows_exporter textfile collector.
.PARAMETER Mode
    Output mode: 'stdout' (default), 'textfile', or 'http'
.PARAMETER Port
    HTTP port for http mode (default: 9210)
.PARAMETER TextfileDir
    Directory for textfile collector output (default: C:\ProgramData\node_exporter)
.PARAMETER OutputFile
    Custom output file path
.PARAMETER InstallScheduledTask
    Switch to create a scheduled task for auto-start on system boot
.PARAMETER TaskIntervalMinutes
    Interval in minutes for the scheduled task (default: 5)
.NOTES
    Author:  Phil Connor
    Contact: contact@mylinux.work
    Website: https://mylinux.work
    License: MIT
    Version: 1.0

    Metrics Exported:
      Core Status:
        - iis_up
        - iis_exporter_info{version}

      Site Status:
        - iis_site_state{site}
        - iis_site_binding_info{site,protocol,address,port,hostname}

      Application Pools:
        - iis_apppool_state{apppool}
        - iis_apppool_worker_process_count{apppool}
        - iis_apppool_recycle_count{apppool}

      Request Throughput:
        - iis_current_connections{site}
        - iis_requests_per_second{site}
        - iis_bytes_sent_per_second{site}
        - iis_bytes_received_per_second{site}
        - iis_total_bytes_sent{site}
        - iis_total_bytes_received{site}
        - iis_requests_total{site}

      HTTP Methods:
        - iis_get_requests_total{site}
        - iis_post_requests_total{site}
        - iis_put_requests_total{site}
        - iis_delete_requests_total{site}

      HTTP Status Codes:
        - iis_status_2xx_total{site}
        - iis_status_3xx_total{site}
        - iis_status_4xx_total{site}
        - iis_status_5xx_total{site}

      SSL/TLS:
        - iis_ssl_connections_current
        - iis_ssl_connections_per_second
        - iis_ssl_handshake_failures

      Worker Processes:
        - iis_worker_cpu_percent{apppool}
        - iis_worker_memory_bytes{apppool}
        - iis_worker_active_requests{apppool}
        - iis_worker_uptime_seconds{apppool}

      Cache:
        - iis_cache_output_entries
        - iis_cache_output_hits
        - iis_cache_output_misses
        - iis_cache_uri_hits
        - iis_cache_uri_misses
        - iis_cache_kernel_hits

      Failed Requests:
        - iis_failed_requests_500_last_hour
        - iis_failed_requests_total{site}

      Exporter:
        - iis_exporter_duration_seconds
        - iis_exporter_last_run_timestamp
#>

param(
    [ValidateSet('stdout', 'textfile', 'http')]
    [string]$Mode = 'stdout',

    [int]$Port = 9210,

    [string]$TextfileDir = 'C:\ProgramData\node_exporter',

    [string]$OutputFile,

    [switch]$InstallScheduledTask,

    [int]$TaskIntervalMinutes = 5
)

# Create a scheduled task to run this script every $TaskIntervalMinutes minutes
# The task will run as SYSTEM and will be set to run at startup
if ($InstallScheduledTask) {
    $taskName = "IISMetricsExporter"
    $existingTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue

    if (-not $existingTask) {
        $taskAction = New-ScheduledTaskAction -Execute "powershell.exe" -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$($MyInvocation.MyCommand.Path)`" -Mode textfile"

        if (-not $TaskIntervalMinutes -or $TaskIntervalMinutes -le 0) {
            throw "TaskIntervalMinutes must be a positive integer"
        }

        $taskTrigger = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes $TaskIntervalMinutes) -RepetitionDuration (New-TimeSpan -Days 365)
        $taskPrincipal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -LogonType ServiceAccount -RunLevel Highest

        try {
            Write-Host "Creating scheduled task: $taskName"
            Register-ScheduledTask -TaskName $taskName -Action $taskAction -Trigger $taskTrigger -Principal $taskPrincipal -Description "Exports IIS metrics for Prometheus every $TaskIntervalMinutes minutes"

            $createdTask = Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue
            if (-not $createdTask) {
                throw "Failed to verify scheduled task creation"
            }
            Write-Host "Successfully created scheduled task: $taskName" -ForegroundColor Green
        } catch {
            Write-Error "Failed to create auto-start task: $($_.Exception.Message)"
            throw
        }
    } else {
        Write-Host "Scheduled task '$taskName' already exists, skipping creation"
    }
}

$ErrorActionPreference = 'SilentlyContinue'

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

function Get-UnixTimestamp {
    [int][double]::Parse((Get-Date -UFormat '%s'))
}

function Format-MetricValue {
    param([double]$Value, [int]$Decimals = 2)
    [math]::Round($Value, $Decimals)
}

function Get-SafeCounter {
    param([string]$CounterPath)
    try {
        $counter = Get-Counter -Counter $CounterPath -ErrorAction Stop
        if ($counter -and $counter.CounterSamples) {
            return [math]::Max(0, [math]::Round($counter.CounterSamples[0].CookedValue, 2))
        }
    } catch {}
    return 0
}

function Get-SafeCounterMulti {
    param([string]$CounterPath)
    try {
        $counter = Get-Counter -Counter $CounterPath -ErrorAction Stop
        if ($counter -and $counter.CounterSamples) {
            return $counter.CounterSamples
        }
    } catch {}
    return @()
}

# ============================================================================
# SITE STATUS
# ============================================================================

function Get-SiteStatusMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        Import-Module WebAdministration -ErrorAction Stop
        $sites = Get-Website -ErrorAction Stop

        [void]$sb.AppendLine('# HELP iis_site_state Site state (1=Started, 0=Stopped)')
        [void]$sb.AppendLine('# TYPE iis_site_state gauge')
        foreach ($site in $sites) {
            $name = $site.Name
            $state = if ($site.State -eq 'Started') { 1 } else { 0 }
            [void]$sb.AppendLine("iis_site_state{site=`"$name`"} $state")
        }
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_site_binding_info Site binding information (always 1)')
        [void]$sb.AppendLine('# TYPE iis_site_binding_info gauge')
        foreach ($site in $sites) {
            $name = $site.Name
            foreach ($binding in $site.Bindings.Collection) {
                $proto = $binding.protocol
                $info = $binding.bindingInformation -split ':'
                $addr = if ($info[0]) { $info[0] } else { '*' }
                $port = if ($info[1]) { $info[1] } else { '80' }
                $host = if ($info[2]) { $info[2] } else { '' }
                [void]$sb.AppendLine("iis_site_binding_info{site=`"$name`",protocol=`"$proto`",address=`"$addr`",port=`"$port`",hostname=`"$host`"} 1")
            }
        }
        [void]$sb.AppendLine('')
    }
    catch {
        Write-Warning "Failed to collect site status metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# APPLICATION POOLS
# ============================================================================

function Get-AppPoolMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        Import-Module WebAdministration -ErrorAction Stop
        $pools = Get-ChildItem IIS:\AppPools -ErrorAction Stop

        [void]$sb.AppendLine('# HELP iis_apppool_state Application pool state (1=Started, 0=Stopped)')
        [void]$sb.AppendLine('# TYPE iis_apppool_state gauge')
        foreach ($pool in $pools) {
            $name = $pool.Name
            $state = if ($pool.State -eq 'Started') { 1 } else { 0 }
            [void]$sb.AppendLine("iis_apppool_state{apppool=`"$name`"} $state")
        }
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_apppool_worker_process_count Number of worker processes per pool')
        [void]$sb.AppendLine('# TYPE iis_apppool_worker_process_count gauge')
        foreach ($pool in $pools) {
            $name = $pool.Name
            $wpCount = 0
            try {
                $workers = Get-ChildItem "IIS:\AppPools\$name\WorkerProcesses" -ErrorAction Stop
                $wpCount = @($workers).Count
            } catch {}
            [void]$sb.AppendLine("iis_apppool_worker_process_count{apppool=`"$name`"} $wpCount")
        }
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_apppool_recycle_count Application pool recycle count')
        [void]$sb.AppendLine('# TYPE iis_apppool_recycle_count gauge')
        foreach ($pool in $pools) {
            $name = $pool.Name
            $recycleCount = Get-SafeCounter "\W3SVC_W3WP(*$name*)\Total Threads"
            $recycleVal = 0
            try {
                $recycleEvents = Get-WinEvent -FilterHashtable @{
                    LogName = 'System'
                    ProviderName = 'WAS'
                    Id = 5074, 5075, 5076, 5077, 5078, 5079, 5080, 5186
                    StartTime = (Get-Date).AddHours(-24)
                } -ErrorAction Stop | Where-Object { $_.Message -like "*$name*" }
                $recycleVal = @($recycleEvents).Count
            } catch {}
            [void]$sb.AppendLine("iis_apppool_recycle_count{apppool=`"$name`"} $recycleVal")
        }
        [void]$sb.AppendLine('')
    }
    catch {
        Write-Warning "Failed to collect application pool metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# REQUEST THROUGHPUT
# ============================================================================

function Get-RequestThroughputMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        $connSamples = Get-SafeCounterMulti '\Web Service(*)\Current Connections'
        if ($connSamples.Count -gt 0) {
            [void]$sb.AppendLine('# HELP iis_current_connections Current active connections per site')
            [void]$sb.AppendLine('# TYPE iis_current_connections gauge')
            foreach ($sample in $connSamples) {
                $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                if ($instance -eq '_total') { continue }
                $val = [math]::Max(0, [math]::Round($sample.CookedValue))
                [void]$sb.AppendLine("iis_current_connections{site=`"$instance`"} $val")
            }
            [void]$sb.AppendLine('')
        }

        $reqSamples = Get-SafeCounterMulti '\Web Service(*)\Total Method Requests/sec'
        if ($reqSamples.Count -gt 0) {
            [void]$sb.AppendLine('# HELP iis_requests_per_second HTTP requests per second per site')
            [void]$sb.AppendLine('# TYPE iis_requests_per_second gauge')
            foreach ($sample in $reqSamples) {
                $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                if ($instance -eq '_total') { continue }
                $val = Format-MetricValue $sample.CookedValue
                [void]$sb.AppendLine("iis_requests_per_second{site=`"$instance`"} $val")
            }
            [void]$sb.AppendLine('')
        }

        $bytesSentSamples = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Sent/sec'
        if ($bytesSentSamples.Count -gt 0) {
            [void]$sb.AppendLine('# HELP iis_bytes_sent_per_second Bytes sent per second per site')
            [void]$sb.AppendLine('# TYPE iis_bytes_sent_per_second gauge')
            foreach ($sample in $bytesSentSamples) {
                $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                if ($instance -eq '_total') { continue }
                $val = Format-MetricValue $sample.CookedValue
                [void]$sb.AppendLine("iis_bytes_sent_per_second{site=`"$instance`"} $val")
            }
            [void]$sb.AppendLine('')
        }

        $bytesRecvSamples = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Received/sec'
        if ($bytesRecvSamples.Count -gt 0) {
            [void]$sb.AppendLine('# HELP iis_bytes_received_per_second Bytes received per second per site')
            [void]$sb.AppendLine('# TYPE iis_bytes_received_per_second gauge')
            foreach ($sample in $bytesRecvSamples) {
                $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                if ($instance -eq '_total') { continue }
                $val = Format-MetricValue $sample.CookedValue
                [void]$sb.AppendLine("iis_bytes_received_per_second{site=`"$instance`"} $val")
            }
            [void]$sb.AppendLine('')
        }

        $totalBytesSent = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Sent'
        if ($totalBytesSent.Count -gt 0) {
            [void]$sb.AppendLine('# HELP iis_total_bytes_sent Total bytes sent per site')
            [void]$sb.AppendLine('# TYPE iis_total_bytes_sent counter')
            foreach ($sample in $totalBytesSent) {
                $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                if ($instance -eq '_total') { continue }
                $val = [math]::Max(0, [math]::Round($sample.CookedValue))
                [void]$sb.AppendLine("iis_total_bytes_sent{site=`"$instance`"} $val")
            }
            [void]$sb.AppendLine('')
        }

        $totalBytesRecv = Get-SafeCounterMulti '\Web Service(*)\Total Bytes Received'
        if ($totalBytesRecv.Count -gt 0) {
            [void]$sb.AppendLine('# HELP iis_total_bytes_received Total bytes received per site')
            [void]$sb.AppendLine('# TYPE iis_total_bytes_received counter')
            foreach ($sample in $totalBytesRecv) {
                $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                if ($instance -eq '_total') { continue }
                $val = [math]::Max(0, [math]::Round($sample.CookedValue))
                [void]$sb.AppendLine("iis_total_bytes_received{site=`"$instance`"} $val")
            }
            [void]$sb.AppendLine('')
        }

        $totalReqs = Get-SafeCounterMulti '\Web Service(*)\Total Method Requests'
        if ($totalReqs.Count -gt 0) {
            [void]$sb.AppendLine('# HELP iis_requests_total Total requests per site')
            [void]$sb.AppendLine('# TYPE iis_requests_total counter')
            foreach ($sample in $totalReqs) {
                $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                if ($instance -eq '_total') { continue }
                $val = [math]::Max(0, [math]::Round($sample.CookedValue))
                [void]$sb.AppendLine("iis_requests_total{site=`"$instance`"} $val")
            }
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect request throughput metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# HTTP METHODS
# ============================================================================

function Get-HttpMethodMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        $methods = @{
            'get'    = '\Web Service(*)\Total Get Requests'
            'post'   = '\Web Service(*)\Total Post Requests'
            'put'    = '\Web Service(*)\Total Put Requests'
            'delete' = '\Web Service(*)\Total Delete Requests'
        }

        foreach ($method in $methods.GetEnumerator()) {
            $samples = Get-SafeCounterMulti $method.Value
            if ($samples.Count -gt 0) {
                [void]$sb.AppendLine("# HELP iis_$($method.Key)_requests_total Total $($method.Key.ToUpper()) requests per site")
                [void]$sb.AppendLine("# TYPE iis_$($method.Key)_requests_total counter")
                foreach ($sample in $samples) {
                    $instance = ($sample.Path -split '\\')[-2] -replace '[()]', ''
                    if ($instance -eq '_total') { continue }
                    $val = [math]::Max(0, [math]::Round($sample.CookedValue))
                    [void]$sb.AppendLine("iis_$($method.Key)_requests_total{site=`"$instance`"} $val")
                }
                [void]$sb.AppendLine('')
            }
        }
    }
    catch {
        Write-Warning "Failed to collect HTTP method metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# HTTP STATUS CODES (from IIS log analysis)
# ============================================================================

function Get-HttpStatusMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        Import-Module WebAdministration -ErrorAction Stop
        $sites = Get-Website -ErrorAction Stop
        $oneHourAgo = (Get-Date).AddHours(-1)

        $siteStatusCounts = @{}

        foreach ($site in $sites) {
            $logDir = "$($site.logFile.directory)\W3SVC$($site.id)" -replace '%SystemDrive%', $env:SystemDrive
            $siteStatusCounts[$site.Name] = @{ '2xx' = 0; '3xx' = 0; '4xx' = 0; '5xx' = 0 }

            if (-not (Test-Path $logDir)) { continue }

            $logFile = Get-ChildItem $logDir -Filter '*.log' -ErrorAction Stop |
                Sort-Object LastWriteTime -Descending |
                Select-Object -First 1

            if (-not $logFile) { continue }

            $reader = [System.IO.StreamReader]::new($logFile.FullName)
            try {
                while ($null -ne ($line = $reader.ReadLine())) {
                    if ($line.StartsWith('#')) { continue }
                    $fields = $line -split '\s+'
                    if ($fields.Count -lt 4) { continue }

                    try {
                        $logDate = [datetime]::ParseExact("$($fields[0]) $($fields[1])", 'yyyy-MM-dd HH:mm:ss', $null)
                        if ($logDate -lt $oneHourAgo) { continue }
                    } catch { continue }

                    $statusIdx = if ($fields.Count -ge 12) { 11 } elseif ($fields.Count -ge 10) { 9 } else { continue }
                    $statusCode = 0
                    if ([int]::TryParse($fields[$statusIdx], [ref]$statusCode)) {
                        if ($statusCode -ge 200 -and $statusCode -lt 300) { $siteStatusCounts[$site.Name]['2xx']++ }
                        elseif ($statusCode -ge 300 -and $statusCode -lt 400) { $siteStatusCounts[$site.Name]['3xx']++ }
                        elseif ($statusCode -ge 400 -and $statusCode -lt 500) { $siteStatusCounts[$site.Name]['4xx']++ }
                        elseif ($statusCode -ge 500 -and $statusCode -lt 600) { $siteStatusCounts[$site.Name]['5xx']++ }
                    }
                }
            }
            finally {
                $reader.Close()
            }
        }

        foreach ($code in @('2xx', '3xx', '4xx', '5xx')) {
            [void]$sb.AppendLine("# HELP iis_status_${code}_total Total ${code} responses per site (last hour)")
            [void]$sb.AppendLine("# TYPE iis_status_${code}_total gauge")
            foreach ($siteName in $siteStatusCounts.Keys) {
                [void]$sb.AppendLine("iis_status_${code}_total{site=`"$siteName`"} $($siteStatusCounts[$siteName][$code])")
            }
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect HTTP status code metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# SSL/TLS
# ============================================================================

function Get-SslMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        $sslCurrent = Get-SafeCounter '\Web Service(_Total)\Current ISAPI Extension Requests'
        $sslTotal = Get-SafeCounter '\Web Service(_Total)\Total Connection Attempts (all instances)'

        [void]$sb.AppendLine('# HELP iis_ssl_connections_current Current SSL connections')
        [void]$sb.AppendLine('# TYPE iis_ssl_connections_current gauge')
        [void]$sb.AppendLine("iis_ssl_connections_current $sslCurrent")
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_ssl_connections_per_second SSL connections per second')
        [void]$sb.AppendLine('# TYPE iis_ssl_connections_per_second gauge')
        $sslPerSec = Get-SafeCounter '\Web Service(_Total)\Connection Attempts/sec'
        [void]$sb.AppendLine("iis_ssl_connections_per_second $sslPerSec")
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_ssl_handshake_failures SSL/TLS handshake failures')
        [void]$sb.AppendLine('# TYPE iis_ssl_handshake_failures counter')
        $sslFailures = 0
        try {
            $sslEvents = Get-WinEvent -FilterHashtable @{
                LogName = 'System'
                ProviderName = 'Schannel'
                Level = 2, 3
                StartTime = (Get-Date).AddHours(-24)
            } -ErrorAction Stop
            $sslFailures = @($sslEvents).Count
        } catch {}
        [void]$sb.AppendLine("iis_ssl_handshake_failures $sslFailures")
        [void]$sb.AppendLine('')
    }
    catch {
        Write-Warning "Failed to collect SSL metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# WORKER PROCESSES
# ============================================================================

function Get-WorkerProcessMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        Import-Module WebAdministration -ErrorAction Stop
        $pools = Get-ChildItem IIS:\AppPools -ErrorAction Stop

        [void]$sb.AppendLine('# HELP iis_worker_cpu_percent Worker process CPU usage percentage per pool')
        [void]$sb.AppendLine('# TYPE iis_worker_cpu_percent gauge')

        $cpuLines = [System.Text.StringBuilder]::new()
        $memLines = [System.Text.StringBuilder]::new()
        $reqLines = [System.Text.StringBuilder]::new()
        $upLines = [System.Text.StringBuilder]::new()

        foreach ($pool in $pools) {
            $name = $pool.Name
            try {
                $workers = Get-ChildItem "IIS:\AppPools\$name\WorkerProcesses" -ErrorAction Stop
                foreach ($worker in $workers) {
                    $pid = $worker.processId
                    if ($pid -and $pid -gt 0) {
                        $proc = Get-Process -Id $pid -ErrorAction Stop
                        $cpuPercent = Format-MetricValue $proc.CPU
                        $memBytes = $proc.WorkingSet64
                        $uptimeSeconds = [math]::Round(((Get-Date) - $proc.StartTime).TotalSeconds)
                        [void]$cpuLines.AppendLine("iis_worker_cpu_percent{apppool=`"$name`"} $cpuPercent")
                        [void]$memLines.AppendLine("iis_worker_memory_bytes{apppool=`"$name`"} $memBytes")
                        [void]$upLines.AppendLine("iis_worker_uptime_seconds{apppool=`"$name`"} $uptimeSeconds")
                    }
                }

                $activeReqs = Get-SafeCounter "\W3SVC_W3WP(*$name*)\Active Requests"
                [void]$reqLines.AppendLine("iis_worker_active_requests{apppool=`"$name`"} $activeReqs")
            } catch {}
        }

        [void]$sb.AppendLine('# HELP iis_worker_cpu_percent Worker process CPU time per pool')
        [void]$sb.AppendLine('# TYPE iis_worker_cpu_percent gauge')
        [void]$sb.Append($cpuLines.ToString())
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_worker_memory_bytes Worker process working set in bytes per pool')
        [void]$sb.AppendLine('# TYPE iis_worker_memory_bytes gauge')
        [void]$sb.Append($memLines.ToString())
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_worker_active_requests Active requests per worker process per pool')
        [void]$sb.AppendLine('# TYPE iis_worker_active_requests gauge')
        [void]$sb.Append($reqLines.ToString())
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_worker_uptime_seconds Worker process uptime in seconds per pool')
        [void]$sb.AppendLine('# TYPE iis_worker_uptime_seconds gauge')
        [void]$sb.Append($upLines.ToString())
        [void]$sb.AppendLine('')
    }
    catch {
        Write-Warning "Failed to collect worker process metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# CACHE
# ============================================================================

function Get-CacheMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        $cacheMetrics = @(
            @{ Name = 'iis_cache_output_entries'; Counter = '\Web Service Cache\Current Files Cached'; Help = 'Current output cache entries' },
            @{ Name = 'iis_cache_output_hits'; Counter = '\Web Service Cache\File Cache Hits'; Help = 'Output cache hit count' },
            @{ Name = 'iis_cache_output_misses'; Counter = '\Web Service Cache\File Cache Misses'; Help = 'Output cache miss count' },
            @{ Name = 'iis_cache_uri_hits'; Counter = '\Web Service Cache\URI Cache Hits'; Help = 'URI cache hit count' },
            @{ Name = 'iis_cache_uri_misses'; Counter = '\Web Service Cache\URI Cache Misses'; Help = 'URI cache miss count' },
            @{ Name = 'iis_cache_kernel_hits'; Counter = '\Web Service Cache\Kernel: URI Cache Hits'; Help = 'Kernel cache hit count' }
        )

        foreach ($metric in $cacheMetrics) {
            $val = Get-SafeCounter $metric.Counter
            [void]$sb.AppendLine("# HELP $($metric.Name) $($metric.Help)")
            [void]$sb.AppendLine("# TYPE $($metric.Name) gauge")
            [void]$sb.AppendLine("$($metric.Name) $val")
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect cache metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# FAILED REQUESTS
# ============================================================================

function Get-FailedRequestMetrics {
    $sb = [System.Text.StringBuilder]::new()

    try {
        Import-Module WebAdministration -ErrorAction Stop
        $sites = Get-Website -ErrorAction Stop
        $oneHourAgo = (Get-Date).AddHours(-1)
        $total500 = 0

        [void]$sb.AppendLine('# HELP iis_failed_requests_total Total failed requests per site (from event log)')
        [void]$sb.AppendLine('# TYPE iis_failed_requests_total gauge')

        foreach ($site in $sites) {
            $logDir = "$($site.logFile.directory)\W3SVC$($site.id)" -replace '%SystemDrive%', $env:SystemDrive
            $count500 = 0

            if (Test-Path $logDir) {
                $logFile = Get-ChildItem $logDir -Filter '*.log' -ErrorAction SilentlyContinue |
                    Sort-Object LastWriteTime -Descending |
                    Select-Object -First 1

                if ($logFile) {
                    $reader = [System.IO.StreamReader]::new($logFile.FullName)
                    try {
                        while ($null -ne ($line = $reader.ReadLine())) {
                            if ($line.StartsWith('#')) { continue }
                            $fields = $line -split '\s+'
                            if ($fields.Count -lt 4) { continue }

                            try {
                                $logDate = [datetime]::ParseExact("$($fields[0]) $($fields[1])", 'yyyy-MM-dd HH:mm:ss', $null)
                                if ($logDate -lt $oneHourAgo) { continue }
                            } catch { continue }

                            $statusIdx = if ($fields.Count -ge 12) { 11 } elseif ($fields.Count -ge 10) { 9 } else { continue }
                            $statusCode = 0
                            if ([int]::TryParse($fields[$statusIdx], [ref]$statusCode)) {
                                if ($statusCode -ge 500) { $count500++ }
                            }
                        }
                    }
                    finally {
                        $reader.Close()
                    }
                }
            }

            $total500 += $count500
            [void]$sb.AppendLine("iis_failed_requests_total{site=`"$($site.Name)`"} $count500")
        }
        [void]$sb.AppendLine('')

        [void]$sb.AppendLine('# HELP iis_failed_requests_500_last_hour HTTP 500 errors in the last hour')
        [void]$sb.AppendLine('# TYPE iis_failed_requests_500_last_hour gauge')
        [void]$sb.AppendLine("iis_failed_requests_500_last_hour $total500")
        [void]$sb.AppendLine('')
    }
    catch {
        Write-Warning "Failed to collect failed request metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# COLLECT ALL METRICS
# ============================================================================

function Get-AllMetrics {
    $scriptStart = Get-Date
    $sb = [System.Text.StringBuilder]::new()

    # Exporter up
    [void]$sb.AppendLine('# HELP iis_up Exporter status (1=up, 0=down)')
    [void]$sb.AppendLine('# TYPE iis_up gauge')
    [void]$sb.AppendLine('iis_up 1')
    [void]$sb.AppendLine('')

    # Exporter info
    [void]$sb.AppendLine('# HELP iis_exporter_info Exporter version information')
    [void]$sb.AppendLine('# TYPE iis_exporter_info gauge')
    [void]$sb.AppendLine('iis_exporter_info{version="1.0"} 1')
    [void]$sb.AppendLine('')

    # Collect all sections
    [void]$sb.Append((Get-SiteStatusMetrics))
    [void]$sb.Append((Get-AppPoolMetrics))
    [void]$sb.Append((Get-RequestThroughputMetrics))
    [void]$sb.Append((Get-HttpMethodMetrics))
    [void]$sb.Append((Get-HttpStatusMetrics))
    [void]$sb.Append((Get-SslMetrics))
    [void]$sb.Append((Get-WorkerProcessMetrics))
    [void]$sb.Append((Get-CacheMetrics))
    [void]$sb.Append((Get-FailedRequestMetrics))

    # Exporter runtime
    $scriptEnd = Get-Date
    $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds
    $timestamp = Get-UnixTimestamp

    [void]$sb.AppendLine('# HELP iis_exporter_duration_seconds Time to generate all metrics')
    [void]$sb.AppendLine('# TYPE iis_exporter_duration_seconds gauge')
    [void]$sb.AppendLine("iis_exporter_duration_seconds $duration")
    [void]$sb.AppendLine('')
    [void]$sb.AppendLine('# HELP iis_exporter_last_run_timestamp Unix timestamp of last successful run')
    [void]$sb.AppendLine('# TYPE iis_exporter_last_run_timestamp gauge')
    [void]$sb.AppendLine("iis_exporter_last_run_timestamp $timestamp")
    [void]$sb.AppendLine('')

    $sb.ToString()
}

# ============================================================================
# HTTP SERVER MODE
# ============================================================================

function Start-HttpServer {
    param([int]$ListenPort)

    $prefix = "http://+:$ListenPort/"
    $listener = [System.Net.HttpListener]::new()
    $listener.Prefixes.Add($prefix)

    try {
        $listener.Start()
        Write-Host "Starting IIS metrics exporter on port $ListenPort..." -ForegroundColor Green
        Write-Host "Metrics available at http://localhost:$ListenPort/metrics"

        while ($listener.IsListening) {
            $context  = $listener.GetContext()
            $request  = $context.Request
            $response = $context.Response

            if ($request.Url.AbsolutePath -eq '/metrics') {
                $metrics = Get-AllMetrics
                $buffer  = [System.Text.Encoding]::UTF8.GetBytes($metrics)
                $response.ContentType = 'text/plain; version=0.0.4; charset=utf-8'
            }
            else {
                $html = @"
<!DOCTYPE html>
<html>
<head><title>IIS Metrics Exporter v1.0</title></head>
<body>
<h1>IIS Metrics Exporter v1.0</h1>
<p><a href="/metrics">Metrics</a></p>
<h2>Sections</h2>
<ul>
<li>Site status and bindings</li>
<li>Application pool state and worker process count</li>
<li>Request throughput (connections, requests/sec, bytes)</li>
<li>HTTP method counts (GET, POST, PUT, DELETE)</li>
<li>HTTP status codes (2xx, 3xx, 4xx, 5xx from log analysis)</li>
<li>SSL/TLS connections and handshake failures</li>
<li>Worker process CPU, memory, active requests, uptime</li>
<li>Cache hit/miss ratios (output, URI, kernel)</li>
<li>Failed requests (500 errors from log analysis)</li>
</ul>
</body>
</html>
"@
                $buffer = [System.Text.Encoding]::UTF8.GetBytes($html)
                $response.ContentType = 'text/html; charset=utf-8'
            }

            $response.ContentLength64 = $buffer.Length
            $response.OutputStream.Write($buffer, 0, $buffer.Length)
            $response.OutputStream.Close()
        }
    }
    catch {
        Write-Error "HTTP server error: $_"
        Write-Error "If access denied, run: netsh http add urlacl url=http://+:$ListenPort/ user=Everyone"
    }
    finally {
        if ($listener.IsListening) {
            $listener.Stop()
        }
    }
}

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

switch ($Mode) {
    'http' {
        Start-HttpServer -ListenPort $Port
    }
    'textfile' {
        if (-not $OutputFile) {
            $OutputFile = Join-Path $TextfileDir 'iis_metrics.prom'
        }

        $outputDir = Split-Path $OutputFile -Parent
        if (-not (Test-Path $outputDir)) {
            New-Item -Path $outputDir -ItemType Directory -Force | Out-Null
        }

        $tempFile = Join-Path $outputDir ".iis_metrics.$PID.tmp"

        try {
            $metrics = Get-AllMetrics
            $metrics | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline

            $lineCount = ($metrics -split "`n").Count
            if ($lineCount -lt 10) {
                Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
                Write-Error "Metrics file too small ($lineCount lines), keeping previous"
                exit 1
            }

            Move-Item -Path $tempFile -Destination $OutputFile -Force
            Write-Host "Metrics written to $OutputFile ($lineCount lines)" -ForegroundColor Green
        }
        catch {
            Remove-Item -Path $tempFile -Force -ErrorAction SilentlyContinue
            Write-Error "Failed to generate metrics: $_"
            exit 1
        }
    }
    default {
        Get-AllMetrics | Write-Output
    }
}
