src/Appliers.ps1

# Appliers.ps1 - write theme into the 4 layers.
# Layer 1 Windows Terminal -> settings.json (hot-reloads instantly)
# Layer 2 PSReadLine -> $PROFILE managed block + live session
# Layer 3 $PSStyle output -> $PROFILE managed block + live session
# Layer 4 oh-my-posh prompt -> $PROFILE managed block + live session

$script:BlockStart = '# >>> PoshPalette >>>'
$script:BlockEnd   = '# <<< PoshPalette <<<'

# --- Layer 1: Windows Terminal ------------------------------------------------

function Get-WindowsTerminalSettingsPath {
    if (-not $IsWindows -and $PSVersionTable.PSEdition -ne 'Desktop') { return $null }
    $candidates = @(
        "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json",
        "$env:LOCALAPPDATA\Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json",
        "$env:APPDATA\Microsoft\Windows Terminal\settings.json"  # unpackaged / scoop
    )
    $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1
}

function Backup-PoshPaletteFile {
    param([string] $Path)
    if ($Path -and (Test-Path $Path)) {
        $stamp  = (Get-Date -Format 'yyyyMMdd-HHmmss')
        $backup = "$Path.poshpalette-$stamp.bak"
        Copy-Item $Path $backup -Force
        return $backup
    }
}

# Build the hashtable we upsert into settings.schemes / settings.profiles.defaults.
function Get-PoshPaletteTerminalEdits {
    param($Theme)
    $scheme = @{}
    $Theme.terminal.scheme.psobject.Properties | ForEach-Object { $scheme[$_.Name] = $_.Value }
    if (-not $scheme['name']) { $scheme['name'] = $Theme.name }
    @{
        SchemeName = $scheme['name']
        Scheme     = $scheme
        Defaults   = [ordered]@{
            colorScheme = $scheme['name']
            opacity     = $Theme.terminal.opacity
            useAcrylic  = [bool]$Theme.terminal.useAcrylic
            font        = [ordered]@{ face = $Theme.terminal.font; size = $Theme.terminal.fontSize }
        }
    }
}

# Comment-preserving write: edit only the spans that change, so the user's
# comments, key order, and formatting survive. Falls back to a parse->reserialize
# round-trip if the file is too irregular to edit surgically.
function Set-PoshPaletteTerminalLayer {
    param($Theme, [string] $SettingsPath, [switch] $DryRun)

    $edits      = Get-PoshPaletteTerminalEdits $Theme
    $schemeName = $edits.SchemeName

    # Don't set a font that isn't installed - that's what makes Windows Terminal
    # pop the "Unable to find the following fonts" warning. Keep the user's font
    # and point them at the installer instead.
    $face = $edits.Defaults.font.face
    if ($face -and -not (Test-PoshPaletteFontInstalled $face)) {
        Write-Host " Font '$face' is not installed - keeping your current terminal font." -ForegroundColor Yellow
        Write-Host " Install it with: Install-PoshPaletteFont robotomono" -ForegroundColor DarkGray
        $edits.Defaults.Remove('font')
    }

    if ($DryRun) {
        Write-Host " [dry-run] would write Terminal scheme '$($Theme.name)' to $SettingsPath" -ForegroundColor DarkGray
        return
    }

    $text = Get-Content $SettingsPath -Raw
    $wrote = $false
    try {
        $rootOpen = $text.IndexOf('{')
        if ($rootOpen -lt 0) { throw 'no root object' }

        # profiles.defaults.{colorScheme,opacity,useAcrylic,font}
        $prof = Find-JsoncMember $text $rootOpen 'profiles'
        if (-not $prof) {
            $text = Set-JsoncMember $text $rootOpen 'profiles' '{ "defaults": {} }'
            $prof = Find-JsoncMember $text $rootOpen 'profiles'
        }
        $profOpen = $text.IndexOf('{', $prof.ValueStart)
        $defM = Find-JsoncMember $text $profOpen 'defaults'
        if (-not $defM) {
            $text = Set-JsoncMember $text $profOpen 'defaults' '{}'
            $defM = Find-JsoncMember $text $profOpen 'defaults'
        }
        $defOpen = $text.IndexOf('{', $defM.ValueStart)
        $text = Set-JsoncMember $text $defOpen 'colorScheme' ('"' + $schemeName + '"')
        $defOpen = $text.IndexOf('{', (Find-JsoncMember $text $profOpen 'defaults').ValueStart)
        $text = Set-JsoncMember $text $defOpen 'opacity' ([string]$edits.Defaults.opacity)
        $defOpen = $text.IndexOf('{', (Find-JsoncMember $text $profOpen 'defaults').ValueStart)
        $text = Set-JsoncMember $text $defOpen 'useAcrylic' ($edits.Defaults.useAcrylic.ToString().ToLower())
        if ($edits.Defaults.Contains('font')) {
            $defOpen = $text.IndexOf('{', (Find-JsoncMember $text $profOpen 'defaults').ValueStart)
            $fontJson = ($edits.Defaults.font | ConvertTo-Json -Depth 5 -Compress)
            $text = Set-JsoncMember $text $defOpen 'font' $fontJson
        }

        # schemes[] upsert by name
        $rootOpen = $text.IndexOf('{')
        $schM = Find-JsoncMember $text $rootOpen 'schemes'
        if (-not $schM) {
            $text = Set-JsoncMember $text $rootOpen 'schemes' '[]'
            $schM = Find-JsoncMember $text $rootOpen 'schemes'
        }
        $arrOpen = $text.IndexOf('[', $schM.ValueStart)
        $schemeJson = ($edits.Scheme | ConvertTo-Json -Depth 5)
        $text = Set-JsoncArrayItemByName $text $arrOpen $schemeName $schemeJson

        # validate before committing; if it doesn't parse, fall back
        $null = ConvertFrom-Jsonc $text
        Set-Content -Path $SettingsPath -Value $text -Encoding utf8
        $wrote = $true
    } catch {
        Write-Verbose "Surgical JSONC edit failed ($_); falling back to round-trip."
    }

    if (-not $wrote) {
        $settings = ConvertFrom-Jsonc (Get-Content $SettingsPath -Raw) -AsHashtable
        if (-not $settings.schemes) { $settings.schemes = @() }
        $settings.schemes = @($settings.schemes | Where-Object { $_.name -ne $schemeName }) + $edits.Scheme
        if (-not $settings.profiles)          { $settings.profiles = @{} }
        if (-not $settings.profiles.defaults) { $settings.profiles.defaults = @{} }
        $d = $settings.profiles.defaults
        $d.colorScheme = $schemeName
        $d.opacity     = $edits.Defaults.opacity
        $d.useAcrylic  = $edits.Defaults.useAcrylic
        if ($edits.Defaults.Contains('font')) { $d.font = @{ face = $edits.Defaults.font.face; size = $edits.Defaults.font.size } }
        Set-Content -Path $SettingsPath -Value ($settings | ConvertTo-Json -Depth 32) -Encoding utf8
    }
}

# --- Layers 2-4: the $PROFILE managed block -----------------------------------

function New-PoshPaletteProfileBlock {
    param($Theme, [switch] $DryRun)
    $sb = [System.Text.StringBuilder]::new()
    [void]$sb.AppendLine($script:BlockStart)
    [void]$sb.AppendLine('# Managed by PoshPalette - edit via the app, not by hand.')

    # Layer 2: PSReadLine input colors
    [void]$sb.AppendLine('if (Get-Module -ListAvailable PSReadLine) {')
    [void]$sb.AppendLine(' Set-PSReadLineOption -Colors @{')
    foreach ($p in $Theme.psReadLine.psobject.Properties) {
        [void]$sb.AppendLine(" $($p.Name) = '$($p.Value)'")
    }
    [void]$sb.AppendLine(' }')
    [void]$sb.AppendLine('}')

    # Layer 3: $PSStyle output colors (PS 7.2+)
    [void]$sb.AppendLine('if ($PSStyle) {')
    if ($Theme.psStyle.Directory)   { [void]$sb.AppendLine(" `$PSStyle.FileInfo.Directory = `$PSStyle.Foreground.FromRgb('$($Theme.psStyle.Directory)')") }
    if ($Theme.psStyle.Error)       { [void]$sb.AppendLine(" `$PSStyle.Formatting.Error = `$PSStyle.Foreground.FromRgb('$($Theme.psStyle.Error)')") }
    if ($Theme.psStyle.TableHeader) { [void]$sb.AppendLine(" `$PSStyle.Formatting.TableHeader = `$PSStyle.Foreground.FromRgb('$($Theme.psStyle.TableHeader)')") }
    [void]$sb.AppendLine('}')

    # Layer 4: oh-my-posh prompt. A generated ('auto') prompt is written to a
    # managed config file and referenced by absolute path; a referenced theme
    # resolves against $env:POSH_THEMES_PATH at load time.
    if ($Theme.prompt.generated) {
        $cfgPath = if ($DryRun) { Join-Path $HOME '.poshpalette/prompts/pp-auto.omp.json' }
                   else { Save-PoshPalettePrompt -Config $Theme.prompt.config -Name $Theme.prompt.name }
        [void]$sb.AppendLine('if (Get-Command oh-my-posh -ErrorAction SilentlyContinue) {')
        [void]$sb.AppendLine(" oh-my-posh init pwsh --config `"$cfgPath`" | Invoke-Expression")
        [void]$sb.AppendLine('}')
    }
    elseif ($Theme.prompt.ohMyPoshTheme) {
        [void]$sb.AppendLine('if (Get-Command oh-my-posh -ErrorAction SilentlyContinue) {')
        [void]$sb.AppendLine(" oh-my-posh init pwsh --config `"`$env:POSH_THEMES_PATH\$($Theme.prompt.ohMyPoshTheme).omp.json`" | Invoke-Expression")
        [void]$sb.AppendLine('}')
    }
    [void]$sb.AppendLine($script:BlockEnd)
    $sb.ToString()
}

function Set-PoshPaletteProfileLayer {
    param($Theme, [string] $ProfilePath = $PROFILE, [switch] $DryRun)

    $block   = New-PoshPaletteProfileBlock $Theme -DryRun:$DryRun
    $existing = if (Test-Path $ProfilePath) { Get-Content $ProfilePath -Raw } else { '' }

    # Replace an existing managed block, otherwise append. Idempotent re-apply.
    $pattern = "(?s)$([regex]::Escape($script:BlockStart)).*?$([regex]::Escape($script:BlockEnd))"
    $updated = if ($existing -match $pattern) {
        [regex]::Replace($existing, $pattern, [System.Text.RegularExpressions.MatchEvaluator]{ param($m) $block.TrimEnd() })
    } else {
        ($existing.TrimEnd() + "`n`n" + $block).TrimStart()
    }

    if ($DryRun) {
        Write-Host " [dry-run] would update PoshPalette block in $ProfilePath" -ForegroundColor DarkGray
        return
    }
    $dir = Split-Path $ProfilePath -Parent
    if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
    Set-Content -Path $ProfilePath -Value $updated -Encoding utf8

    # Apply to the *current* session too, so the change is visible immediately.
    $inner = ($block -split "`n" | Where-Object { $_ -notmatch '^\s*#' }) -join "`n"
    try { . ([scriptblock]::Create($inner)) } catch { Write-Verbose "Live apply skipped: $_" }
}

# --- Orchestrator -------------------------------------------------------------

function Set-PoshPaletteTheme {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $Theme,
        [string] $SettingsPath = (Get-WindowsTerminalSettingsPath),
        [string] $ProfilePath  = $PROFILE,
        [switch] $DryRun
    )

    Write-Host "Applying '$($Theme.name)'..." -ForegroundColor Cyan

    if ($SettingsPath) {
        if (-not $DryRun) { Backup-PoshPaletteFile $SettingsPath | Out-Null }
        Set-PoshPaletteTerminalLayer -Theme $Theme -SettingsPath $SettingsPath -DryRun:$DryRun
        Write-Host " Terminal scheme applied (hot-reloads instantly)" -ForegroundColor Green
    } else {
        Write-Host " Windows Terminal settings.json not found - skipping Terminal layer." -ForegroundColor Yellow
        Write-Host " (This is expected when not on Windows / Windows Terminal.)" -ForegroundColor DarkGray
    }

    if (-not $DryRun) { Backup-PoshPaletteFile $ProfilePath | Out-Null }
    Set-PoshPaletteProfileLayer -Theme $Theme -ProfilePath $ProfilePath -DryRun:$DryRun
    Write-Host " Prompt + input/output colors applied" -ForegroundColor Green
    Write-Host "Done. Open a new tab if the prompt didn't refresh." -ForegroundColor Cyan
}

# --- Revert -------------------------------------------------------------------

# Backups are named "<file>.poshpalette-<timestamp>.bak" next to the original.
function Get-PoshPaletteBackups {
    param([string] $Path)
    if (-not $Path) { return @() }
    $dir  = Split-Path $Path -Parent
    $leaf = Split-Path $Path -Leaf
    Get-ChildItem -Path $dir -Filter "$leaf.poshpalette-*.bak" -ErrorAction SilentlyContinue |
        Sort-Object LastWriteTime -Descending
}

function Restore-PoshPalette {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [string] $SettingsPath = (Get-WindowsTerminalSettingsPath),
        [string] $ProfilePath  = $PROFILE,
        [switch] $KeepProfileBlock   # only revert Terminal settings, leave the profile block
    )

    Write-Host "Reverting PoshPalette..." -ForegroundColor Cyan

    # 1. Restore Windows Terminal settings.json from the newest backup.
    if ($SettingsPath) {
        $bak = Get-PoshPaletteBackups -Path $SettingsPath | Select-Object -First 1
        if ($bak -and $PSCmdlet.ShouldProcess($SettingsPath, "Restore from $($bak.Name)")) {
            Copy-Item $bak.FullName $SettingsPath -Force
            Write-Host " Restored Terminal settings from $($bak.Name)" -ForegroundColor Green
        } elseif (-not $bak) {
            Write-Host " No Terminal settings backup found - skipping." -ForegroundColor Yellow
        }
    }

    # 2. Remove the managed profile block (clean revert of layers 2-4).
    if (-not $KeepProfileBlock -and (Test-Path $ProfilePath)) {
        $content = Get-Content $ProfilePath -Raw
        $pattern = "(?s)\r?\n?$([regex]::Escape($script:BlockStart)).*?$([regex]::Escape($script:BlockEnd))\r?\n?"
        if ($content -match $pattern) {
            if ($PSCmdlet.ShouldProcess($ProfilePath, 'Remove PoshPalette block')) {
                $new = ([regex]::Replace($content, $pattern, "`n")).TrimEnd() + "`n"
                Set-Content -Path $ProfilePath -Value $new -Encoding utf8
                Write-Host " Removed PoshPalette block from profile." -ForegroundColor Green
            }
        } else {
            Write-Host " No PoshPalette block in profile." -ForegroundColor Yellow
        }
    }

    Write-Host "Restart PowerShell for the prompt/input/output to fully revert." -ForegroundColor Cyan
}