Public/Undo-FileNameOptimization.ps1

<#
.SYNOPSIS
    Undo-FileNameOptimization - Complete rollback of filename optimization operations
.DESCRIPTION
    Provides comprehensive undo capability for filename optimization operations with validation,
    dependency tracking, and conflict resolution. Supports complete restoration of original
    filenames and directory structures with integrity checking and error handling.
.PARAMETER UndoLogPath
    Path to the undo log JSON file created during filename optimization (Required)
.PARAMETER BackupUndoLogPath
    Create backup copy of undo log before processing (optional safety measure)
.PARAMETER Force
    Overwrite existing files when conflicts occur during undo operations
.PARAMETER Silent
    Suppress progress output and display minimal information
.PARAMETER WhatIf
    Preview undo operations without making any changes
.EXAMPLE
    Undo-FileNameOptimization -UndoLogPath "D:\Logs\undo.json"
    Restore all files and directories to original names using undo log
.EXAMPLE
    Undo-FileNameOptimization -UndoLogPath "D:\Logs\undo.json" -Force -BackupUndoLogPath "D:\Backups\undo_backup.json"
    Force restoration with conflict resolution and create backup of undo log
.EXAMPLE
    Undo-FileNameOptimization -UndoLogPath "D:\Logs\undo.json" -WhatIf
    Preview undo operations without making any changes
.NOTES
    Author: Andres Yuhnke, Claude (Anthropic)
    Version: 1.6.0
    
    Requirements:
    - Valid undo log JSON file created by Optimize-FileNames function
    - Files and directories must still exist at their optimized locations
    - Sufficient permissions to rename/move files and directories
    
    Safety Features:
    - Two-phase validation system (integrity + conflict detection)
    - Dependency tracking for proper restoration order
    - Complete backup and rollback capabilities
    - Comprehensive error handling and reporting
    
    Limitations:
    - Only works with undo logs from Optimize-FileNames operations
    - Cannot undo operations where OutputRoot was used (copy operations)
    - Files must not have been moved/deleted since optimization
.LINK
    https://github.com/andresyuhnke/ConvertVTTAssets
.LINK
    https://www.powershellgallery.com/packages/ConvertVTTAssets
#>


function Undo-FileNameOptimization {
[CmdletBinding(SupportsShouldProcess = $true)]
param(
    [Parameter(Mandatory=$true)]
    [string]$UndoLogPath,
    
    [switch]$Force,
    
    [switch]$Silent,
    
    [string]$BackupUndoLogPath  # Create backup of undo log before processing
)

# [UNDO-001] Validate undo log file exists and is accessible
if (-not (Test-Path -LiteralPath $UndoLogPath)) {
    throw "Undo log not found: $UndoLogPath"
}

# [UNDO-001.1] Read and parse undo log with comprehensive error handling
try {
    $undoLogContent = Get-Content -LiteralPath $UndoLogPath -Raw -Encoding UTF8
    $undoLog = $undoLogContent | ConvertFrom-Json
} catch {
    throw "Failed to read or parse undo log: $($_.Exception.Message)"
}

# [UNDO-001.2] Validate undo log structure and content
if (-not $undoLog.metadata -or -not $undoLog.operations) {
    throw "Invalid undo log format. Missing required 'metadata' or 'operations' sections."
}

if (-not $undoLog.operations -or $undoLog.operations.Count -eq 0) {
    Write-Warning "Undo log contains no operations to reverse."
    return
}

# [UNDO-002] Create backup of undo log if requested
if ($BackupUndoLogPath) {
    $backupDir = [System.IO.Path]::GetDirectoryName($BackupUndoLogPath)
    if ($backupDir -and -not (Test-Path $backupDir)) {
        New-Item -ItemType Directory -Force -Path $backupDir | Out-Null
    }
    
    Copy-Item -LiteralPath $UndoLogPath -Destination $BackupUndoLogPath -Force
    if (-not $Silent) {
        Write-Host "Undo log backed up to: $BackupUndoLogPath" -ForegroundColor DarkGray
    }
}

# [UNDO-003] Initialize processing collections and statistics
$undoOperations = @()
$errors = @()
$warnings = @()
$skipped = @()

# [UNDO-003.1] Get operations and sort for proper undo order
$operations = $undoLog.operations

# [UNDO-003.2] Sort operations for safe restoration order
# Files first, then directories by dependency depth (deepest first)
$fileOps = $operations | Where-Object { $_.type -eq "File" }
$dirOps = $operations | Where-Object { $_.type -eq "Directory" }

# Sort directories by dependency count (most dependencies = deepest = undo last)
$sortedDirOps = $dirOps | Sort-Object { $_.dependencies.Count } -Descending

# [UNDO-003.3] Combine operations in proper order: files first, then directories
$sortedOperations = @($fileOps) + @($sortedDirOps)

# [UNDO-004] Initialize progress tracking and user feedback
$totalOps = $sortedOperations.Count
$opNum = 0

if (-not $Silent) {
    Write-Host ""
    Write-Host "=== Undo-FileNameOptimization Starting ===" -ForegroundColor Cyan
    Write-Host "Original optimization: $($undoLog.metadata.timestamp)" -ForegroundColor DarkGray
    Write-Host "Root path: $($undoLog.metadata.root_path)" -ForegroundColor DarkGray
    Write-Host "Operations to undo: $totalOps" -ForegroundColor Yellow
    Write-Host ""
}

# [UNDO-005] Validation pass - check current state before making changes
if (-not $Silent) {
    Write-Host "Validating current state..." -ForegroundColor Yellow
}

$validationErrors = @()
$currentPathMap = @{}  # Track current state

foreach ($op in $sortedOperations) {
    $opNum++
    
    # [UNDO-005.1] Display validation progress
    if (-not $Silent) {
        $percentComplete = [math]::Round(($opNum / $totalOps) * 50, 0)  # Validation is first 50%
        Write-Host ("[{0,3}%] Validating {1}/{2}: {3}" -f $percentComplete, $opNum, $totalOps, $op.new_name) -ForegroundColor Cyan
    }
    
    # [UNDO-005.2] Check if the "new" file/directory still exists at expected location
    if (-not (Test-Path -LiteralPath $op.new_path)) {
        $validationErrors += "Missing renamed item: $($op.new_path)"
        continue
    }
    
    # [UNDO-005.3] Get current item information for validation
    $currentItem = Get-Item -LiteralPath $op.new_path
    
    # [UNDO-005.4] For files, validate they haven't been significantly modified
    if ($op.type -eq "File" -and $op.file_size -ne $null) {
        if ($currentItem.Length -ne $op.file_size) {
            $validationErrors += "File size changed: $($op.new_path) (was $($op.file_size), now $($currentItem.Length))"
        }
        
        # [UNDO-005.5] Check if last write time is significantly different
        # Allow for small clock differences (file system precision)
        $originalTime = [DateTime]::Parse($op.last_write_time)
        $timeDiff = [Math]::Abs(($currentItem.LastWriteTimeUtc - $originalTime).TotalSeconds)
        if ($timeDiff -gt 2) {  # Allow 2 second difference for file system precision
            $warnings += "File modified since optimization: $($op.new_path)"
        }
    }
    
    # [UNDO-005.6] Check if original name would cause conflicts
    if ((Test-Path -LiteralPath $op.original_path) -and -not $Force) {
        $validationErrors += "Original name already exists: $($op.original_path) (use -Force to overwrite)"
    }
    
    # [UNDO-005.7] Track current state for dependency validation
    $currentPathMap[$op.new_path] = $op
}

# [UNDO-006] Report validation results and handle errors
if ($validationErrors.Count -gt 0) {
    Write-Host ""
    Write-Host "Validation failed with $($validationErrors.Count) error(s):" -ForegroundColor Red
    foreach ($err in $validationErrors) {
        Write-Host " ✗ $err" -ForegroundColor Red
    }
    
    if (-not $Force) {
        Write-Host ""
        Write-Host "Use -Force to attempt undo despite validation errors, or fix the issues above." -ForegroundColor Yellow
        return
    } else {
        Write-Host ""
        Write-Host "-Force specified, continuing despite validation errors..." -ForegroundColor Yellow
    }
}

# [UNDO-006.1] Display validation warnings if present
if ($warnings.Count -gt 0 -and -not $Silent) {
    Write-Host ""
    Write-Host "Validation warnings:" -ForegroundColor Yellow
    foreach ($warn in $warnings) {
        Write-Host " ⚠ $warn" -ForegroundColor Yellow
    }
}

if (-not $Silent) {
    Write-Host ""
    Write-Host "Validation complete. Beginning undo operations..." -ForegroundColor Green
    Write-Host ""
}

# [UNDO-007] Reset progress counter for undo operations phase
$opNum = 0

# [UNDO-007.1] Perform actual undo operations with comprehensive error handling
foreach ($op in $sortedOperations) {
    $opNum++
    
    # [UNDO-007.2] Display undo operation progress
    if (-not $Silent) {
        $percentComplete = [math]::Round(50 + ($opNum / $totalOps) * 50, 0)  # Undo is second 50%
        Write-Host ("[{0,3}%] Undoing {1}/{2}: {3} → {4}" -f $percentComplete, $opNum, $totalOps, $op.new_name, $op.original_name) -ForegroundColor Cyan
    }
    
    # [UNDO-007.3] Create undo operation record for tracking
    $undoResult = [PSCustomObject]@{
        OperationId = $op.operation_id
        Type = $op.type
        CurrentPath = $op.new_path
        TargetPath = $op.original_path
        CurrentName = $op.new_name
        TargetName = $op.original_name
        Status = "Pending"
        Error = ""
        Timestamp = (Get-Date).ToString('s')
    }
    
    try {
        # [UNDO-007.4] Check if current path still exists
        if (-not (Test-Path -LiteralPath $op.new_path)) {
            $undoResult.Status = "Skipped"
            $undoResult.Error = "Source no longer exists"
            $skipped += $undoResult
            
            if (-not $Silent) {
                Write-Host " ⚠ Skipped: Source no longer exists" -ForegroundColor Yellow
            }
            continue
        }
        
        # [UNDO-007.5] Handle target path conflicts with force resolution
        if ((Test-Path -LiteralPath $op.original_path)) {
            if ($Force) {
                if (-not $Silent) {
                    Write-Host " ⚠ Overwriting existing file: $($op.original_name)" -ForegroundColor Yellow
                }
                Remove-Item -LiteralPath $op.original_path -Force -Recurse:($op.type -eq "Directory")
            } else {
                $undoResult.Status = "Skipped"
                $undoResult.Error = "Target already exists (use -Force to overwrite)"
                $skipped += $undoResult
                
                if (-not $Silent) {
                    Write-Host " ⚠ Skipped: Target already exists" -ForegroundColor Yellow
                }
                continue
            }
        }
        
        # [UNDO-007.6] Perform the actual undo rename operation
        if ($PSCmdlet.ShouldProcess($op.new_path, "Undo rename to $($op.original_name)")) {
            # [UNDO-007.7] Ensure target directory exists
            $targetDir = [System.IO.Path]::GetDirectoryName($op.original_path)
            
            if (-not (Test-Path $targetDir)) {
                New-Item -ItemType Directory -Force -Path $targetDir | Out-Null
            }
            
            # [UNDO-007.8] Execute the rename operation
            Rename-Item -LiteralPath $op.new_path -NewName $op.original_name -Force:$Force -ErrorAction Stop
            
            $undoResult.Status = "Success"
            
            if (-not $Silent) {
                Write-Host " ✓ Restored: $($op.new_name) → $($op.original_name)" -ForegroundColor Green
            }
        } else {
            # [UNDO-007.9] Handle WhatIf preview mode
            $undoResult.Status = "WhatIf"
            if (-not $Silent) {
                Write-Host " → Would restore: $($op.new_name) → $($op.original_name)" -ForegroundColor Cyan
            }
        }
        
    } catch {
        # [UNDO-007.10] Handle operation failures with detailed error tracking
        $undoResult.Status = "Failed"
        $undoResult.Error = $_.Exception.Message
        $errors += $undoResult
        
        if (-not $Silent) {
            Write-Host " ✗ Failed: $($_.Exception.Message)" -ForegroundColor Red
        }
    }
    
    $undoOperations += $undoResult
}

# [UNDO-008] Generate comprehensive operation summary and statistics
$successful = ($undoOperations | Where-Object { $_.Status -eq "Success" }).Count
$failed = ($undoOperations | Where-Object { $_.Status -eq "Failed" }).Count
$whatif = ($undoOperations | Where-Object { $_.Status -eq "WhatIf" }).Count
$skippedCount = ($undoOperations | Where-Object { $_.Status -eq "Skipped" }).Count

# [UNDO-008.1] Display comprehensive summary with statistics
if (-not $Silent) { Write-Host "" }
Write-Host "=== Undo-FileNameOptimization Summary ===" -ForegroundColor Cyan
Write-Host "Restored: $successful" -ForegroundColor Green
Write-Host "Skipped: $skippedCount" -ForegroundColor Yellow
if ($whatif -gt 0) { Write-Host "WhatIf: $whatif" -ForegroundColor Cyan }
if ($failed -gt 0) { Write-Host "Failed: $failed" -ForegroundColor Red }

# [UNDO-008.2] Display detailed error information if failures occurred
if ($errors.Count -gt 0) {
    Write-Host ""
    Write-Host "Errors encountered:" -ForegroundColor Red
    foreach ($err in $errors) {
        Write-Host " - Operation $($err.OperationId): $($err.Error)" -ForegroundColor Red
    }
}

# [UNDO-009] Create comprehensive undo operation log for audit trails
$undoLogDir = [System.IO.Path]::GetDirectoryName($UndoLogPath)
$undoLogName = [System.IO.Path]::GetFileNameWithoutExtension($UndoLogPath)
$undoOpLogPath = Join-Path $undoLogDir "${undoLogName}_undo_operations.json"

# [UNDO-009.1] Assemble complete undo operation log with metadata
$undoOpLog = @{
    metadata = @{
        timestamp = (Get-Date).ToString('o')
        original_undo_log = $UndoLogPath
        original_optimization_timestamp = $undoLog.metadata.timestamp
        total_undo_operations = $undoOperations.Count
        successful_undos = $successful
        powershell_version = $PSVersionTable.PSVersion.ToString()
        module_version = (Get-Module ConvertVTTAssets).Version.ToString()
    }
    operations = $undoOperations
    validation_errors = $validationErrors
    warnings = $warnings
}

# [UNDO-009.2] Write comprehensive undo operation log to file
$undoOpLog | ConvertTo-Json -Depth 10 | Set-Content -Path $undoOpLogPath -Encoding UTF8

if (-not $Silent) {
    Write-Host ""
    Write-Host "Undo operations logged to: $undoOpLogPath" -ForegroundColor Green
}

# [UNDO-010] Return comprehensive results object with all metrics
return [PSCustomObject]@{
    TotalOperations = $totalOps
    Restored = $successful
    Skipped = $skippedCount
    Failed = $failed
    WhatIf = $whatif
    ValidationErrors = $validationErrors.Count
    Warnings = $warnings.Count
    UndoOperations = $undoOperations
    UndoLogPath = $undoOpLogPath
}

}