src/UI/BreadcrumbBuilder.ps1

# BreadcrumbBuilder — Convierte un path en segmentos para el AppHeader.
#
# Responsabilidad ÚNICA: dado un path file-system, devolver
# @{ Segs = [string[]] ancestros; Current = [string] último segmento }
# con colapso opcional cuando el path es muy profundo (>4 ancestros) — preserva
# el drive + '…' + 2 últimos para mantener contexto sin romper layout horizontal.
#
# Función pura. No conoce UI ni Renderer. Sin state — solo métodos estáticos.
#
# Cross-platform: detecta paths POSIX (que empiezan con '/') y los procesa con
# separador '/'. Paths con drive Windows (C:\, D:/) o relativos usan '\'.

class BreadcrumbBuilder {

    # Por defecto colapsa cuando hay más de 4 ancestros. Caller puede pasar otro
    # límite si querés más detalle (e.g. screens con sidebar narrow).
    static [int] $DefaultMaxAncestors = 4

    static [hashtable] Build([string]$path) {
        return [BreadcrumbBuilder]::Build($path, [BreadcrumbBuilder]::DefaultMaxAncestors)
    }

    static [hashtable] Build([string]$path, [int]$maxAncestors) {
        $tokens = [BreadcrumbBuilder]::_Tokenize($path)
        $parts = $tokens.Parts

        if ($parts.Count -eq 0) { return @{ Segs = @(); Current = '' } }
        if ($parts.Count -eq 1) { return @{ Segs = @(); Current = $parts[0] } }

        $current = $parts[-1]
        $ancestors = $parts[0..($parts.Count - 2)]

        if ($maxAncestors -ge 0 -and $ancestors.Count -gt $maxAncestors) {
            # Conservar drive + '…' + 2 últimos ancestros.
            $ancestors = @($ancestors[0], '…', $ancestors[-2], $ancestors[-1])
        }

        return @{ Segs = $ancestors; Current = $current }
    }

    # Variante extendida: devuelve también AbsolutePaths — paths absolutos
    # reconstruidos hasta cada segmento. Sirve para hacer "drill" a un segmento
    # específico (Enter sobre el breadcrumb interactivo). El último elemento
    # siempre corresponde al Current.
    #
    # NO colapsa con … porque la navegación necesita TODOS los segmentos —
    # el caller decide si rendear todos o algunos.
    #
    # Resultado:
    # @{
    # Segs = [string[]] todos los segmentos en orden (incluyendo current)
    # Current = último segmento
    # AbsolutePaths = [string[]] path absoluto correspondiente a cada segmento
    # }
    static [hashtable] BuildWithPaths([string]$path) {
        $tokens = [BreadcrumbBuilder]::_Tokenize($path)
        $parts = $tokens.Parts

        if ($parts.Count -eq 0) {
            return @{ Segs = @(); Current = ''; AbsolutePaths = @() }
        }

        $absolutePaths = @()

        if ($tokens.IsPosix) {
            # POSIX absoluto: cada AbsolutePath es '/' + parts unidos con '/'.
            $acc = ''
            foreach ($p in $parts) {
                $acc = "$acc/$p"
                $absolutePaths += $acc
            }
        } else {
            # Windows o relativo. Concatenación manual con '\' — NO Join-Path,
            # que tira DriveNotFoundException en POSIX cuando el primer parte
            # no parece un drive.
            for ($i = 0; $i -lt $parts.Count; $i++) {
                if ($i -eq 0) {
                    # Primer segmento: si parece drive (termina en :), agregar separador.
                    $absolutePaths += if ($parts[0] -match ':$') { $parts[0] + '\' } else { $parts[0] }
                } else {
                    $prev = $absolutePaths[$i - 1].TrimEnd('\')
                    $absolutePaths += "$prev\$($parts[$i])"
                }
            }
        }

        return @{
            Segs           = $parts
            Current        = $parts[-1]
            AbsolutePaths  = $absolutePaths
        }
    }

    # Tokeniza un path en segmentos detectando si es POSIX absoluto, Windows
    # con drive, o relativo. Centraliza la lógica de detección para que Build
    # y BuildWithPaths la compartan.
    hidden static [hashtable] _Tokenize([string]$path) {
        if ([string]::IsNullOrWhiteSpace($path)) {
            return @{ Parts = @(); IsPosix = $false }
        }

        # POSIX absoluto: empieza con '/' y NO es un path con drive Windows.
        $isPosix = $path.StartsWith('/') -and ($path -notmatch '^[A-Za-z]:')

        if ($isPosix) {
            $parts = @($path -split '/' | Where-Object { $_ -ne '' })
            return @{ Parts = $parts; IsPosix = $true }
        }

        # Windows o relativo: normalizamos forward slashes a backslash y
        # spliteamos descartando vacíos (trailing slash, doble).
        $norm = $path -replace '/', '\'
        $parts = @($norm -split '\\' | Where-Object { $_ -ne '' })
        return @{ Parts = $parts; IsPosix = $false }
    }
}