Install.ps1

#Requires -Version 7.0
<#
.SYNOPSIS
    Install / uninstall del comando `rnav` (alias del repo-nav v3) en el $PROFILE.
 
.DESCRIPTION
    Idempotente: si el alias ya está instalado, no duplica. Detecta el path
    actual del repo (donde vive este script) y crea una function que dot-sourcea
    `repo-nav.ps1`. Verifica deps (pwsh 7+, git en PATH).
 
    Bloque agregado al $PROFILE entre marcadores BEGIN/END para que -Uninstall
    pueda quitarlo limpio.
 
.PARAMETER Uninstall
    Quita la function `rnav` del $PROFILE. Restaura el archivo a su estado
    previo (sin el bloque del repo-nav).
 
.PARAMETER ProfilePath
    Override del path del $PROFILE (útil para tests). Default: el $PROFILE de
    pwsh actual (current host).
 
.PARAMETER Force
    Reescribe el bloque aunque ya esté instalado (útil después de mover el repo).
 
.EXAMPLE
    .\Install.ps1
    Instala el alias rnav apuntando a este repo.
 
.EXAMPLE
    .\Install.ps1 -Uninstall
    Quita el alias.
 
.EXAMPLE
    .\Install.ps1 -Force
    Reinstala (reescribe el bloque, útil después de git pull si la entry
    function cambió).
 
.NOTES
    REQUIERE pwsh 7+ (NO Windows PowerShell 5.1). Lo chequea explícito.
#>


[CmdletBinding()]
param(
    [switch] $Uninstall,
    [string] $ProfilePath,
    [switch] $Force,
    # Si se pasa, persiste el modo en settings.json sin preguntar interactivo.
    # Útil para `repo-nav init -AutoLoadMode None` automatizado.
    [ValidateSet('All', 'Favorites', 'None', '')]
    [string] $AutoLoadMode = '',
    # Skipea el prompt interactivo si no hay tty (CI, scripts).
    [switch] $NonInteractive,
    # Nombre del alias a registrar. Default: 'rnav'. ConfigScreen lo usa para
    # renombrar el alias existente (se llama install -AliasName <new> -Force).
    [string] $AliasName = 'rnav'
)

if (-not ($AliasName -match '^[a-zA-Z][\w\-]*$')) {
    Write-Host "[error] Nombre de alias inválido: '$AliasName'. Debe empezar con letra y contener solo letras, dígitos, '-' o '_'." -ForegroundColor Red
    exit 1
}

# Marcadores idempotentes — el bloque entre estos comments se reemplaza/quita.
$BEGIN_MARKER = '# >>> repo-nav v3 — managed block (do not edit)'
$END_MARKER   = '# <<< repo-nav v3 — managed block end'

function Write-Status {
    param([string]$Message, [string]$Kind = 'info')
    $color = switch ($Kind) {
        'ok'    { 'Green' }
        'warn'  { 'Yellow' }
        'error' { 'Red' }
        default { 'Cyan' }
    }
    $prefix = switch ($Kind) {
        'ok'    { '[ok] ' }
        'warn'  { '[warn] ' }
        'error' { '[error]' }
        default { '[info] ' }
    }
    Write-Host "$prefix $Message" -ForegroundColor $color
}

function Get-EffectiveProfilePath {
    param([string]$Override)
    if ($Override) { return $Override }
    if (-not $PROFILE) {
        throw "No hay `$PROFILE definido. ¿Estás en un host PS válido?"
    }
    return $PROFILE
}

function Test-Dependencies {
    $ok = $true

    # pwsh 7+
    if ($PSVersionTable.PSVersion.Major -lt 7) {
        Write-Status "PowerShell version: $($PSVersionTable.PSVersion). repo-nav v3 requiere pwsh 7+." 'error'
        $ok = $false
    } else {
        Write-Status "PowerShell $($PSVersionTable.PSVersion) OK." 'ok'
    }

    # git en PATH
    $gitCmd = Get-Command git -ErrorAction SilentlyContinue
    if (-not $gitCmd) {
        Write-Status "git no está en el PATH. Instalalo desde https://git-scm.com" 'error'
        $ok = $false
    } else {
        $gitVer = (& git --version) -replace 'git version ', ''
        Write-Status "git $gitVer OK." 'ok'
    }

    return $ok
}

function Get-ManagedBlock {
    param([string]$RepoRoot, [string]$Name = 'rnav')
    $entryScript = Join-Path $RepoRoot 'repo-nav.ps1'
    # Quote-safe: usamos single-quotes en el path para evitar interpolación al
    # escribir, y dejamos que pwsh lo resuelva al runtime. El path nunca debería
    # contener single-quotes (paths Windows típicos no las tienen) — si llegara
    # a tenerlas, se rompería; lo validamos explícito.
    if ($entryScript -match "'") {
        throw "Path con caracteres no soportados (single quote): $entryScript"
    }

    # Nombre de variable FIJO (no derivado del alias). Antes usábamos
    # `$Name + 'Args'`, lo que generaba '$rnav-devArgs' para alias con guión —
    # y PS parsea '$rnav-devArgs' como '$rnav - devArgs', tirando ParserError
    # al cargar el profile. `$cmdArgs` es siempre un identifier válido y queda
    # local al scope de la function — no choca con nada externo.
    return @"
$BEGIN_MARKER
# Generado por Install.ps1 — para reinstalar: ejecutá Install.ps1 -Force.
# Para quitar: Install.ps1 -Uninstall.
function $Name {
    param([Parameter(ValueFromRemainingArguments)] `$cmdArgs)
    & '$entryScript' @cmdArgs
}
$END_MARKER
"@

}

function Read-ProfileSafely {
    param([string]$Path)
    if (-not (Test-Path -LiteralPath $Path)) { return '' }
    return [System.IO.File]::ReadAllText($Path, [System.Text.UTF8Encoding]::new($false))
}

function Write-ProfileSafely {
    param([string]$Path, [string]$Content)
    # .NET puro — los cmdlets PS (Set-Content / New-Item) tienen quirks de
    # parameter sets con strings multi-línea que tiran ParameterBindingException.
    $dir = [System.IO.Path]::GetDirectoryName($Path)
    if ($dir -and -not [System.IO.Directory]::Exists($dir)) {
        [System.IO.Directory]::CreateDirectory($dir) | Out-Null
    }
    $utf8 = New-Object System.Text.UTF8Encoding $false
    [System.IO.File]::WriteAllText($Path, [string]$Content, $utf8)
}

function Remove-ExistingBlock {
    param([string]$ProfileContent)
    $lines = $ProfileContent -split "`r?`n"
    $kept = @()
    $insideBlock = $false
    foreach ($line in $lines) {
        if ($line -eq $BEGIN_MARKER) { $insideBlock = $true; continue }
        if ($line -eq $END_MARKER)   { $insideBlock = $false; continue }
        if (-not $insideBlock) { $kept += $line }
    }
    # Trim trailing newlines duplicados.
    $result = ($kept -join "`n")
    return $result.TrimEnd("`n", "`r")
}

function Test-BlockInstalled {
    param([string]$ProfileContent)
    return $ProfileContent.Contains($BEGIN_MARKER)
}

# Prompt interactivo para AutoLoadMode. Devuelve uno de 'All' | 'Favorites' | 'None'.
# Si la sesión es non-interactive (input redirected) o se pasó -NonInteractive,
# devuelve 'All' como default sin preguntar.
function Read-AutoLoadModeInteractive {
    if ($NonInteractive -or [Console]::IsInputRedirected) {
        return 'All'
    }
    Write-Host ''
    Write-Host '¿Cómo querés cargar git status al iniciar repo-nav?' -ForegroundColor Cyan
    Write-Host ' Si tenés muchos repos o red lenta, podés posponer la carga.'
    Write-Host ''
    Write-Host ' [1] All — carga todos los repos al boot (default, UX más rico)'
    Write-Host ' [2] Favorites — solo los repos marcados como favoritos; el resto on-demand'
    Write-Host ' [3] None — ninguno; pulsás R o navegás con el cursor para cargar'
    Write-Host ''
    while ($true) {
        $choice = Read-Host 'Elegí 1, 2 o 3 (Enter = 1)'
        if ([string]::IsNullOrWhiteSpace($choice) -or $choice -eq '1') { return 'All' }
        if ($choice -eq '2') { return 'Favorites' }
        if ($choice -eq '3') { return 'None' }
        Write-Host " '$choice' no es válido. Elegí 1, 2 o 3." -ForegroundColor Yellow
    }
}

# Persiste AutoLoadMode en settings.json. Self-contained: NO carga el v3 entero
# para evitar el costo de Bootstrap durante el install. Lee/escribe el JSON
# directo via System.IO + ConvertTo-Json.
function Save-AutoLoadModeToSettings {
    param([string]$Mode)
    # `$home` es read-only built-in en PS — no podemos pisarlo. Usamos otro nombre.
    $userHome = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
    $settingsPath = Join-Path $userHome '.repo-nav/preferences.json'
    $settingsDir = Split-Path -Path $settingsPath -Parent
    if (-not [System.IO.Directory]::Exists($settingsDir)) {
        [System.IO.Directory]::CreateDirectory($settingsDir) | Out-Null
    }

    # Leer JSON existente o arrancar con hashtable vacío.
    $obj = $null
    if ([System.IO.File]::Exists($settingsPath)) {
        try {
            $raw = [System.IO.File]::ReadAllText($settingsPath, [System.Text.UTF8Encoding]::new($false))
            if ($raw) { $obj = $raw | ConvertFrom-Json -AsHashtable }
        } catch {
            # JSON corrupto — empezamos de cero. El usuario perdió config rara
            # pero el install sigue. Si era importante, había que revisar antes.
            $obj = $null
        }
    }
    if (-not $obj) { $obj = @{} }
    $obj.AutoLoadMode = $Mode

    $json = $obj | ConvertTo-Json -Depth 10
    $utf8 = New-Object System.Text.UTF8Encoding $false
    [System.IO.File]::WriteAllText($settingsPath, $json, $utf8)
}

# ─── Main ───────────────────────────────────────────────────────────────────

$repoRoot = $PSScriptRoot
$entryScript = Join-Path $repoRoot 'repo-nav.ps1'

if (-not (Test-Path -LiteralPath $entryScript)) {
    Write-Status "No se encontró $entryScript. ¿Estás corriendo Install.ps1 desde la raíz del repo?" 'error'
    exit 1
}

$profilePath = Get-EffectiveProfilePath -Override $ProfilePath
$existing = Read-ProfileSafely -Path $profilePath
$alreadyInstalled = Test-BlockInstalled -ProfileContent $existing

# ─── Modo uninstall ─────────────────────────────────────────────────────────
if ($Uninstall) {
    if (-not $alreadyInstalled) {
        Write-Status "El alias rnav no estaba instalado en $profilePath." 'warn'
        exit 0
    }
    $cleaned = Remove-ExistingBlock -ProfileContent $existing
    Write-ProfileSafely -Path $profilePath -Content $cleaned
    Write-Status "Alias rnav removido de $profilePath." 'ok'
    Write-Status "Abrí una pwsh nueva para que el cambio tome efecto." 'info'
    exit 0
}

# ─── Modo install ──────────────────────────────────────────────────────────
Write-Status "Instalando repo-nav v3..." 'info'
Write-Host ''

# 1. Verificar dependencias
if (-not (Test-Dependencies)) {
    Write-Status "Dependencias faltantes. Resolvé los errores y volvé a correr." 'error'
    exit 1
}

# 2. Bloque a escribir (usa $AliasName, default 'rnav')
$block = Get-ManagedBlock -RepoRoot $repoRoot -Name $AliasName

# 3. Si ya está instalado y no hay -Force, salir.
if ($alreadyInstalled -and -not $Force) {
    Write-Status "El alias $AliasName ya está instalado en $profilePath. Usá -Force para reescribir." 'warn'
    Write-Status "Repo path: $repoRoot" 'info'
    exit 0
}

# 4. Limpiar bloque previo (si existe), después append el nuevo.
$cleaned = Remove-ExistingBlock -ProfileContent $existing
$separator = if ([string]::IsNullOrWhiteSpace($cleaned)) { '' } else { "`n`n" }
$newContent = $cleaned + $separator + $block + "`n"

Write-ProfileSafely -Path $profilePath -Content $newContent

# 5. AutoLoadMode: si se pasó por flag, lo usamos directo. Si no, preguntamos
# (a menos que sea non-interactive). Persistimos el resultado en settings.json
# para que MainScreen lo lea al boot.
$effectiveMode = if ($AutoLoadMode) { $AutoLoadMode } else { Read-AutoLoadModeInteractive }
Save-AutoLoadModeToSettings -Mode $effectiveMode

Write-Host ''
Write-Status "Alias $AliasName instalado." 'ok'
Write-Status "Profile: $profilePath" 'info'
Write-Status "Repo: $repoRoot" 'info'
Write-Status "AutoLoadMode: $effectiveMode" 'info'
Write-Host ''
Write-Status "Abrí una pwsh nueva y tipeá '$AliasName' para usar el v3." 'info'