src/Services/SettingsService.ps1

# SettingsService — Carga y persistencia de las preferencias del usuario.
#
# Responsabilidad ÚNICA: serializar/deserializar Settings contra un JSON.
# No conoce UI, no conoce repos, no aplica defaults de negocio (eso lo hace el model).
#
# Path por defecto: ~/.repo-nav/preferences.json
# Path inyectable por constructor para tests con fixture en TestDrive.

class SettingsService {
    [string] $Path

    SettingsService() {
        $this.Path = [SettingsService]::DefaultPath()
    }

    SettingsService([string] $path) {
        if ([string]::IsNullOrWhiteSpace($path)) {
            throw "SettingsService: path no puede ser vacío."
        }
        $this.Path = $path
    }

    static [string] DefaultPath() {
        # Ojo: $HOME es read-only en PS. Usamos var local con otro nombre.
        $homeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
        return Join-Path $homeDir '.repo-nav/preferences.json'
    }

    # Carga desde disco. Si el archivo no existe → defaults. Si está corrupto → defaults +
    # warning (no tira excepción: las preferencias rotas no deben tirar abajo la app).
    [Settings] Load() {
        if (-not (Test-Path -LiteralPath $this.Path)) {
            return [Settings]::new()
        }

        try {
            $raw = Get-Content -LiteralPath $this.Path -Raw -Encoding UTF8
            if ([string]::IsNullOrWhiteSpace($raw)) {
                return [Settings]::new()
            }
            $h = $raw | ConvertFrom-Json -AsHashtable -ErrorAction Stop
            return [SettingsService]::Hydrate($h)
        } catch {
            Write-Warning "SettingsService: settings corruptos en '$($this.Path)' — uso defaults. Detalle: $_"
            return [Settings]::new()
        }
    }

    # Guarda en disco. Crea el directorio padre si no existe. Devuelve $true si OK,
    # $false con warning si falló (no tira: caller decide si reintentar).
    [bool] Save([Settings] $settings) {
        if ($null -eq $settings) {
            throw "SettingsService.Save: settings no puede ser null."
        }

        try {
            # .NET puro para evitar quirks de parameter sets de New-Item / Set-Content.
            $dir = [System.IO.Path]::GetDirectoryName($this.Path)
            if ($dir -and -not [System.IO.Directory]::Exists($dir)) {
                [System.IO.Directory]::CreateDirectory($dir) | Out-Null
            }

            $payload = [SettingsService]::Serialize($settings)
            $json = $payload | ConvertTo-Json -Depth 8
            $utf8 = New-Object System.Text.UTF8Encoding $false
            [System.IO.File]::WriteAllText($this.Path, $json, $utf8)
            return $true
        } catch {
            Write-Warning "SettingsService: no pude guardar en '$($this.Path)'. Detalle: $($_.Exception.Message)"
            return $false
        }
    }

    # Convierte un hashtable (proveniente de ConvertFrom-Json -AsHashtable) en Settings.
    # Solo asigna las propiedades que existen en el hashtable; las demás quedan con default.
    # Robust: si una propiedad falta, no es error.
    hidden static [Settings] Hydrate([hashtable] $h) {
        $s = [Settings]::new()
        if ($null -eq $h) { return $s }

        foreach ($prop in $s.PSObject.Properties) {
            $name = $prop.Name
            if (-not $h.ContainsKey($name)) { continue }
            $value = $h[$name]
            if ($null -eq $value) { continue }
            try {
                $s.$name = $value
            } catch {
                Write-Warning "SettingsService.Hydrate: no pude asignar '$name' (tipo incompatible). Skip."
            }
        }
        return $s
    }

    # Convierte Settings → hashtable plana lista para ConvertTo-Json.
    hidden static [hashtable] Serialize([Settings] $s) {
        $h = @{}
        foreach ($prop in $s.PSObject.Properties) {
            $h[$prop.Name] = $prop.Value
        }
        return $h
    }
}