<#
.SYNOPSIS
    SMART Drive Health Prometheus Metrics Exporter (Windows)
.DESCRIPTION
    Prometheus exporter for SMART drive health metrics on Windows -- reads SMART
    attributes from SATA and NVMe drives using smartctl (smartmontools) and
    exports temperature, reallocated sectors, pending sectors, uncorrectable
    errors, power-on hours, wear leveling, NVMe health, and overall drive
    health status as Prometheus-compatible text format.

    Outputs identical smart_drive_* metrics as the Linux bash version so the
    same Grafana dashboard works for both platforms.
.PARAMETER Mode
    Output mode: 'stdout' (default), 'textfile', or 'http'
.PARAMETER Port
    HTTP port for http mode (default: 9198)
.PARAMETER TextfileDir
    Directory for textfile collector output
    (default: C:\ProgramData\windows_exporter\textfile_inputs)
.PARAMETER SmartctlPath
    Path to smartctl.exe (default: C:\Program Files\smartmontools\bin\smartctl.exe)
.PARAMETER Devices
    Comma-separated device list, e.g. '/dev/sda,/dev/nvme0' or 'auto' (default: auto)
.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

    Prerequisites:
      - smartmontools for Windows (https://www.smartmontools.org/)
      - Administrator privileges for smartctl access
      - windows_exporter for textfile collector mode

    Metrics Exported:
      Core Status:
        - smart_drive_up
        - smart_drive_exporter_info{version}

      Drive Health:
        - smart_drive_health_ok{device,model,serial,type}
        - smart_drive_temperature_celsius{device,model,serial}
        - smart_drive_power_on_hours{device,model,serial}
        - smart_drive_power_cycle_count{device,model,serial}
        - smart_drive_capacity_bytes{device,model,serial}

      SATA Attributes:
        - smart_drive_reallocated_sectors{device,model,serial}
        - smart_drive_pending_sectors{device,model,serial}
        - smart_drive_uncorrectable_errors{device,model,serial}
        - smart_drive_spin_retry_count{device,model,serial}
        - smart_drive_command_timeout{device,model,serial}
        - smart_drive_start_stop_count{device,model,serial}
        - smart_drive_wear_leveling_count{device,model,serial}
        - smart_drive_interface_speed{device,model,serial,speed}

      NVMe Attributes:
        - smart_drive_percentage_used{device,model,serial}
        - smart_drive_available_spare{device,model,serial}
        - smart_drive_available_spare_threshold{device,model,serial}
        - smart_drive_media_errors{device,model,serial}
        - smart_drive_critical_warning{device,model,serial}

      Exporter:
        - smart_drive_exporter_duration_seconds
        - smart_drive_exporter_last_run_timestamp
        - smart_drive_devices_total
#>

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

    [int]$Port = 9198,

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

    [string]$SmartctlPath = 'C:\Program Files\smartmontools\bin\smartctl.exe',

    [string]$Devices = 'auto',

    [switch]$InstallScheduledTask,

    [int]$TaskIntervalMinutes = 5
)

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

if ($InstallScheduledTask) {
    $taskName = "SmartDriveExporter"
    $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 SMART drive health 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 scheduled 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-CleanLabelValue {
    param([string]$Value, [int]$MaxLength = 120)
    if (-not $Value) { return '' }
    $Value = $Value -replace '\\', '\\\\'
    $Value = $Value -replace '"', '\"'
    $Value = $Value -replace "`n", '\n'
    if ($Value.Length -gt $MaxLength) {
        $Value = $Value.Substring(0, $MaxLength)
    }
    return $Value
}

# ============================================================================
# DRIVE DETECTION
# ============================================================================

function Get-DriveList {
    if ($Devices -ne 'auto') {
        return $Devices -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
    }

    $scanOutput = & $SmartctlPath --scan 2>$null
    if (-not $scanOutput) { return @() }

    $drives = @()
    foreach ($line in $scanOutput) {
        if ($line -match '^(\S+)') {
            $drives += $Matches[1]
        }
    }
    return $drives
}

# ============================================================================
# DRIVE DATA PARSING
# ============================================================================

function Get-DriveData {
    param([string]$Device)

    $rawOutput = & $SmartctlPath -iHA $Device 2>$null
    if (-not $rawOutput) { return $null }

    $data = @{
        Device = $Device
        Model = ''
        Serial = ''
        Type = 'unknown'
        Health = -1
        CapacityBytes = 0
        Temperature = -1
        PowerOnHours = -1
        PowerCycleCount = -1
        ReallocatedSectors = -1
        PendingSectors = -1
        Uncorrectable = -1
        SpinRetry = -1
        CommandTimeout = -1
        StartStop = -1
        WearLeveling = -1
        NvmePctUsed = -1
        NvmeSpare = -1
        NvmeSpareThresh = -1
        NvmeMediaErrors = -1
        NvmeCriticalWarn = -1
        SataSpeed = ''
    }

    $inSmartAttrs = $false
    $inNvmeHealth = $false

    foreach ($line in $rawOutput) {
        # Drive info
        if ($line -match '^Device Model:\s+(.+)$') {
            $data.Model = $Matches[1].Trim()
        }
        if ($line -match '^Model Number:\s+(.+)$') {
            $data.Model = $Matches[1].Trim()
            $data.Type = 'nvme'
        }
        if ($line -match '^Serial Number:\s+(.+)$') {
            $data.Serial = $Matches[1].Trim()
        }
        if ($line -match '^User Capacity:\s+([\d,]+)\s+bytes') {
            $data.CapacityBytes = [long]($Matches[1] -replace ',', '')
        }
        if ($line -match '^Total NVM Capacity:\s+([\d,]+)\s+bytes' -or
            $line -match '^Namespace 1 Size/Capacity:\s+([\d,]+)\s+bytes') {
            $data.CapacityBytes = [long]($Matches[1] -replace ',', '')
        }
        if ($line -match '^SATA Version is:.*?([\d.]+\s+Gb/s)') {
            $data.SataSpeed = $Matches[1]
        }
        if ($line -match '^Rotation Rate:') {
            if ($line -match 'Solid State') {
                $data.Type = 'ssd'
            } elseif ($line -match '\d+\s+rpm') {
                $data.Type = 'hdd'
            }
        }

        # Health status
        if ($line -match 'SMART overall-health self-assessment test result:\s+(\S+)') {
            $data.Health = if ($Matches[1] -eq 'PASSED') { 1 } else { 0 }
        }
        if ($line -match 'SMART Health Status:\s+(\S+)') {
            $data.Health = if ($Matches[1] -eq 'OK') { 1 } else { 0 }
        }

        # SATA SMART attributes table
        if ($line -match '^ID#\s+ATTRIBUTE_NAME') {
            $inSmartAttrs = $true
            continue
        }
        if ($inSmartAttrs -and $line -match '^\s*$') {
            $inSmartAttrs = $false
        }

        if ($inSmartAttrs -and $line -match '^\s*(\d+)\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+\S+\s+(\S+)') {
            $attrId = [int]$Matches[1]
            $rawVal = $Matches[2]

            # Extract first number from raw value (handles "35 (Min/Max 22/42)")
            $numVal = 0
            if ($rawVal -match '^(\d+)') {
                $numVal = [long]$Matches[1]
            }

            switch ($attrId) {
                4   { $data.StartStop = $numVal }
                5   { $data.ReallocatedSectors = $numVal }
                9   { $data.PowerOnHours = $numVal }
                10  { $data.SpinRetry = $numVal }
                12  { $data.PowerCycleCount = $numVal }
                177 { $data.WearLeveling = $numVal }
                188 { $data.CommandTimeout = $numVal }
                190 { if ($data.Temperature -lt 0) { $data.Temperature = $numVal } }
                194 { $data.Temperature = $numVal }
                197 { $data.PendingSectors = $numVal }
                198 { $data.Uncorrectable = $numVal }
                233 { if ($data.WearLeveling -lt 0) { $data.WearLeveling = $numVal } }
            }
        }

        # NVMe SMART/Health Information
        if ($line -match '^SMART/Health Information') {
            $inNvmeHealth = $true
            continue
        }
        if ($inNvmeHealth -and $line -match '^\s*$') {
            $inNvmeHealth = $false
        }

        if ($inNvmeHealth) {
            if ($line -match '^Temperature:\s+(\d+)') {
                $data.Temperature = [int]$Matches[1]
            }
            if ($line -match '^Percentage Used:\s+(\d+)%') {
                $data.NvmePctUsed = [int]$Matches[1]
            }
            if ($line -match '^Available Spare:\s+(\d+)%' -and $line -notmatch 'Threshold') {
                $data.NvmeSpare = [int]$Matches[1]
            }
            if ($line -match '^Available Spare Threshold:\s+(\d+)%') {
                $data.NvmeSpareThresh = [int]$Matches[1]
            }
            if ($line -match '^Power On Hours:\s+([\d,]+)') {
                $data.PowerOnHours = [long]($Matches[1] -replace ',', '')
            }
            if ($line -match '^Power Cycles:\s+([\d,]+)') {
                $data.PowerCycleCount = [long]($Matches[1] -replace ',', '')
            }
            if ($line -match '^Media and Data Integrity Errors:\s+([\d,]+)') {
                $data.NvmeMediaErrors = [long]($Matches[1] -replace ',', '')
            }
            if ($line -match '^Critical Warning:\s+(\S+)') {
                $val = $Matches[1]
                if ($val -match '^0x') {
                    $data.NvmeCriticalWarn = [Convert]::ToInt32($val, 16)
                } else {
                    $data.NvmeCriticalWarn = [int]$val
                }
            }
        }
    }

    # Fix type for NVMe if not already detected
    if ($data.Type -eq 'unknown' -and $data.NvmePctUsed -ge 0) { $data.Type = 'nvme' }
    if ($data.Type -eq 'unknown') { $data.Type = 'sata' }

    return $data
}

# ============================================================================
# METRICS GENERATION
# ============================================================================

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

    # ========================================================================
    # Check smartctl availability
    # ========================================================================
    if (-not (Test-Path $SmartctlPath)) {
        [void]$sb.AppendLine('# HELP smart_drive_up Exporter status (1=up, 0=down)')
        [void]$sb.AppendLine('# TYPE smart_drive_up gauge')
        [void]$sb.AppendLine('smart_drive_up 0')
        [void]$sb.AppendLine('')
        [void]$sb.AppendLine('# HELP smart_drive_exporter_info Exporter version information')
        [void]$sb.AppendLine('# TYPE smart_drive_exporter_info gauge')
        [void]$sb.AppendLine('smart_drive_exporter_info{version="1.0"} 1')
        [void]$sb.AppendLine('')
        return $sb.ToString()
    }

    # ========================================================================
    # Exporter Status
    # ========================================================================
    [void]$sb.AppendLine('# HELP smart_drive_up Exporter status (1=up, 0=down)')
    [void]$sb.AppendLine('# TYPE smart_drive_up gauge')
    [void]$sb.AppendLine('smart_drive_up 1')
    [void]$sb.AppendLine('')
    [void]$sb.AppendLine('# HELP smart_drive_exporter_info Exporter version information')
    [void]$sb.AppendLine('# TYPE smart_drive_exporter_info gauge')
    [void]$sb.AppendLine('smart_drive_exporter_info{version="1.0"} 1')
    [void]$sb.AppendLine('')

    # ========================================================================
    # Drive Detection & Parsing
    # ========================================================================
    $driveList = Get-DriveList

    if (-not $driveList -or $driveList.Count -eq 0) {
        [void]$sb.AppendLine('# HELP smart_drive_devices_total Total drives detected')
        [void]$sb.AppendLine('# TYPE smart_drive_devices_total gauge')
        [void]$sb.AppendLine('smart_drive_devices_total 0')
        [void]$sb.AppendLine('')
    } else {
        # Collect per-drive data
        $allDrives = @()
        foreach ($device in $driveList) {
            $driveData = Get-DriveData -Device $device
            if ($driveData) {
                $allDrives += $driveData
            }
        }

        [void]$sb.AppendLine('# HELP smart_drive_devices_total Total drives detected')
        [void]$sb.AppendLine('# TYPE smart_drive_devices_total gauge')
        [void]$sb.AppendLine("smart_drive_devices_total $($allDrives.Count)")
        [void]$sb.AppendLine('')

        # ================================================================
        # Health Status
        # ================================================================
        $healthLines = @()
        foreach ($d in $allDrives) {
            if ($d.Health -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $healthLines += "smart_drive_health_ok{device=`"$dev`",model=`"$model`",serial=`"$serial`",type=`"$($d.Type)`"} $($d.Health)"
            }
        }
        if ($healthLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_health_ok SMART health status (1=passed, 0=failed)')
            [void]$sb.AppendLine('# TYPE smart_drive_health_ok gauge')
            foreach ($line in $healthLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Temperature
        # ================================================================
        $tempLines = @()
        foreach ($d in $allDrives) {
            if ($d.Temperature -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $tempLines += "smart_drive_temperature_celsius{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.Temperature)"
            }
        }
        if ($tempLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_temperature_celsius Current drive temperature in Celsius')
            [void]$sb.AppendLine('# TYPE smart_drive_temperature_celsius gauge')
            foreach ($line in $tempLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Power-On Hours
        # ================================================================
        $pohLines = @()
        foreach ($d in $allDrives) {
            if ($d.PowerOnHours -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $pohLines += "smart_drive_power_on_hours{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.PowerOnHours)"
            }
        }
        if ($pohLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_power_on_hours Total power-on hours')
            [void]$sb.AppendLine('# TYPE smart_drive_power_on_hours gauge')
            foreach ($line in $pohLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Power Cycle Count
        # ================================================================
        $pccLines = @()
        foreach ($d in $allDrives) {
            if ($d.PowerCycleCount -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $pccLines += "smart_drive_power_cycle_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.PowerCycleCount)"
            }
        }
        if ($pccLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_power_cycle_count Total power cycle count')
            [void]$sb.AppendLine('# TYPE smart_drive_power_cycle_count gauge')
            foreach ($line in $pccLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Capacity
        # ================================================================
        $capLines = @()
        foreach ($d in $allDrives) {
            if ($d.CapacityBytes -gt 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $capLines += "smart_drive_capacity_bytes{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.CapacityBytes)"
            }
        }
        if ($capLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_capacity_bytes Drive capacity in bytes')
            [void]$sb.AppendLine('# TYPE smart_drive_capacity_bytes gauge')
            foreach ($line in $capLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Reallocated Sectors
        # ================================================================
        $reallocLines = @()
        foreach ($d in $allDrives) {
            if ($d.ReallocatedSectors -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $reallocLines += "smart_drive_reallocated_sectors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.ReallocatedSectors)"
            }
        }
        if ($reallocLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_reallocated_sectors Reallocated sector count')
            [void]$sb.AppendLine('# TYPE smart_drive_reallocated_sectors gauge')
            foreach ($line in $reallocLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Pending Sectors
        # ================================================================
        $pendingLines = @()
        foreach ($d in $allDrives) {
            if ($d.PendingSectors -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $pendingLines += "smart_drive_pending_sectors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.PendingSectors)"
            }
        }
        if ($pendingLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_pending_sectors Current pending sector count')
            [void]$sb.AppendLine('# TYPE smart_drive_pending_sectors gauge')
            foreach ($line in $pendingLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Uncorrectable Errors
        # ================================================================
        $uncorrLines = @()
        foreach ($d in $allDrives) {
            if ($d.Uncorrectable -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $uncorrLines += "smart_drive_uncorrectable_errors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.Uncorrectable)"
            }
        }
        if ($uncorrLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_uncorrectable_errors Offline uncorrectable error count')
            [void]$sb.AppendLine('# TYPE smart_drive_uncorrectable_errors gauge')
            foreach ($line in $uncorrLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Spin Retry Count
        # ================================================================
        $spinLines = @()
        foreach ($d in $allDrives) {
            if ($d.SpinRetry -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $spinLines += "smart_drive_spin_retry_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.SpinRetry)"
            }
        }
        if ($spinLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_spin_retry_count Spin retry count')
            [void]$sb.AppendLine('# TYPE smart_drive_spin_retry_count gauge')
            foreach ($line in $spinLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Command Timeout
        # ================================================================
        $cmdtoLines = @()
        foreach ($d in $allDrives) {
            if ($d.CommandTimeout -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $cmdtoLines += "smart_drive_command_timeout{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.CommandTimeout)"
            }
        }
        if ($cmdtoLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_command_timeout Command timeout count')
            [void]$sb.AppendLine('# TYPE smart_drive_command_timeout gauge')
            foreach ($line in $cmdtoLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Start/Stop Count
        # ================================================================
        $ssLines = @()
        foreach ($d in $allDrives) {
            if ($d.StartStop -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $ssLines += "smart_drive_start_stop_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.StartStop)"
            }
        }
        if ($ssLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_start_stop_count Start/stop count')
            [void]$sb.AppendLine('# TYPE smart_drive_start_stop_count gauge')
            foreach ($line in $ssLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Wear Leveling Count
        # ================================================================
        $wearLines = @()
        foreach ($d in $allDrives) {
            if ($d.WearLeveling -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $wearLines += "smart_drive_wear_leveling_count{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.WearLeveling)"
            }
        }
        if ($wearLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_wear_leveling_count SSD wear leveling count')
            [void]$sb.AppendLine('# TYPE smart_drive_wear_leveling_count gauge')
            foreach ($line in $wearLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # NVMe Percentage Used
        # ================================================================
        $nvmePctLines = @()
        foreach ($d in $allDrives) {
            if ($d.NvmePctUsed -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $nvmePctLines += "smart_drive_percentage_used{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmePctUsed)"
            }
        }
        if ($nvmePctLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_percentage_used NVMe percentage used estimate')
            [void]$sb.AppendLine('# TYPE smart_drive_percentage_used gauge')
            foreach ($line in $nvmePctLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # NVMe Available Spare
        # ================================================================
        $nvmeSpareLines = @()
        foreach ($d in $allDrives) {
            if ($d.NvmeSpare -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $nvmeSpareLines += "smart_drive_available_spare{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeSpare)"
            }
        }
        if ($nvmeSpareLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_available_spare NVMe available spare percentage')
            [void]$sb.AppendLine('# TYPE smart_drive_available_spare gauge')
            foreach ($line in $nvmeSpareLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # NVMe Available Spare Threshold
        # ================================================================
        $nvmeThreshLines = @()
        foreach ($d in $allDrives) {
            if ($d.NvmeSpareThresh -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $nvmeThreshLines += "smart_drive_available_spare_threshold{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeSpareThresh)"
            }
        }
        if ($nvmeThreshLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_available_spare_threshold NVMe available spare threshold percentage')
            [void]$sb.AppendLine('# TYPE smart_drive_available_spare_threshold gauge')
            foreach ($line in $nvmeThreshLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # NVMe Media Errors
        # ================================================================
        $nvmeMediaLines = @()
        foreach ($d in $allDrives) {
            if ($d.NvmeMediaErrors -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $nvmeMediaLines += "smart_drive_media_errors{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeMediaErrors)"
            }
        }
        if ($nvmeMediaLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_media_errors NVMe media and data integrity errors')
            [void]$sb.AppendLine('# TYPE smart_drive_media_errors gauge')
            foreach ($line in $nvmeMediaLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # NVMe Critical Warning
        # ================================================================
        $nvmeCritLines = @()
        foreach ($d in $allDrives) {
            if ($d.NvmeCriticalWarn -ge 0) {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $nvmeCritLines += "smart_drive_critical_warning{device=`"$dev`",model=`"$model`",serial=`"$serial`"} $($d.NvmeCriticalWarn)"
            }
        }
        if ($nvmeCritLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_critical_warning NVMe critical warning bitmap')
            [void]$sb.AppendLine('# TYPE smart_drive_critical_warning gauge')
            foreach ($line in $nvmeCritLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }

        # ================================================================
        # Interface Speed
        # ================================================================
        $speedLines = @()
        foreach ($d in $allDrives) {
            if ($d.SataSpeed -ne '') {
                $dev = Get-CleanLabelValue $d.Device
                $model = Get-CleanLabelValue $d.Model
                $serial = Get-CleanLabelValue $d.Serial
                $speed = Get-CleanLabelValue $d.SataSpeed
                $speedLines += "smart_drive_interface_speed{device=`"$dev`",model=`"$model`",serial=`"$serial`",speed=`"$speed`"} 1"
            }
        }
        if ($speedLines.Count -gt 0) {
            [void]$sb.AppendLine('# HELP smart_drive_interface_speed SATA interface speed info metric')
            [void]$sb.AppendLine('# TYPE smart_drive_interface_speed gauge')
            foreach ($line in $speedLines) { [void]$sb.AppendLine($line) }
            [void]$sb.AppendLine('')
        }
    }

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

    [void]$sb.AppendLine('# HELP smart_drive_exporter_duration_seconds Time to generate all metrics')
    [void]$sb.AppendLine('# TYPE smart_drive_exporter_duration_seconds gauge')
    [void]$sb.AppendLine("smart_drive_exporter_duration_seconds $duration")
    [void]$sb.AppendLine('')
    [void]$sb.AppendLine('# HELP smart_drive_exporter_last_run_timestamp Unix timestamp of last successful run')
    [void]$sb.AppendLine('# TYPE smart_drive_exporter_last_run_timestamp gauge')
    [void]$sb.AppendLine("smart_drive_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 SMART drive 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>SMART Drive Exporter v1.0</title></head>
<body>
<h1>SMART Drive Exporter v1.0 (Windows)</h1>
<p><a href="/metrics">Metrics</a></p>
<h2>Sections</h2>
<ul>
<li>Drive health status (PASSED/FAILED)</li>
<li>Temperature per drive</li>
<li>SATA attributes (reallocated sectors, pending sectors, etc.)</li>
<li>NVMe health (percentage used, available spare, media errors)</li>
<li>Power-on hours and power cycle count</li>
<li>SSD wear leveling</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' {
        $OutputFile = Join-Path $TextfileDir 'smart_drive.prom'

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

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

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

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