src/PowerShell/StatusOutput.ps1

# cdp PowerShell domain: StatusOutput.ps1
# Loaded by src/cdp.psm1; do not dot-source peer files.

function Get-CdpStatusCode {
    param([Parameter(Mandatory = $true)][object]$Info)

    if ($Info.StatusLabel -eq 'path profile invalid') { return 'path_profile_invalid' }
    if (-not $Info.PathExists) { return 'path_missing' }
    if ($Info.StatusLabel -eq 'status timed out') { return 'scan_timeout' }
    if ($Info.StatusLabel -eq 'status failed') { return 'scan_failed' }
    if (-not $Info.IsGitRepo) { return 'not_git' }
    if ($Info.DirtyCount -gt 0 -or $Info.UntrackedCount -gt 0) { return 'changed' }
    'clean'
}

function Get-CdpStatusAttentionReasons {
    param([Parameter(Mandatory = $true)][object]$Info)

    $reasons = @()
    if ($Info.StatusLabel -eq 'path profile invalid') { $reasons += 'path_profile_invalid' }
    elseif (-not $Info.PathExists) { $reasons += 'path_missing' }
    if ($Info.StatusLabel -eq 'status timed out') { $reasons += 'scan_timeout' }
    if ($Info.StatusLabel -eq 'status failed') { $reasons += 'scan_failed' }
    if ($Info.DirtyCount -gt 0) { $reasons += 'dirty' }
    if ($Info.UntrackedCount -gt 0) { $reasons += 'untracked' }
    if ($Info.BehindCount -gt 0) { $reasons += 'behind' }
    @($reasons)
}

function Get-CdpStatusError {
    param([Parameter(Mandatory = $true)][object]$Info)

    if ($Info.StatusLabel -eq 'status timed out') {
        return [PSCustomObject]@{ code = 'scan_timeout'; message = 'Git status scan timed out.' }
    }
    if ($Info.StatusLabel -eq 'status failed') {
        return [PSCustomObject]@{ code = 'scan_failed'; message = 'Git status scan failed.' }
    }
    $null
}

function ConvertTo-CdpStatusProject {
    param([Parameter(Mandatory = $true)][object]$Info)

    $branch = if ([string]::IsNullOrWhiteSpace([string]$Info.Branch)) { $null } else { [string]$Info.Branch }
    $lastCommit = if ([string]::IsNullOrWhiteSpace([string]$Info.LastCommitRelative)) { $null } else { [string]$Info.LastCommitRelative }
    $resolvedPath = if ($Info.PSObject.Properties['ResolvedPath']) { [string]$Info.ResolvedPath } else { [string]$Info.RootPath }
    [PSCustomObject]@{
        name = [string]$Info.Name
        rawPath = [string]$Info.RootPath
        resolvedPath = $resolvedPath
        pathExists = [bool]$Info.PathExists
        status = Get-CdpStatusCode -Info $Info
        needsAttention = [bool]$Info.NeedsAttention
        attentionReasons = @(Get-CdpStatusAttentionReasons -Info $Info)
        error = Get-CdpStatusError -Info $Info
        git = [PSCustomObject]@{
            isRepository = [bool]$Info.IsGitRepo
            branch = $branch
            dirtyCount = [int]$Info.DirtyCount
            untrackedCount = [int]$Info.UntrackedCount
            aheadCount = [int]$Info.AheadCount
            behindCount = [int]$Info.BehindCount
            lastCommitRelative = $lastCommit
        }
    }
}

function Get-CdpStatusJsonExitCode {
    param([Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$Projects)

    if (@($Projects | Where-Object { $null -ne $_.Error }).Count -gt 0) { return 2 }
    if (@($Projects | Where-Object { $_.NeedsAttention }).Count -gt 0) { return 1 }
    0
}

function New-CdpStatusDocument {
    param(
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$AllStatus,
        [Parameter(Mandatory = $true)][AllowEmptyCollection()][object[]]$VisibleStatus,
        [Parameter(Mandatory = $true)][int]$DurationMs,
        [switch]$DirtyOnly,
        [string]$TagFilter,
        [switch]$Refresh
    )

    $projects = @($VisibleStatus | ForEach-Object { ConvertTo-CdpStatusProject -Info $_ })
    $exitCode = Get-CdpStatusJsonExitCode -Projects $projects
    [PSCustomObject]@{
        schemaVersion = 1
        generatedAt = [DateTime]::UtcNow.ToString('yyyy-MM-ddTHH:mm:ss.fffZ', [Globalization.CultureInfo]::InvariantCulture)
        durationMs = $DurationMs
        filters = [PSCustomObject]@{
            dirtyOnly = [bool]$DirtyOnly
            tag = if ([string]::IsNullOrWhiteSpace($TagFilter)) { $null } else { $TagFilter }
            refresh = [bool]$Refresh
        }
        summary = [PSCustomObject]@{
            total = $AllStatus.Count
            shown = $projects.Count
            attention = @($projects | Where-Object { $_.needsAttention }).Count
            partialFailures = @($projects | Where-Object { $null -ne $_.error }).Count
            exitCode = $exitCode
        }
        projects = $projects
    }
}

function Write-CdpStatusJson {
    param([Parameter(Mandatory = $true)][object]$Document)

    try {
        $json = $Document | ConvertTo-Json -Depth 7 -ErrorAction Stop
    } catch {
        [Console]::Error.WriteLine('Error: Failed to serialize status JSON.')
        $global:LASTEXITCODE = 3
        return
    }
    $global:LASTEXITCODE = [int]$Document.Summary.ExitCode
    Write-Output $json
}

function Get-CdpPlainStatusRow {
    param([Parameter(Mandatory = $true)][object]$Info)

    $sync = @()
    if ($Info.AheadCount -gt 0) { $sync += "^$($Info.AheadCount)" }
    if ($Info.BehindCount -gt 0) { $sync += "v$($Info.BehindCount)" }
    [PSCustomObject]@{
        Branch = if ($Info.IsGitRepo) { [string]$Info.Branch } else { '-' }
        Status = if ($Info.IsGitRepo) { [string]$Info.StatusLabel } else { [string]$Info.StatusLabel }
        Sync = $sync -join ' '
    }
}

function Write-CdpPlainStatusTable {
    param(
        [Parameter(Mandatory = $true)][object[]]$StatusList,
        [switch]$DirtyOnly,
        [string]$TagFilter
    )

    $nameWidth = 14
    $branchWidth = 12
    foreach ($item in $StatusList) {
        $nameWidth = [Math]::Min(24, [Math]::Max($nameWidth, (Get-CdpDisplayWidth $item.Name)))
        $branchWidth = [Math]::Min(20, [Math]::Max($branchWidth, (Get-CdpDisplayWidth $item.Branch)))
    }
    $filter = if ($DirtyOnly) { ' (dirty only)' } elseif ($TagFilter) { " ($TagFilter)" } else { '' }
    Write-Host ""
    Write-Host "cdp project status ($($StatusList.Count) projects$filter)"
    Write-Host ('-' * 110)
    Write-Host (" {0,-4} {1,-$nameWidth} {2,-$branchWidth} {3,-24} {4,-10} {5}" -f '#', 'Project', 'Branch', 'Status', 'Sync', 'Last Commit')
    Write-Host ('-' * 110)
    $index = 1
    foreach ($item in $StatusList) {
        $row = Get-CdpPlainStatusRow -Info $item
        $name = Pad-CdpText (Limit-CdpText -Text $item.Name -MaxLength $nameWidth) $nameWidth
        $branch = Pad-CdpText (Limit-CdpText -Text $row.Branch -MaxLength $branchWidth) $branchWidth
        Write-Host (" {0,-4} {1} {2} {3,-24} {4,-10} {5}" -f ("{0:00}" -f $index), $name, $branch, $row.Status, $row.Sync, $item.LastCommitRelative)
        $index++
    }
    Write-Host ('-' * 110)
    $attention = @($StatusList | Where-Object { $_.NeedsAttention -and $_.IsGitRepo }).Count
    $missing = @($StatusList | Where-Object { -not $_.PathExists }).Count
    $parts = @()
    if ($attention -gt 0) { $parts += "$attention repos need attention" }
    if ($missing -gt 0) { $parts += "$missing path missing" }
    if ($parts.Count -gt 0) { Write-Host ($parts -join ' | ') } else { Write-Host 'All projects clean.' }
}

function Write-CdpStatusFatal {
    param([Parameter(Mandatory = $true)][string]$Message, [switch]$Json)

    if ($Json) {
        [Console]::Error.WriteLine("Error: $Message")
        $global:LASTEXITCODE = 3
    } else {
        Write-Host "Error: $Message" -ForegroundColor Red
    }
}