<#
.SYNOPSIS
    Windows LAPS Prometheus Metrics Exporter
.DESCRIPTION
    Prometheus exporter for Windows Local Administrator Password Solution (LAPS).
    Monitors password expiration, rotation status, coverage across OUs, and
    stale passwords. Exports metrics for windows_exporter textfile collector
    or standalone HTTP listener.
.PARAMETER TextFile
    Write to windows_exporter textfile directory
.PARAMETER OutFile
    Write metrics to a specific file path
.PARAMETER Install
    Create a scheduled task for automatic collection
.PARAMETER Listen
    Start HTTP listener on specified address:port
.PARAMETER Interval
    Collection interval in seconds for scheduled task (default: 300)
.NOTES
    Author:  Phil Connor
    Contact: contact@mylinux.work
    Website: https://mylinux.work
    License: MIT
    Version: 1.0
#>

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

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

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

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

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

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

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

    # Check if LAPS module is available
    $lapsAvailable = $false
    if (Get-Module -ListAvailable -Name "LAPS" -ErrorAction SilentlyContinue) {
        Import-Module LAPS -ErrorAction SilentlyContinue
        $lapsAvailable = $true
    }
    # Also check for Windows LAPS (new in Server 2022+)
    $windowsLaps = $false
    if (Get-Command "Get-LapsAADPassword" -ErrorAction SilentlyContinue) {
        $windowsLaps = $true
        $lapsAvailable = $true
    }

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

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

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

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

    # ========================================================================
    # COMPUTER ACCOUNT LAPS STATUS
    # ========================================================================

    try {
        $computers = Get-ADComputer -Filter {Enabled -eq $true} -Properties `
            "ms-Mcs-AdmPwdExpirationTime", "ms-Mcs-AdmPwd", "DistinguishedName", "OperatingSystem" `
            -ErrorAction Stop

        $totalComputers = @($computers).Count
        $lapsManaged = 0
        $lapsUnmanaged = 0
        $passwordExpired = 0
        $passwordExpiringSoon = 0
        $now = Get-Date
        $soonThreshold = $now.AddDays(7)

        foreach ($computer in $computers) {
            $expTime = $computer."ms-Mcs-AdmPwdExpirationTime"

            if ($expTime -and $expTime -gt 0) {
                $lapsManaged++
                $expDate = [DateTime]::FromFileTime($expTime)

                if ($expDate -lt $now) {
                    $passwordExpired++
                } elseif ($expDate -lt $soonThreshold) {
                    $passwordExpiringSoon++
                }
            } else {
                $lapsUnmanaged++
            }
        }

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_computers_total" "Total enabled computer accounts" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_computers_total $totalComputers")
        [void]$metrics.Add("")

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_computers_managed" "Computers with LAPS password set" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_computers_managed $lapsManaged")
        [void]$metrics.Add("")

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_computers_unmanaged" "Computers without LAPS password" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_computers_unmanaged $lapsUnmanaged")
        [void]$metrics.Add("")

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_passwords_expired" "Computers with expired LAPS passwords" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_passwords_expired $passwordExpired")
        [void]$metrics.Add("")

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_passwords_expiring_soon" "Computers with LAPS passwords expiring within 7 days" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_passwords_expiring_soon $passwordExpiringSoon")
        [void]$metrics.Add("")

        # Coverage percentage
        $coverage = if ($totalComputers -gt 0) { [math]::Round(($lapsManaged / $totalComputers) * 100, 1) } else { 0 }
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_coverage_percent" "LAPS coverage percentage" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_coverage_percent $coverage")
        [void]$metrics.Add("")

        # ====================================================================
        # PER-OU BREAKDOWN
        # ====================================================================

        $ouStats = @{}
        foreach ($computer in $computers) {
            $dn = $computer.DistinguishedName
            $ouParts = ($dn -split ",") | Where-Object { $_ -match "^OU=" }
            $ou = if ($ouParts.Count -gt 0) { ($ouParts[0] -replace "^OU=", "") } else { "Default" }

            if (-not $ouStats.ContainsKey($ou)) {
                $ouStats[$ou] = @{ Total = 0; Managed = 0 }
            }
            $ouStats[$ou].Total++

            $expTime = $computer."ms-Mcs-AdmPwdExpirationTime"
            if ($expTime -and $expTime -gt 0) {
                $ouStats[$ou].Managed++
            }
        }

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_ou_computers_total" "Computers per OU" "gauge"))
        foreach ($ou in $ouStats.Keys) {
            $ouName = Get-PrometheusEscape $ou
            [void]$metrics.Add("${MetricPrefix}_ou_computers_total{ou=`"$ouName`"} $($ouStats[$ou].Total)")
        }
        [void]$metrics.Add("")

        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_ou_computers_managed" "LAPS-managed computers per OU" "gauge"))
        foreach ($ou in $ouStats.Keys) {
            $ouName = Get-PrometheusEscape $ou
            [void]$metrics.Add("${MetricPrefix}_ou_computers_managed{ou=`"$ouName`"} $($ouStats[$ou].Managed)")
        }
        [void]$metrics.Add("")

    } catch {
        [void]$metrics.Add("# ERROR: Failed to query AD computers: $_")
        [void]$metrics.Add("")
    }

    # ========================================================================
    # LAPS GPO STATUS
    # ========================================================================

    try {
        $gpos = Get-GPO -All -ErrorAction Stop | Where-Object {
            $_.DisplayName -match "LAPS|Local Administrator Password"
        }

        $lapsGpos = @($gpos).Count
        [void]$metrics.Add((Write-MetricHeader "${MetricPrefix}_gpo_count" "LAPS-related GPOs" "gauge"))
        [void]$metrics.Add("${MetricPrefix}_gpo_count $lapsGpos")
        [void]$metrics.Add("")
    } catch {
        # GPO query may not be available on all systems
    }

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

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

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

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

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

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

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

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

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