Private/FilenameHelpers.ps1

<#
.SYNOPSIS
    ConvertVTTAssets FilenameHelpers - Core filename sanitization and validation functions
.DESCRIPTION
    Private module containing specialized functions for filename and directory name sanitization,
    conflict detection, and extension filtering. Provides the core logic for web-safe filename
    generation with comprehensive character handling and validation capabilities.
.AUTHOR
    Andres Yuhnke, Claude (Anthropic)
.VERSION
    1.6.0
.DATE
    2025-08-24
.COPYRIGHT
    (c) 2025 Andres Yuhnke. MIT License.
.NOTES
    Private functions included:
    - Get-SanitizedName: Core filename sanitization with character replacement
    - Test-FilenameConflicts: Conflict detection and resolution strategies
    - Get-ExtensionFilters: Include/exclude extension filtering logic
    
    Features comprehensive handling of:
    - Problematic web server characters
    - Metadata removal patterns
    - Space replacement strategies
    - Ampersand expansion options
    - Case conversion preferences
#>


# [FNAME-001] Core filename sanitization function with comprehensive character handling
function Get-SanitizedName {
    param(
        [string]$Name,
        [string]$Extension = "",
        [hashtable]$Settings
    )
    
    $newName = $Name
    
    # [FNAME-001.1] Remove metadata patterns if requested
    if ($Settings.RemoveMetadata) {
        $newName = $newName -replace '\([^)]*\)', ''      # Remove (content)
        $newName = $newName -replace '\[[^\]]*\]', ''     # Remove [content]
        $newName = $newName -replace '_\d+x\d+', ''       # Remove _1920x1080 patterns
        $newName = $newName -replace '-\d+x\d+', ''       # Remove -1920x1080 patterns
        $newName = $newName -replace '__+', '_'           # Collapse multiple underscores
        $newName = $newName -replace '--+', '-'           # Collapse multiple dashes
        $newName = $newName -replace '[_-]+$', ''         # Remove trailing separators
        $newName = $newName -replace '^[_-]+', ''         # Remove leading separators
    }
    
    # [FNAME-001.2] Handle space replacement according to user preference
    switch ($Settings.SpaceReplacement) {
        'Remove'     { $newName = $newName -replace '\s+', '' }
        'Dash'       { $newName = $newName -replace '\s+', '-' }
        'Underscore' { $newName = $newName -replace '\s+', '_' }
    }
    
    # [FNAME-001.3] Handle ampersand replacement with smart expansion
    if ($Settings.ExpandAmpersand) {
        $newName = $newName -replace '&', '_and_'
        $newName = $newName -replace '_and_+', '_and_'    # Prevent multiple 'and' sequences
    } else {
        $newName = $newName -replace '&', '_'
    }
    
    # [FNAME-001.4] Remove problematic characters for web server compatibility
    $problematicChars = @(
        '*', '"', ':', ';', '|', ',', '=', '+', '$', '?', '%', '#',
        '(', ')', '{', '}', '<', '>', '!', '@', '^', '~', '`', "'"
    )
    
    # [FNAME-001.5] Process each problematic character with appropriate replacement
    foreach ($char in $problematicChars) {
        $escaped = [Regex]::Escape($char)
        if ($char -in @('(', ')')) {
            $newName = $newName -replace $escaped, '-'  # Convert parentheses to dashes
        } else {
            $newName = $newName -replace $escaped, ''   # Remove other problematic chars
        }
    }
    
    # [FNAME-001.6] Handle square brackets separately (they need special escaping)
    $newName = $newName -replace '\[', '-'              # Convert [ to -
    $newName = $newName -replace '\]', '-'              # Convert ] to -
    
    # [FNAME-001.7] Clean up any duplicate separators and edge cases
    $newName = $newName -replace '_{2,}', '_'           # Collapse multiple underscores
    $newName = $newName -replace '-{2,}', '-'           # Collapse multiple dashes
    $newName = $newName -replace '\.{2,}', '.'          # Collapse multiple dots
    $newName = $newName -replace '^[_.-]+', ''          # Remove leading separators
    $newName = $newName -replace '[_.-]+$', ''          # Remove trailing separators
    
    # [FNAME-001.8] Apply case conversion based on user preference
    if (-not $Settings.PreserveCase) {
        $newName = $newName.ToLower()
    }
    
    # [FNAME-001.9] Handle extension case conversion
    $newExt = $Extension
    if ($Settings.LowercaseExtensions -and $Extension) {
        $newExt = $Extension.ToLower()
    }
    
    # [FNAME-001.10] Construct final filename and handle empty results
    if ($Extension) {
        $finalName = "${newName}${newExt}"
    } else {
        $finalName = $newName
    }
    
    # [FNAME-001.11] Provide fallback for completely sanitized names
    if ([string]::IsNullOrWhiteSpace($finalName)) {
        $finalName = "unnamed_$(Get-Random -Maximum 9999)"
        if ($Extension) { $finalName += $newExt }
    }
    
    return $finalName
}

# [FNAME-002] Conflict detection and validation for rename operations
function Test-FilenameConflicts {
    param(
        [array]$ProposedItems,
        [hashtable]$Settings
    )
    
    $conflicts = @()
    $proposedNames = @{}
    
    # [FNAME-002.1] Build conflict detection map using case-insensitive comparison
    foreach ($item in $ProposedItems) {
        if ($item.NewName -and $item.NewName -ne $item.OriginalName) {
            $targetPath = Join-Path $item.Directory $item.NewName
            $keyPath = $targetPath.ToLower()
            
            # [FNAME-002.2] Check for duplicate target names
            if ($proposedNames.ContainsKey($keyPath)) {
                $conflicts += @{
                    Type = "Duplicate"
                    Path = $targetPath
                    Items = @($proposedNames[$keyPath], $item)
                    Message = "Multiple items would be renamed to: $($item.NewName)"
                }
            } else {
                $proposedNames[$keyPath] = $item
            }
            
            # [FNAME-002.3] Check if target already exists on disk
            if ((Test-Path -LiteralPath $targetPath) -and -not $Settings.Force) {
                $conflicts += @{
                    Type = "Existing"
                    Path = $targetPath
                    Items = @($item)
                    Message = "Target already exists: $($item.NewName)"
                }
            }
        }
    }
    
    return $conflicts
}

# [FNAME-003] Extension filtering logic for include/exclude patterns
function Get-ExtensionFilters {
    param(
        [string[]]$IncludeExt,
        [string[]]$ExcludeExt
    )
    
    # [FNAME-003.1] Build include filter set if specified
    $includeSet = if ($IncludeExt) { 
        [System.Collections.Generic.HashSet[string]]::new([string[]]($IncludeExt | ForEach-Object { $_.ToLower() }))
    } else { 
        $null 
    }
    
    # [FNAME-003.2] Build exclude filter set if specified
    $excludeSet = if ($ExcludeExt) { 
        [System.Collections.Generic.HashSet[string]]::new([string[]]($ExcludeExt | ForEach-Object { $_.ToLower() }))
    } else { 
        $null 
    }
    
    # [FNAME-003.3] Return filter function for easy application
    return {
        param($FileItem)
        $ext = [System.IO.Path]::GetExtension($FileItem.Name).ToLower()
        $include = if ($includeSet) { $includeSet.Contains($ext) } else { $true }
        $exclude = if ($excludeSet) { $excludeSet.Contains($ext) } else { $false }
        return $include -and -not $exclude
    }
}

# [FNAME-004] Validation helper for problematic filename patterns
function Test-ProblematicPatterns {
    param(
        [string]$Filename,
        [hashtable]$Settings
    )
    
    $issues = @()
    
    # [FNAME-004.1] Check for Windows reserved names
    $reservedNames = @('CON', 'PRN', 'AUX', 'NUL', 'COM1', 'COM2', 'COM3', 'COM4', 'COM5', 'COM6', 'COM7', 'COM8', 'COM9', 'LPT1', 'LPT2', 'LPT3', 'LPT4', 'LPT5', 'LPT6', 'LPT7', 'LPT8', 'LPT9')
    $nameWithoutExt = [System.IO.Path]::GetFileNameWithoutExtension($Filename)
    
    if ($reservedNames -contains $nameWithoutExt.ToUpper()) {
        $issues += "Reserved Windows filename: $nameWithoutExt"
    }
    
    # [FNAME-004.2] Check for extremely long filenames (Windows 260 char limit)
    if ($Filename.Length -gt 240) {  # Leave some buffer for path length
        $issues += "Filename too long: $($Filename.Length) characters (limit ~240)"
    }
    
    # [FNAME-004.3] Check for problematic ending characters
    if ($Filename -match '\.$') {
        $issues += "Filename ends with period"
    }
    
    if ($Filename -match '\s$') {
        $issues += "Filename ends with space"
    }
    
    return $issues
}

# [FNAME-005] Generate filename statistics for reporting
function Get-FilenameStatistics {
    param(
        [array]$Items,
        [hashtable]$Settings
    )
    
    $stats = @{
        TotalItems = $Items.Count
        ItemsNeedingChanges = 0
        ItemsAlreadyOptimized = 0
        DirectoryCount = 0
        FileCount = 0
        ProblematicPatterns = 0
        ConflictCount = 0
    }
    
    # [FNAME-005.1] Analyze each item for statistics
    foreach ($item in $Items) {
        if ($item.Type -eq "Directory") {
            $stats.DirectoryCount++
        } else {
            $stats.FileCount++
        }
        
        if ($item.NeedsChange) {
            $stats.ItemsNeedingChanges++
        } else {
            $stats.ItemsAlreadyOptimized++
        }
        
        # [FNAME-005.2] Check for problematic patterns
        $issues = Test-ProblematicPatterns -Filename $item.OriginalName -Settings $Settings
        if ($issues.Count -gt 0) {
            $stats.ProblematicPatterns++
        }
    }
    
    # [FNAME-005.3] Check for conflicts
    $conflicts = Test-FilenameConflicts -ProposedItems $Items -Settings $Settings
    $stats.ConflictCount = $conflicts.Count
    
    return $stats
}