src/PowerShell/WorkspaceLifecycle.ps1

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

function ConvertTo-CdpWorkspaceLayout {
    param([string]$Value, [object]$Existing)

    if ([string]::IsNullOrWhiteSpace($Value)) {
        if ($null -ne $Existing) { return $Existing }
        return [PSCustomObject]@{ mode = 'tabs' }
    }
    switch ($Value.ToLowerInvariant()) {
        'tabs' { return [PSCustomObject]@{ mode = 'tabs' } }
        'split-horizontal' { return [PSCustomObject]@{ mode = 'split'; direction = 'horizontal' } }
        'split-vertical' { return [PSCustomObject]@{ mode = 'split'; direction = 'vertical' } }
        default { throw 'Workspace layout must be tabs, split-horizontal, or split-vertical.' }
    }
}

function Test-CdpWorkspaceLayout {
    param([object]$Layout)

    if ($null -eq $Layout) { return $true }
    if (-not ($Layout -is [PSCustomObject] -or $Layout -is [System.Collections.IDictionary])) { return $false }
    $mode = [string]$Layout.mode
    if ($mode -eq 'tabs') { return $true }
    $mode -eq 'split' -and [string]$Layout.direction -in @('horizontal', 'vertical')
}

function Test-CdpWorkspaceObject {
    param([object]$Value)

    $null -ne $Value -and
        ($Value -is [PSCustomObject] -or $Value -is [System.Collections.IDictionary])
}

function Test-CdpWorkspaceSize {
    param([object]$Value)

    $integerTypes = @(
        [System.Byte], [System.SByte], [System.Int16], [System.UInt16],
        [System.Int32], [System.UInt32], [System.Int64], [System.UInt64]
    )
    $isInteger = $false
    foreach ($integerType in $integerTypes) {
        if ($Value -is $integerType) { $isInteger = $true; break }
    }
    if (-not $isInteger) { return $false }
    [decimal]$Value -ge 10 -and [decimal]$Value -le 90
}

function New-CdpWorkspaceProjectReference {
    param([Parameter(Mandatory = $true)][object]$Project)

    [PSCustomObject][ordered]@{
        name = [string]$Project.name
        rootPath = [string]$Project.rootPath
    }
}

function Get-CdpWorkspaceProjectReferences {
    param([string[]]$Names, [object[]]$Projects)

    $references = @()
    foreach ($name in $Names) {
        $projectMatches = @($Projects | Where-Object {
            $_.enabled -eq $true -and [string]::Equals([string]$_.name, $name, [StringComparison]::Ordinal)
        })
        if ($projectMatches.Count -ne 1) { throw "Workspace project '$name' must match one enabled project." }
        $references += New-CdpWorkspaceProjectReference -Project $projectMatches[0]
    }
    $references
}

function Get-CdpWorkspaceReferenceValue {
    param([object]$Reference, [string]$Name)

    if ($Reference -is [string] -or $null -eq $Reference) { return $null }
    if ($Reference -is [System.Collections.IDictionary] -and $Reference.Contains($Name)) {
        return $Reference[$Name]
    }
    $property = $Reference.PSObject.Properties[$Name]
    if ($property) { return $property.Value }
    $null
}

function Set-CdpWorkspaceReferenceValue {
    param([object]$Reference, [string]$Name, [object]$Value)

    if ($Reference -is [System.Collections.IDictionary]) {
        $Reference[$Name] = $Value
        return
    }
    if ($Reference.PSObject.Properties[$Name]) { $Reference.$Name = $Value }
    else { $Reference | Add-Member -NotePropertyName $Name -NotePropertyValue $Value }
}

function Get-CdpWorkspaceReferenceSchemaStatus {
    param([object]$Reference)

    if ($Reference -is [string]) {
        if ([string]::IsNullOrWhiteSpace([string]$Reference)) { return 'invalid-reference' }
        return 'ok'
    }
    if (-not (Test-CdpWorkspaceObject $Reference)) { return 'invalid-reference' }
    $name = [string](Get-CdpWorkspaceReferenceValue $Reference 'name')
    $rootPath = [string](Get-CdpWorkspaceReferenceValue $Reference 'rootPath')
    if ([string]::IsNullOrWhiteSpace($name) -or [string]::IsNullOrWhiteSpace($rootPath)) {
        return 'invalid-reference'
    }
    $sizeProperty = $Reference.PSObject.Properties['size']
    if ($Reference -is [System.Collections.IDictionary]) { $sizeProperty = $Reference.Contains('size') }
    if ($sizeProperty -and -not (Test-CdpWorkspaceSize (Get-CdpWorkspaceReferenceValue $Reference 'size'))) {
        return 'invalid-size'
    }
    $open = Get-CdpWorkspaceReferenceValue $Reference 'open'
    if ($null -ne $open) {
        if ([string]::IsNullOrWhiteSpace([string]$open)) { return 'invalid-launcher' }
        try { [void](Get-CdpWorkspaceLauncher -Open ([string]$open)) }
        catch { return 'invalid-launcher' }
    }
    'ok'
}

function Resolve-CdpWorkspaceReference {
    param([object]$Reference, [object[]]$Projects, [string]$WorkspaceOpen)

    $schemaStatus = Get-CdpWorkspaceReferenceSchemaStatus -Reference $Reference
    $legacy = $Reference -is [string]
    $configuredName = if ($legacy) { [string]$Reference } else { [string](Get-CdpWorkspaceReferenceValue $Reference 'name') }
    $rawPath = if ($legacy) { '' } else { [string](Get-CdpWorkspaceReferenceValue $Reference 'rootPath') }
    $projectMatches = @(
        if ($legacy) {
            $Projects | Where-Object { [string]::Equals([string]$_.name, $configuredName, [StringComparison]::Ordinal) }
        } else {
            $Projects | Where-Object { [string]::Equals([string]$_.rootPath, $rawPath, [StringComparison]::Ordinal) }
        }
    )
    $project = if ($projectMatches.Count -eq 1) { $projectMatches[0] } else { $null }
    $status = if ($schemaStatus -ne 'ok') {
        $schemaStatus
    } elseif ($projectMatches.Count -eq 0) {
        'missing-project'
    } elseif ($projectMatches.Count -gt 1) {
        'ambiguous-project'
    } elseif ($project.enabled -ne $true) {
        'disabled-project'
    } elseif ($legacy) {
        'legacy'
    } elseif (-not [string]::Equals([string]$project.name, $configuredName, [StringComparison]::Ordinal)) {
        'renamed'
    } else { 'ok' }
    New-CdpWorkspaceResolvedReference -Reference $Reference -Project $project -Status $status -WorkspaceOpen $WorkspaceOpen
}

function New-CdpWorkspaceResolvedReference {
    param([object]$Reference, [object]$Project, [string]$Status, [string]$WorkspaceOpen)

    $legacy = $Reference -is [string]
    $hasProject = $null -ne $Project
    $configuredName = if ($legacy) { [string]$Reference } else { [string](Get-CdpWorkspaceReferenceValue $Reference 'name') }
    $rawPath = if ($legacy -and $hasProject) { [string]$Project.rootPath } elseif ($legacy) { '' } else { [string](Get-CdpWorkspaceReferenceValue $Reference 'rootPath') }
    $resolution = if ($hasProject) { Resolve-CdpProjectPath -Project $Project } else { $null }
    $canReplaceStatus = $Status -in @('ok', 'legacy', 'renamed')
    if ($canReplaceStatus -and $null -ne $resolution -and $resolution.ErrorCode) { $Status = 'invalid-path-profile' }
    elseif ($canReplaceStatus -and $null -ne $resolution -and -not (Test-Path -LiteralPath $resolution.ResolvedPath)) { $Status = 'missing-path' }
    $projectOpen = [string](Get-CdpWorkspaceReferenceValue $Reference 'open')
    $launcherName = if ($projectOpen) { $projectOpen } else { $WorkspaceOpen }
    try { if ($launcherName) { [void](Get-CdpWorkspaceLauncher -Open $launcherName) } }
    catch { if ($Status -in @('ok', 'legacy', 'renamed')) { $Status = 'invalid-launcher' } }
    [PSCustomObject]@{ IsProjectReference=$true; Reference=$Reference; ConfiguredName=$configuredName; Name=if($hasProject){[string]$Project.name}else{$configuredName}; RawPath=$rawPath; ResolvedPath=if($null -ne $resolution){$resolution.ResolvedPath}else{''}; Project=$Project; Status=$Status; Launcher=$launcherName; Size=Get-CdpWorkspaceReferenceValue $Reference 'size' }
}

function Get-CdpWorkspaceValidation {
    param([object]$Workspace, [object[]]$Projects)

    $results = @()
    if (-not (Test-CdpWorkspaceObject $Workspace) -or [string]::IsNullOrWhiteSpace([string]$Workspace.name)) {
        return ,([PSCustomObject]@{ Name=''; Status='invalid-workspace'; RawPath=''; ResolvedPath=''; Reference=$null; Project=$null })
    }
    if (-not (Test-CdpWorkspaceLayout -Layout $Workspace.layout)) {
        $results += [PSCustomObject]@{ Name=[string]$Workspace.name; Status='invalid-layout'; RawPath=''; ResolvedPath=''; Reference=$null; Project=$null }
    }
    try { if ($Workspace.open) { [void](Get-CdpWorkspaceLauncher -Open ([string]$Workspace.open)) } } catch {
        $results += [PSCustomObject]@{ Name=[string]$Workspace.name; Status='invalid-launcher'; RawPath=''; ResolvedPath=''; Reference=$null; Project=$null }
    }
    $references = @($Workspace.projects)
    if ($references.Count -eq 0) {
        $results += [PSCustomObject]@{ Name=[string]$Workspace.name; Status='invalid-reference'; RawPath=''; ResolvedPath=''; Reference=$null; Project=$null }
    }
    foreach ($reference in $references) {
        $results += Resolve-CdpWorkspaceReference -Reference $reference -Projects $Projects -WorkspaceOpen ([string]$Workspace.open)
    }
    $results
}

function Convert-CdpWorkspaceReferences {
    param([object]$Workspace, [object[]]$Validation)

    $updated = @()
    $changed = $false
    foreach ($result in $Validation) {
        if (-not $result.IsProjectReference) { continue }
        if ($null -ne $result.Project -and $result.Reference -is [string]) {
            $reference = New-CdpWorkspaceProjectReference -Project $result.Project
            $updated += $reference
            $changed = $true
        } elseif ($null -ne $result.Project -and -not [string]::Equals([string]$result.Project.name, [string](Get-CdpWorkspaceReferenceValue $result.Reference 'name'), [StringComparison]::Ordinal)) {
            Set-CdpWorkspaceReferenceValue -Reference $result.Reference -Name name -Value ([string]$result.Project.name)
            $updated += $result.Reference
            $changed = $true
        } else { $updated += $result.Reference }
    }
    $Workspace.projects = @($updated)
    [PSCustomObject]@{ Workspace=$Workspace; Changed=$changed }
}

function Get-CdpWorkspaceByName {
    param([object[]]$Workspaces, [string]$Name)

    @($Workspaces | Where-Object {
        [string]::Equals([string]$_.name, $Name, [StringComparison]::Ordinal)
    } | Select-Object -First 1)
}

function Read-CdpWorkspaceDocument {
    param([Parameter(Mandatory = $true)][string]$Path)

    if (Test-Path -LiteralPath $Path) {
        $content = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
        if (-not $content.TrimStart().StartsWith('[')) { throw 'Workspace configuration must be a JSON array.' }
        $document = Read-CdpJsonDocument -LiteralPath $Path
        return $document
    }
    [PSCustomObject]@{ Value = @(); Fingerprint = 'missing' }
}

function Get-CdpWorkspaceConfigContext {
    param([string]$ConfigPath)

    if ([string]::IsNullOrWhiteSpace($ConfigPath)) { $ConfigPath = Get-DefaultConfigPath }
    [PSCustomObject]@{
        ConfigPath = $ConfigPath
        Data = Get-CdpProjectConfig -ConfigPath $ConfigPath
    }
}

function Get-CdpWorkspaceLayoutLabel {
    param([object]$Layout)

    $normalized = ConvertTo-CdpWorkspaceLayout -Existing $Layout
    if (-not (Test-CdpWorkspaceLayout -Layout $normalized)) { return 'invalid' }
    if ($normalized.mode -eq 'split') { return "split-$($normalized.direction)" }
    'tabs'
}

function Show-CdpWorkspaceList {
    param([object[]]$Workspaces)

    if ($Workspaces.Count -eq 0) {
        Write-Host 'No workspaces defined.' -ForegroundColor Yellow
        Write-Host 'Create one: cdp workspace add <name> <project1> <project2> ...' -ForegroundColor Gray
        return
    }
    Write-Host "`ncdp workspaces" -ForegroundColor Cyan
    Write-Host ('-' * 60)
    foreach ($workspace in $Workspaces) {
        $names = @($workspace.projects | ForEach-Object { if ($_ -is [string]) { $_ } else { $_.name } })
        $openLabel = if ($workspace.open) { " [$($workspace.open)]" } else { '' }
        Write-Host " $($workspace.name)" -ForegroundColor Green -NoNewline
        Write-Host $openLabel -ForegroundColor Cyan -NoNewline
        Write-Host " -> $($names -join ', ')" -ForegroundColor Gray
    }
    Write-Host ('-' * 60)
}

function Show-CdpWorkspaceDetail {
    param([object]$Workspace, [object[]]$Projects, [switch]$PassThru)

    $validation = @(Get-CdpWorkspaceValidation -Workspace $Workspace -Projects $Projects)
    $layoutLabel = Get-CdpWorkspaceLayoutLabel -Layout $Workspace.layout
    Write-Host "`nworkspace: $($Workspace.name)" -ForegroundColor Cyan
    Write-Host "layout: $layoutLabel" -ForegroundColor Gray
    foreach ($item in $validation) {
        $launcher = if ($item.Launcher) { $item.Launcher } else { '-' }
        Write-Host " $($item.Name) [$($item.Status)] raw=$($item.RawPath) resolved=$($item.ResolvedPath) launcher=$launcher" -ForegroundColor Gray
    }
    if ($PassThru) {
        [PSCustomObject]@{
            name = [string]$Workspace.name
            open = [string]$Workspace.open
            layout = $layoutLabel
            workspace = $Workspace
            projects = $validation
        }
    }
}

function New-CdpWorkspaceDefinition {
    param([string]$Name, [string[]]$ProjectNames, [object[]]$Projects, [string]$Open, [string]$Layout)

    if ([string]::IsNullOrWhiteSpace($Name) -or $ProjectNames.Count -eq 0) {
        throw 'Workspace add requires a name and at least one project.'
    }
    if ($Open) { [void](Get-CdpWorkspaceLauncher -Open $Open) }
    $workspace = [PSCustomObject][ordered]@{
        name = $Name
        projects = @(Get-CdpWorkspaceProjectReferences -Names $ProjectNames -Projects $Projects)
    }
    if ($Open) { $workspace | Add-Member -NotePropertyName open -NotePropertyValue $Open }
    if ($Layout) { $workspace | Add-Member -NotePropertyName layout -NotePropertyValue (ConvertTo-CdpWorkspaceLayout $Layout) }
    $workspace
}

function Update-CdpWorkspaceDefinition {
    param([object]$Workspace, [string[]]$ProjectNames, [object[]]$Projects, [string]$Open, [string]$Layout, [switch]$ClearOpen)

    $before = ConvertTo-Json -InputObject $Workspace -Depth 100 -Compress
    if ($ProjectNames.Count -gt 0) {
        $Workspace.projects = @(Get-CdpWorkspaceProjectReferences -Names $ProjectNames -Projects $Projects)
    }
    if ($ClearOpen -and $Workspace.PSObject.Properties['open']) { $Workspace.PSObject.Properties.Remove('open') }
    elseif ($Open) {
        [void](Get-CdpWorkspaceLauncher -Open $Open)
        Set-CdpWorkspaceReferenceValue -Reference $Workspace -Name open -Value $Open
    }
    if ($Layout) {
        Set-CdpWorkspaceReferenceValue -Reference $Workspace -Name layout -Value (ConvertTo-CdpWorkspaceLayout $Layout)
    }
    $after = ConvertTo-Json -InputObject $Workspace -Depth 100 -Compress
    [PSCustomObject]@{ Workspace=$Workspace; Changed=($before -ne $after) }
}

function Write-CdpWorkspaceValidation {
    param([object[]]$Results)

    foreach ($item in $Results) {
        $color = if ($item.Status -in @('ok', 'legacy', 'renamed')) { 'Yellow' } else { 'Red' }
        Write-Host " $($item.Name): $($item.Status)" -ForegroundColor $color
    }
}

function New-CdpWorkspaceLaunchPlan {
    param([object]$Workspace, [object[]]$Projects, [string]$OpenOverride)

    $overrideLauncher = $null
    if ($OpenOverride) { $overrideLauncher = Get-CdpWorkspaceLauncher -Open $OpenOverride }
    $validation = @(Get-CdpWorkspaceValidation -Workspace $Workspace -Projects $Projects)
    foreach ($item in $validation) {
        $launcherName = if ($OpenOverride) { $OpenOverride } else { [string]$item.Launcher }
        $launcher = if ($OpenOverride) { $overrideLauncher } else { $null }
        if (-not $OpenOverride -and $launcherName) { try { $launcher = Get-CdpWorkspaceLauncher -Open $launcherName } catch { $item.Status = 'invalid-launcher' } }
        [PSCustomObject]@{
            Name = $item.Name
            RawPath = $item.RawPath
            ResolvedPath = $item.ResolvedPath
            Project = $item.Project
            Status = $item.Status
            Launcher = $launcher
            Size = $item.Size
        }
    }
}

function New-CdpLegacyWorkspaceLaunchPlan {
    param([object[]]$Projects, [string[]]$ProjectNames, [object]$Launcher)

    foreach ($name in $ProjectNames) {
        $projectMatches = @($Projects | Where-Object {
            [string]::Equals([string]$_.name, $name, [StringComparison]::Ordinal)
        })
        if ($projectMatches.Count -ne 1) {
            [PSCustomObject]@{ Name=$name; Status='missing-project'; ResolvedPath=''; Launcher=$Launcher }
            continue
        }
        $resolution = Resolve-CdpProjectPath -Project $projectMatches[0]
        $status = if ($resolution.ErrorCode) { 'invalid-path-profile' } elseif (Test-Path -LiteralPath $resolution.ResolvedPath) { 'ok' } else { 'missing-path' }
        [PSCustomObject]@{ Name=$name; Project=$projectMatches[0]; RawPath=[string]$projectMatches[0].rootPath; ResolvedPath=$resolution.ResolvedPath; Status=$status; Launcher=$Launcher; Size=$null }
    }
}

function New-CdpWindowsTerminalLaunchPlan {
    param([object[]]$Plan, [object]$Layout)

    foreach ($item in $Plan) {
        $launchable = $item.Status -in @('ok', 'legacy', 'renamed')
        [PSCustomObject]@{
            Item = $item
            FirstArguments = if ($launchable) { @(Get-CdpWindowsTerminalWorkspaceArguments -Item $item -Layout $Layout -First $true) } else { @() }
            NextArguments = if ($launchable) { @(Get-CdpWindowsTerminalWorkspaceArguments -Item $item -Layout $Layout -First $false) } else { @() }
        }
    }
}

function Get-CdpWindowsTerminalWorkspaceArguments {
    param([object]$Item, [object]$Layout, [bool]$First)

    $arguments = @('-w', '0')
    if ($Layout.mode -eq 'split' -and -not $First) {
        $arguments += 'split-pane'
        $arguments += if ($Layout.direction -eq 'vertical') { '-V' } else { '-H' }
        if ($Item.Size) { $arguments += @('-s', ([double]$Item.Size / 100).ToString('0.##', [Globalization.CultureInfo]::InvariantCulture)) }
    } else { $arguments += 'new-tab' }
    $arguments += @('-d', $Item.ResolvedPath, '--title', $Item.Name)
    if ($Item.Launcher) { $arguments += @('--', $Item.Launcher.Command) + @($Item.Launcher.Arguments) }
    $arguments
}