WorkflowEngine.psm1

#requires -Version 5.1

<#
.SYNOPSIS
    Enhanced Workflow Engine with Runspaces - PowerShell 5.1+ Compatible
.DESCRIPTION
    Provides sequential, parallel, and conditional workflow execution using runspace pools
    for efficient parallel processing.
#>


#region Enums

enum StepType {
    Sequential
    Parallel
    Conditional
}

enum StepStatus {
    Pending
    Running
    Completed
    Failed
    Skipped
}

#endregion

#region Classes

class WorkflowContext {
    [hashtable]$Variables
    
    WorkflowContext() {
        $this.Variables = @{}
    }
    
    [void] SetValue([string]$key, [object]$value) {
        $this.Variables[$key] = $value
    }
    
    [void] Set([string]$key, [object]$value) {
        $this.Variables[$key] = $value
    }
    
    [object] GetValue([string]$key) {
        if ($this.Variables.ContainsKey($key)) {
            return $this.Variables[$key]
        }
        return $null
    }
    
    [object] Get([string]$key) {
        if ($this.Variables.ContainsKey($key)) {
            return $this.Variables[$key]
        }
        return $null
    }
    
    [hashtable] GetSnapshot() {
        return $this.Variables.Clone()
    }
    
    [void] MergeUpdates([hashtable]$updates) {
        if ($null -ne $updates) {
            foreach ($key in $updates.Keys) {
                $this.SetValue($key, $updates[$key])
            }
        }
    }
}

class WorkflowStep {
    [string]$Name
    [string]$Id
    [StepType]$Type
    [scriptblock]$Action
    [scriptblock]$Condition
    [string[]]$DependsOn
    [int]$Retries
    [int]$RetryDelay
    [int]$Timeout
    [StepStatus]$Status
    [object]$Result
    [string]$ErrorMessage
    [datetime]$StartTime
    [datetime]$EndTime
    
    WorkflowStep([string]$name, [scriptblock]$action) {
        $this.Name = $name
        $this.Id = [Guid]::NewGuid().ToString()
        $this.Type = [StepType]::Sequential
        $this.Action = $action
        $this.Condition = { $true }
        $this.DependsOn = @()
        $this.Retries = 3
        $this.RetryDelay = 30
        $this.Timeout = 0
        $this.Status = [StepStatus]::Pending
        $this.StartTime = [datetime]::MinValue
        $this.EndTime = [datetime]::MinValue
    }
    
    [bool] ShouldExecute([object]$context) {
        try {
            $contextResult = & $this.Condition $context
            return $contextResult
        } catch {
            return $false
        }
    }
    
    [bool] AreDependenciesMet([hashtable]$completedSteps) {
        if ($this.DependsOn.Count -eq 0) {
            return $true
        }
        
        foreach ($depId in $this.DependsOn) {
            if (-not $completedSteps.ContainsKey($depId)) {
                return $false
            }
            
            $depStep = $completedSteps[$depId]
            if ($depStep.Status -ne [StepStatus]::Completed -and 
                $depStep.Status -ne [StepStatus]::Skipped) {
                return $false
            }
        }
        return $true
    }
    
    [double] GetDurationSeconds() {
        if ($this.StartTime -ne [datetime]::MinValue -and 
            $this.EndTime -ne [datetime]::MinValue) {
            return ($this.EndTime - $this.StartTime).TotalSeconds
        }
        return 0
    }
}

class ParallelGroup {
    [string]$Name
    [string]$Id
    [System.Collections.ArrayList]$Steps
    [int]$MaxParallelism
    
    ParallelGroup([string]$name) {
        $this.Name = $name
        $this.Id = [Guid]::NewGuid().ToString()
        $this.Steps = [System.Collections.ArrayList]::new()
        $this.MaxParallelism = 5
    }
    
    [void] AddStep([object]$step) {
        # Only set to Parallel if not already Conditional
        if ($step.Type -ne [StepType]::Conditional) {
            $step.Type = [StepType]::Parallel
        }
        $this.Steps.Add($step) | Out-Null
    }
}

class WfeWorkflow {
    [int]$WorkflowRetries
    [int]$WorkflowDelay
    [System.Collections.ArrayList]$Steps
    [hashtable]$StepRegistry
    [WorkflowContext]$Context
    [bool]$ContinueOnError
    [datetime]$StartTime
    [datetime]$EndTime
    
    WfeWorkflow() {
        $this.WorkflowRetries = 1
        $this.WorkflowDelay = 60
        $this.Steps = [System.Collections.ArrayList]::new()
        $this.StepRegistry = @{}
        $this.Context = [WorkflowContext]::new()
        $this.ContinueOnError = $false
        $this.StartTime = [datetime]::MinValue
        $this.EndTime = [datetime]::MinValue
    }
    
    [object] AddStep([string]$name, [scriptblock]$action) {
        $step = [WorkflowStep]::new($name, $action)
        $this.Steps.Add($step) | Out-Null
        $this.StepRegistry[$step.Id] = $step
        return $step
    }
    
    [object] AddConditionalStep([string]$name, [scriptblock]$condition, [scriptblock]$action) {
        $step = [WorkflowStep]::new($name, $action)
        $step.Type = [StepType]::Conditional
        $step.Condition = $condition
        $this.Steps.Add($step) | Out-Null
        $this.StepRegistry[$step.Id] = $step
        return $step
    }
    
    [object] AddParallelGroup([string]$name) {
        $group = [ParallelGroup]::new($name)
        $this.Steps.Add($group) | Out-Null
        return $group
    }
    
    [object] AddDependentStep([string]$name, [scriptblock]$action, [string[]]$dependsOn) {
        $step = [WorkflowStep]::new($name, $action)
        $step.DependsOn = $dependsOn
        $this.Steps.Add($step) | Out-Null
        $this.StepRegistry[$step.Id] = $step
        return $step
    }
    
    [bool] Execute() {
        $this.StartTime = Get-Date
        
        for ($attempt = 1; $attempt -le $this.WorkflowRetries; $attempt++) {
            try {
                Write-Host "========================================" -ForegroundColor Cyan
                Write-Host "Workflow Attempt $attempt/$($this.WorkflowRetries)" -ForegroundColor Cyan
                Write-Host "========================================" -ForegroundColor Cyan
                Write-Host ""
                
                $this.ExecuteSteps()
                
                $this.EndTime = Get-Date
                $duration = $this.EndTime - $this.StartTime
                
                Write-Host ""
                Write-Host "========================================" -ForegroundColor Green
                Write-Host "Workflow Completed Successfully" -ForegroundColor Green
                Write-Host ("Duration: " + $duration.ToString('hh\:mm\:ss')) -ForegroundColor Green
                Write-Host "========================================" -ForegroundColor Green
                Write-Host ""
                
                return $true
                
            } catch {
                Write-Host ""
                Write-Host "Workflow attempt $attempt failed: $_" -ForegroundColor Red
                
                if ($attempt -lt $this.WorkflowRetries) {
                    Write-Host "Retrying in $($this.WorkflowDelay) seconds..." -ForegroundColor Yellow
                    Write-Host ""
                    Start-Sleep -Seconds $this.WorkflowDelay
                    $this.ResetSteps()
                } else {
                    $this.EndTime = Get-Date
                    Write-Host ""
                    Write-Host "========================================" -ForegroundColor Red
                    Write-Host "Workflow Failed" -ForegroundColor Red
                    Write-Host "========================================" -ForegroundColor Red
                    Write-Host ""
                    return $false
                }
            }
        }
        return $false
    }
    
    hidden [void] ExecuteSteps() {
        $completedSteps = @{}
        $stepNumber = 1
        
        foreach ($item in $this.Steps) {
            if ($item.GetType().Name -eq 'ParallelGroup') {
                $this.ExecuteParallelGroup($item, $completedSteps)
            }
            else {
                $this.ExecuteSequentialStep($item, $completedSteps, $stepNumber)
                $stepNumber++
            }
        }
    }
    
    hidden [void] ExecuteSequentialStep([object]$step, [hashtable]$completedSteps, [int]$stepNumber) {
        Write-Host ("-" * 50) -ForegroundColor Cyan
        Write-Host "Step ${stepNumber}: $($step.Name)" -ForegroundColor Cyan
        if ($step.Timeout -gt 0) {
            Write-Host "Timeout: $($step.Timeout) seconds" -ForegroundColor DarkGray
        }
        Write-Host ("-" * 50) -ForegroundColor Cyan
        
        if (-not $step.AreDependenciesMet($completedSteps)) {
            throw "Dependencies not met for step: $($step.Name)"
        }
        
        if ($step.Type -eq [StepType]::Conditional) {
            if (-not $step.ShouldExecute($this.Context)) {
                Write-Host "[SKIP] Skipped (condition not met)" -ForegroundColor Yellow
                $step.Status = [StepStatus]::Skipped
                $completedSteps[$step.Id] = $step
                return
            }
        }
        
        $step.Status = [StepStatus]::Running
        $step.StartTime = Get-Date
        
        for ($i = 1; $i -le $step.Retries; $i++) {
            try {
                # Check if timeout is configured
                if ($step.Timeout -gt 0) {
                    # Use a job for timeout support
                    $contextSnapshot = $this.Context.GetSnapshot()
                    $job = Start-Job -ScriptBlock {
                        param($actionString, $contextVars)
                        
                        $stepAction = [scriptblock]::Create($actionString)
                        $ctx = New-Object PSObject -Property @{ Variables = $contextVars }
                        
                        Add-Member -InputObject $ctx -MemberType ScriptMethod -Name Set -Value {
                            param($key, $value)
                            $this.Variables[$key] = $value
                        } -Force
                        
                        Add-Member -InputObject $ctx -MemberType ScriptMethod -Name Get -Value {
                            param($key)
                            if ($this.Variables.ContainsKey($key)) {
                                return $this.Variables[$key]
                            }
                            return $null
                        } -Force
                        
                        $result = & $stepAction $ctx
                        
                        return @{
                            Result = $result
                            UpdatedContext = $ctx.Variables
                        }
                    } -ArgumentList $step.Action.ToString(), $contextSnapshot
                    
                    $completed = Wait-Job -Job $job -Timeout $step.Timeout
                    
                    if ($null -eq $completed) {
                        # Timeout occurred
                        Stop-Job -Job $job
                        Remove-Job -Job $job -Force
                        throw "Step timed out after $($step.Timeout) seconds"
                    }
                    
                    # Check for job errors
                    if ($job.State -eq 'Failed') {
                        $errorMsg = $job.ChildJobs[0].JobStateInfo.Reason.Message
                        Remove-Job -Job $job -Force
                        throw $errorMsg
                    }
                    
                    $jobResult = Receive-Job -Job $job
                    Remove-Job -Job $job -Force
                    
                    # Merge context updates
                    if ($null -ne $jobResult -and $null -ne $jobResult.UpdatedContext) {
                        $this.Context.MergeUpdates($jobResult.UpdatedContext)
                    }
                    
                    $step.Result = $jobResult.Result
                    
                    if ($step.Result -eq $false) {
                        throw "Step returned false"
                    }
                } else {
                    # No timeout - run directly
                    $step.Result = & $step.Action $this.Context
                    
                    if ($step.Result -eq $false) { 
                        throw "Step returned false" 
                    }
                }
                
                $step.EndTime = Get-Date
                $duration = $step.GetDurationSeconds()
                $durationText = $duration.ToString('F2') + "s"
                
                Write-Host "[OK] Completed ($durationText)" -ForegroundColor Green
                $step.Status = [StepStatus]::Completed
                $completedSteps[$step.Id] = $step
                return
                
            } catch {
                $step.ErrorMessage = $_.ToString()
                Write-Host "[FAIL] Attempt $i/$($step.Retries) failed: $_" -ForegroundColor Yellow
                
                if ($i -lt $step.Retries) {
                    Write-Host "Retrying in $($step.RetryDelay) seconds..." -ForegroundColor Yellow
                    Start-Sleep -Seconds $step.RetryDelay
                } else {
                    $step.Status = [StepStatus]::Failed
                    $step.EndTime = Get-Date
                    
                    if (-not $this.ContinueOnError) {
                        throw "Step '$($step.Name)' failed: $_"
                    }
                }
            }
        }
    }
    
    hidden [void] ExecuteParallelGroup([object]$group, [hashtable]$completedSteps) {
        Write-Host ("=" * 50) -ForegroundColor Magenta
        Write-Host "Parallel Group: $($group.Name)" -ForegroundColor Magenta
        Write-Host ("=" * 50) -ForegroundColor Magenta

        $stepsToRun = [System.Collections.ArrayList]::new()

        foreach ($step in $group.Steps) {
            if (-not $step.AreDependenciesMet($completedSteps)) {
                Write-Host "[SKIP] '$($step.Name)' - Dependencies not met" -ForegroundColor Yellow
                $step.Status = [StepStatus]::Skipped
                continue
            }
            
            if ($step.Type -eq [StepType]::Conditional -and -not $step.ShouldExecute($this.Context)) {
                Write-Host "[SKIP] '$($step.Name)' - Condition not met" -ForegroundColor Yellow
                $step.Status = [StepStatus]::Skipped
                continue
            }

            $stepsToRun.Add($step) | Out-Null
        }

        if ($stepsToRun.Count -eq 0) {
            Write-Host "No steps to execute in this group" -ForegroundColor Yellow
            Write-Host ("=" * 50) -ForegroundColor Magenta
            Write-Host ""
            return
        }

        # Use runspace pool for true parallel execution (much faster than Start-Job)
        $runspacePool = [runspacefactory]::CreateRunspacePool(1, [Math]::Max($group.MaxParallelism, $stepsToRun.Count))
        $runspacePool.Open()
        
        $runspaces = @{}
        
        # Start all runspaces
        foreach ($step in $stepsToRun) {
            Write-Host "[START] '$($step.Name)'" -ForegroundColor Cyan
            $step.Status = [StepStatus]::Running
            $step.StartTime = Get-Date

            $contextSnapshot = $this.Context.GetSnapshot()
            $stepAction = $step.Action
            $retries = $step.Retries
            $retryDelay = $step.RetryDelay
            
            $powershell = [powershell]::Create()
            $powershell.RunspacePool = $runspacePool
            
            [void]$powershell.AddScript({
                param($actionString, $contextVars, $retries, $retryDelay)
                
                # Recreate the scriptblock from string
                $stepAction = [scriptblock]::Create($actionString)
                
                # Create simple context object
                $ctx = New-Object PSObject -Property @{ Variables = $contextVars }
                
                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name Set -Value {
                    param($key, $value)
                    $this.Variables[$key] = $value
                } -Force
                
                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name Get -Value {
                    param($key)
                    if ($this.Variables.ContainsKey($key)) {
                        return $this.Variables[$key]
                    }
                    return $null
                } -Force
                
                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name SetValue -Value {
                    param($key, $value)
                    $this.Variables[$key] = $value
                } -Force
                
                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name GetValue -Value {
                    param($key)
                    if ($this.Variables.ContainsKey($key)) {
                        return $this.Variables[$key]
                    }
                    return $null
                } -Force
                
                # Execute with retries
                for ($i = 1; $i -le $retries; $i++) {
                    try {
                        $result = & $stepAction $ctx
                        
                        return @{
                            Success = $true
                            Result = $result
                            UpdatedContext = $ctx.Variables
                            Error = $null
                        }
                        
                    } catch {
                        if ($i -lt $retries) {
                            Start-Sleep -Seconds $retryDelay
                        } else {
                            return @{
                                Success = $false
                                Result = $null
                                UpdatedContext = $ctx.Variables
                                Error = $_.Exception.Message
                            }
                        }
                    }
                }
            })
            
            [void]$powershell.AddArgument($stepAction.ToString())
            [void]$powershell.AddArgument($contextSnapshot)
            [void]$powershell.AddArgument($retries)
            [void]$powershell.AddArgument($retryDelay)
            
            $handle = $powershell.BeginInvoke()
            
            $runspaces[$step.Id] = @{
                PowerShell = $powershell
                Handle = $handle
                Step = $step
            }
        }

        if ($runspaces.Count -gt 0) {
            Write-Host "Waiting for $($runspaces.Count) parallel tasks..." -ForegroundColor Cyan
            
            # Wait for all runspaces to complete
            $failedStep = $null
            
            foreach ($stepId in $runspaces.Keys) {
                $runspaceInfo = $runspaces[$stepId]
                $step = $runspaceInfo.Step
                $powershell = $runspaceInfo.PowerShell
                $handle = $runspaceInfo.Handle
                
                try {
                    # Wait for this runspace to complete
                    $result = $powershell.EndInvoke($handle)
                    
                    $step.EndTime = Get-Date
                    $duration = $step.GetDurationSeconds()
                    $durationText = $duration.ToString('F2') + "s"
                    
                    # Check for errors in the stream
                    if ($powershell.Streams.Error.Count -gt 0) {
                        $errorMsg = $powershell.Streams.Error[0].ToString()
                        Write-Host "[FAIL] '$($step.Name)' stream error: $errorMsg" -ForegroundColor Red
                        $step.Status = [StepStatus]::Failed
                        $step.ErrorMessage = $errorMsg
                        
                        if (-not $this.ContinueOnError -and $null -eq $failedStep) {
                            $failedStep = $step
                        }
                    }
                    elseif ($null -ne $result -and $result.Count -gt 0 -and $result[0].Success) {
                        Write-Host "[OK] '$($step.Name)' ($durationText)" -ForegroundColor Green
                        $step.Status = [StepStatus]::Completed
                        $step.Result = $result[0].Result
                        $completedSteps[$step.Id] = $step
                        $this.Context.MergeUpdates($result[0].UpdatedContext)
                    } else {
                        $errorMsg = if ($null -ne $result -and $result.Count -gt 0) { $result[0].Error } else { "Unknown error" }
                        Write-Host "[FAIL] '$($step.Name)' failed: $errorMsg" -ForegroundColor Red
                        $step.Status = [StepStatus]::Failed
                        $step.ErrorMessage = $errorMsg
                        
                        if (-not $this.ContinueOnError -and $null -eq $failedStep) {
                            $failedStep = $step
                        }
                    }
                } catch {
                    $step.EndTime = Get-Date
                    Write-Host "[FAIL] '$($step.Name)' exception: $_" -ForegroundColor Red
                    $step.Status = [StepStatus]::Failed
                    $step.ErrorMessage = $_.ToString()
                    
                    if (-not $this.ContinueOnError -and $null -eq $failedStep) {
                        $failedStep = $step
                    }
                } finally {
                    $powershell.Dispose()
                }
            }
            
            # Clean up runspace pool
            $runspacePool.Close()
            $runspacePool.Dispose()
            
            # If we had a failure and ContinueOnError is false, throw now
            if ($null -ne $failedStep) {
                throw "Parallel step '$($failedStep.Name)' failed: $($failedStep.ErrorMessage)"
            }
        }

        Write-Host ("=" * 50) -ForegroundColor Magenta
        Write-Host ""
    }
    
    hidden [void] ResetSteps() {
        foreach ($item in $this.Steps) {
            if ($item.GetType().Name -eq 'WorkflowStep') {
                $item.Status = [StepStatus]::Pending
                $item.ErrorMessage = $null
                $item.Result = $null
                $item.StartTime = [datetime]::MinValue
                $item.EndTime = [datetime]::MinValue
            }
            elseif ($item.GetType().Name -eq 'ParallelGroup') {
                foreach ($step in $item.Steps) {
                    $step.Status = [StepStatus]::Pending
                    $step.ErrorMessage = $null
                    $step.Result = $null
                    $step.StartTime = [datetime]::MinValue
                    $step.EndTime = [datetime]::MinValue
                }
            }
        }
        $this.Context = [WorkflowContext]::new()
    }
    
    [void] PrintSummary() {
        Write-Host ""
        Write-Host "+================================================+" -ForegroundColor Cyan
        Write-Host "| WORKFLOW SUMMARY |" -ForegroundColor Cyan
        Write-Host "+================================================+" -ForegroundColor Cyan
        
        $allSteps = [System.Collections.ArrayList]::new()
        
        foreach ($item in $this.Steps) {
            if ($item.GetType().Name -eq 'WorkflowStep') {
                $allSteps.Add($item) | Out-Null
            }
            elseif ($item.GetType().Name -eq 'ParallelGroup') {
                foreach ($step in $item.Steps) {
                    $allSteps.Add($step) | Out-Null
                }
            }
        }
        
        $completed = 0
        $failed = 0
        $skipped = 0
        
        foreach ($step in $allSteps) {
            if ($step.Status -eq [StepStatus]::Completed) { $completed++ }
            elseif ($step.Status -eq [StepStatus]::Failed) { $failed++ }
            elseif ($step.Status -eq [StepStatus]::Skipped) { $skipped++ }
        }
        
        Write-Host ""
        Write-Host "Total Steps: $($allSteps.Count)"
        Write-Host "Completed: $completed" -ForegroundColor Green
        Write-Host "Failed: $failed" -ForegroundColor Red
        Write-Host "Skipped: $skipped" -ForegroundColor Yellow
        
        if ($this.StartTime -ne [datetime]::MinValue -and $this.EndTime -ne [datetime]::MinValue) {
            $duration = $this.EndTime - $this.StartTime
            Write-Host ""
            Write-Host ("Total Duration: " + $duration.ToString('hh\:mm\:ss'))
        }
        
        Write-Host ""
        Write-Host ("-" * 50)
        Write-Host "Step Details:" -ForegroundColor Cyan
        Write-Host ("-" * 50)
        
        foreach ($step in $allSteps) {
            $statusColor = 'Gray'
            $statusSymbol = '[ ]'
            
            if ($step.Status -eq [StepStatus]::Completed) {
                $statusColor = 'Green'
                $statusSymbol = '[OK]'
            }
            elseif ($step.Status -eq [StepStatus]::Failed) {
                $statusColor = 'Red'
                $statusSymbol = '[FAIL]'
            }
            elseif ($step.Status -eq [StepStatus]::Skipped) {
                $statusColor = 'Yellow'
                $statusSymbol = '[SKIP]'
            }
            
            $durationStr = ""
            if ($step.StartTime -ne [datetime]::MinValue -and $step.EndTime -ne [datetime]::MinValue) {
                $dur = $step.GetDurationSeconds()
                $durationStr = " (" + $dur.ToString('F2') + "s)"
            }
            
            Write-Host "$statusSymbol $($step.Name)$durationStr" -ForegroundColor $statusColor
            
            if ($step.Status -eq [StepStatus]::Failed -and $step.ErrorMessage) {
                Write-Host " Error: $($step.ErrorMessage)" -ForegroundColor Red
            }
        }
        
        Write-Host ""
    }

    # ============================================================================
    # INTERACTIVE MODE METHODS
    # ============================================================================

    hidden [string] ExtractStepDescription([object]$step) {
        return ""
    }

    hidden [array] BuildStepList() {
        $stepList = @()
        $parallelGroupIndex = 0

        foreach ($item in $this.Steps) {
            if ($item.GetType().Name -eq 'ParallelGroup') {
                $parallelGroupIndex++
                $stepIndexInGroup = 0
                $totalInGroup = $item.Steps.Count
                
                foreach ($parallelStep in $item.Steps) {
                    $stepIndexInGroup++
                    $stepList += @{
                        OriginalStep = $parallelStep
                        ParallelGroup = $item
                        ParallelGroupName = $item.Name
                        ParallelGroupIndex = $parallelGroupIndex
                        StepIndexInGroup = $stepIndexInGroup
                        TotalStepsInGroup = $totalInGroup
                        Name = $parallelStep.Name
                        Status = $parallelStep.Status
                        IsParallel = $true
                        Description = $this.ExtractStepDescription($parallelStep)
                    }
                }
            } else {
                $stepList += @{
                    OriginalStep = $item
                    ParallelGroup = $null
                    ParallelGroupName = $null
                    ParallelGroupIndex = 0
                    StepIndexInGroup = 0
                    TotalStepsInGroup = 0
                    Name = $item.Name
                    Status = $item.Status
                    IsParallel = $false
                    Description = $this.ExtractStepDescription($item)
                }
            }
        }

        return $stepList
    }

    hidden [hashtable] ParseStepSelection([string]$input, [array]$stepList) {
        $input = $input.Trim().ToLower()

        if ($input -in @("exit", "quit", "q")) {
            return @{ Action = "Exit" }
        }

        if ($input -eq "all") {
            $selectedIndices = 1..$stepList.Count
            return @{
                Action = "Execute"
                SelectedIndices = $selectedIndices
                StepList = $stepList
            }
        }

        if ($input -match "^from\s+(\d+)$") {
            $start = [int]$matches[1]
            $selectedIndices = $start..$stepList.Count
            return @{
                Action = "Execute"
                SelectedIndices = $selectedIndices
                StepList = $stepList
            }
        }

        if ($input -match "^to\s+(\d+)$") {
            $end = [int]$matches[1]
            $selectedIndices = 1..$end
            return @{
                Action = "Execute"
                SelectedIndices = $selectedIndices
                StepList = $stepList
            }
        }

        $selectedIndices = @()
        $parts = $input -split ','
        foreach ($part in $parts) {
            $part = $part.Trim()
            if ($part -match '^(\d+)-(\d+)$') {
                $start = [int]$matches[1]
                $end = [int]$matches[2]
                $selectedIndices += $start..$end
            } elseif ($part -match '^\d+$') {
                $selectedIndices += [int]$part
            }
        }

        $selectedIndices = $selectedIndices | Where-Object { $_ -ge 1 -and $_ -le $stepList.Count } | Sort-Object -Unique

        return @{
            Action = "Execute"
            SelectedIndices = $selectedIndices
            StepList = $stepList
        }
    }

    hidden [hashtable] ShowInteractiveMenu() {
        Clear-Host
        Write-Host "========================================" -ForegroundColor Cyan
        Write-Host " WORKFLOW INTERACTIVE MODE" -ForegroundColor Cyan
        Write-Host "========================================" -ForegroundColor Cyan
        Write-Host ""

        $stepList = $this.BuildStepList()

        $index = 1
        $currentParallelGroupIndex = 0
        
        foreach ($item in $stepList) {
            $status = $item.Status
            $color = switch ($status) {
                "Completed" { "Green" }
                "Failed" { "Red" }
                "Skipped" { "Yellow" }
                "Running" { "Cyan" }
                default { "White" }
            }

            # Display parallel group header when entering a new group
            if ($item.IsParallel -and $item.ParallelGroupIndex -ne $currentParallelGroupIndex) {
                $currentParallelGroupIndex = $item.ParallelGroupIndex
                Write-Host ""
                Write-Host (" +-- PARALLEL GROUP: " + $item.ParallelGroupName + " ") -ForegroundColor Magenta -NoNewline
                Write-Host ("(" + $item.TotalStepsInGroup + " steps run together)") -ForegroundColor DarkMagenta
            }

            $prefix = "[$index]"
            $statusText = "[$status]"

            if ($item.IsParallel) {
                # Parallel step with tree-style indicator
                $isLast = ($item.StepIndexInGroup -eq $item.TotalStepsInGroup)
                $treeChar = if ($isLast) { " +--" } else { " |--" }
                
                Write-Host "$treeChar $prefix " -ForegroundColor DarkGray -NoNewline
                Write-Host $item.Name -ForegroundColor $color -NoNewline
                Write-Host " $statusText" -ForegroundColor $color
                
                # Close the group visually after the last item
                if ($isLast) {
                    $currentParallelGroupIndex = 0
                    Write-Host ""
                }
            } else {
                # Sequential step
                Write-Host "$prefix [SEQUENTIAL] " -NoNewline
                Write-Host $item.Name -ForegroundColor $color -NoNewline
                Write-Host " $statusText" -ForegroundColor $color
            }

            if ($item.Description) {
                $indent = if ($item.IsParallel) { " " } else { " " }
                Write-Host "$indent$($item.Description)" -ForegroundColor Gray
            }

            $index++
        }

        Write-Host ""
        Write-Host "========================================" -ForegroundColor Cyan
        Write-Host "COMMANDS:" -ForegroundColor Yellow
        Write-Host " - Enter step numbers (e.g., 1,3,5)" -ForegroundColor Gray
        Write-Host " - Enter range (e.g., 2-6)" -ForegroundColor Gray
        Write-Host " - From step to end (e.g., from 3)" -ForegroundColor Gray
        Write-Host " - Up to step (e.g., to 5)" -ForegroundColor Gray
        Write-Host " - All steps (e.g., all)" -ForegroundColor Gray
        Write-Host " - Exit (e.g., exit or quit)" -ForegroundColor Gray
        Write-Host "========================================" -ForegroundColor Cyan
        Write-Host "NOTE: Steps within a parallel group will execute" -ForegroundColor DarkGray
        Write-Host " simultaneously when multiple are selected." -ForegroundColor DarkGray
        Write-Host ""

        $userInput = Read-Host "Select steps to execute"

        return $this.ParseStepSelection($userInput, $stepList)
    }

    hidden [void] ExecuteParallelGroupFiltered([object]$group, [array]$selectedSteps, [hashtable]$completedSteps) {
        Write-Host ("=" * 50) -ForegroundColor Magenta
        Write-Host "Parallel Group: $($group.Name)" -ForegroundColor Magenta
        Write-Host ("=" * 50) -ForegroundColor Magenta

        if ($selectedSteps.Count -eq 0) {
            Write-Host "No steps to execute in this group" -ForegroundColor Yellow
            Write-Host ("=" * 50) -ForegroundColor Magenta
            Write-Host ""
            return
        }

        $runspacePool = [runspacefactory]::CreateRunspacePool(1, [Math]::Max($group.MaxParallelism, $selectedSteps.Count))
        $runspacePool.Open()

        $runspaces = @{}

        foreach ($step in $selectedSteps) {
            Write-Host "[START] '$($step.Name)'" -ForegroundColor Cyan
            $step.Status = [StepStatus]::Running
            $step.StartTime = Get-Date

            $contextSnapshot = $this.Context.GetSnapshot()
            $stepAction = $step.Action
            $retries = $step.Retries
            $retryDelay = $step.RetryDelay

            $powershell = [powershell]::Create()
            $powershell.RunspacePool = $runspacePool

            [void]$powershell.AddScript({
                param($actionString, $contextVars, $retries, $retryDelay)

                $stepAction = [scriptblock]::Create($actionString)

                $ctx = New-Object PSObject -Property @{ Variables = $contextVars }

                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name Set -Value {
                    param($key, $value)
                    $this.Variables[$key] = $value
                } -Force

                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name Get -Value {
                    param($key)
                    if ($this.Variables.ContainsKey($key)) {
                        return $this.Variables[$key]
                    }
                    return $null
                } -Force

                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name SetValue -Value {
                    param($key, $value)
                    $this.Variables[$key] = $value
                } -Force

                Add-Member -InputObject $ctx -MemberType ScriptMethod -Name GetValue -Value {
                    param($key)
                    if ($this.Variables.ContainsKey($key)) {
                        return $this.Variables[$key]
                    }
                    return $null
                } -Force

                for ($i = 1; $i -le $retries; $i++) {
                    try {
                        $result = & $stepAction $ctx

                        return @{
                            Success = $true
                            Result = $result
                            UpdatedContext = $ctx.Variables
                            Error = $null
                        }

                    } catch {
                        if ($i -lt $retries) {
                            Start-Sleep -Seconds $retryDelay
                        } else {
                            return @{
                                Success = $false
                                Result = $null
                                UpdatedContext = $ctx.Variables
                                Error = $_.Exception.Message
                            }
                        }
                    }
                }
            })

            [void]$powershell.AddArgument($stepAction.ToString())
            [void]$powershell.AddArgument($contextSnapshot)
            [void]$powershell.AddArgument($retries)
            [void]$powershell.AddArgument($retryDelay)

            $handle = $powershell.BeginInvoke()

            $runspaces[$step.Id] = @{
                PowerShell = $powershell
                Handle = $handle
                Step = $step
            }
        }

        if ($runspaces.Count -gt 0) {
            Write-Host "Waiting for $($runspaces.Count) parallel tasks..." -ForegroundColor Cyan

            $failedStep = $null

            foreach ($stepId in $runspaces.Keys) {
                $runspaceInfo = $runspaces[$stepId]
                $step = $runspaceInfo.Step
                $powershell = $runspaceInfo.PowerShell
                $handle = $runspaceInfo.Handle

                try {
                    $result = $powershell.EndInvoke($handle)

                    $step.EndTime = Get-Date
                    $duration = $step.GetDurationSeconds()
                    $durationText = $duration.ToString('F2') + "s"

                    if ($powershell.Streams.Error.Count -gt 0) {
                        $errorMsg = $powershell.Streams.Error[0].ToString()
                        Write-Host "[FAIL] '$($step.Name)' stream error: $errorMsg" -ForegroundColor Red
                        $step.Status = [StepStatus]::Failed
                        $step.ErrorMessage = $errorMsg

                        if (-not $this.ContinueOnError -and $null -eq $failedStep) {
                            $failedStep = $step
                        }
                    }
                    elseif ($null -ne $result -and $result.Count -gt 0 -and $result[0].Success) {
                        Write-Host "[OK] '$($step.Name)' ($durationText)" -ForegroundColor Green
                        $step.Status = [StepStatus]::Completed
                        $step.Result = $result[0].Result
                        $completedSteps[$step.Id] = $step
                        $this.Context.MergeUpdates($result[0].UpdatedContext)
                    } else {
                        $errorMsg = if ($null -ne $result -and $result.Count -gt 0) { $result[0].Error } else { "Unknown error" }
                        Write-Host "[FAIL] '$($step.Name)' failed: $errorMsg" -ForegroundColor Red
                        $step.Status = [StepStatus]::Failed
                        $step.ErrorMessage = $errorMsg

                        if (-not $this.ContinueOnError -and $null -eq $failedStep) {
                            $failedStep = $step
                        }
                    }
                } catch {
                    $step.EndTime = Get-Date
                    Write-Host "[FAIL] '$($step.Name)' exception: $_" -ForegroundColor Red
                    $step.Status = [StepStatus]::Failed
                    $step.ErrorMessage = $_.ToString()

                    if (-not $this.ContinueOnError -and $null -eq $failedStep) {
                        $failedStep = $step
                    }
                } finally {
                    $powershell.Dispose()
                }
            }

            $runspacePool.Close()
            $runspacePool.Dispose()

            if ($null -ne $failedStep) {
                throw "Parallel step '$($failedStep.Name)' failed: $($failedStep.ErrorMessage)"
            }
        }

        Write-Host ("=" * 50) -ForegroundColor Magenta
        Write-Host ""
    }

    hidden [void] ExecuteSelectedSteps([hashtable]$selection) {
        $selectedIndices = $selection.SelectedIndices
        $stepList = $selection.StepList

        if ($selectedIndices.Count -eq 0) {
            Write-Host "No valid steps selected." -ForegroundColor Yellow
            return
        }

        $selectedSteps = @()
        foreach ($index in $selectedIndices) {
            $selectedSteps += $stepList[$index - 1]
        }

        $parallelGroupSelections = @{}
        $sequentialSteps = @()

        foreach ($stepInfo in $selectedSteps) {
            if ($stepInfo.IsParallel) {
                $groupId = $stepInfo.ParallelGroup.Id
                if (-not $parallelGroupSelections.ContainsKey($groupId)) {
                    $parallelGroupSelections[$groupId] = @{
                        Group = $stepInfo.ParallelGroup
                        SelectedSteps = @()
                    }
                }
                $parallelGroupSelections[$groupId].SelectedSteps += $stepInfo.OriginalStep
            } else {
                $sequentialSteps += $stepInfo.OriginalStep
            }
        }

        $completedSteps = @{}
        $stepNumber = 1

        foreach ($item in $this.Steps) {
            if ($item.GetType().Name -eq 'ParallelGroup') {
                $groupId = $item.Id
                if ($parallelGroupSelections.ContainsKey($groupId)) {
                    $stepsToRun = $parallelGroupSelections[$groupId].SelectedSteps

                    if ($stepsToRun.Count -lt $item.Steps.Count) {
                        Write-Host ""
                        Write-Host "Parallel Group: $($item.Name)" -ForegroundColor Yellow
                        Write-Host " Selected $($stepsToRun.Count) of $($item.Steps.Count) parallel steps" -ForegroundColor Gray
                        Write-Host " Selected steps will run in parallel" -ForegroundColor Gray
                        Write-Host ""
                    }

                    try {
                        $this.ExecuteParallelGroupFiltered($item, $stepsToRun, $completedSteps)
                    } catch {
                        if (-not $this.ContinueOnError) {
                            throw
                        }
                    }
                }
            } else {
                if ($item -in $sequentialSteps) {
                    try {
                        $this.ExecuteSequentialStep($item, $completedSteps, $stepNumber)
                        $stepNumber++
                        if ($item.Status -eq [StepStatus]::Completed) {
                            $completedSteps[$item.Id] = $item
                        }
                    } catch {
                        if (-not $this.ContinueOnError) {
                            throw
                        }
                    }
                }
            }
        }
    }

    [bool] ExecuteInteractive() {
        $this.StartTime = Get-Date

        while ($true) {
            $selection = $this.ShowInteractiveMenu()

            if ($selection.Action -eq "Exit") {
                Write-Host ""
                Write-Host "Exiting interactive mode..." -ForegroundColor Yellow
                Write-Host ""
                break
            }

            try {
                Write-Host ""
                Write-Host "========================================" -ForegroundColor Cyan
                Write-Host "Executing selected steps..." -ForegroundColor Cyan
                Write-Host "========================================" -ForegroundColor Cyan
                Write-Host ""

                $this.ExecuteSelectedSteps($selection)

                Write-Host ""
                Write-Host "========================================" -ForegroundColor Green
                Write-Host "Execution completed" -ForegroundColor Green
                Write-Host "========================================" -ForegroundColor Green

            } catch {
                Write-Host ""
                Write-Host "========================================" -ForegroundColor Red
                Write-Host "Execution failed: $_" -ForegroundColor Red
                Write-Host "========================================" -ForegroundColor Red
            }

            $this.PrintSummary()

            Write-Host ""
            Write-Host "Press Enter to return to menu..." -ForegroundColor Cyan
            try {
                [void][System.Console]::ReadKey($true)
            } catch {
                # Fallback for environments without console (ISE, VS Code, etc.)
                Read-Host
            }
        }

        $this.EndTime = Get-Date
        return $true
    }
}

#endregion

#region Public Functions

function New-Workflow {
    <#
    .SYNOPSIS
        Creates a new workflow instance
    .DESCRIPTION
        Creates a new WfeWorkflow object that can be used to define and execute workflow steps.
    .PARAMETER WorkflowRetries
        Number of times to retry the entire workflow on failure. Default is 1.
    .PARAMETER WorkflowDelay
        Seconds to wait between workflow retries. Default is 60.
    .PARAMETER ContinueOnError
        If true, continue executing steps even if one fails. Default is false.
    .EXAMPLE
        $workflow = New-Workflow
        $workflow.AddStep("Step 1", { param($ctx) Write-Host "Hello" })
        $workflow.Execute()
    .EXAMPLE
        $workflow = New-Workflow -WorkflowRetries 3 -ContinueOnError $true
    #>

    [CmdletBinding()]
    [OutputType([WfeWorkflow])]
    param(
        [Parameter()]
        [int]$WorkflowRetries = 1,
        
        [Parameter()]
        [int]$WorkflowDelay = 60,
        
        [Parameter()]
        [bool]$ContinueOnError = $false
    )
    
    $workflow = [WfeWorkflow]::new()
    $workflow.WorkflowRetries = $WorkflowRetries
    $workflow.WorkflowDelay = $WorkflowDelay
    $workflow.ContinueOnError = $ContinueOnError
    
    return $workflow
}

function New-WorkflowStep {
    <#
    .SYNOPSIS
        Creates a new workflow step (standalone, not added to a workflow)
    .DESCRIPTION
        Creates a WorkflowStep object that can be customized before adding to a workflow or parallel group.
    .PARAMETER Name
        The name of the step
    .PARAMETER Action
        The scriptblock to execute
    .EXAMPLE
        $step = New-WorkflowStep -Name "My Step" -Action { param($ctx) Write-Host "Running" }
        $step.Retries = 5
        $step.Timeout = 120
    #>

    [CmdletBinding()]
    [OutputType([WorkflowStep])]
    param(
        [Parameter(Mandatory)]
        [string]$Name,
        
        [Parameter(Mandatory)]
        [scriptblock]$Action
    )
    
    return [WorkflowStep]::new($Name, $Action)
}

#endregion

# Export public functions and types
Export-ModuleMember -Function @(
    'New-Workflow',
    'New-WorkflowStep'
)