health.psm1

# Health module for Delta-Code
# Single authoritative implementation of installation / runtime verification.
# Consumed by Install-DeltaCode.ps1 now and by the `delta-code health` command later.

$script:ModuleNames = @('delta-code', 'health', 'satellite', 'workspace', 'workflow', 'entry')

$script:SatelliteNames = @('git', 'build')

$script:DataDirectories = @('data', 'data\workspaces', 'data\workflows', 'data\satellites')

# Registry file -> expected top-level property
$script:RegistryContracts = [ordered]@{
    'data\workspaces.json'          = 'Workspaces'
    'data\workflows.json'           = 'Workflows'
    'data\workflow-history.json'    = 'History'
    'data\automation-profiles.json' = 'Profiles'
}

function New-DeltaCodeHealthCheck {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$Name,

        [Parameter(Mandatory = $true)]
        [ValidateSet('Pass', 'Warn', 'Fail')]
        [string]$Status,

        [string]$Detail = ''
    )
    [pscustomobject]@{
        Name   = $Name
        Status = $Status
        Detail = $Detail
    }
}

function Test-DeltaCodeModuleLayout {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )
    $results = @()
    $modulesPath = Join-Path $RootPath 'modules'

    if (-not (Test-Path -LiteralPath $modulesPath)) {
        $results += New-DeltaCodeHealthCheck -Name 'modules' -Status 'Fail' -Detail "Directory not found: $modulesPath"
        return $results
    }
    $results += New-DeltaCodeHealthCheck -Name 'modules' -Status 'Pass' -Detail $modulesPath

    foreach ($name in $script:ModuleNames) {
        $manifest = Join-Path $modulesPath "$name.psd1"
        $rootModule = Join-Path $modulesPath "$name.psm1"

        if (-not (Test-Path -LiteralPath $manifest)) {
            $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail "Missing $name.psd1"
            continue
        }
        if (-not (Test-Path -LiteralPath $rootModule)) {
            $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail "Missing $name.psm1"
            continue
        }

        try {
            $data = Import-PowerShellDataFile -Path $manifest -ErrorAction Stop
            if (-not $data.RootModule) {
                $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail 'Manifest has no RootModule'
                continue
            }
            if (-not $data.ModuleVersion) {
                $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail 'Manifest has no ModuleVersion'
                continue
            }
            $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Pass' -Detail "v$($data.ModuleVersion)"
        }
        catch {
            $results += New-DeltaCodeHealthCheck -Name "module:$name" -Status 'Fail' -Detail $_.Exception.Message
        }
    }
    return $results
}

function Test-DeltaCodeSatelliteLayout {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )
    $results = @()
    $satellitesPath = Join-Path $RootPath 'satellites'

    if (-not (Test-Path -LiteralPath $satellitesPath)) {
        $results += New-DeltaCodeHealthCheck -Name 'satellites' -Status 'Warn' -Detail "Directory not found: $satellitesPath"
        return $results
    }
    $results += New-DeltaCodeHealthCheck -Name 'satellites' -Status 'Pass' -Detail $satellitesPath

    foreach ($name in $script:SatelliteNames) {
        $satPath = Join-Path $satellitesPath $name
        if (-not (Test-Path -LiteralPath $satPath)) {
            $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Warn' -Detail "Not installed: $satPath"
            continue
        }
        $manifest = Join-Path $satPath "$name.psd1"
        $rootModule = Join-Path $satPath "$name.psm1"
        if (-not (Test-Path -LiteralPath $manifest)) {
            $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Warn' -Detail "Missing $name.psd1"
            continue
        }
        if (-not (Test-Path -LiteralPath $rootModule)) {
            $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Warn' -Detail "Missing $name.psm1"
            continue
        }
        $results += New-DeltaCodeHealthCheck -Name "satellite:$name" -Status 'Pass' -Detail $satPath
    }
    return $results
}

function Test-DeltaCodeDataLayout {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )
    $results = @()

    foreach ($dir in $script:DataDirectories) {
        $full = Join-Path $RootPath $dir
        if (-not (Test-Path -LiteralPath $full)) {
            $results += New-DeltaCodeHealthCheck -Name "data:$dir" -Status 'Warn' -Detail "Directory not found: $full"
        }
        else {
            $results += New-DeltaCodeHealthCheck -Name "data:$dir" -Status 'Pass' -Detail $full
        }
    }
    return $results
}

function Test-DeltaCodeRegistryData {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )
    $results = @()

    foreach ($relative in $script:RegistryContracts.Keys) {
        $expectedProperty = $script:RegistryContracts[$relative]
        $full = Join-Path $RootPath $relative

        if (-not (Test-Path -LiteralPath $full)) {
            # Registry files are recreated automatically on first use, so a missing
            # file is a first-run condition rather than a broken installation.
            $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Warn' -Detail 'Not initialised yet (created on first use)'
            continue
        }

        try {
            $raw = Get-Content -LiteralPath $full -Raw -ErrorAction Stop
            if ([string]::IsNullOrWhiteSpace($raw)) {
                $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Fail' -Detail 'File is empty'
                continue
            }
            $parsed = $raw | ConvertFrom-Json -ErrorAction Stop
            if ($null -eq $parsed.PSObject.Properties[$expectedProperty]) {
                $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Fail' -Detail "Missing '$expectedProperty' property"
                continue
            }
            $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Pass' -Detail "Property '$expectedProperty' present"
        }
        catch {
            $results += New-DeltaCodeHealthCheck -Name "registry:$relative" -Status 'Fail' -Detail $_.Exception.Message
        }
    }
    return $results
}

function Test-DeltaCodeModuleLoad {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath
    )
    $results = @()
    $modulesPath = Join-Path $RootPath 'modules'

    # Never re-import the module that is currently executing: `Import-Module -Force`
    # rebuilds its script scope and invalidates the function table of the call
    # stack that is still running, producing "term not recognized" errors.
    $runningModuleName = $ExecutionContext.SessionState.Module.Name

    foreach ($name in $script:ModuleNames) {
        $manifest = Join-Path $modulesPath "$name.psd1"
        if (-not (Test-Path -LiteralPath $manifest)) {
            $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Fail' -Detail 'Manifest missing'
            continue
        }
        if ($name -eq $runningModuleName) {
            $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Pass' -Detail 'Verified in place (module is running)'
            continue
        }
        try {
            Import-Module $manifest -Force -ErrorAction Stop -DisableNameChecking
            $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Pass' -Detail 'Imported'
        }
        catch {
            $results += New-DeltaCodeHealthCheck -Name "load:$name" -Status 'Fail' -Detail $_.Exception.Message
        }
    }
    return $results
}

function Test-DeltaCodeCli {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RootPath,
        [int]$TimeoutSeconds = 60
    )
    $launcher = Join-Path $RootPath 'delta-code.ps1'
    if (-not (Test-Path -LiteralPath $launcher)) {
        return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Fail' -Detail "Launcher not found: $launcher")
    }

    $pwsh = (Get-Process -Id $PID).Path
    if (-not $pwsh) {
        $pwsh = 'pwsh'
    }

    try {
        $proc = Start-Process -FilePath $pwsh `
            -ArgumentList @('-NoProfile', '-NonInteractive', '-File', $launcher, '-h') `
            -Wait -PassThru -NoNewWindow `
            -RedirectStandardOutput ([System.IO.Path]::GetTempFileName()) `
            -RedirectStandardError ([System.IO.Path]::GetTempFileName())

        if ($proc.ExitCode -eq 0) {
            return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Pass' -Detail 'Launcher responded to -h')
        }
        return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Fail' -Detail "Launcher exited with code $($proc.ExitCode)")
    }
    catch {
        return @(New-DeltaCodeHealthCheck -Name 'cli' -Status 'Fail' -Detail $_.Exception.Message)
    }
}

function Test-DeltaCodeInstallation {
    <#
    .SYNOPSIS
        Verifies a Delta-Code installation or working tree.
    .DESCRIPTION
        Single authoritative verification implementation. Used by Install-DeltaCode.ps1
        for post-install validation and (later) by the `delta-code health` command.
    .PARAMETER RootPath
        Root of the installation or repository to verify.
    .PARAMETER SkipModuleLoad
        Skip importing each module.
    .PARAMETER SkipCli
        Skip the launcher smoke test.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $false)]
        [string]$RootPath = (Split-Path -Parent $PSScriptRoot),

        [switch]$SkipModuleLoad,

        [switch]$SkipCli
    )

    $resolvedRoot = $RootPath
    if (Test-Path -LiteralPath $RootPath) {
        $resolvedRoot = (Get-Item -LiteralPath $RootPath).FullName
    }

    $checks = @()
    $checks += Test-DeltaCodeModuleLayout -RootPath $resolvedRoot
    $checks += Test-DeltaCodeSatelliteLayout -RootPath $resolvedRoot
    $checks += Test-DeltaCodeDataLayout -RootPath $resolvedRoot
    $checks += Test-DeltaCodeRegistryData -RootPath $resolvedRoot

    if (-not $SkipModuleLoad) {
        $checks += Test-DeltaCodeModuleLoad -RootPath $resolvedRoot
    }
    if (-not $SkipCli) {
        $checks += Test-DeltaCodeCli -RootPath $resolvedRoot
    }

    $errors = @($checks | Where-Object { $_.Status -eq 'Fail' } | ForEach-Object { "$($_.Name): $($_.Detail)" })
    $warnings = @($checks | Where-Object { $_.Status -eq 'Warn' } | ForEach-Object { "$($_.Name): $($_.Detail)" })

    [pscustomobject]@{
        RootPath = $resolvedRoot
        Success  = ($errors.Count -eq 0)
        Errors   = $errors
        Warnings = $warnings
        Checks   = $checks
    }
}

function Format-DeltaCodeHealthReport {
    <#
    .SYNOPSIS
        Renders a health result object as console output.
    .PARAMETER Result
        Result object returned by Test-DeltaCodeInstallation.
    .PARAMETER PassThru
        Return the result object (allows piping after rendering).
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [psobject]$Result,

        [switch]$PassThru
    )
    process {
        Write-Host "Delta-Code health check: $($Result.RootPath)"
        Write-Host ""

        foreach ($check in $Result.Checks) {
            $symbol = switch ($check.Status) {
                'Pass' { '[ OK ]' }
                'Warn' { '[WARN]' }
                'Fail' { '[FAIL]' }
            }
            $color = switch ($check.Status) {
                'Pass' { 'Green' }
                'Warn' { 'Yellow' }
                'Fail' { 'Red' }
            }
            Write-Host ("{0} {1,-28} {2}" -f $symbol, $check.Name, $check.Detail) -ForegroundColor $color
        }

        Write-Host ""
        if ($Result.Success) {
            $summary = "Health check PASSED ($($Result.Checks.Count) checks, $($Result.Warnings.Count) warnings)"
            Write-Host $summary -ForegroundColor Green
        }
        else {
            $summary = "Health check FAILED ($($Result.Errors.Count) errors, $($Result.Warnings.Count) warnings)"
            Write-Host $summary -ForegroundColor Red
        }

        if ($PassThru) {
            return $Result
        }
    }
}

Export-ModuleMember -Function Test-DeltaCodeInstallation, Format-DeltaCodeHealthReport, New-DeltaCodeHealthCheck