src/Theme.ps1

# Theme.ps1 - composition model.
#
# A *theme* is a composition: a set of slot references into per-layer catalogs.
# themes/*.json -> { scheme, palette, prompt, font, opacity, acrylic, fontSize }
# schemes/*.json -> Windows Terminal color scheme (16 ANSI + bg/fg/cursor)
# palettes/*.json -> PSReadLine input colors + $PSStyle output colors
# prompts/*.json -> oh-my-posh theme reference
# fonts.json -> list of installed (nerd) fonts
#
# Resolve-PoshPaletteTheme expands a composition into the flat shape the
# appliers consume (terminal/psReadLine/psStyle/prompt), so Simple mode (pick a
# preset), Detail mode (override one slot) and headless install all share one path.

function Get-PoshPaletteDataRoot { Split-Path $PSScriptRoot -Parent }
function Get-PoshPaletteThemeRoot { Join-Path (Get-PoshPaletteDataRoot) 'themes' }

# --- Catalog loaders ----------------------------------------------------------

function Get-PoshPaletteCatalog {
    param([Parameter(Mandatory)][ValidateSet('schemes', 'palettes', 'prompts')] [string] $Kind)
    $dir = Join-Path (Get-PoshPaletteDataRoot) $Kind
    Get-ChildItem -Path $dir -Filter '*.json' -File | Sort-Object Name | ForEach-Object {
        $data = Get-Content $_.FullName -Raw | ConvertFrom-Json
        [pscustomobject]@{ Id = $data.id; Name = $data.name; Data = $data }
    }
}

function Get-PoshPaletteCatalogItem {
    param([string] $Kind, [string] $Id)
    $item = Get-PoshPaletteCatalog -Kind $Kind | Where-Object { $_.Id -eq $Id } | Select-Object -First 1
    if (-not $item) { throw "No '$Kind' entry with id '$Id'." }
    $item.Data
}

function Get-PoshPaletteFonts {
    Get-Content (Join-Path (Get-PoshPaletteDataRoot) 'fonts.json') -Raw | ConvertFrom-Json
}

# --- Compositions (presets) ---------------------------------------------------

function Get-PoshPaletteThemes {
    Get-ChildItem -Path (Get-PoshPaletteThemeRoot) -Filter '*.json' -File | Sort-Object Name | ForEach-Object {
        $data = Get-Content $_.FullName -Raw | ConvertFrom-Json
        [pscustomobject]@{
            Id          = $data.id
            Name        = $data.name
            Description = $data.description
            Path        = $_.FullName
            Data        = $data   # the composition
        }
    }
}

# --- Resolver -----------------------------------------------------------------

function ConvertTo-PoshPaletteHashtable {
    param($Object)
    $h = @{}
    if ($Object) { $Object.psobject.Properties | ForEach-Object { $h[$_.Name] = $_.Value } }
    $h
}

# Expand a composition into the flat shape the appliers expect. Returns a
# PSCustomObject identical in shape to a hand-written full theme.
function Resolve-PoshPaletteTheme {
    param([Parameter(Mandatory)] $Composition)

    $scheme  = Get-PoshPaletteCatalogItem -Kind 'schemes'  -Id $Composition.scheme
    $palette = Get-PoshPaletteCatalogItem -Kind 'palettes' -Id $Composition.palette
    $prompt  = Get-PoshPaletteCatalogItem -Kind 'prompts'  -Id $Composition.prompt
    $font    = Get-PoshPaletteFonts | Where-Object { $_.id -eq $Composition.font } | Select-Object -First 1
    if (-not $font) { throw "No font with id '$($Composition.font)'." }

    $schemeBlock = ConvertTo-PoshPaletteHashtable $scheme.colors
    $schemeBlock['name'] = $scheme.name   # WT scheme is named after the scheme, not the composition

    # A prompt is either a reference to a fixed-color oh-my-posh theme, or 'auto',
    # which generates a config from this scheme's colors so the prompt matches.
    $promptBlock = if ($prompt.generate) {
        $style = if ($prompt.style) { $prompt.style } else { 'classic' }
        @{ generated = $true; name = "pp-$($prompt.id)"; config = (New-PoshPaletteOmpConfig $scheme.colors -Style $style) }
    } else {
        @{ ohMyPoshTheme = $prompt.ohMyPoshTheme }
    }

    $resolved = @{
        name     = $Composition.name
        terminal = @{
            font       = $font.face
            fontSize   = ($Composition.fontSize ?? 11)
            opacity    = ($Composition.opacity ?? 100)
            useAcrylic = [bool]($Composition.acrylic ?? $false)
            scheme     = $schemeBlock
        }
        psReadLine = (ConvertTo-PoshPaletteHashtable $palette.psReadLine)
        psStyle    = (ConvertTo-PoshPaletteHashtable $palette.psStyle)
        prompt     = $promptBlock
    }
    # Round-trip to PSCustomObjects so appliers see the same shape as file themes.
    $resolved | ConvertTo-Json -Depth 32 | ConvertFrom-Json
}

# Resolve a composition by id/name/path into the applier-ready shape.
function Import-PoshPaletteTheme {
    param([Parameter(Mandatory)][string] $NameOrPath)

    $composition = if (Test-Path $NameOrPath) {
        Get-Content $NameOrPath -Raw | ConvertFrom-Json
    } else {
        $match = Get-PoshPaletteThemes | Where-Object { $_.Id -eq $NameOrPath -or $_.Name -eq $NameOrPath } | Select-Object -First 1
        if (-not $match) {
            $available = (Get-PoshPaletteThemes | ForEach-Object Id) -join ', '
            throw "Theme '$NameOrPath' not found. Available: $available"
        }
        $match.Data
    }
    Resolve-PoshPaletteTheme -Composition $composition
}