src/UI/Screens/ReleaseScreen.ps1

# ReleaseScreen — Dashboard de release flow (replica ReleaseFlowController v2).
#
# MVP enfocado en lo más usado:
# - Create Tag (input nombre, annotated)
# - Push All Tags
# - Cancel
#
# Muestra arriba el estado: branch actual, latest tag, hasPackageJson + version.
# Bump Version queda para iteración futura — requiere npm service que el v3
# todavía no tiene.

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

    [object]   $Repo
    [int]      $SelectedIndex = 0
    [string]   $Mode = 'list'   # 'list' | 'input-tag'
    [bool]     $QuitRequested = $false
    [bool]     $UseAltBuffer = $true

    # Estado calculado
    [string]   $CurrentBranch = ''
    [string]   $LatestTag = ''
    [int]      $TagCount = 0
    [bool]     $HasPackageJson = $false
    [string]   $CurrentVersion = ''

    [string]   $InputBuffer = ''

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

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

    [void] Open([object]$repo) {
        if ($null -eq $repo) { throw "ReleaseScreen.Open: repo no puede ser null." }

        $this.Repo = $repo
        $this.SelectedIndex = 0
        $this.Mode = 'list'
        $this.StatusMessage = ''
        $this.StatusKind = ''
        $this.RefreshState()

        if ([Console]::IsInputRedirected) {
            $lines = $this.BuildLines()
            foreach ($l in $lines) { 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] RefreshState() {
        $repoPath = $this.Repo.Path
        $this.Git.InvalidatePath($repoPath)
        $state = $this.Git.GetRepoState($repoPath)
        $this.CurrentBranch = $state.Branch

        $tags = @($this.Git.GetTags($repoPath))
        $this.TagCount = $tags.Count
        $this.LatestTag = if ($tags.Count -gt 0) { $tags[0] } else { '(none)' }

        # Detectar package.json + version sin npm service (Get-Content directo).
        $pkg = Join-Path $repoPath 'package.json'
        if (Test-Path -LiteralPath $pkg) {
            $this.HasPackageJson = $true
            try {
                $json = Get-Content -LiteralPath $pkg -Raw -Encoding UTF8 | ConvertFrom-Json
                $this.CurrentVersion = if ($json.version) { [string]$json.version } else { '(no version field)' }
            } catch {
                $this.CurrentVersion = '(parse error)'
            }
        } else {
            $this.HasPackageJson = $false
            $this.CurrentVersion = ''
        }
    }

    [object[]] AvailableActions() {
        $items = @()
        $items += @{ Label = 'Create Tag'; Action = 'create-tag' }
        $items += @{ Label = 'Push All Tags'; Action = 'push-tags' }
        $items += @{ Label = 'Cancel'; Action = 'cancel' }
        return $items
    }

    hidden [void] Render([object]$errOut) {
        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) }
            'input-tag' { return $this.HandleKeyInputTag($key) }
        }
        return ''
    }

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

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

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

        if ($k -eq 'Enter') {
            $action = $items[$this.SelectedIndex].Action
            switch ($action) {
                'create-tag' { $this.OpenTagInput(); return '' }
                'push-tags'  { $this.DoPushTags(); return '' }
                'cancel'     { return 'back' }
            }
        }
        return ''
    }

    hidden [void] OpenTagInput() {
        $this.InputBuffer = ''
        $this.Mode = 'input-tag'
    }

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

        if ($k -eq 'Escape') {
            $this.InputBuffer = ''
            $this.Mode = 'list'
            return ''
        }
        if ($k -eq 'Enter') {
            $name = $this.InputBuffer.Trim()
            $this.InputBuffer = ''
            $this.Mode = 'list'
            if (-not $name) {
                $this.SetStatus('nombre del tag vacío — cancelado', 'error')
                return ''
            }
            $this.DoCreateTag($name)
            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 [void] DoCreateTag([string]$tagName) {
        $r = $this.Git.CreateTag($this.Repo.Path, $tagName, '')
        $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' })))
        if ($r.Ok) { $this.RefreshState() }
    }

    hidden [void] DoPushTags() {
        $r = $this.Git.PushTags($this.Repo.Path)
        $this.SetStatus($r.Message, ($(if ($r.Ok) { 'ok' } else { 'error' })))
    }

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

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

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

        # Status / prompt line
        if ($this.Mode -eq 'input-tag') {
            $cursor = $this.Theme.Fg('acc') + '_' + $reset
            $label  = $this.Theme.Fg('fg2') + 'Nombre del tag (ej v1.2.3): ' + $reset
            $buf    = $this.Theme.Fg('fg1') + $this.InputBuffer + $reset
            $lines.Add(' ' + $label + $buf + $cursor)
        } elseif ($this.StatusMessage) {
            $color = if ($this.StatusKind -eq 'error') { 'gitConflict' } else { 'gitClean' }
            $lines.Add(' ' + $this.Theme.Fg($color) + $this.StatusMessage + $reset)
        } else {
            $lines.Add('')
        }

        # State summary
        $summary = @()
        $summary += $this.Theme.Fg('fg2') + 'branch:' + $reset + ' ' + $this.Theme.Fg('acc') + $this.CurrentBranch + $reset
        $summary += $this.Theme.Fg('fg2') + 'latest tag:' + $reset + ' ' + $this.Theme.Fg('fg1') + $this.LatestTag + $reset
        $summary += $this.Theme.Fg('fg2') + 'tags:' + $reset + ' ' + $this.Theme.Fg('fg1') + $this.TagCount + $reset
        if ($this.HasPackageJson) {
            $summary += $this.Theme.Fg('fg2') + 'version:' + $reset + ' ' + $this.Theme.Fg('fg1') + $this.CurrentVersion + $reset
        }
        $lines.Add(' ' + ($summary -join ' · '))
        $lines.Add($r.HRule())

        # Actions
        $items = $this.AvailableActions()
        for ($i = 0; $i -lt $items.Count; $i++) {
            $sel = ($this.SelectedIndex -eq $i -and $this.Mode -eq 'list')
            $marker = if ($sel) { $this.Theme.Fg('acc') + '▎ ▶ ' + $reset } else { ' · ' }
            $color = if ($items[$i].Action -eq 'cancel') { 'fg2' } elseif ($sel) { 'acc' } else { 'fg1' }
            $lines.Add($marker + $this.Theme.Fg($color) + $items[$i].Label + $reset)
        }

        $hints = $this.StatusBarHints()
        $lines.Add($r.HRule())
        return @($lines.ToArray()) + $this.StatusBar.RenderAnchored($hints, '', $lines.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() {
        switch ($this.Mode) {
            'input-tag' {
                return @(
                    @{ k = $this.T('hint.type'); label = $this.T('hint.save') }
                    @{ k = '↵';                  label = $this.T('hint.accept') }
                    @{ k = 'Esc';                label = $this.T('hint.cancel') }
                )
            }
            default {
                return @(
                    @{ k = '↑↓'; label = $this.T('hint.move') }
                    @{ k = '↵';  label = $this.T('hint.run') }
                    @{ k = '←';  label = $this.T('hint.back') }
                    @{ k = 'Q';  label = $this.T('hint.quit') }
                )
            }
        }
        return @()
    }
}