<#
.SYNOPSIS
    Windows IIS Log Prometheus Exporter
.DESCRIPTION
    Prometheus exporter for IIS log files - parses W3C Extended log format to
    extract request counts, status code distribution, response times, bandwidth,
    top URIs, and error rates per IIS site. Exports metrics as Prometheus text.
.PARAMETER Mode
    Output mode: 'stdout' (default), 'textfile', or 'http'
.PARAMETER Port
    HTTP port for http mode (default: 9542)
.PARAMETER TextfileDir
    Directory for textfile collector output (default: C:\ProgramData\node_exporter)
.PARAMETER LogDir
    IIS log directory (default: C:\inetpub\logs\LogFiles)
.PARAMETER WindowMinutes
    Parse logs from the last N minutes (default: 5)
.PARAMETER InstallScheduledTask
    Switch to create a scheduled task for automatic execution
.PARAMETER TaskIntervalMinutes
    Interval in minutes for the scheduled task (default: 2)
.NOTES
    Author:  Phil Connor
    Contact: contact@mylinux.work
    Website: https://mylinux.work
    License: MIT
    Version: 1.0

    Metrics Exported:
      Core:
        - windows_iis_log_up
        - windows_iis_log_exporter_info{version}

      Requests:
        - windows_iis_log_requests_total{site,method,status}
        - windows_iis_log_requests_by_status_class{site,class}
        - windows_iis_log_bytes_sent_total{site}
        - windows_iis_log_bytes_received_total{site}

      Response Times:
        - windows_iis_log_time_taken_avg_ms{site}
        - windows_iis_log_time_taken_max_ms{site}
        - windows_iis_log_time_taken_p95_ms{site}

      Errors:
        - windows_iis_log_errors_total{site}
        - windows_iis_log_error_rate{site}

      Sites:
        - windows_iis_log_sites_total
        - windows_iis_log_log_file_age_seconds{site}

      Exporter:
        - windows_iis_log_exporter_duration_seconds
        - windows_iis_log_exporter_last_run_timestamp
#>

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

    [int]$Port = 9542,

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

    [string]$LogDir = 'C:\inetpub\logs\LogFiles',

    [int]$WindowMinutes = 5,

    [switch]$InstallScheduledTask,

    [int]$TaskIntervalMinutes = 2
)

$ErrorActionPreference = 'SilentlyContinue'
$Version = '1.0'

# ============================================================================
# SCHEDULED TASK INSTALLER
# ============================================================================

if ($InstallScheduledTask) {
    $scriptPath = $MyInvocation.MyCommand.Path
    $action = New-ScheduledTaskAction -Execute 'powershell.exe' `
        -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" -Mode textfile"
    $trigger = New-ScheduledTaskTrigger -RepetitionInterval (New-TimeSpan -Minutes $TaskIntervalMinutes) `
        -Once -At (Get-Date)
    $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries `
        -StartWhenAvailable -ExecutionTimeLimit (New-TimeSpan -Minutes 5)
    Register-ScheduledTask -TaskName 'WindowsIISLogExporter' -Action $action -Trigger $trigger `
        -Settings $settings -RunLevel Highest -User 'SYSTEM' -Force
    Write-Host 'Scheduled task "WindowsIISLogExporter" installed successfully.'
    exit 0
}

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

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

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

# ============================================================================
# LOG PARSING
# ============================================================================

function Parse-IISLogFile {
    param([string]$FilePath, [datetime]$Since)

    $results = @{
        Requests = @{}
        StatusClasses = @{}
        BytesSent = 0
        BytesReceived = 0
        TimeTaken = [System.Collections.Generic.List[int]]::new()
        Errors = 0
        Total = 0
    }

    $fieldMap = @{}

    Get-Content $FilePath -Tail 10000 | ForEach-Object {
        $line = $_
        if ($line.StartsWith('#Fields:')) {
            $fields = $line.Substring(9).Trim() -split '\s+'
            for ($i = 0; $i -lt $fields.Count; $i++) {
                $fieldMap[$fields[$i]] = $i
            }
            return
        }
        if ($line.StartsWith('#') -or [string]::IsNullOrWhiteSpace($line)) { return }
        if ($fieldMap.Count -eq 0) { return }

        $parts = $line -split '\s+'

        # Parse date/time
        $dateIdx = $fieldMap['date']
        $timeIdx = $fieldMap['time']
        if ($null -ne $dateIdx -and $null -ne $timeIdx -and $parts.Count -gt [Math]::Max($dateIdx,$timeIdx)) {
            try {
                $entryTime = [datetime]::ParseExact("$($parts[$dateIdx]) $($parts[$timeIdx])", 'yyyy-MM-dd HH:mm:ss', $null)
                if ($entryTime -lt $Since) { return }
            } catch { return }
        }

        $results.Total++

        # Method
        $methodIdx = $fieldMap['cs-method']
        $method = if ($null -ne $methodIdx -and $parts.Count -gt $methodIdx) { $parts[$methodIdx] } else { 'UNKNOWN' }

        # Status
        $statusIdx = $fieldMap['sc-status']
        $status = if ($null -ne $statusIdx -and $parts.Count -gt $statusIdx) { $parts[$statusIdx] } else { '0' }

        $key = "${method}_${status}"
        if (-not $results.Requests.ContainsKey($key)) { $results.Requests[$key] = 0 }
        $results.Requests[$key]++

        # Status class
        $class = "${status}".Substring(0,1) + "xx"
        if (-not $results.StatusClasses.ContainsKey($class)) { $results.StatusClasses[$class] = 0 }
        $results.StatusClasses[$class]++

        # Errors (4xx and 5xx)
        if ($status -match '^[45]') { $results.Errors++ }

        # Bytes
        $sentIdx = $fieldMap['sc-bytes']
        if ($null -ne $sentIdx -and $parts.Count -gt $sentIdx -and $parts[$sentIdx] -match '^\d+$') {
            $results.BytesSent += [long]$parts[$sentIdx]
        }
        $recvIdx = $fieldMap['cs-bytes']
        if ($null -ne $recvIdx -and $parts.Count -gt $recvIdx -and $parts[$recvIdx] -match '^\d+$') {
            $results.BytesReceived += [long]$parts[$recvIdx]
        }

        # Time taken
        $ttIdx = $fieldMap['time-taken']
        if ($null -ne $ttIdx -and $parts.Count -gt $ttIdx -and $parts[$ttIdx] -match '^\d+$') {
            $results.TimeTaken.Add([int]$parts[$ttIdx])
        }
    }

    return $results
}

# ============================================================================
# METRIC COLLECTION
# ============================================================================

function Get-IISLogMetrics {
    $startTime = Get-Date
    $metrics = [System.Collections.Generic.List[string]]::new()

    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_up' 'gauge' 'Exporter status (1=up, 0=down)'))

    if (-not (Test-Path $LogDir)) {
        $metrics.Add('windows_iis_log_up 0')
        return ($metrics -join "`n")
    }

    $metrics.Add('windows_iis_log_up 1')
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_exporter_info' 'gauge' 'Exporter version'))
    $metrics.Add("windows_iis_log_exporter_info{version=`"$Version`"} 1")

    $since = (Get-Date).AddMinutes(-$WindowMinutes)
    $siteDirs = Get-ChildItem -Path $LogDir -Directory -ErrorAction SilentlyContinue
    $siteCount = 0

    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_requests_total' 'gauge' 'Requests by method and status'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_requests_by_status_class' 'gauge' 'Requests by status class'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_bytes_sent_total' 'gauge' 'Total bytes sent'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_bytes_received_total' 'gauge' 'Total bytes received'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_time_taken_avg_ms' 'gauge' 'Average response time in ms'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_time_taken_max_ms' 'gauge' 'Max response time in ms'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_time_taken_p95_ms' 'gauge' '95th percentile response time in ms'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_errors_total' 'gauge' 'Total 4xx and 5xx errors'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_error_rate' 'gauge' 'Error rate (errors/total)'))
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_log_file_age_seconds' 'gauge' 'Seconds since log file was last modified'))

    foreach ($siteDir in $siteDirs) {
        $siteName = Get-PrometheusEscape $siteDir.Name
        $logFiles = Get-ChildItem -Path $siteDir.FullName -Filter "*.log" -ErrorAction SilentlyContinue |
            Sort-Object LastWriteTime -Descending | Select-Object -First 1

        if (-not $logFiles) { continue }
        $siteCount++

        $logFile = $logFiles[0]
        $fileAge = [math]::Round(((Get-Date) - $logFile.LastWriteTime).TotalSeconds)
        $metrics.Add("windows_iis_log_log_file_age_seconds{site=`"$siteName`"} $fileAge")

        $parsed = Parse-IISLogFile -FilePath $logFile.FullName -Since $since
        if ($null -eq $parsed -or $parsed.Total -eq 0) { continue }

        # Requests by method+status
        foreach ($key in $parsed.Requests.Keys) {
            $parts = $key -split '_'
            $method = $parts[0]
            $status = $parts[1]
            $count = $parsed.Requests[$key]
            $metrics.Add("windows_iis_log_requests_total{site=`"$siteName`",method=`"$method`",status=`"$status`"} $count")
        }

        # Status classes
        foreach ($class in $parsed.StatusClasses.Keys) {
            $count = $parsed.StatusClasses[$class]
            $metrics.Add("windows_iis_log_requests_by_status_class{site=`"$siteName`",class=`"$class`"} $count")
        }

        # Bytes
        $metrics.Add("windows_iis_log_bytes_sent_total{site=`"$siteName`"} $($parsed.BytesSent)")
        $metrics.Add("windows_iis_log_bytes_received_total{site=`"$siteName`"} $($parsed.BytesReceived)")

        # Response times
        if ($parsed.TimeTaken.Count -gt 0) {
            $sorted = $parsed.TimeTaken | Sort-Object
            $avg = [math]::Round(($sorted | Measure-Object -Average).Average)
            $max = $sorted[-1]
            $p95idx = [math]::Floor($sorted.Count * 0.95)
            $p95 = $sorted[[math]::Min($p95idx, $sorted.Count - 1)]
            $metrics.Add("windows_iis_log_time_taken_avg_ms{site=`"$siteName`"} $avg")
            $metrics.Add("windows_iis_log_time_taken_max_ms{site=`"$siteName`"} $max")
            $metrics.Add("windows_iis_log_time_taken_p95_ms{site=`"$siteName`"} $p95")
        }

        # Errors
        $metrics.Add("windows_iis_log_errors_total{site=`"$siteName`"} $($parsed.Errors)")
        $errorRate = if ($parsed.Total -gt 0) { [math]::Round($parsed.Errors / $parsed.Total, 4) } else { 0 }
        $metrics.Add("windows_iis_log_error_rate{site=`"$siteName`"} $errorRate")
    }

    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_sites_total' 'gauge' 'Total IIS sites with logs'))
    $metrics.Add("windows_iis_log_sites_total $siteCount")

    # Duration
    $duration = [math]::Round(((Get-Date) - $startTime).TotalSeconds, 2)
    $timestamp = [math]::Round((Get-Date -UFormat %s), 0)
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_exporter_duration_seconds' 'gauge' 'Script execution time'))
    $metrics.Add("windows_iis_log_exporter_duration_seconds $duration")
    $metrics.AddRange([string[]](Write-MetricHeader 'windows_iis_log_exporter_last_run_timestamp' 'gauge' 'Unix timestamp of last run'))
    $metrics.Add("windows_iis_log_exporter_last_run_timestamp $timestamp")

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

# ============================================================================
# OUTPUT
# ============================================================================

switch ($Mode) {
    'stdout' {
        Get-IISLogMetrics
    }
    'textfile' {
        if (-not (Test-Path $TextfileDir)) { New-Item -ItemType Directory -Path $TextfileDir -Force | Out-Null }
        $tempFile = Join-Path $TextfileDir "windows-iis-log-metrics.tmp"
        $finalFile = Join-Path $TextfileDir "windows-iis-log-metrics.prom"
        Get-IISLogMetrics | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline
        Move-Item -Path $tempFile -Destination $finalFile -Force
        Write-Host "Wrote metrics to $finalFile"
    }
    'http' {
        $prefix = "http://+:$Port/metrics/"
        $listener = [System.Net.HttpListener]::new()
        $listener.Prefixes.Add($prefix)
        $listener.Start()
        Write-Host "Listening on port $Port..."
        try {
            while ($listener.IsListening) {
                $context = $listener.GetContext()
                $response = $context.Response
                $metricsOutput = Get-IISLogMetrics
                $buffer = [System.Text.Encoding]::UTF8.GetBytes($metricsOutput)
                $response.ContentType = 'text/plain; version=0.0.4; charset=utf-8'
                $response.ContentLength64 = $buffer.Length
                $response.OutputStream.Write($buffer, 0, $buffer.Length)
                $response.OutputStream.Close()
            }
        } finally {
            $listener.Stop()
        }
    }
}
