<#
.SYNOPSIS
    MSSQL Prometheus Metrics Exporter
.DESCRIPTION
    Prometheus exporter for Microsoft SQL Server. Queries DMVs (Dynamic Management
    Views) and system catalog views to collect instance health, database sizes,
    backup freshness, connection counts, wait statistics, buffer cache performance,
    SQL Agent job status, and TempDB usage. Outputs Prometheus-compatible text
    format for consumption by windows_exporter textfile collector.
.PARAMETER Mode
    Output mode: 'stdout' (default), 'textfile', or 'http'
.PARAMETER SqlInstance
    SQL Server instance name or hostname (default: localhost)
.PARAMETER ConnectionString
    Full connection string (overrides SqlInstance)
.PARAMETER TextfileDir
    Directory for textfile collector output
.PARAMETER HttpPort
    HTTP port for http mode (default: 9399)
.PARAMETER InstallScheduledTask
    Switch to create a scheduled task for periodic collection
.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:
      Instance:
        - mssql_up
        - mssql_instance_uptime_seconds
        - mssql_version_info{version,edition}

      Database Sizes:
        - mssql_database_size_bytes{database}
        - mssql_database_log_size_bytes{database}
        - mssql_database_log_usage_percent{database}

      Backup Freshness:
        - mssql_backup_age_full_hours{database}
        - mssql_backup_age_diff_hours{database}

      Connections:
        - mssql_connections_active{database}
        - mssql_sessions_total
        - mssql_sessions_blocked

      Wait Statistics:
        - mssql_wait_time_seconds{wait_type}
        - mssql_waiting_tasks{wait_type}

      Buffer Cache:
        - mssql_buffer_cache_hit_ratio
        - mssql_page_life_expectancy_seconds

      SQL Agent Jobs:
        - mssql_agent_job_status{job}
        - mssql_agent_job_duration_seconds{job}

      TempDB:
        - mssql_tempdb_size_bytes
        - mssql_tempdb_usage_bytes

      Errors:
        - mssql_error_count

      Exporter:
        - mssql_collector_duration_seconds
#>

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

    [string]$SqlInstance = 'localhost',

    [string]$ConnectionString,

    [string]$TextfileDir = 'C:\Program Files\windows_exporter\textfile_inputs',

    [int]$HttpPort = 9399,

    [switch]$InstallScheduledTask,

    [int]$TaskIntervalMinutes = 5
)

# Handle --textfile and --http as positional arguments
if ($args -contains '--textfile') { $Mode = 'textfile' }
if ($args -contains '--http') { $Mode = 'http' }

# ============================================================================
# SCHEDULED TASK INSTALLATION
# ============================================================================

if ($InstallScheduledTask) {
    $taskName = "MSSQLMetricsExporter"
    $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 MSSQL 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"
    }
    if ($Mode -eq 'stdout') { return }
}

$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 Invoke-SqlQuery {
    param([string]$Query)
    try {
        if ($ConnectionString) {
            Invoke-Sqlcmd -ConnectionString $ConnectionString -Query $Query -ErrorAction Stop
        } else {
            Invoke-Sqlcmd -ServerInstance $SqlInstance -Query $Query -TrustServerCertificate -ErrorAction Stop
        }
    } catch {
        Write-Warning "SQL query failed: $_"
        return $null
    }
}

function Format-Label {
    param([string]$Value)
    $Value -replace '"', '' -replace '\\', '' -replace "`n", ' ' -replace "`r", ''
}

# ============================================================================
# INSTANCE METRICS
# ============================================================================

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

    try {
        # Version info
        $version = Invoke-SqlQuery "SELECT SERVERPROPERTY('ProductVersion') AS Version, SERVERPROPERTY('Edition') AS Edition"
        if ($version) {
            $ver = Format-Label $version.Version
            $ed = Format-Label $version.Edition
            [void]$sb.AppendLine('# HELP mssql_version_info SQL Server version information (always 1)')
            [void]$sb.AppendLine('# TYPE mssql_version_info gauge')
            [void]$sb.AppendLine("mssql_version_info{version=`"$ver`",edition=`"$ed`"} 1")
            [void]$sb.AppendLine('')
        }

        # Uptime
        $uptime = Invoke-SqlQuery "SELECT DATEDIFF(SECOND, sqlserver_start_time, GETDATE()) AS uptime_seconds FROM sys.dm_os_sys_info"
        if ($uptime) {
            [void]$sb.AppendLine('# HELP mssql_instance_uptime_seconds Seconds since SQL Server instance started')
            [void]$sb.AppendLine('# TYPE mssql_instance_uptime_seconds gauge')
            [void]$sb.AppendLine("mssql_instance_uptime_seconds $($uptime.uptime_seconds)")
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect instance metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# DATABASE SIZE METRICS
# ============================================================================

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

    try {
        $sizes = Invoke-SqlQuery "
            SELECT
                d.name AS database_name,
                SUM(CASE WHEN mf.type = 0 THEN mf.size END) * 8 * 1024 AS data_size_bytes,
                SUM(CASE WHEN mf.type = 1 THEN mf.size END) * 8 * 1024 AS log_size_bytes
            FROM sys.databases d
            JOIN sys.master_files mf ON d.database_id = mf.database_id
            WHERE d.state = 0
            GROUP BY d.name"

        if ($sizes) {
            [void]$sb.AppendLine('# HELP mssql_database_size_bytes Data file size per database in bytes')
            [void]$sb.AppendLine('# TYPE mssql_database_size_bytes gauge')
            foreach ($row in $sizes) {
                $dbName = Format-Label $row.database_name
                $dataSize = if ($row.data_size_bytes) { $row.data_size_bytes } else { 0 }
                [void]$sb.AppendLine("mssql_database_size_bytes{database=`"$dbName`"} $dataSize")
            }
            [void]$sb.AppendLine('')

            [void]$sb.AppendLine('# HELP mssql_database_log_size_bytes Log file size per database in bytes')
            [void]$sb.AppendLine('# TYPE mssql_database_log_size_bytes gauge')
            foreach ($row in $sizes) {
                $dbName = Format-Label $row.database_name
                $logSize = if ($row.log_size_bytes) { $row.log_size_bytes } else { 0 }
                [void]$sb.AppendLine("mssql_database_log_size_bytes{database=`"$dbName`"} $logSize")
            }
            [void]$sb.AppendLine('')
        }

        # Log usage percentage
        $logUsage = Invoke-SqlQuery "DBCC SQLPERF(LOGSPACE) WITH NO_INFOMSGS"
        if ($logUsage) {
            [void]$sb.AppendLine('# HELP mssql_database_log_usage_percent Log file usage percentage per database')
            [void]$sb.AppendLine('# TYPE mssql_database_log_usage_percent gauge')
            foreach ($row in $logUsage) {
                $dbName = Format-Label $row.'Database Name'
                $usage = Format-MetricValue $row.'Log Space Used (%)'
                [void]$sb.AppendLine("mssql_database_log_usage_percent{database=`"$dbName`"} $usage")
            }
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect database size metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# BACKUP FRESHNESS METRICS
# ============================================================================

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

    try {
        # Full backup age
        $fullBackups = Invoke-SqlQuery "
            SELECT
                d.name AS database_name,
                CASE
                    WHEN MAX(b.backup_finish_date) IS NULL THEN -1
                    ELSE DATEDIFF(HOUR, MAX(b.backup_finish_date), GETDATE())
                END AS hours_since_backup
            FROM sys.databases d
            LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'D'
            WHERE d.database_id > 4 AND d.state = 0
            GROUP BY d.name"

        if ($fullBackups) {
            [void]$sb.AppendLine('# HELP mssql_backup_age_full_hours Hours since last full backup per database (-1 = no backup)')
            [void]$sb.AppendLine('# TYPE mssql_backup_age_full_hours gauge')
            foreach ($row in $fullBackups) {
                $dbName = Format-Label $row.database_name
                [void]$sb.AppendLine("mssql_backup_age_full_hours{database=`"$dbName`"} $($row.hours_since_backup)")
            }
            [void]$sb.AppendLine('')
        }

        # Differential backup age
        $diffBackups = Invoke-SqlQuery "
            SELECT
                d.name AS database_name,
                CASE
                    WHEN MAX(b.backup_finish_date) IS NULL THEN -1
                    ELSE DATEDIFF(HOUR, MAX(b.backup_finish_date), GETDATE())
                END AS hours_since_backup
            FROM sys.databases d
            LEFT JOIN msdb.dbo.backupset b ON d.name = b.database_name AND b.type = 'I'
            WHERE d.database_id > 4 AND d.state = 0
            GROUP BY d.name"

        if ($diffBackups) {
            [void]$sb.AppendLine('# HELP mssql_backup_age_diff_hours Hours since last differential backup per database (-1 = no backup)')
            [void]$sb.AppendLine('# TYPE mssql_backup_age_diff_hours gauge')
            foreach ($row in $diffBackups) {
                $dbName = Format-Label $row.database_name
                [void]$sb.AppendLine("mssql_backup_age_diff_hours{database=`"$dbName`"} $($row.hours_since_backup)")
            }
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect backup metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# CONNECTION METRICS
# ============================================================================

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

    try {
        # Active connections per database
        $connections = Invoke-SqlQuery "
            SELECT DB_NAME(database_id) AS database_name, COUNT(*) AS active_connections
            FROM sys.dm_exec_sessions
            WHERE is_user_process = 1 AND database_id > 0
            GROUP BY database_id"

        if ($connections) {
            [void]$sb.AppendLine('# HELP mssql_connections_active Active connections per database')
            [void]$sb.AppendLine('# TYPE mssql_connections_active gauge')
            foreach ($row in $connections) {
                $dbName = Format-Label $row.database_name
                [void]$sb.AppendLine("mssql_connections_active{database=`"$dbName`"} $($row.active_connections)")
            }
            [void]$sb.AppendLine('')
        }

        # Total active sessions
        $totalSessions = Invoke-SqlQuery "SELECT COUNT(*) AS total FROM sys.dm_exec_sessions WHERE is_user_process = 1"
        if ($totalSessions) {
            [void]$sb.AppendLine('# HELP mssql_sessions_total Total active user sessions')
            [void]$sb.AppendLine('# TYPE mssql_sessions_total gauge')
            [void]$sb.AppendLine("mssql_sessions_total $($totalSessions.total)")
            [void]$sb.AppendLine('')
        }

        # Blocked sessions
        $blocked = Invoke-SqlQuery "SELECT COUNT(*) AS blocked FROM sys.dm_exec_requests WHERE blocking_session_id > 0"
        if ($blocked) {
            [void]$sb.AppendLine('# HELP mssql_sessions_blocked Currently blocked sessions')
            [void]$sb.AppendLine('# TYPE mssql_sessions_blocked gauge')
            [void]$sb.AppendLine("mssql_sessions_blocked $($blocked.blocked)")
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect connection metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# WAIT STATISTICS
# ============================================================================

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

    try {
        $waits = Invoke-SqlQuery "
            SELECT TOP 10
                wait_type,
                wait_time_ms / 1000.0 AS wait_time_seconds,
                waiting_tasks_count
            FROM sys.dm_os_wait_stats
            WHERE wait_type NOT IN (
                'SLEEP_TASK', 'BROKER_TO_FLUSH', 'SQLTRACE_BUFFER_FLUSH',
                'CLR_AUTO_EVENT', 'CLR_MANUAL_EVENT', 'LAZYWRITER_SLEEP',
                'CHECKPOINT_QUEUE', 'WAITFOR', 'XE_TIMER_EVENT',
                'BROKER_EVENTHANDLER', 'FT_IFTS_SCHEDULER_IDLE_WAIT',
                'XE_DISPATCHER_WAIT', 'SQLTRACE_INCREMENTAL_FLUSH_SLEEP',
                'HADR_FILESTREAM_IOMGR_IOCOMPLETION', 'DIRTY_PAGE_POLL',
                'SP_SERVER_DIAGNOSTICS_SLEEP', 'BROKER_TASK_STOP',
                'HADR_LOGCAPTURE_WAIT', 'ONDEMAND_TASK_QUEUE',
                'DBMIRROR_EVENTS_QUEUE', 'QDS_PERSIST_TASK_MAIN_LOOP_SLEEP',
                'QDS_ASYNC_QUEUE', 'QDS_CLEANUP_STALE_QUERIES_TASK_MAIN_LOOP_SLEEP',
                'DISPATCHER_QUEUE_SEMAPHORE', 'REQUEST_FOR_DEADLOCK_SEARCH',
                'HADR_TIMER_TASK', 'BROKER_RECEIVE_WAITFOR',
                'PREEMPTIVE_XE_GETTARGETSTATE', 'PREEMPTIVE_XE_SESSIONCOMMIT',
                'SLEEP_BPOOL_FLUSH', 'SLEEP_DBSTARTUP', 'SLEEP_DCOMSTARTUP',
                'SLEEP_MASTERDBREADY', 'SLEEP_MASTERMDREADY', 'SLEEP_MASTERUPGRADED',
                'SLEEP_MSDBSTARTUP', 'SLEEP_SYSTEMTASK', 'SLEEP_TEMPDBSTARTUP',
                'SNI_HTTP_ACCEPT', 'WAIT_XTP_OFFLINE_CKPT_NEW_LOG'
            )
            AND wait_time_ms > 0
            ORDER BY wait_time_ms DESC"

        if ($waits) {
            [void]$sb.AppendLine('# HELP mssql_wait_time_seconds Cumulative wait time by wait type')
            [void]$sb.AppendLine('# TYPE mssql_wait_time_seconds gauge')
            foreach ($row in $waits) {
                $waitType = Format-Label $row.wait_type
                $waitTime = Format-MetricValue $row.wait_time_seconds
                [void]$sb.AppendLine("mssql_wait_time_seconds{wait_type=`"$waitType`"} $waitTime")
            }
            [void]$sb.AppendLine('')

            [void]$sb.AppendLine('# HELP mssql_waiting_tasks Waiting task count by wait type')
            [void]$sb.AppendLine('# TYPE mssql_waiting_tasks gauge')
            foreach ($row in $waits) {
                $waitType = Format-Label $row.wait_type
                [void]$sb.AppendLine("mssql_waiting_tasks{wait_type=`"$waitType`"} $($row.waiting_tasks_count)")
            }
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect wait metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# BUFFER CACHE METRICS
# ============================================================================

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

    try {
        # Buffer cache hit ratio
        $hitRatio = Invoke-SqlQuery "
            SELECT
                CAST(
                    (SELECT cntr_value FROM sys.dm_os_performance_counters
                     WHERE counter_name = 'Buffer cache hit ratio'
                     AND object_name LIKE '%Buffer Manager%') * 100.0
                    /
                    NULLIF((SELECT cntr_value FROM sys.dm_os_performance_counters
                     WHERE counter_name = 'Buffer cache hit ratio base'
                     AND object_name LIKE '%Buffer Manager%'), 0)
                AS DECIMAL(5,2)) AS hit_ratio"

        if ($hitRatio -and $hitRatio.hit_ratio) {
            [void]$sb.AppendLine('# HELP mssql_buffer_cache_hit_ratio Buffer cache hit ratio (0-100)')
            [void]$sb.AppendLine('# TYPE mssql_buffer_cache_hit_ratio gauge')
            [void]$sb.AppendLine("mssql_buffer_cache_hit_ratio $(Format-MetricValue $hitRatio.hit_ratio)")
            [void]$sb.AppendLine('')
        }

        # Page life expectancy
        $ple = Invoke-SqlQuery "
            SELECT cntr_value AS ple_seconds
            FROM sys.dm_os_performance_counters
            WHERE counter_name = 'Page life expectancy'
            AND object_name LIKE '%Buffer Manager%'"

        if ($ple) {
            [void]$sb.AppendLine('# HELP mssql_page_life_expectancy_seconds Page life expectancy in seconds')
            [void]$sb.AppendLine('# TYPE mssql_page_life_expectancy_seconds gauge')
            [void]$sb.AppendLine("mssql_page_life_expectancy_seconds $($ple.ple_seconds)")
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect buffer cache metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# SQL AGENT JOB METRICS
# ============================================================================

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

    try {
        $jobs = Invoke-SqlQuery "
            SELECT
                j.name AS job_name,
                CASE h.run_status
                    WHEN 1 THEN 1
                    ELSE 0
                END AS last_run_succeeded,
                CASE
                    WHEN h.run_duration IS NULL THEN 0
                    ELSE (h.run_duration / 10000) * 3600
                         + ((h.run_duration / 100) % 100) * 60
                         + (h.run_duration % 100)
                END AS duration_seconds
            FROM msdb.dbo.sysjobs j
            OUTER APPLY (
                SELECT TOP 1 run_status, run_duration
                FROM msdb.dbo.sysjobhistory
                WHERE job_id = j.job_id AND step_id = 0
                ORDER BY run_date DESC, run_time DESC
            ) h
            WHERE j.enabled = 1"

        if ($jobs) {
            [void]$sb.AppendLine('# HELP mssql_agent_job_status Last run outcome per job (1=success, 0=fail)')
            [void]$sb.AppendLine('# TYPE mssql_agent_job_status gauge')
            foreach ($row in $jobs) {
                $jobName = Format-Label $row.job_name
                [void]$sb.AppendLine("mssql_agent_job_status{job=`"$jobName`"} $($row.last_run_succeeded)")
            }
            [void]$sb.AppendLine('')

            [void]$sb.AppendLine('# HELP mssql_agent_job_duration_seconds Last run duration per job in seconds')
            [void]$sb.AppendLine('# TYPE mssql_agent_job_duration_seconds gauge')
            foreach ($row in $jobs) {
                $jobName = Format-Label $row.job_name
                [void]$sb.AppendLine("mssql_agent_job_duration_seconds{job=`"$jobName`"} $($row.duration_seconds)")
            }
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect SQL Agent job metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# TEMPDB METRICS
# ============================================================================

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

    try {
        $tempdb = Invoke-SqlQuery "
            SELECT
                SUM(size) * 8 * 1024 AS total_size_bytes,
                SUM(FILEPROPERTY(name, 'SpaceUsed')) * 8 * 1024 AS used_bytes
            FROM tempdb.sys.database_files
            WHERE type = 0"

        if ($tempdb) {
            [void]$sb.AppendLine('# HELP mssql_tempdb_size_bytes TempDB total data file size in bytes')
            [void]$sb.AppendLine('# TYPE mssql_tempdb_size_bytes gauge')
            $totalSize = if ($tempdb.total_size_bytes) { $tempdb.total_size_bytes } else { 0 }
            [void]$sb.AppendLine("mssql_tempdb_size_bytes $totalSize")
            [void]$sb.AppendLine('')

            [void]$sb.AppendLine('# HELP mssql_tempdb_usage_bytes TempDB space used in bytes')
            [void]$sb.AppendLine('# TYPE mssql_tempdb_usage_bytes gauge')
            $usedBytes = if ($tempdb.used_bytes) { $tempdb.used_bytes } else { 0 }
            [void]$sb.AppendLine("mssql_tempdb_usage_bytes $usedBytes")
            [void]$sb.AppendLine('')
        }
    }
    catch {
        Write-Warning "Failed to collect TempDB metrics: $_"
    }

    $sb.ToString()
}

# ============================================================================
# ERROR COUNT
# ============================================================================

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

    try {
        $errors = Invoke-SqlQuery "
            SELECT COUNT(*) AS error_count
            FROM sys.dm_exec_requests
            WHERE status = 'running' AND total_elapsed_time > 0
            AND sql_handle IS NOT NULL"

        [void]$sb.AppendLine('# HELP mssql_error_count Active error count from current requests')
        [void]$sb.AppendLine('# TYPE mssql_error_count gauge')
        $count = if ($errors -and $errors.error_count) { $errors.error_count } else { 0 }
        [void]$sb.AppendLine("mssql_error_count $count")
        [void]$sb.AppendLine('')
    }
    catch {
        Write-Warning "Failed to collect error metrics: $_"
    }

    $sb.ToString()
}

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

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

    # Test connectivity
    $testResult = Invoke-SqlQuery "SELECT 1 AS connected"
    $isUp = if ($testResult) { 1 } else { 0 }

    [void]$sb.AppendLine('# HELP mssql_up SQL Server reachable (1=up, 0=down)')
    [void]$sb.AppendLine('# TYPE mssql_up gauge')
    [void]$sb.AppendLine("mssql_up $isUp")
    [void]$sb.AppendLine('')

    if ($isUp -eq 1) {
        [void]$sb.Append((Get-InstanceMetrics))
        [void]$sb.Append((Get-DatabaseSizeMetrics))
        [void]$sb.Append((Get-BackupMetrics))
        [void]$sb.Append((Get-ConnectionMetrics))
        [void]$sb.Append((Get-WaitMetrics))
        [void]$sb.Append((Get-BufferCacheMetrics))
        [void]$sb.Append((Get-AgentJobMetrics))
        [void]$sb.Append((Get-TempDbMetrics))
        [void]$sb.Append((Get-ErrorMetrics))
    }

    # Collector duration
    $scriptEnd = Get-Date
    $duration = Format-MetricValue ($scriptEnd - $scriptStart).TotalSeconds

    [void]$sb.AppendLine('# HELP mssql_collector_duration_seconds Time to generate all metrics')
    [void]$sb.AppendLine('# TYPE mssql_collector_duration_seconds gauge')
    [void]$sb.AppendLine("mssql_collector_duration_seconds $duration")
    [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 MSSQL 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>MSSQL Metrics Exporter v1.0</title></head>
<body>
<h1>MSSQL Metrics Exporter v1.0</h1>
<p><a href="/metrics">Metrics</a></p>
<h2>Sections</h2>
<ul>
<li>Instance health (version, uptime)</li>
<li>Database sizes (data files, log files, log usage)</li>
<li>Backup freshness (full, differential)</li>
<li>Connections and sessions (active, blocked)</li>
<li>Wait statistics (top 10 waits)</li>
<li>Buffer cache (hit ratio, page life expectancy)</li>
<li>SQL Agent jobs (status, duration)</li>
<li>TempDB (size, usage)</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 $HttpPort
    }
    'textfile' {
        $outputFile = Join-Path $TextfileDir 'mssql_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 ".mssql_metrics.$PID.tmp"

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

            $lineCount = ($metrics -split "`n").Count
            if ($lineCount -lt 5) {
                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
    }
}
