# RescuePC Protected Module - DO NOT EDIT
# Source code is maintained separately in the development repository.

$script:MAX_PROOF_SIZE_BYTES = 5MB
$script:MAX_PROBES_PER_REPAIR = 30
$script:MAX_ARTIFACTS_PER_REPAIR = 50
$script:MAX_ERROR_LINES = 50
$script:MAX_STRING_LENGTH = 1000
$script:MAX_PROOF_FOLDERS = 10
$script:SCHEMA_VERSION = "1.0.0"

$script:CurrentSession = $null
$script:CurrentRepair = $null
$script:ProofPath = $null

function Start-ProofSession {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$AppVersion,

        [Parameter(Mandatory)]
        [string]$PackVersion,

        [Parameter()]
        [bool]$PackSignatureVerified = $false,

        [Parameter()]
        [string]$PackKeyId = "",

        [Parameter()]
        [string]$PackSignatureHash = ""
    )

    $sessionId = [guid]::NewGuid().ToString("N").Substring(0, 12)
    $timestamp = Get-Date -Format "yyyyMMdd_HHmmss"

    $proofRoot = Join-Path $env:ProgramData "RescuePC\proof"
    $script:ProofPath = Join-Path $proofRoot "${timestamp}_${sessionId}"

    New-Item -ItemType Directory -Path $script:ProofPath -Force | Out-Null
    New-Item -ItemType Directory -Path (Join-Path $script:ProofPath "repairs") -Force | Out-Null
    New-Item -ItemType Directory -Path (Join-Path $script:ProofPath "logs") -Force | Out-Null

    Invoke-ProofRotation -ProofRoot $proofRoot

    $machineId = (Get-CimInstance -ClassName Win32_ComputerSystemProduct).UUID
    $machineIdHash = Get-StringHash -InputString $machineId

    $os = Get-CimInstance -ClassName Win32_OperatingSystem

    $script:CurrentSession = [ordered]@{
        schemaVersion = $script:SCHEMA_VERSION
        sessionId = $sessionId
        appVersion = $AppVersion
        packVersion = $PackVersion
        packSignature = [ordered]@{
            verified = $PackSignatureVerified
            keyId = $PackKeyId
            signatureHash = $PackSignatureHash
        }
        timestampStartUtc = (Get-Date).ToUniversalTime().ToString("o")
        timestampEndUtc = $null
        durationMs = 0
        machineIdHash = $machineIdHash
        os = [ordered]@{
            caption = $os.Caption
            version = $os.Version
            build = $os.BuildNumber
            architecture = $env:PROCESSOR_ARCHITECTURE
        }
        elevation = [ordered]@{
            ui = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
            runner = $false
        }
        repairsRequested = @()
        repairsExecuted = @()
        globalBefore = [ordered]@{}
        globalAfter = [ordered]@{}
        resultsSummary = [ordered]@{
            ok = 0
            warn = 0
            fail = 0
            skipped = 0
        }
        evidenceBudget = [ordered]@{
            maxBytes = $script:MAX_PROOF_SIZE_BYTES
            usedBytes = 0
        }
    }

    $script:CurrentSession.globalBefore = Get-GlobalProbes

    return $script:ProofPath
}

function Get-GlobalProbes {
    [CmdletBinding()]
    param()

    $probes = [ordered]@{
        timestamp = (Get-Date).ToUniversalTime().ToString("o")
    }

    try {
        $systemDrive = $env:SystemDrive
        $disk = Get-PSDrive -Name $systemDrive.TrimEnd(':') -ErrorAction SilentlyContinue
        if ($disk) {
            $probes.diskFreeBytes = $disk.Free
            $probes.diskFreeMB = [math]::Round($disk.Free / 1MB, 2)
        }
    } catch { $probes.diskFreeBytes = -1 }

    try {
        $probes.pendingReboot = (
            (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending") -or
            (Test-Path "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired") -or
            (Test-Path "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\PendingFileRenameOperations")
        )
    } catch { $probes.pendingReboot = $null }

    try {
        $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
        $probes.windowsUpdateService = [ordered]@{
            status = $wuService.Status.ToString()
            startType = $wuService.StartType.ToString()
        }
    } catch { $probes.windowsUpdateService = $null }

    try {
        $defender = Get-MpComputerStatus -ErrorAction SilentlyContinue
        if ($defender) {
            $probes.defender = [ordered]@{
                enabled = $defender.AntivirusEnabled
                realTimeProtection = $defender.RealTimeProtectionEnabled
                signatureAge = $defender.AntivirusSignatureAge
            }
        }
    } catch { $probes.defender = $null }

    try {
        $yesterday = (Get-Date).AddDays(-1)
        $probes.eventLogErrors24h = [ordered]@{
            system = (Get-WinEvent -FilterHashtable @{LogName='System'; Level=2; StartTime=$yesterday} -ErrorAction SilentlyContinue | Measure-Object).Count
            application = (Get-WinEvent -FilterHashtable @{LogName='Application'; Level=2; StartTime=$yesterday} -ErrorAction SilentlyContinue | Measure-Object).Count
        }
    } catch { $probes.eventLogErrors24h = $null }

    try {
        $adapters = Get-NetAdapter -ErrorAction SilentlyContinue
        $probes.networkAdapters = [ordered]@{
            up = ($adapters | Where-Object Status -eq 'Up').Count
            down = ($adapters | Where-Object Status -ne 'Up').Count
        }
    } catch { $probes.networkAdapters = $null }

    try {
        $criticalServices = @('wuauserv', 'BITS', 'CryptSvc', 'Dnscache', 'EventLog')
        $probes.criticalServices = [ordered]@{}
        foreach ($svc in $criticalServices) {
            $service = Get-Service -Name $svc -ErrorAction SilentlyContinue
            if ($service) {
                $probes.criticalServices[$svc] = $service.Status.ToString()
            }
        }
    } catch { $probes.criticalServices = $null }

    return $probes
}

function Add-GlobalProbe {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter(Mandatory)]
        $Value,

        [Parameter()]
        [ValidateSet('before', 'after')]
        [string]$Phase = 'before'
    )

    if (-not $script:CurrentSession) {
        Write-Warning "No proof session active"
        return
    }

    $target = if ($Phase -eq 'before') { 'globalBefore' } else { 'globalAfter' }
    $script:CurrentSession[$target][$Name] = Limit-StringLength -Value $Value
}

function Start-RepairEvidence {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$RepairId,

        [Parameter()]
        [string]$RepairName = "",

        [Parameter()]
        [string]$RepairVersion = "1.0.0"
    )

    if (-not $script:CurrentSession) {
        Write-Warning "No proof session active"
        return
    }

    $script:CurrentRepair = [ordered]@{
        repairId = $RepairId
        name = if ($RepairName) { $RepairName } else { $RepairId }
        version = $RepairVersion
        status = "running"
        timestampStart = (Get-Date).ToUniversalTime().ToString("o")
        timestampEnd = $null
        durationMs = 0
        steps = @()
        probes = @()
        artifacts = @()
        errors = @()
    }

    $script:CurrentSession.repairsRequested += $RepairId
}

function Add-Probe {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter()]
        $Before,

        [Parameter()]
        $After,

        [Parameter()]
        [string]$Unit = "",

        [Parameter()]
        [string]$Source = ""
    )

    if (-not $script:CurrentRepair) {
        Write-Warning "No repair evidence active"
        return
    }

    if ($script:CurrentRepair.probes.Count -ge $script:MAX_PROBES_PER_REPAIR) {
        Write-Warning "Probe limit reached ($script:MAX_PROBES_PER_REPAIR)"
        return
    }

    $probe = [ordered]@{
        name = $Name
        before = Limit-StringLength -Value $Before
        after = Limit-StringLength -Value $After
        unit = $Unit
        source = $Source
        timestamp = (Get-Date).ToUniversalTime().ToString("o")
    }

    $script:CurrentRepair.probes += $probe
}

function Add-Step {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Name,

        [Parameter()]
        [ValidateSet('ok', 'warn', 'fail', 'skipped')]
        [string]$Outcome = 'ok',

        [Parameter()]
        [string]$Detail = ""
    )

    if (-not $script:CurrentRepair) {
        Write-Warning "No repair evidence active"
        return
    }

    $step = [ordered]@{
        name = $Name
        outcome = $Outcome
        detail = Limit-StringLength -Value $Detail -MaxLength 500
        timestamp = (Get-Date).ToUniversalTime().ToString("o")
    }

    $script:CurrentRepair.steps += $step
}

function Add-Artifact {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('file', 'registry', 'service', 'command', 'other')]
        [string]$Type,

        [Parameter(Mandatory)]
        [string]$Path,

        [Parameter()]
        [ValidateSet('created', 'modified', 'deleted', 'started', 'stopped', 'executed')]
        [string]$Action = 'modified',

        [Parameter()]
        [string]$Hash = ""
    )

    if (-not $script:CurrentRepair) {
        Write-Warning "No repair evidence active"
        return
    }

    if ($script:CurrentRepair.artifacts.Count -ge $script:MAX_ARTIFACTS_PER_REPAIR) {
        Write-Warning "Artifact limit reached ($script:MAX_ARTIFACTS_PER_REPAIR)"
        return
    }

    $artifact = [ordered]@{
        type = $Type
        path = Limit-StringLength -Value $Path -MaxLength 500
        action = $Action
        hash = $Hash
        timestamp = (Get-Date).ToUniversalTime().ToString("o")
    }

    $script:CurrentRepair.artifacts += $artifact
}

function Add-Error {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$Message,

        [Parameter()]
        [string]$Code = ""
    )

    if (-not $script:CurrentRepair) {
        Write-Warning "No repair evidence active"
        return
    }

    if ($script:CurrentRepair.errors.Count -ge $script:MAX_ERROR_LINES) {
        return
    }

    $sanitized = $Message -replace '(?i)(password|secret|key|token|api)[=:]\s*\S+', '$1=[REDACTED]'
    $sanitized = Limit-StringLength -Value $sanitized -MaxLength 500

    $script:CurrentRepair.errors += [ordered]@{
        message = $sanitized
        code = $Code
        timestamp = (Get-Date).ToUniversalTime().ToString("o")
    }
}

function Complete-RepairEvidence {
    [CmdletBinding()]
    param(
        [Parameter()]
        [ValidateSet('ok', 'warn', 'fail', 'skipped')]
        [string]$Status = 'ok'
    )

    if (-not $script:CurrentRepair) {
        Write-Warning "No repair evidence active"
        return
    }

    $endTime = Get-Date
    $startTime = [datetime]::Parse($script:CurrentRepair.timestampStart)

    $script:CurrentRepair.status = $Status
    $script:CurrentRepair.timestampEnd = $endTime.ToUniversalTime().ToString("o")
    $script:CurrentRepair.durationMs = [int]($endTime - $startTime).TotalMilliseconds

    $script:CurrentSession.resultsSummary[$Status]++
    $script:CurrentSession.repairsExecuted += $script:CurrentRepair.repairId

    $repairFile = Join-Path $script:ProofPath "repairs\repair_$($script:CurrentRepair.repairId).json"
    $script:CurrentRepair | ConvertTo-Json -Depth 10 | Out-File -FilePath $repairFile -Encoding UTF8

    $script:CurrentRepair = $null
}

function Complete-ProofSession {
    [CmdletBinding()]
    param()

    if (-not $script:CurrentSession) {
        Write-Warning "No proof session active"
        return
    }

    $endTime = Get-Date
    $startTime = [datetime]::Parse($script:CurrentSession.timestampStartUtc)

    $script:CurrentSession.timestampEndUtc = $endTime.ToUniversalTime().ToString("o")
    $script:CurrentSession.durationMs = [int]($endTime - $startTime).TotalMilliseconds

    $script:CurrentSession.globalAfter = Get-GlobalProbes

    $script:CurrentSession.evidenceBudget.usedBytes = (Get-ChildItem -Path $script:ProofPath -Recurse -File | Measure-Object -Property Length -Sum).Sum

    $sessionFile = Join-Path $script:ProofPath "session.json"
    $script:CurrentSession | ConvertTo-Json -Depth 10 | Out-File -FilePath $sessionFile -Encoding UTF8

    $hashesFile = Join-Path $script:ProofPath "hashes.txt"
    $hashes = @()

    $sessionHash = Get-FileHash -Path $sessionFile -Algorithm SHA256
    $hashes += "SHA256 ($($sessionHash.Path | Split-Path -Leaf)) = $($sessionHash.Hash)"

    $repairFiles = Get-ChildItem -Path (Join-Path $script:ProofPath "repairs") -Filter "*.json" -ErrorAction SilentlyContinue
    foreach ($file in $repairFiles) {
        $hash = Get-FileHash -Path $file.FullName -Algorithm SHA256
        $hashes += "SHA256 ($($file.Name)) = $($hash.Hash)"
    }

    $logFiles = Get-ChildItem -Path (Join-Path $script:ProofPath "logs") -Filter "*.log" -ErrorAction SilentlyContinue
    foreach ($file in $logFiles) {
        $hash = Get-FileHash -Path $file.FullName -Algorithm SHA256
        $hashes += "SHA256 ($($file.Name)) = $($hash.Hash)"
    }

    $hashes -join "`n" | Out-File -FilePath $hashesFile -Encoding UTF8

    $result = [ordered]@{
        proofPath = $script:ProofPath
        sessionId = $script:CurrentSession.sessionId
        duration = $script:CurrentSession.durationMs
        results = $script:CurrentSession.resultsSummary
        bytesUsed = $script:CurrentSession.evidenceBudget.usedBytes
    }

    $script:CurrentSession = $null
    $script:ProofPath = $null

    return $result
}

function Invoke-ProofRotation {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ProofRoot
    )

    if (-not (Test-Path $ProofRoot)) { return }

    $folders = Get-ChildItem -Path $ProofRoot -Directory | Sort-Object CreationTime -Descending

    if ($folders.Count -gt $script:MAX_PROOF_FOLDERS) {
        $toDelete = $folders | Select-Object -Skip $script:MAX_PROOF_FOLDERS
        foreach ($folder in $toDelete) {
            Remove-Item -Path $folder.FullName -Recurse -Force -ErrorAction SilentlyContinue
        }
    }
}

function Get-StringHash {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$InputString
    )

    $bytes = [System.Text.Encoding]::UTF8.GetBytes($InputString)
    $hash = [System.Security.Cryptography.SHA256]::Create().ComputeHash($bytes)
    return [BitConverter]::ToString($hash).Replace("-", "").ToLower()
}

function Limit-StringLength {
    [CmdletBinding()]
    param(
        [Parameter()]
        $Value,

        [Parameter()]
        [int]$MaxLength = $script:MAX_STRING_LENGTH
    )

    if ($null -eq $Value) { return $null }
    if ($Value -isnot [string]) { return $Value }
    if ($Value.Length -le $MaxLength) { return $Value }

    return $Value.Substring(0, $MaxLength - 3) + "..."
}

Export-ModuleMember -Function @(
    'Start-ProofSession',
    'Add-GlobalProbe',
    'Start-RepairEvidence',
    'Add-Probe',
    'Add-Step',
    'Add-Artifact',
    'Add-Error',
    'Complete-RepairEvidence',
    'Complete-ProofSession',
    'Get-GlobalProbes'
)