src/Services/SetupService.ps1

# SetupService — Inspección del entorno y estado de instalación.
#
# Lectura pura: no escribe nada, no muta state. Usa System.IO directo (no
# cmdlets PS) para evitar parameter set quirks. La parte mutadora (install /
# uninstall del alias) la maneja Install.ps1 — esta clase solo reporta.

class SetupService {

    # Path del $PROFILE current. Si la sesión PS no tiene $PROFILE válido (ej:
    # corriendo en un host minimalista), devuelve string vacío.
    [string] GetProfilePath() {
        if ($global:PROFILE) { return [string]$global:PROFILE }
        return ''
    }

    # Estado del file de profile.
    # Devuelve hashtable @{ Path; Exists; Size }.
    [hashtable] GetProfileInfo() {
        $path = $this.GetProfilePath()
        if (-not $path) {
            return @{ Path = ''; Exists = $false; Size = 0 }
        }
        if (-not [System.IO.File]::Exists($path)) {
            return @{ Path = $path; Exists = $false; Size = 0 }
        }
        $info = [System.IO.FileInfo]::new($path)
        return @{
            Path   = $path
            Exists = $true
            Size   = $info.Length
        }
    }

    # Parsea el $PROFILE buscando `function NAME` (top-level). Devuelve array
    # de hashtables @{ Name; Source = 'managed'|'user'; Line }.
    # 'managed' = generada por Install.ps1 (entre marcadores).
    # 'user' = otra function que el usuario tiene.
    [object[]] GetInstalledFunctions() {
        $info = $this.GetProfileInfo()
        if (-not $info.Exists) { return @() }

        $content = [System.IO.File]::ReadAllText($info.Path, [System.Text.UTF8Encoding]::new($false))
        $lines = $content -split "`r?`n"

        $beginMarker = '# >>> repo-nav v3 — managed block (do not edit)'
        $endMarker   = '# <<< repo-nav v3 — managed block end'

        $results = @()
        $insideManaged = $false
        $lineNum = 0

        foreach ($line in $lines) {
            $lineNum++
            if ($line -eq $beginMarker) { $insideManaged = $true; continue }
            if ($line -eq $endMarker)   { $insideManaged = $false; continue }

            # Match top-level `function NAME` ignorando indentación dentro de
            # cuerpos de funciones nested (PS no permite real nesting top-level
            # de profile pero por las dudas matcheamos solo line-start).
            if ($line -match '^\s*function\s+([\w\-]+)\s*\{?') {
                $results += @{
                    Name   = $Matches[1]
                    Source = if ($insideManaged) { 'managed' } else { 'user' }
                    Line   = $lineNum
                }
            }
        }

        return $results
    }

    # ¿Está instalado el alias bajo el bloque managed? Default: 'rnav'.
    # Mantenemos param para facilitar tests y para el futuro feature de
    # "agregar otro alias" (ConfigScreen) sin acoplar el método a un nombre.
    [bool] IsAliasInstalled() {
        return $this.IsAliasInstalled('rnav')
    }

    [bool] IsAliasInstalled([string]$aliasName) {
        if (-not $aliasName) { return $false }
        $functions = $this.GetInstalledFunctions()
        return @($functions | Where-Object { $_.Name -eq $aliasName -and $_.Source -eq 'managed' }).Count -gt 0
    }

    # Lista todos los aliases registrados en el bloque managed. Permite mostrar
    # en ConfigScreen "tenés rnav y nav instalados" cuando el user agregó varios.
    [string[]] GetInstalledAliases() {
        $functions = $this.GetInstalledFunctions()
        return @($functions | Where-Object Source -eq 'managed' | ForEach-Object Name)
    }

    # Path del settings JSON.
    [string] GetSettingsPath() {
        $homeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
        return Join-Path $homeDir '.repo-nav/preferences.json'
    }

    # Estado del settings file.
    # Devuelve hashtable @{ Path; Exists; Valid; Size }.
    [hashtable] GetSettingsInfo() {
        $path = $this.GetSettingsPath()
        if (-not [System.IO.File]::Exists($path)) {
            return @{ Path = $path; Exists = $false; Valid = $false; Size = 0 }
        }
        $info = [System.IO.FileInfo]::new($path)
        $valid = $true
        try {
            $raw = [System.IO.File]::ReadAllText($path, [System.Text.UTF8Encoding]::new($false))
            if ($raw) { $null = $raw | ConvertFrom-Json -ErrorAction Stop }
        } catch {
            $valid = $false
        }
        return @{
            Path   = $path
            Exists = $true
            Valid  = $valid
            Size   = $info.Length
        }
    }

    # Versión de pwsh actual.
    [string] GetPsVersion() {
        if ($null -eq $global:PSVersionTable) { return 'unknown' }
        return [string]$global:PSVersionTable.PSVersion
    }

    # ¿Hay git en el PATH? Devuelve string version o '' si no.
    [string] GetGitVersion() {
        try {
            $gitCmd = Get-Command git -ErrorAction SilentlyContinue
            if (-not $gitCmd) { return '' }
            $out = & git --version 2>$null
            if ($LASTEXITCODE -ne 0) { return '' }
            return ($out -replace '^git version ', '').Trim()
        } catch {
            return ''
        }
    }

    # ¿Hay node en el PATH? Opcional — no se usa hoy pero los flows futuros
    # (npm install, package detection) lo van a necesitar.
    [string] GetNodeVersion() {
        try {
            $nodeCmd = Get-Command node -ErrorAction SilentlyContinue
            if (-not $nodeCmd) { return '' }
            $out = & node --version 2>$null
            if ($LASTEXITCODE -ne 0) { return '' }
            return ([string]$out).Trim().TrimStart('v')
        } catch {
            return ''
        }
    }

    # ¿Hay gh (GitHub CLI) en el PATH? Opcional — mejora la detección de PR
    # URLs del IntegrateScreen, no es crítico para el flujo principal.
    [string] GetGhVersion() {
        try {
            $ghCmd = Get-Command gh -ErrorAction SilentlyContinue
            if (-not $ghCmd) { return '' }
            $out = & gh --version 2>$null
            if ($LASTEXITCODE -ne 0) { return '' }
            # Output típico: "gh version 2.40.1 (2024-01-04)"
            $first = ([string[]]$out)[0]
            if ($first -match 'gh version (\S+)') { return $Matches[1] }
            return ''
        } catch {
            return ''
        }
    }

    # Snapshot del estado de dependencias. Devuelve hashtable con una entry por
    # dep, cada una con @{ Available, Version, Required, Notes }. La consume
    # ConfigScreen (toggle visual) y `repo-nav doctor` (output plano).
    #
    # Required marca si la dep es necesaria para que la app arranque vs
    # opcional para features puntuales (gh = PR URLs, node = futuro).
    [hashtable] CheckDependencies() {
        $psVer = $this.GetPsVersion()
        $psMajor = 0
        if ($psVer -match '^(\d+)') { $psMajor = [int]$Matches[1] }

        return @{
            PowerShell = @{
                Available = ($psMajor -ge 7)
                Version   = $psVer
                Required  = $true
                Notes     = if ($psMajor -lt 7) { 'repo-nav v3 necesita pwsh 7+ (no Windows PowerShell 5.1).' } else { '' }
            }
            Git = @{
                Available = [bool]$this.GetGitVersion()
                Version   = $this.GetGitVersion()
                Required  = $true
                Notes     = if (-not $this.GetGitVersion()) { 'Instalá git desde https://git-scm.com.' } else { '' }
            }
            Node = @{
                Available = [bool]$this.GetNodeVersion()
                Version   = $this.GetNodeVersion()
                Required  = $false
                Notes     = if (-not $this.GetNodeVersion()) { 'Opcional — para flows npm futuros.' } else { '' }
            }
            Gh = @{
                Available = [bool]$this.GetGhVersion()
                Version   = $this.GetGhVersion()
                Required  = $false
                Notes     = if (-not $this.GetGhVersion()) { 'Opcional — mejora la detección de PR URLs.' } else { '' }
            }
        }
    }

    # Path del repo donde vive este servicio (resuelve dinámicamente desde el
    # archivo del SetupService → src/Services/ → repo root).
    [string] GetRepoRoot() {
        # Si está cargado vía dot-source, $PSScriptRoot apunta al folder del
        # archivo. Subimos 2 niveles: Services → src → repo root.
        if (-not $script:RNRoot) {
            # Fallback: usar Get-Location, no ideal pero defensa.
            return (Get-Location).Path
        }
        return $script:RNRoot
    }
}