src/UI/Screens/PreferencesScreen.ps1

# PreferencesScreen — Edita Settings vivos.
#
# Lista de preferencias editables. Cada item es una fila navegable; ↵ alterna
# (toggle bool, cycle theme/language). Persiste via SettingsService inmediato
# después de cada cambio.
#
# Diseño deliberado: cero modos extra (input, confirm). Cada acción es atómica —
# enter aplica, no hay nada que confirmar. Si surge una pref que requiera input
# (ej: ReposPath custom), se suma un modo después.
#
# Bindings:
# ↑↓ / jk navegar
# g / G home / end
# ↵ alterna/cycle el item bajo cursor
# ←/h/Esc back al MainScreen
# Q quit
#
# Cursor + alt buffer son del root loop. Sub-screen NO los toca.

class PreferencesScreen {
    [object] $Theme
    [object] $Renderer
    [object] $Primitives
    [object] $Frame
    [object] $AppHeader
    [object] $StatusBar
    [object] $SettingsSvc
    [object] $I18n = $null   # Inyectable post-construct (MainScreen lo pasa).

    [object]   $Settings = $null
    [int]      $SelectedIndex = 0
    [object]   $Viewport = [Viewport]::new()
    [bool]     $QuitRequested = $false
    [bool]     $UseAltBuffer = $true

    [string]   $StatusMessage = ''
    [string]   $StatusKind = ''

    PreferencesScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $settingsSvc) {
        $this.Theme       = $theme
        $this.Renderer    = $renderer
        $this.Primitives  = $primitives
        $this.Frame       = $frame
        $this.AppHeader   = $appHeader
        $this.StatusBar   = $statusBar
        $this.SettingsSvc = $settingsSvc
    }

    [void] Open() {
        if ($null -eq $this.SettingsSvc) {
            throw "PreferencesScreen.Open: SettingsSvc requerido para persistir cambios."
        }
        $this.Settings = $this.SettingsSvc.Load()
        $this.SelectedIndex = 0
        $this.Viewport.Reset()
        $this.StatusMessage = ''
        $this.StatusKind = ''

        if ([Console]::IsInputRedirected) {
            $lines = $this.BuildLines('1.0.0')
            foreach ($l in $lines) { Write-Host $l }
            return
        }

        # Pre-frame INSTANTÁNEO + Flush para pisar el header del MainScreen sin
        # esperar a que el while complete EnsureViewport + BuildLines + Write.
        # Sin esto, hay un breve gap donde el header de la vista anterior queda
        # visible (Tino lo reportó como parpadeo).
        $errOut = [Console]::Error
        $earlyFrame = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + (($this.BuildLines('1.0.0')) -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync()
        $errOut.Write($earlyFrame)
        $errOut.Flush()

        try {
            while ($true) {
                $this.EnsureViewport()
                $lines = $this.BuildLines('1.0.0')
                $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($lines -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync()
                $errOut.Write($framePayload)

                $key = [Console]::ReadKey($true)
                $exit = $this.HandleKey($key)
                if ($exit -eq 'back') { return }
                if ($exit -eq 'quit') { $this.QuitRequested = $true; return }
            }
        }
        finally { }
    }

    hidden [string] HandleKey([System.ConsoleKeyInfo]$key) {
        $k = $key.Key
        $c = $key.KeyChar
        $count = $this.Items().Count

        if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' }
        if ($k -eq 'LeftArrow' -or $c -eq 'h' -or $k -eq 'Escape') { return 'back' }

        if ($k -eq 'UpArrow' -or $c -eq 'k') {
            if ($count -gt 0) {
                $this.SelectedIndex = if ($this.SelectedIndex -le 0) { $count - 1 } else { $this.SelectedIndex - 1 }
            }
            return ''
        }
        if ($k -eq 'DownArrow' -or $c -eq 'j') {
            if ($count -gt 0) {
                $this.SelectedIndex = if ($this.SelectedIndex -ge $count - 1) { 0 } else { $this.SelectedIndex + 1 }
            }
            return ''
        }
        if ($k -eq 'Home' -or $c -eq 'g') { $this.SelectedIndex = 0; return '' }
        if ($k -eq 'End'  -or $c -eq 'G') { $this.SelectedIndex = [Math]::Max(0, $count - 1); return '' }

        if ($k -eq 'Enter') { $this.ApplyCurrent(); return '' }
        return ''
    }

    # Helper i18n con fallback graceful (igual patrón que MainScreen.T).
    hidden [string] T([string]$key) {
        if ($null -ne $this.I18n) { return $this.I18n.T($key) }
        return ([I18nService]::new('es')).T($key)
    }

    hidden [string] T([string]$key, [object[]]$values) {
        if ($null -ne $this.I18n) { return $this.I18n.T($key, $values) }
        return ([I18nService]::new('es')).T($key, $values)
    }

    # Ítems disponibles. Cada uno @{ Key; Label; Type='bool'|'cycle'; Options }.
    # El render y el handler los usan vía SelectedIndex. Los Labels se traducen
    # cada vez que se llama (re-render) — refleja el locale actual aunque el
    # user lo haya cambiado en runtime.
    [object[]] Items() {
        $themesAvailable = @($global:RNThemes.Keys | Where-Object { $null -ne $global:RNThemes[$_] } | Sort-Object)
        return @(
            @{ Key = 'ThemeKey';           Label = $this.T('prefs.theme');         Type = 'cycle'; Options = $themesAvailable }
            @{ Key = 'Language';           Label = $this.T('prefs.language');      Type = 'cycle'; Options = @('es', 'en') }
            @{ Key = 'AutoLoadMode';       Label = $this.T('prefs.autoLoadMode');  Type = 'cycle'; Options = @('All', 'Favorites', 'None') }
            @{ Key = 'AutoFetch';          Label = $this.T('prefs.autoFetch');     Type = 'bool'  }
            @{ Key = 'FavoritesFirst';     Label = $this.T('prefs.favoritesFirst'); Type = 'bool'  }
            @{ Key = 'ShowAliases';        Label = $this.T('prefs.showAliases');   Type = 'bool'  }
            @{ Key = 'ShowTags';           Label = $this.T('prefs.showTags');      Type = 'bool'  }
            @{ Key = 'ShowLastCommit';     Label = $this.T('prefs.showLastCommit'); Type = 'bool'  }
            @{ Key = 'UseAlternateBuffer'; Label = $this.T('prefs.useAlt');        Type = 'bool'  }
            @{ Key = '_setup';             Label = $this.T('prefs.setupAndStatus'); Type = 'action' }
        )
    }

    hidden [void] ApplyCurrent() {
        $items = $this.Items()
        if ($items.Count -eq 0) { return }
        if ($this.SelectedIndex -lt 0 -or $this.SelectedIndex -ge $items.Count) { return }

        $item = $items[$this.SelectedIndex]
        $key  = $item.Key

        switch ($item.Type) {
            'bool' {
                $this.Settings.$key = -not $this.Settings.$key
            }
            'cycle' {
                $opts = @($item.Options)
                if ($opts.Count -le 1) {
                    $this.SetStatus($this.T('prefs.singleValue', @($item.Label)), 'error')
                    return
                }
                $current = [string]$this.Settings.$key
                $idx = [Array]::IndexOf($opts, $current)
                $next = $opts[(($idx + 1) % $opts.Count)]
                $this.Settings.$key = $next
                # Cambios runtime que necesitan propagarse a los services
                # compartidos para que el re-render siguiente refleje el cambio
                # sin esperar a que el user salga de Preferences.
                if ($key -eq 'Language' -and $null -ne $this.I18n) {
                    $this.I18n.SetLocale($next)
                } elseif ($key -eq 'ThemeKey' -and $null -ne $this.Theme) {
                    try { $this.Theme.SetActive($next) } catch { $null = $_ }
                }
            }
            'action' {
                # Acciones no mutan settings; abren sub-screen.
                $this.RunActionItem($item.Key)
                return
            }
        }

        $ok = $this.SettingsSvc.Save($this.Settings)
        if ($ok) {
            $this.SetStatus($this.T('prefs.savedKv', @($item.Label, $this.Settings.$key)), 'ok')
        } else {
            $this.SetStatus($this.T('prefs.saveError'), 'error')
        }
    }

    # Handler para items tipo 'action' — abre sub-screens según el Key.
    hidden [void] RunActionItem([string]$key) {
        if ($key -eq '_setup') {
            $setupSvc = [SetupService]::new()
            $setupScreen = [SetupScreen]::new(
                $this.Theme, $this.Renderer, $this.Primitives,
                $this.Frame, $this.AppHeader, $this.StatusBar, $setupSvc)
            $setupScreen.UseAltBuffer = $this.UseAltBuffer
            $setupScreen.I18n = $this.I18n
            $setupScreen.Open()
            if ($setupScreen.QuitRequested) {
                $this.QuitRequested = $true
            }
        }
    }

    hidden [void] SetStatus([string]$msg, [string]$kind) {
        $this.StatusMessage = $msg
        $this.StatusKind = $kind
    }

    hidden [void] EnsureViewport() {
        $count = $this.Items().Count
        if ($count -eq 0) { $this.Viewport.Reset(); return }
        $available = [Console]::WindowHeight - 8
        $this.Viewport.Resize($available, 1)
        $this.Viewport.EnsureVisible($this.SelectedIndex, $count)
    }

    [string[]] BuildLines([string]$version) {
        $reset = [AnsiService]::Reset
        $r = $this.Renderer
        $items = $this.Items()
        $lines = [System.Collections.Generic.List[string]]::new()

        $title = $this.T('prefs.title')
        # 1. Title bar
        $lines.Add($this.Frame.TitleBar('repo-nav', $title, $version))

        # 2. AppHeader: breadcrumb terminado en el title del screen.
        $lines.Add($this.AppHeader.Render(@(), $title, @()))
        $lines.Add($r.HRule())

        # 3. Status / línea fija para no saltar
        if ($this.StatusMessage) {
            $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' }
            $lines.Add(' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset)
        } else {
            $lines.Add('')
        }

        # 4. Section title
        $lines.Add($this.Theme.Fg('fg2') + ' ' + $title + ' · ' + $items.Count + $reset)
        $lines.Add($r.HRule())

        # 5. Filas
        if ($null -ne $this.Viewport -and $this.Viewport.Capacity -gt 0 -and -not [Console]::IsInputRedirected) {
            $sliceStart = $this.Viewport.Start
            $sliceEnd   = $this.Viewport.EndExclusive($items.Count)
        } else {
            $sliceStart = 0
            $sliceEnd   = $items.Count
        }

        for ($i = $sliceStart; $i -lt $sliceEnd; $i++) {
            $item = $items[$i]
            $lines.Add($this.RenderItemRow($item, ($i -eq $this.SelectedIndex)))
        }

        # 6. StatusBar
        $hints = @(
            @{ k = '↑↓'; label = $this.T('hint.move') }
            @{ k = '↵';  label = $this.T('hint.toggleCycle') }
            @{ k = '←';  label = $this.T('hint.back') }
            @{ k = 'Q';  label = $this.T('hint.quit') }
        )
        $lines.Add($r.HRule())
        return @($lines.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $lines.Count)
    }

    hidden [string] RenderItemRow([hashtable]$item, [bool]$selected) {
        $reset = [AnsiService]::Reset

        $marker = if ($selected) { $this.Theme.Fg('acc') + '▎' + $reset } else { ' ' }
        $cursor = if ($selected) { $this.Theme.Fg('acc') + '▶' + $reset } else { $this.Theme.Fg('fg3') + '·' + $reset }

        $labelFg = if ($selected) { 'acc' } else { 'fg1' }
        $label = [Renderer]::PadRight($this.Theme.Fg($labelFg) + $item.Label + $reset, 22)

        $val = ''
        # Solo leemos $this.Settings.X cuando el item lo necesita (bool/cycle).
        # Para 'action' no hay valor — mostramos un indicador `→`.
        if ($item.Type -eq 'action') {
            $val = '→'
        } elseif ($null -ne $this.Settings) {
            $raw = $this.Settings.($item.Key)
            $val = switch ($item.Type) {
                'bool'  { if ($raw) { $this.T('common.on') } else { $this.T('common.off') } }
                'cycle' { [string]$raw }
                default { [string]$raw }
            }
        }
        $valColor = switch ($item.Type) {
            'bool'   { if ($val -eq 'on') { 'gitClean' } else { 'fg3' } }
            'cycle'  { 'gitAhead' }
            'action' { 'acc' }
            default  { 'fg1' }
        }
        $valFmt = $this.Theme.Fg($valColor) + $val + $reset

        return "$marker $cursor $label $valFmt"
    }
}