src/App/Startup.ps1

# Startup — Punto de entrada del v3.
# Construye el grafo de servicios + UI y renderiza MainScreen con datos reales.

function Start-RepoNavV3 {
    param(
        [string] $ThemeKey  = 'midnight',
        [string] $ReposPath = ''
    )

    # UTF-8 output para glifos Unicode (●, ⎇, ▲, etc.)
    try {
        [Console]::OutputEncoding = [Text.Encoding]::UTF8
        $OutputEncoding = [Text.Encoding]::UTF8
    } catch {
        # Algunos hosts (ISE legacy) no soportan setear OutputEncoding — seguimos
        # con el default. No queremos abortar el startup por esto.
        $null = $_
    }

    if (-not [AnsiService]::SupportsTrueColor()) {
        Write-Warning "Esta terminal no parece soportar truecolor. La paleta puede degradarse."
    }

    # 1) Servicios
    $settingsSvc = [SettingsService]::new()
    $settings    = $settingsSvc.Load()

    # ThemeKey: param explícito > settings persistidos > default 'midnight'
    $effectiveTheme = if ($PSBoundParameters.ContainsKey('ThemeKey')) {
        $ThemeKey
    } elseif ($settings.ThemeKey) {
        $settings.ThemeKey
    } else {
        'midnight'
    }

    # Validamos el theme antes de instanciar el service. Si el key no existe
    # (typo del usuario o settings stale apuntando a un theme removido), aviso
    # y caigo a midnight — preferible a tirar excepción cruda en el primer render.
    if (-not $global:RNThemes.ContainsKey($effectiveTheme)) {
        $available = @($global:RNThemes.Keys | Sort-Object) -join ', '
        Write-Warning "Theme '$effectiveTheme' no existe. Disponibles: $available. Usando 'midnight'."
        $effectiveTheme = 'midnight'
    }

    $themeSvc  = [ThemeService]::new($effectiveTheme)
    $i18nSvc   = [I18nService]::new($settings.Language)
    $gitSvc    = [GitService]::new()
    $disco     = [RepoDiscoveryService]::new($gitSvc)

    # Cableamos el callback OnFetchSuccess: cada vez que GitService completa un
    # pull/push/fetch, actualizamos LastFetchByRepo + persistimos. El scriptblock
    # captura $settingsSvc / $settings por closure — sobreviven mientras Startup
    # esté en stack (lo cual es siempre durante la sesión).
    $gitSvc.OnFetchSuccess = {
        param($repoPath)
        if (-not $repoPath) { return }
        # Derivamos el repoId del path (mismo algoritmo que Repo.Id en GetRepoState).
        $name = Split-Path $repoPath -Leaf
        $repoId = $name.ToLower() -replace '[^a-z0-9]', '-'
        $settings.RecordFetch($repoId)
        # Persiste async-fire-and-forget — Save() no bloquea visible al usuario,
        # y si falla por race con otro Save (poco probable), el siguiente lo arregla.
        $null = $settingsSvc.Save($settings)
    }.GetNewClosure()

    # 2) UI primitives + componentes
    $renderer   = [Renderer]::new($themeSvc)
    $primitives = [Primitives]::new($themeSvc)
    $frame      = [Frame]::new($themeSvc, $renderer)
    $header     = [AppHeader]::new($themeSvc, $renderer, $primitives)
    $statusBar  = [StatusBar]::new($themeSvc, $renderer, $primitives)

    # 3) Path raíz: param explícito > cwd > carpeta padre del entry
    if (-not $ReposPath) {
        $ReposPath = (Get-Location).Path
        # Si estamos parados DENTRO de un repo, escaneamos al padre
        if ([GitService]::new().IsGitRepo($ReposPath)) {
            $ReposPath = Split-Path $ReposPath -Parent
        }
    }

    # 4) Discover — modo controlado por Settings.AutoLoadMode
    # All → carga todos los repos con git status (default UX rico)
    # Favorites → solo los que están en FavoriteIds; el resto unloaded
    # None → todos unloaded; user pulsa R o navega para cargar
    $autoLoadMode = if ($settings.AutoLoadMode) { $settings.AutoLoadMode } else { 'All' }

    if ($autoLoadMode -eq 'All') {
        # Paralelo cuando hay suficientes repos para que valga la pena el
        # spinup. DiscoverParallel ya hace el threshold internamente — fallback
        # a secuencial si N < 5.
        $repos = $disco.DiscoverParallel($ReposPath)
    } else {
        $shallow = $disco.DiscoverShallow($ReposPath)
        if ($autoLoadMode -eq 'Favorites' -and $settings.FavoriteIds.Count -gt 0) {
            # Cargo status solo de los favoritos. El resto queda unloaded.
            $favSet = @{}
            foreach ($id in $settings.FavoriteIds) { $favSet[$id] = $true }
            $repos = @()
            foreach ($r in $shallow) {
                if ($favSet.ContainsKey($r.Id)) {
                    $repos += $gitSvc.GetRepoState($r.Path)
                } else {
                    $repos += $r
                }
            }
        } else {
            # 'None' o 'Favorites' sin favoritos definidos: todos unloaded.
            $repos = $shallow
        }
    }

    # 5) Loop interactivo — le pasamos settingsSvc para favoritos persistentes.
    $main = [MainScreen]::new($themeSvc, $renderer, $primitives, $frame, $header, $statusBar, $gitSvc, $settingsSvc)
    $main.I18n = $i18nSvc
    $selected = $main.RunInteractive($ReposPath, $repos, '3.0.0-dev')

    # 6) Si seleccionó algo, cambia el cwd al path del repo. En PS \$PWD es estado de
    # la sesión (no del scope), entonces Set-Location adentro del script propaga al
    # shell del usuario MIENTRAS el script se invoque en el mismo proceso PS — o sea
    # `.\repo-nav.ps1` o `& '...repo-nav.ps1'`. Si lo lanzás con `pwsh script.ps1`
    # (proceso hijo), no propaga y vas a quedar en el cwd original.
    if ($selected) {
        Set-Location -LiteralPath $selected.Path
    }
}