lib/Remediation.ps1

#############################################################################
# Remediation.ps1 - Auto-fix and Remediation Functions
#############################################################################
# This module provides functions for auto-fixing validation issues and
# generating manual remediation commands.
#############################################################################

# NOTE: Get-ImmutableReleaseRemediationCommands was removed as it was unused.
# The functionality is now handled by RemediationAction classes in RemediationActions.ps1

function Get-ManualInstruction {
    <#
    .SYNOPSIS
    Prints manual remediation instructions for all issues that need manual intervention
    
    .DESCRIPTION
    Extracts and displays manual fix commands from RemediationAction objects for issues
    that are unfixable, failed, require manual fixes, or are still pending (not auto-fixed).
    Groups commands by action type for better readability.
    
    .PARAMETER State
    The RepositoryState object containing all validation issues
    
    .PARAMETER GroupByType
    If true, groups commands by remediation action type. Default is false.
    #>

    param(
        [Parameter(Mandatory)]
        [RepositoryState]$State,
        [bool]$GroupByType = $false
    )
    
    # Include pending issues (not yet fixed) as well as failed/unfixable ones
    # Sort by priority (lower number = higher priority), then by version for consistent ordering
    # Priority is taken from: 1) Issue.Priority if set, 2) RemediationAction.Priority, 3) default 100
    $issuesNeedingManualFix = $State.Issues | Where-Object { 
        $_.Status -eq "pending" -or $_.Status -eq "unfixable" -or $_.Status -eq "failed" -or $_.Status -eq "manual_fix_required"
    } | Sort-Object @(
        @{
            Expression = {
                # Use Issue.Priority if explicitly set (non-default)
                if ($_.Priority -ne 100) {
                    $_.Priority
                }
                # Otherwise use RemediationAction.Priority if available
                elseif ($_.RemediationAction -and ($_.RemediationAction -is [RemediationAction])) {
                    $_.RemediationAction.Priority
                } else {
                    100  # Default priority for issues without RemediationAction
                }
            }
            Ascending = $true
        },
        @{
            Expression = {
                # Parse version for proper sorting using .NET Version object
                $version = $_.Version
                if ($version -eq 'latest') {
                    # Sort 'latest' after all versioned items
                    return [Version]::new([int]::MaxValue, 0, 0)
                }
                if ($version -match '^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?') {
                    $major = [int]($Matches[1] ?? 0)
                    $minor = [int]($Matches[2] ?? 0)
                    $patch = [int]($Matches[3] ?? 0)
                    return [Version]::new($major, $minor, $patch)
                }
                # Non-parseable versions sort last
                return [Version]::new([int]::MaxValue, 0, 0)
            }
            Ascending = $true
        }
    )
    
    if ($issuesNeedingManualFix.Count -eq 0) {
        return
    }
    
    Write-Host "##[group]Manual Remediation Instructions"
    
    # Collect all manual commands first to determine if we need clone instructions
    $allCommands = @()
    foreach ($issue in $issuesNeedingManualFix) {
        if ($issue.RemediationAction -and ($issue.RemediationAction -is [RemediationAction])) {
            $commands = $issue.RemediationAction.GetManualCommands($State)
            if ($commands) {
                $allCommands += $commands
            }
        } elseif ($issue.ManualFixCommand) {
            $allCommands += $issue.ManualFixCommand
        }
    }
    
    # Stop workflow commands to prevent command injection in output
    $stopToken = [guid]::NewGuid().ToString()
    Write-Output "::stop-commands::$stopToken"
    
    # If we have manual commands and repo info is available, add clone instructions at the top
    if ($allCommands.Count -gt 0 -and $State.ServerUrl -and $State.RepoOwner -and $State.RepoName) {
        Write-Output "# Setup - Clone the repository and fetch all tags and branches:"
        Write-Output "git clone $($State.ServerUrl)/$($State.RepoOwner)/$($State.RepoName).git"
        Write-Output "cd $($State.RepoName)"
        Write-Output "git fetch --all --tags"
        Write-Output ""
        Write-Output "# Remediation Steps:"
    }
    
    if ($GroupByType) {
        # Group by action type
        $grouped = $issuesNeedingManualFix | Group-Object { 
            if ($_.RemediationAction) {
                $_.RemediationAction.GetType().Name
            } else {
                "Other"
            }
        }
        
        foreach ($group in $grouped) {
            Write-Output "#### $($group.Name) ($($group.Count) issue(s))"
            Write-Output ""
            
            foreach ($issue in $group.Group) {
                Write-Output "**$($issue.Version):** $($issue.Message)"
                
                if ($issue.RemediationAction -and ($issue.RemediationAction -is [RemediationAction])) {
                    $commands = $issue.RemediationAction.GetManualCommands($State)
                    foreach ($cmd in $commands) {
                        Write-Output " ``````"
                        Write-Output " $cmd"
                        Write-Output " ``````"
                    }
                } elseif ($issue.ManualFixCommand) {
                    Write-Output " ``````"
                    Write-Output " $($issue.ManualFixCommand)"
                    Write-Output " ``````"
                }
                Write-Output ""
            }
        }
    }
    else {
        # List all issues with their commands - clean format without emojis
        foreach ($issue in $issuesNeedingManualFix) {
            if ($issue.RemediationAction -and ($issue.RemediationAction -is [RemediationAction])) {
                $commands = $issue.RemediationAction.GetManualCommands($State)
                if ($commands) {
                    foreach ($cmd in $commands) {
                        Write-Output "$cmd"
                    }
                }
            } elseif ($issue.ManualFixCommand) {
                Write-Output "$($issue.ManualFixCommand)"
            }
        }
    }
    
    Write-Output "::$stopToken::"
    Write-Host "##[endgroup]"
}

function Write-ManualInstructionsToStepSummary
{
    <#
    .SYNOPSIS
    Writes manual remediation instructions to GitHub Actions step summary
    
    .DESCRIPTION
    Formats and writes manual fix commands to the GITHUB_STEP_SUMMARY file
    for easy viewing in the GitHub Actions UI.
    
    .PARAMETER State
    The RepositoryState object containing all validation issues
    #>

    param(
        [Parameter(Mandatory)]
        [RepositoryState]$State
    )
    
    if (-not $env:GITHUB_STEP_SUMMARY) {
        return
    }
    
    # Include pending issues (not yet fixed) as well as failed/unfixable ones
    # Sort by priority (lower number = higher priority), then by version for consistent ordering
    $issuesNeedingManualFix = $State.Issues | Where-Object { 
        $_.Status -eq "pending" -or $_.Status -eq "unfixable" -or $_.Status -eq "failed" -or $_.Status -eq "manual_fix_required"
    } | Sort-Object @(
        @{
            Expression = {
                # Use Issue.Priority if explicitly set (non-default)
                if ($_.Priority -ne 100) {
                    $_.Priority
                }
                # Otherwise use RemediationAction.Priority if available
                elseif ($_.RemediationAction -and ($_.RemediationAction -is [RemediationAction])) {
                    $_.RemediationAction.Priority
                } else {
                    100  # Default priority for issues without RemediationAction
                }
            }
            Ascending = $true
        },
        @{
            Expression = {
                # Parse version for proper sorting using .NET Version object
                $version = $_.Version
                if ($version -eq 'latest') {
                    # Sort 'latest' after all versioned items
                    return [Version]::new([int]::MaxValue, 0, 0)
                }
                if ($version -match '^v?(\d+)(?:\.(\d+))?(?:\.(\d+))?') {
                    $major = [int]($Matches[1] ?? 0)
                    $minor = [int]($Matches[2] ?? 0)
                    $patch = [int]($Matches[3] ?? 0)
                    return [Version]::new($major, $minor, $patch)
                }
                # Non-parseable versions sort last
                return [Version]::new([int]::MaxValue, 0, 0)
            }
            Ascending = $true
        }
    )
    
    if ($issuesNeedingManualFix.Count -eq 0) {
        return
    }
    
    # Write to step summary
    "## Manual Remediation Required" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
    "" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
    
    # Collect all manual commands first to determine if we need clone instructions
    $allCommands = @()
    foreach ($issue in $issuesNeedingManualFix) {
        if ($issue.RemediationAction -and ($issue.RemediationAction -is [RemediationAction])) {
            $commands = $issue.RemediationAction.GetManualCommands($State)
            if ($commands) {
                $allCommands += $commands
            }
        } elseif ($issue.ManualFixCommand) {
            $allCommands += $issue.ManualFixCommand
        }
    }
    
    # If we have manual commands and repo info is available, add clone instructions at the top
    if ($allCommands.Count -gt 0 -and $State.ServerUrl -and $State.RepoOwner -and $State.RepoName) {
        "### Setup" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "Clone the repository and fetch all tags and branches:" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "``````bash" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "git clone $($State.ServerUrl)/$($State.RepoOwner)/$($State.RepoName).git" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "cd $($State.RepoName)" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "git fetch --all --tags" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "``````" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "### Remediation Steps" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        "" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
    }
    
    foreach ($issue in $issuesNeedingManualFix) {
        $statusEmoji = if ($issue.Status -eq "failed") { "❌" } else { "⚠️" }
        "$statusEmoji **$($issue.Version):** $($issue.Message)" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        
        if ($issue.RemediationAction -and ($issue.RemediationAction -is [RemediationAction])) {
            $commands = $issue.RemediationAction.GetManualCommands($State)
            if ($commands) {
                "``````bash" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
                foreach ($cmd in $commands) {
                    $cmd | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
                }
                "``````" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
            }
        } elseif ($issue.ManualFixCommand) {
            "``````bash" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
            $issue.ManualFixCommand | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
            "``````" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
        }
        "" | Out-File -Append -FilePath $env:GITHUB_STEP_SUMMARY
    }
}

function Invoke-AutoFix {
    <#
    .SYNOPSIS
    Executes all auto-fix actions for pending issues in the State
    
    .DESCRIPTION
    This function processes all ValidationIssues in the State that have RemediationAction objects
    defined. It executes the actions in priority order and updates the issue statuses accordingly.
    This should be called AFTER displaying the planned changes to the user.
    
    .PARAMETER State
    The RepositoryState object containing all validation issues
    
    .PARAMETER AutoFix
    Boolean indicating if auto-fix mode is enabled
    #>

    param(
        [Parameter(Mandatory)]
        [RepositoryState]$State,
        [bool]$AutoFix = $false
    )
    
    if (-not $AutoFix) {
        # Not in auto-fix mode, mark all issues as manual_fix_required so manual instructions show
        foreach ($issue in $State.Issues) {
            if ($issue.Status -eq "pending") {
                $issue.Status = "manual_fix_required"
            }
        }
        return
    }
    
    # Separate issues by whether they have RemediationAction objects
    $issuesWithActions = $State.Issues | Where-Object { 
        $_.Status -eq "pending" -and $_.RemediationAction
    }
    
    # Sort issues by priority (RemediationAction.Priority, lower = higher priority)
    # This ensures: Delete (10) → Create/Update (20) → Release operations (30-40)
    $sortedIssues = $issuesWithActions | Sort-Object { 
        if ($_.RemediationAction -and ($_.RemediationAction -is [RemediationAction])) {
            $_.RemediationAction.Priority
        } else {
            50  # Default priority for scriptblock-based actions
        }
    }
    
    # Process all issues in priority order
    foreach ($issue in $sortedIssues) {
        # RemediationAction object handling
        if ($issue.RemediationAction -and ($issue.RemediationAction -is [RemediationAction])) {
            $action = $issue.RemediationAction
            
            try {
                $result = $action.Execute($State)
                
                if ($result) {
                    $issue.Status = "fixed"
                } else {
                    # Only set status to "failed" if the action didn't already mark it as something else
                    # (e.g., "unfixable" or "manual_fix_required")
                    if ($issue.Status -eq "pending") {
                        $issue.Status = "failed"
                    }
                }
            }
            catch {
                Write-Host "✗ Failed: $($action.Description)"
                Write-SafeOutput -Message ([string]$_) -Prefix "::debug::Exception during auto-fix: "
                # Only set status to "failed" if the action didn't already mark it as something else
                if ($issue.Status -eq "pending") {
                    $issue.Status = "failed"
                }
            }
        }
        else {
            # No auto-fix action available, mark as unfixable
            $issue.Status = "unfixable"
        }
    }
    
    # Mark any remaining pending issues as unfixable
    foreach ($issue in $State.Issues) {
        if ($issue.Status -eq "pending") {
            $issue.Status = "unfixable"
        }
    }
}

function Write-UnresolvedIssue {
    <#
    .SYNOPSIS
    Logs all unresolved issues (failed or unfixable) as errors or warnings
    
    .DESCRIPTION
    This function should be called at the end of validation, after all auto-fixes
    have been attempted. It logs all issues that remain unresolved based on their
    severity level.
    
    .PARAMETER State
    The RepositoryState object containing all validation issues
    #>

    param(
        [Parameter(Mandatory)]
        [RepositoryState]$State
    )
    
    # Get all unresolved issues (failed, manual_fix_required, unfixable, or pending)
    $unresolvedIssues = $State.Issues | Where-Object { 
        $_.Status -in @("failed", "manual_fix_required", "unfixable", "pending")
    }
    
    if ($unresolvedIssues.Count -eq 0) {
        return
    }
    
    # Log each unresolved issue based on its severity
    foreach ($issue in $unresolvedIssues) {
        $messageType = $issue.Severity
        $titlePrefix = if ($issue.Status -eq "unfixable") { "Unfixable" } elseif ($issue.Status -eq "manual_fix_required") { "Manual fix required" } elseif ($issue.Status -eq "failed") { "Failed to fix" } else { "Unresolved" }
        
        if ($messageType -eq "error") {
            Write-Output "::error title=$titlePrefix issue::$($issue.Message)"
        } elseif ($messageType -eq "warning") {
            Write-Output "::warning title=$titlePrefix issue::$($issue.Message)"
        }
    }
}