src/UI/Screens/IntegrateScreen.ps1

# IntegrateScreen — Dashboard interactivo del flujo Integrate (replica
# IntegrationFlowController del v2).
#
# Permite al usuario configurar 4 inputs + 1 toggle y después ejecutar 4 pasos
# encadenados:
# 1. Create new branch from TARGET (with checkout)
# 2. Merge SOURCE into the new branch
# 3. Push (with or without --no-verify according to toggle)
# 4. Detect PR URL on GitHub and show / open
#
# Diseño:
# - 4 inputs editables (TARGET, NAME, SOURCE, NO-VERIFY) + Execute + Cancel = 6 ítems.
# - Execute solo aparece cuando IsReadyToExecute() del modelo.
# - State machine de modos: list / select-target / select-source / input-name / executing.
# - Cada modo BuildLines ramifica el render.

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

    [object]   $Repo
    [object]   $Flow = [IntegrationFlow]::new()
    [int]      $SelectedIndex = 0     # 0..3 (inputs), 4 (Execute), 5 (Cancel)
    [string]   $Mode = 'list'         # 'list' | 'select-target' | 'select-source' | 'input-name' | 'executing'
    [bool]     $QuitRequested = $false
    [bool]     $UseAltBuffer = $true

    # Selector compartido para target/source — instancia única reutilizada.
    [object]   $Picker = $null

    # Input state (input-name)
    [string]   $InputBuffer = ''
    [string]   $InputLabel = ''

    # Status feedback
    [string]   $StatusMessage = ''
    [string]   $StatusKind = ''       # 'ok' | 'error' | ''

    # Resultado terminal del flow ejecutado (mantener visible hasta que el user
    # presione algo y la screen vuelva).
    [bool]     $Done = $false
    [string]   $DoneMessage = ''

    IntegrateScreen($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
    }

    # Abre la pantalla para un repo. Loop hasta que user vuelve o quit.
    [void] Open([object]$repo) {
        if ($null -eq $repo) { throw "IntegrateScreen.Open: repo no puede ser null." }

        $this.Repo = $repo
        $this.Flow = [IntegrationFlow]::new()
        $this.SelectedIndex = 0
        $this.Mode = 'list'
        $this.StatusMessage = ''
        $this.StatusKind = ''
        $this.Done = $false
        $this.DoneMessage = ''

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

        # Pre-frame inmediato ANTES de cualquier git call (sincrónico) para que
        # el usuario vea algo apenas entra (sin esperar a la primera iteración del while).
        $errOut = [Console]::Error
        $this.Render($errOut)

        # Validación previa: uncommitted changes — git lo rechaza igual pero
        # mejor avisar antes de empezar.
        if ($this.HasUncommittedChanges()) {
            $this.SetStatus('cuidado: hay cambios sin commitear (git rechazará el merge)', '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 { }
    }

    # Detecta cambios sin commit con git status --porcelain (vacío = clean).
    hidden [bool] HasUncommittedChanges() {
        $out = $this.Git.RunGit($this.Repo.Path, @('status', '--porcelain'))
        if ($null -eq $out) { return $false }
        return @($out | Where-Object { $_ -and $_.Trim() }).Count -gt 0
    }

    hidden [void] Render([object]$errOut) {
        # Skipea render en modo no-tty (tests, pipes). El stdin redirected señala
        # que no estamos en una terminal interactiva — no tiene sentido emitir
        # ANSI escapes y los stubs UI de tests no implementan los métodos.
        if ([Console]::IsInputRedirected) { return }
        $lines = $this.BuildLines()
        $framePayload = [AnsiService]::BeginSync() + [AnsiService]::MoveTo(1, 1) + ($lines -join "`e[K`n") + "`e[K`e[J" + [AnsiService]::EndSync()
        $errOut.Write($framePayload)
        $errOut.Flush()
    }

    hidden [string] HandleKey([System.ConsoleKeyInfo]$key) {
        switch ($this.Mode) {
            'list'           { return $this.HandleKeyList($key) }
            'select-target'  { return $this.HandleKeySelector($key, 'target') }
            'select-source'  { return $this.HandleKeySelector($key, 'source') }
            'input-name'     { return $this.HandleKeyInputName($key) }
            'executing'      { return $this.HandleKeyExecuting($key) }
        }
        return ''
    }

    hidden [string] HandleKeyList([System.ConsoleKeyInfo]$key) {
        $k = $key.Key
        $c = $key.KeyChar

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

        $maxIdx = $(if ($this.Flow.IsReadyToExecute()) { 5 } else { 4 })

        if ($k -eq 'UpArrow' -or $c -eq 'k') {
            $this.SelectedIndex = if ($this.SelectedIndex -le 0) { $maxIdx } else { $this.SelectedIndex - 1 }
            return ''
        }
        if ($k -eq 'DownArrow' -or $c -eq 'j') {
            $this.SelectedIndex = if ($this.SelectedIndex -ge $maxIdx) { 0 } else { $this.SelectedIndex + 1 }
            return ''
        }

        if ($k -eq 'Enter') {
            switch ($this.SelectedIndex) {
                0 { $this.OpenTargetSelector(); return '' }
                1 { $this.OpenNameInput(); return '' }
                2 { $this.OpenSourceSelector(); return '' }
                3 { $this.Flow.ToggleNoVerify(); return '' }
                4 {
                    if ($this.Flow.IsReadyToExecute()) { $this.ExecuteFlow() }
                    return ''
                }
                5 {
                    # Cancel → back. SelectedIndex=5 solo aparece cuando IsReady,
                    # entonces siempre que llegue acá es Cancel.
                    return 'back'
                }
            }
        }
        return ''
    }

    # Lazy-init para tests que invocan OpenTargetSelector sin Open().
    hidden [void] EnsurePicker() {
        if ($null -eq $this.Picker) {
            $this.Picker = [FilteredListPicker]::new()
        }
    }

    [void] OpenTargetSelector() {
        $this.SetStatus('cargando remotos…', 'ok')
        $this.EnsurePicker()
        $this.Picker.Title = 'TARGET (Remote)'
        $this.Picker.SetItems(@($this.Git.GetRemoteBranches($this.Repo.Path)))
        $this.Picker.Reset()
        $this.Mode = 'select-target'
        $this.SetStatus('', '')
    }

    [void] OpenSourceSelector() {
        $this.EnsurePicker()
        $branches = @($this.Git.GetBranches($this.Repo.Path))
        $this.Picker.Title = 'SOURCE (Local)'
        $this.Picker.SetItems(@($branches | ForEach-Object Name))
        $this.Picker.Reset()
        $this.Mode = 'select-source'
    }

    hidden [void] OpenNameInput() {
        $this.InputBuffer = $this.Flow.NewBranchName
        $this.InputLabel = 'BRANCH NAME'
        $this.Mode = 'input-name'
    }

    # Selector compartido para target/source. $kind decide qué setter del modelo invocar.
    hidden [string] HandleKeySelector([System.ConsoleKeyInfo]$key, [string]$kind) {
        $c = $key.KeyChar
        if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' }

        $action = $this.Picker.HandleKey($key)
        if ($action -eq 'enter') {
            $value = $this.Picker.Selected()
            if ($null -ne $value) {
                if ($kind -eq 'target') { $this.Flow.SetTarget([string]$value) }
                else                    { $this.Flow.SetSource([string]$value) }
                $this.Picker.Reset()
                $this.Mode = 'list'
            }
        } elseif ($action -eq 'escape') {
            # Esc con filter activo limpia filter; con filter vacío vuelve al list.
            if ($this.Picker.Filter) {
                $this.Picker.Reset()
            } else {
                $this.Mode = 'list'
            }
        }
        return ''
    }

    hidden [string] HandleKeyInputName([System.ConsoleKeyInfo]$key) {
        $k = $key.Key
        $c = $key.KeyChar

        if ($k -eq 'Escape') {
            $this.InputBuffer = ''
            $this.Mode = 'list'
            return ''
        }
        if ($k -eq 'Enter') {
            $this.Flow.SetName($this.InputBuffer)
            $this.InputBuffer = ''
            $this.Mode = 'list'
            return ''
        }
        if ($k -eq 'Backspace') {
            if ($this.InputBuffer.Length -gt 0) {
                $this.InputBuffer = $this.InputBuffer.Substring(0, $this.InputBuffer.Length - 1)
            }
            return ''
        }
        if (-not [char]::IsControl($c)) {
            $this.InputBuffer += $c
        }
        return ''
    }

    hidden [string] HandleKeyExecuting([System.ConsoleKeyInfo]$key) {
        # Después de Execute, esperamos cualquier tecla para volver. Q quit.
        $c = $key.KeyChar
        if ($c -eq 'q' -or $c -eq 'Q') { return 'quit' }
        return 'back'
    }

    # Ejecuta los 4 pasos del flow secuencialmente. Cada paso flashea un status
    # y, si falla, deja el error visible y termina (sin revertir lo hecho —
    # el user resuelve manualmente).
    hidden [void] ExecuteFlow() {
        $this.Mode = 'executing'
        $errOut = [Console]::Error

        $target   = $this.Flow.TargetBranch
        $name     = $this.Flow.NewBranchName
        $source   = $this.Flow.SourceBranch
        $noVer    = $this.Flow.NoVerifyPush
        # `$repo` chocaría con la property $this.Repo (PS classes case-insensitive).
        $repoPath = $this.Repo.Path

        # 1. Create branch from target
        $this.SetStatus("creating $name from $target…", 'ok')
        $this.Render($errOut)
        # Para crear desde un remote: `git checkout -b $name $target` (GitService.CreateBranch
        # usa HEAD actual como base, no nos sirve). Usamos RunGitCapturingStderr directo.
        $rCreate = $this.Git.RunGitCapturingStderr($repoPath, @('checkout', '-b', $name, $target))
        if ($rCreate.ExitCode -ne 0) {
            $msg = $this.Git.FirstStderrLine($rCreate, "create falló (exit $($rCreate.ExitCode))")
            $this.FinishWithError("create falló: $msg")
            return
        }
        $this.Git.InvalidatePath($repoPath)

        # 2. Merge source
        $this.SetStatus("merging $source…", 'ok')
        $this.Render($errOut)
        $rMerge = $this.Git.MergeBranch($repoPath, $source)
        if (-not $rMerge.Ok) {
            $this.FinishWithError("merge falló: $($rMerge.Message)")
            return
        }

        # 3. Push (con o sin --no-verify según toggle)
        $pushLabel = if ($noVer) { 'pushing (--no-verify)…' } else { 'pushing…' }
        $this.SetStatus($pushLabel, 'ok')
        $this.Render($errOut)
        $rPush = $this.Git.Push($repoPath, $noVer)
        if (-not $rPush.Ok) {
            $this.FinishWithError("push falló: $($rPush.Message)")
            return
        }

        # 4. Detectar PR URL
        $prUrl = $this.Git.GetPullRequestUrl($repoPath, $name)
        if ($prUrl) {
            $this.FinishWithSuccess("done — PR url: $prUrl")
        } else {
            $this.FinishWithSuccess("done — branch $name pusheada (no es github)")
        }
    }

    hidden [void] FinishWithError([string]$msg) {
        $this.Done = $true
        $this.DoneMessage = $msg
        $this.SetStatus($msg, 'error')
    }

    hidden [void] FinishWithSuccess([string]$msg) {
        $this.Done = $true
        $this.DoneMessage = $msg
        $this.SetStatus($msg, 'ok')
    }

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

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

        # 1. Title bar
        $repoName = if ($this.Repo) { $this.Repo.Name } else { '—' }
        $lines.Add($this.Frame.TitleBar('repo-nav', "Integrate · $repoName", '1.0.0'))

        # 2. AppHeader: breadcrumb terminado en 'Integrate'
        $bc = [BreadcrumbBuilder]::Build($this.Repo.Path)
        $segs = @($bc.Segs) + @($bc.Current) + @('Branches')
        $lines.Add($this.AppHeader.Render($segs, 'Integrate', @()))
        $lines.Add($r.HRule())

        # Cuerpo según modo
        if ($this.Mode -eq 'select-target' -or $this.Mode -eq 'select-source') {
            $this.AppendSelectorBody($lines, $r, $reset)
        } elseif ($this.Mode -eq 'input-name') {
            $this.AppendDashboardBody($lines, $r, $reset)
            # Override de la línea de status con el prompt input.
        } else {
            # 'list' o 'executing'
            $this.AppendDashboardBody($lines, $r, $reset)
        }

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

    hidden [void] AppendDashboardBody([object]$lines, [object]$r, [string]$reset) {
        # Status / prompt line
        $lines.Add($this.RenderStatusOrPrompt())

        # Section title
        $lines.Add($this.Theme.Fg('fg2') + ' Configuración del flow' + $reset)
        $lines.Add($r.HRule())

        # 4 input rows
        $lines.Add($this.RenderInputRow(0, 'TARGET (Remote)', $this.Flow.TargetBranch, $this.Flow.TargetValid))
        $lines.Add($this.RenderInputRow(1, 'BRANCH NAME',     $this.Flow.NewBranchName, $this.Flow.NameValid))
        $lines.Add($this.RenderInputRow(2, 'SOURCE (Local)',  $this.Flow.SourceBranch,  $this.Flow.SourceValid))
        $lines.Add($this.RenderToggleRow(3, 'NO-VERIFY PUSH', $this.Flow.NoVerifyPush))

        $lines.Add('')
        $lines.Add($r.HRule())

        # Action buttons (Execute solo si IsReady)
        $canExecute = $this.Flow.IsReadyToExecute()
        if ($canExecute) {
            $lines.Add($this.RenderButton(4, '[ Execute ]', 'gitClean'))
        }
        $cancelIdx = if ($canExecute) { 5 } else { 4 }
        $lines.Add($this.RenderButton($cancelIdx, '[ Cancel ]', 'fg2'))
    }

    hidden [void] AppendSelectorBody([object]$lines, [object]$r, [string]$reset) {
        $maxVisible = [Math]::Max(5, [Console]::WindowHeight - 12)
        $body = $this.Picker.BuildBody($this.Theme, $r, $maxVisible)
        foreach ($l in $body) { $lines.Add($l) }
    }

    hidden [string] RenderStatusOrPrompt() {
        $reset = [AnsiService]::Reset

        if ($this.Mode -eq 'input-name') {
            $cursor = $this.Theme.Fg('acc') + '_' + $reset
            $label  = $this.Theme.Fg('fg2') + ($this.InputLabel + ': ') + $reset
            $buf    = $this.Theme.Fg('fg1') + $this.InputBuffer + $reset
            return ' ' + $label + $buf + $cursor
        }

        if ($this.StatusMessage) {
            $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' }
            return ' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset
        }
        return ''
    }

    hidden [string] RenderInputRow([int]$idx, [string]$label, [string]$value, [bool]$valid) {
        $reset = [AnsiService]::Reset
        $sel = ($this.SelectedIndex -eq $idx -and $this.Mode -eq 'list')

        $marker = if ($sel) { $this.Theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' }
        $labelFmt = [Renderer]::PadRight($this.Theme.Fg('fg2') + $label + $reset, 22)
        $valFg = if ($valid) { 'fg0' } else { 'fg3' }
        $valText = if ($value) { $value } else { '(vacío)' }
        $valFmt = $this.Theme.Fg($valFg) + $valText + $reset
        $tick = if ($valid) { $this.Theme.Fg('gitClean') + '✓' + $reset } else { '' }
        return $marker + $labelFmt + ' ' + $valFmt + ' ' + $tick
    }

    hidden [string] RenderToggleRow([int]$idx, [string]$label, [bool]$value) {
        $reset = [AnsiService]::Reset
        $sel = ($this.SelectedIndex -eq $idx -and $this.Mode -eq 'list')
        $marker = if ($sel) { $this.Theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' }
        $labelFmt = [Renderer]::PadRight($this.Theme.Fg('fg2') + $label + $reset, 22)
        $box = if ($value) { '[x]' } else { '[ ]' }
        $stateText = if ($value) { 'on' } else { 'off' }
        $stateFg = if ($value) { 'gitClean' } else { 'fg3' }
        return $marker + $labelFmt + ' ' + $this.Theme.Fg($stateFg) + ($box + ' ' + $stateText) + $reset
    }

    hidden [string] RenderButton([int]$idx, [string]$label, [string]$colorKey) {
        $reset = [AnsiService]::Reset
        $sel = ($this.SelectedIndex -eq $idx -and $this.Mode -eq 'list')
        if ($sel) {
            $bg = $this.Theme.Bg('acc')
            $fg = $this.Theme.Fg('bg0')
            return ' ▎ ' + $bg + $fg + ' ' + $label + ' ' + $reset
        }
        return ' ' + $this.Theme.Fg($colorKey) + $label + $reset
    }

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

    hidden [array] StatusBarHints() {
        switch ($this.Mode) {
            'select-target' {
                return @(
                    @{ k = '↑↓';                 label = $this.T('hint.move') }
                    @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
                    @{ k = '↵';                  label = $this.T('hint.accept') }
                    @{ k = 'Esc';                label = $this.T('hint.cancel') }
                )
            }
            'select-source' {
                return @(
                    @{ k = '↑↓';                 label = $this.T('hint.move') }
                    @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
                    @{ k = '↵';                  label = $this.T('hint.accept') }
                    @{ k = 'Esc';                label = $this.T('hint.cancel') }
                )
            }
            'input-name' {
                return @(
                    @{ k = $this.T('hint.type'); label = $this.T('hint.save') }
                    @{ k = '↵';                  label = $this.T('hint.accept') }
                    @{ k = 'Esc';                label = $this.T('hint.cancel') }
                )
            }
            'executing' {
                return @(
                    @{ k = '*'; label = $this.T('hint.back') }
                    @{ k = 'Q'; label = $this.T('hint.quit') }
                )
            }
            default {
                return @(
                    @{ k = '↑↓'; label = $this.T('hint.move') }
                    @{ k = '↵';  label = 'Edit/Run' }
                    @{ k = '←';  label = $this.T('hint.back') }
                    @{ k = 'Q';  label = $this.T('hint.quit') }
                )
            }
        }
        return @()
    }
}