src/UI/Screens/CompareScreen.ps1

# CompareScreen — Compare entre dos branches con file list + unified diff.
#
# 4 modos secuenciales:
# 1. select-a — selector branch A (base)
# 2. select-b — selector branch B (compare)
# 3. select-file — lista de archivos cambiados (con icono A/M/D/R)
# 4. view-diff — unified diff con scroll H/V y colorización
#
# Esc en cada modo retrocede al previo. En select-a, Esc sale.
#
# El v2 tenía split-pane con syntax highlighting. Esta versión usa unified diff
# (más universal, +/- legible en cualquier ancho de terminal). Syntax
# highlighting puede sumarse en iteración futura.
#
# Usa 2 FilteredListPicker: BranchPicker (reusado entre A y B) y FilePicker
# (RenderRowFn custom con icono de status). El diff view es scroll puro, no
# necesita picker.

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

    [object]   $Repo
    [string]   $BranchA = ''
    [string]   $BranchB = ''
    [string]   $Mode = 'select-a'

    # Pickers — uno reusado para branches (A y B con reset), otro para files.
    [object]   $BranchPicker = $null
    [object]   $FilePicker = $null

    # Diff view state.
    [string]   $CurrentFile = ''
    [string[]] $DiffLines = @()
    [int]      $DiffScrollY = 0
    [int]      $DiffScrollX = 0

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

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

    CompareScreen($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 branch'

        $this.FilePicker = [FilteredListPicker]::new()
        $this.FilePicker.Title = 'Filtrar archivos'
        # Match contra File path.
        $this.FilePicker.LabelFn = { param($item) [string]$item.File }
        # Render row con icono A/M/D/R.
        $this.FilePicker.RenderRowFn = {
            param($item, $sel, $theme, $reset)
            $marker = if ($sel) { $theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' }
            $iconKey = switch ($item.Status) {
                'A' { 'gitClean' }
                'D' { 'gitConflict' }
                'M' { 'gitDirty' }
                'R' { 'gitAhead' }
                default { 'fg2' }
            }
            $iconChar = switch ($item.Status) {
                'A' { '+' }
                'D' { '-' }
                'M' { '~' }
                'R' { '>' }
                default { '?' }
            }
            $icon = $theme.Fg($iconKey) + "[$iconChar]" + $reset
            $name = $theme.Fg($(if ($sel) { 'acc' } else { 'fg1' })) + $item.File + $reset
            return $marker + $icon + ' ' + $name
        }
    }

    [void] Open([object]$repo) {
        if ($null -eq $repo) { throw "CompareScreen.Open: repo no puede ser null." }
        $this.Repo = $repo
        $this.BranchA = ''
        $this.BranchB = ''
        $this.Mode = 'select-a'
        $this.LoadBranches()

        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] LoadBranches() {
        $branches = @($this.Git.GetBranches($this.Repo.Path))
        $this.BranchPicker.SetItems(@($branches | ForEach-Object Name))
        $this.BranchPicker.Reset()
    }

    hidden [void] LoadFiles() {
        $this.SetStatus('cargando diff…', 'ok')
        $files = @($this.Git.DiffNameStatus($this.Repo.Path, $this.BranchA, $this.BranchB))
        $this.FilePicker.SetItems($files)
        $this.FilePicker.Reset()
        if ($files.Count -eq 0) {
            $this.SetStatus("sin diferencias entre $($this.BranchA) y $($this.BranchB)", 'error')
        } else {
            $this.SetStatus('', '')
        }
    }

    hidden [void] LoadDiff([string]$file) {
        $this.SetStatus('cargando archivo…', 'ok')
        $this.CurrentFile = $file
        $this.DiffLines = @($this.Git.DiffFile($this.Repo.Path, $this.BranchA, $this.BranchB, $file))
        $this.DiffScrollY = 0
        $this.DiffScrollX = 0
        if ($this.DiffLines.Count -eq 0) {
            $this.SetStatus('archivo sin diff visible', 'error')
        } else {
            $this.SetStatus('', '')
        }
    }

    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) {
        switch ($this.Mode) {
            'select-a'    { return $this.HandleKeyBranchSelector($key, 'a') }
            'select-b'    { return $this.HandleKeyBranchSelector($key, 'b') }
            'select-file' { return $this.HandleKeyFileSelector($key) }
            'view-diff'   { return $this.HandleKeyDiff($key) }
        }
        return ''
    }

    hidden [string] HandleKeyBranchSelector([System.ConsoleKeyInfo]$key, [string]$slot) {
        $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) {
                if ($slot -eq 'a') {
                    $this.BranchA = [string]$value
                    $this.Mode = 'select-b'
                    $this.BranchPicker.Reset()
                } else {
                    $candidate = [string]$value
                    if ($candidate -eq $this.BranchA) {
                        $this.SetStatus('no podés comparar una branch contra sí misma', 'error')
                        return ''
                    }
                    $this.BranchB = $candidate
                    $this.Mode = 'select-file'
                    $this.LoadFiles()
                }
            }
        } elseif ($action -eq 'escape') {
            if ($this.BranchPicker.Filter) {
                $this.BranchPicker.Reset()
            } elseif ($slot -eq 'a') {
                return 'back'
            } else {
                $this.Mode = 'select-a'
                $this.BranchPicker.Reset()
            }
        }
        return ''
    }

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

        $action = $this.FilePicker.HandleKey($key)
        if ($action -eq 'enter') {
            $picked = $this.FilePicker.Selected()
            if ($null -ne $picked) {
                $this.LoadDiff($picked.File)
                $this.Mode = 'view-diff'
            }
        } elseif ($action -eq 'escape') {
            if ($this.FilePicker.Filter) {
                $this.FilePicker.Reset()
            } else {
                $this.Mode = 'select-b'
                $this.BranchPicker.Reset()
            }
        }
        return ''
    }

    hidden [string] HandleKeyDiff([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 'Backspace') {
            $this.Mode = 'select-file'
            return ''
        }

        $contentH = $this.DiffContentHeight()
        $maxY = [Math]::Max(0, $this.DiffLines.Count - $contentH)
        $maxLen = 0
        foreach ($l in $this.DiffLines) { if ($l.Length -gt $maxLen) { $maxLen = $l.Length } }
        $width = $this.Renderer.Width()
        $maxX = [Math]::Max(0, $maxLen - $width + 4)

        if ($k -eq 'UpArrow' -or $c -eq 'k') { $this.DiffScrollY = [Math]::Max(0, $this.DiffScrollY - 1); return '' }
        if ($k -eq 'DownArrow' -or $c -eq 'j') { $this.DiffScrollY = [Math]::Min($maxY, $this.DiffScrollY + 1); return '' }
        if ($k -eq 'PageUp')   { $this.DiffScrollY = [Math]::Max(0, $this.DiffScrollY - ($contentH - 1)); return '' }
        if ($k -eq 'PageDown') { $this.DiffScrollY = [Math]::Min($maxY, $this.DiffScrollY + ($contentH - 1)); return '' }
        if ($k -eq 'Home' -or $c -eq 'g') { $this.DiffScrollY = 0; return '' }
        if ($k -eq 'End'  -or $c -eq 'G') { $this.DiffScrollY = $maxY; return '' }
        if ($k -eq 'LeftArrow' -or $c -eq 'h') { $this.DiffScrollX = [Math]::Max(0, $this.DiffScrollX - 8); return '' }
        if ($k -eq 'RightArrow' -or $c -eq 'l') { $this.DiffScrollX = [Math]::Min($maxX, $this.DiffScrollX + 8); return '' }
        return ''
    }

    hidden [int] DiffContentHeight() {
        return [Math]::Max(3, [Console]::WindowHeight - 8)
    }

    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()

        # Title bar refleja el progreso del flow.
        $repoName = if ($this.Repo) { $this.Repo.Name } else { '—' }
        $title = switch ($this.Mode) {
            'select-a'    { "Compare · $repoName · pick A" }
            'select-b'    { "Compare · $repoName · A=$($this.BranchA) · pick B" }
            'select-file' { "Compare · $repoName · $($this.BranchA) ↔ $($this.BranchB)" }
            'view-diff'   { "Compare · $repoName · $($this.BranchA) ↔ $($this.BranchB) · $($this.CurrentFile)" }
        }
        $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, 'Compare', @()))
        $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 — pickers tienen su propio prompt + count + lista.
        $maxVisible = [Math]::Max(5, [Console]::WindowHeight - 13)
        switch ($this.Mode) {
            'select-a' {
                $this.BranchPicker.Title = 'Branch A (base)'
                $body = $this.BranchPicker.BuildBody($this.Theme, $r, $maxVisible)
                foreach ($l in $body) { $out.Add($l) }
            }
            'select-b' {
                $this.BranchPicker.Title = "Branch B (vs $($this.BranchA))"
                $body = $this.BranchPicker.BuildBody($this.Theme, $r, $maxVisible)
                foreach ($l in $body) { $out.Add($l) }
            }
            'select-file' {
                $body = $this.FilePicker.BuildBody($this.Theme, $r, $maxVisible)
                foreach ($l in $body) { $out.Add($l) }
            }
            'view-diff' {
                $this.AppendDiff($out, $r, $reset)
            }
        }

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

    hidden [void] AppendDiff([object]$out, [object]$r, [string]$reset) {
        $totalLines = $this.DiffLines.Count
        $contentH = $this.DiffContentHeight()
        $titleText = ' Diff · ' + $this.CurrentFile + ' · ' + $totalLines + ' líneas'
        if ($totalLines -gt $contentH) {
            $start = $this.DiffScrollY + 1
            $end = [Math]::Min($totalLines, $this.DiffScrollY + $contentH)
            $titleText += " · viendo $start–$end"
        }
        $out.Add($this.Theme.Fg('fg2') + $titleText + $reset)
        $out.Add($r.HRule())

        $sliceStart = $this.DiffScrollY
        $sliceEnd = [Math]::Min($totalLines, $this.DiffScrollY + $contentH)
        $width = $r.Width()
        $contentWidth = [Math]::Max(10, $width - 2)

        if ($totalLines -eq 0) {
            $out.Add($this.Theme.Fg('fg3') + ' (sin diff)' + $reset)
            return
        }

        for ($i = $sliceStart; $i -lt $sliceEnd; $i++) {
            $line = $this.DiffLines[$i]
            # Slice horizontal
            if ($this.DiffScrollX -lt $line.Length) {
                $line = $line.Substring($this.DiffScrollX)
            } else {
                $line = ''
            }
            if ($line.Length -gt $contentWidth) {
                $line = $line.Substring(0, $contentWidth)
            }
            $out.Add(' ' + $this.ColorizeDiffLine($line))
        }
    }

    hidden [string] ColorizeDiffLine([string]$line) {
        $reset = [AnsiService]::Reset
        # Detect prefix para color.
        if ($line.StartsWith('+++') -or $line.StartsWith('---') -or $line.StartsWith('diff --git') -or $line.StartsWith('index ')) {
            return $this.Theme.Fg('fg2') + $line + $reset
        }
        if ($line.StartsWith('@@')) {
            return $this.Theme.Fg('gitDirty') + $line + $reset
        }
        if ($line.StartsWith('+')) {
            return $this.Theme.Fg('gitClean') + $line + $reset
        }
        if ($line.StartsWith('-')) {
            return $this.Theme.Fg('gitConflict') + $line + $reset
        }
        return $this.Theme.Fg('fg1') + $line + $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-a' {
                return @(
                    @{ k = '↑↓';                 label = $this.T('hint.move') }
                    @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
                    @{ k = '↵';                  label = 'Pick A' }
                    @{ k = 'Esc';                label = $this.T('hint.back') }
                    @{ k = 'Q';                  label = $this.T('hint.quit') }
                )
            }
            'select-b' {
                return @(
                    @{ k = '↑↓';                 label = $this.T('hint.move') }
                    @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
                    @{ k = '↵';                  label = 'Pick B' }
                    @{ k = 'Esc';                label = $this.T('hint.back') }
                    @{ k = 'Q';                  label = $this.T('hint.quit') }
                )
            }
            'select-file' {
                return @(
                    @{ k = '↑↓';                 label = $this.T('hint.move') }
                    @{ k = $this.T('hint.type'); label = $this.T('hint.search') }
                    @{ k = '↵';                  label = 'Diff' }
                    @{ k = 'Esc';                label = $this.T('hint.back') }
                    @{ k = 'Q';                  label = $this.T('hint.quit') }
                )
            }
            'view-diff' {
                return @(
                    @{ k = '↑↓';        label = 'Scroll' }
                    @{ k = '←→';        label = 'Horiz' }
                    @{ k = 'PgUp/PgDn'; label = 'Page' }
                    @{ k = 'g/G';       label = 'Top/End' }
                    @{ k = 'Esc';       label = $this.T('hint.back') }
                    @{ k = 'Q';         label = $this.T('hint.quit') }
                )
            }
        }
        return @()
    }
}