src/UI/Screens/CherryPickScreen.ps1

# CherryPickScreen — Selector de source branch + lista de commits exclusivos +
# cherry-pick uno por uno (réplica del CherryPickFlowController v2).
#
# Flow:
# 1. select-source — lista de branches != current. ←→ filter, ↵ pick.
# 2. pick-commits — lista de commits que están en source pero no en current,
# ↵ aplica cherry-pick. Loop hasta Esc para volver al source.
#
# Si el cherry-pick falla por conflict, hacemos --abort y mostramos el error
# (el user decide si reintentar manual con git en otra terminal).
#
# Usa 2 FilteredListPicker instances — uno para branches (label simple) y otro
# para commits (RenderRow custom con hash/subject/author + MatchFn que matchea
# subject + hash juntos).

class CherryPickScreen {
    [object] $Theme
    [object] $Renderer
    [object] $Primitives
    [object] $Frame
    [object] $AppHeader
    [object] $StatusBar
    [object] $Git
    [object] $I18n = $null   # Inyectable post-construct.

    [object]   $Repo
    [string]   $CurrentBranch = ''
    [string]   $SourceBranch = ''
    [string]   $Mode = 'select-source'   # 'select-source' | 'pick-commits'

    [object]   $BranchPicker = $null
    [object]   $CommitPicker = $null

    [int]      $PickedCount = 0

    [bool]     $QuitRequested = $false
    [bool]     $UseAltBuffer = $true

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

    CherryPickScreen($theme, $renderer, $primitives, $frame, $appHeader, $statusBar, $git) {
        $this.Theme       = $theme
        $this.Renderer    = $renderer
        $this.Primitives  = $primitives
        $this.Frame       = $frame
        $this.AppHeader   = $appHeader
        $this.StatusBar   = $statusBar
        $this.Git         = $git
        $this.InitPickers()
    }

    hidden [void] InitPickers() {
        $this.BranchPicker = [FilteredListPicker]::new()
        $this.BranchPicker.Title = 'Filtrar branches'

        $this.CommitPicker = [FilteredListPicker]::new()
        $this.CommitPicker.Title = 'Filtrar commits'
        # Match contra subject + hash juntos (igual que el v2).
        $this.CommitPicker.MatchFn = {
            param($item, $needleLower)
            return ($item.Subject + ' ' + $item.Hash).ToLowerInvariant().Contains($needleLower)
        }
        # Render row: hash coloreado + subject + ' · author · date' atenuado.
        $this.CommitPicker.RenderRowFn = {
            param($item, $sel, $theme, $reset)
            $marker = if ($sel) { $theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' }
            $hashFg = if ($sel) { $theme.Fg('acc') } else { $theme.Fg('gitAhead') }
            $hash = $hashFg + $item.Hash + $reset
            $subject = $theme.Fg($(if ($sel) { 'acc' } else { 'fg1' })) + $item.Subject + $reset
            $meta = $theme.Fg('fg3') + (' · ' + $item.Author + ' · ' + $item.Date) + $reset
            return $marker + $hash + ' ' + $subject + $meta
        }
    }

    [void] Open([object]$repo) {
        if ($null -eq $repo) { throw "CherryPickScreen.Open: repo no puede ser null." }
        $this.Repo = $repo
        $this.PickedCount = 0
        $this.StatusMessage = ''
        $this.StatusKind = ''
        $this.Mode = 'select-source'

        # Detectar current branch.
        $current = $this.Git.RunGit($repo.Path, @('rev-parse', '--abbrev-ref', 'HEAD'))
        if (-not $current -or -not $current[0]) {
            return
        }
        $this.CurrentBranch = $current[0].Trim()
        $this.LoadOtherBranches()

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

        $errOut = [Console]::Error
        $this.Render($errOut)

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

    hidden [void] LoadOtherBranches() {
        $branches = @($this.Git.GetBranches($this.Repo.Path))
        $items = @($branches |
            ForEach-Object Name |
            Where-Object { $_ -ne $this.CurrentBranch })
        $this.BranchPicker.SetItems($items)
        $this.BranchPicker.Reset()
    }

    hidden [void] LoadCommitsFromSource() {
        $items = @($this.Git.GetBranchLog($this.Repo.Path, $this.SourceBranch, $this.CurrentBranch, 100))
        $this.CommitPicker.SetItems($items)
        $this.CommitPicker.Reset()
    }

    hidden [void] Render([object]$errOut) {
        if ([Console]::IsInputRedirected) { return }
        $rendered = $this.BuildLines()
        $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($rendered -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync()
        $errOut.Write($framePayload)
        $errOut.Flush()
    }

    hidden [string] HandleKey([System.ConsoleKeyInfo]$key) {
        if ($this.Mode -eq 'select-source')  { return $this.HandleKeyBranchSelector($key) }
        if ($this.Mode -eq 'pick-commits')   { return $this.HandleKeyCommitSelector($key) }
        return ''
    }

    hidden [string] HandleKeyBranchSelector([System.ConsoleKeyInfo]$key) {
        $c = $key.KeyChar
        if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' }

        $action = $this.BranchPicker.HandleKey($key)
        if ($action -eq 'enter') {
            $value = $this.BranchPicker.Selected()
            if ($null -ne $value) {
                $this.SourceBranch = [string]$value
                $this.SetStatus('cargando commits…', 'ok')
                $this.LoadCommitsFromSource()
                $this.SetStatus('', '')
                $this.Mode = 'pick-commits'
            }
        } elseif ($action -eq 'escape') {
            # Esc con filter activo limpia filter, sino vuelve atrás (al menú).
            if ($this.BranchPicker.Filter) {
                $this.BranchPicker.Reset()
            } else {
                return 'back'
            }
        }
        return ''
    }

    hidden [string] HandleKeyCommitSelector([System.ConsoleKeyInfo]$key) {
        $c = $key.KeyChar
        if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' }

        $action = $this.CommitPicker.HandleKey($key)
        if ($action -eq 'enter') {
            $picked = $this.CommitPicker.Selected()
            if ($null -ne $picked) {
                $r = $this.Git.CherryPick($this.Repo.Path, $picked.Hash)
                if ($r.Ok) {
                    $this.PickedCount++
                    $this.SetStatus("✓ pickeado $($picked.Hash) — $($picked.Subject)", 'ok')
                    # Reload commits — el que acabamos de picar ya no está pendiente.
                    $this.LoadCommitsFromSource()
                } else {
                    $this.SetStatus($r.Message, 'error')
                }
            }
        } elseif ($action -eq 'escape') {
            # Esc con filter activo limpia filter, sino vuelve al source selector.
            if ($this.CommitPicker.Filter) {
                $this.CommitPicker.Reset()
            } else {
                $this.Mode = 'select-source'
                $this.LoadOtherBranches()
            }
        }
        return ''
    }

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

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

        $repoName = if ($this.Repo) { $this.Repo.Name } else { '—' }
        $title = if ($this.SourceBranch) { "Cherry-Pick · $repoName · ← $($this.SourceBranch)" } else { "Cherry-Pick · $repoName" }
        $out.Add($this.Frame.TitleBar('repo-nav', $title, '1.0.0'))

        $bc = [BreadcrumbBuilder]::Build($this.Repo.Path)
        $segs = @($bc.Segs) + @($bc.Current) + @('Branches')
        $out.Add($this.AppHeader.Render($segs, 'Cherry-Pick', @()))
        $out.Add($r.HRule())

        # Status / prompt
        if ($this.StatusMessage) {
            $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' }
            $out.Add(' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset)
        } else {
            $out.Add('')
        }

        # Cuerpo según modo — el picker render ya incluye prompt + count + lista.
        $maxVisible = [Math]::Max(5, [Console]::WindowHeight - 13)
        if ($this.Mode -eq 'select-source') {
            $body = $this.BranchPicker.BuildBody($this.Theme, $r, $maxVisible)
        } else {
            $body = $this.CommitPicker.BuildBody($this.Theme, $r, $maxVisible)
        }
        foreach ($l in $body) { $out.Add($l) }

        # Sufijo informativo en pick-commits — pickeados hasta ahora.
        if ($this.Mode -eq 'pick-commits' -and $this.PickedCount -gt 0) {
            $out.Add($this.Theme.Fg('fg3') + (' pickeados hasta ahora: ' + $this.PickedCount) + $reset)
        }

        # StatusBar
        $hints = $this.StatusBarHints()
        $out.Add($r.HRule())
        return @($out.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $out.Count)
    }

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

    hidden [array] StatusBarHints() {
        if ($this.Mode -eq 'select-source') {
            return @(
                @{ k = '↑↓';                 label = $this.T('hint.move') }
                @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
                @{ k = '↵';                  label = 'Pick branch' }
                @{ k = 'Esc';                label = $this.T('hint.back') }
                @{ k = 'Q';                  label = $this.T('hint.quit') }
            )
        }
        return @(
            @{ k = '↑↓';                 label = $this.T('hint.move') }
            @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
            @{ k = '↵';                  label = 'Pick commit' }
            @{ k = 'Esc';                label = $this.T('hint.back') }
            @{ k = 'Q';                  label = $this.T('hint.quit') }
        )
    }
}