src/public/Orchestration/Invoke-AitherPlaybook.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Execute a playbook with orchestration and dependency management

.DESCRIPTION
    Executes a playbook definition, running scripts in sequence or parallel
    based on the playbook configuration. This is the primary way to run
    automation workflows defined as playbooks.

    Playbooks can execute scripts in parallel (for speed), sequentially
    (for dependencies), or in a mixed mode (some parallel, some sequential).
    The orchestration engine automatically handles dependencies, retries,
    and error handling.

.PARAMETER Name
    Name of the playbook to execute. This parameter is REQUIRED when using
    the ByName parameter set. The playbook must exist in the playbooks directory.

    Examples:
    - "test-orchestration"
    - "pr-validation"
    - "deployment"

.PARAMETER Playbook
    Playbook object from Get-AitherPlaybook. This parameter is REQUIRED when
    using the ByObject parameter set. Allows piping playbook objects directly.

    Use this when you've already loaded a playbook and want to execute it.

.PARAMETER Variables
    Variables to pass to the playbook execution. This is a hashtable containing
    key-value pairs that will be available to all scripts in the playbook.

    Examples:
    - @{ Environment = "Production"; Approval = "Automatic" }
    - @{ OutputPath = "C:\Reports"; Verbose = $true }

    Variables can be accessed in scripts using $Variables.Environment, etc.

.PARAMETER DryRun
    Show what would be executed without actually running the playbook.
    Displays the playbook structure, scripts that would run, and execution order.
    Useful for verifying playbook configuration before execution.

.PARAMETER ContinueOnError
    Continue execution even if a script fails. By default, playbook execution
    stops on the first error. With this parameter, execution continues through
    all scripts and reports all failures at the end.

    Useful for:
    - Running validation scripts where you want to see all failures
    - Testing multiple components independently
    - Gathering comprehensive status information

.PARAMETER Parallel
    Override playbook's parallel setting. Forces parallel execution if set to $true,
    or sequential execution if set to $false. If not specified, uses the playbook's
    default execution mode.

    Note: Some scripts may require sequential execution due to dependencies,
    which will be respected even when Parallel is $true.

.PARAMETER MaxConcurrency
    Maximum concurrent script executions when running in parallel mode.
    Defaults to the value in configuration (usually 4).

    Increase this for:
    - Systems with more CPU cores
    - Scripts that are I/O bound rather than CPU bound
    - Faster execution when dependencies allow

    Decrease this for:
    - Resource-constrained systems
    - Scripts that consume significant resources
    - Better error visibility (fewer simultaneous failures)

.INPUTS
    System.String
    You can pipe playbook names to Invoke-AitherPlaybook.

    Hashtable
    You can pipe playbook objects from Get-AitherPlaybook to Invoke-AitherPlaybook.

.OUTPUTS
    PSCustomObject
    Returns execution result with properties:
    - Total: Total number of scripts
    - Completed: Number of successfully completed scripts
    - Failed: Number of failed scripts
    - Duration: Total execution time
    - Results: Detailed results for each script

.EXAMPLE
    Invoke-AitherPlaybook -Name 'test-orchestration'

    Executes the 'test-orchestration' playbook with default settings.

.EXAMPLE
    $playbook = Get-AitherPlaybook -Name 'pr-validation'
    Invoke-AitherPlaybook -Playbook $playbook -DryRun

    Loads a playbook and shows what would be executed without running it.

.EXAMPLE
    Invoke-AitherPlaybook -Name 'deployment' -Variables @{ Environment = "Production" } -ContinueOnError

    Executes deployment playbook with variables and continues on errors.

.EXAMPLE
    Get-AitherPlaybook -Name 'validation' | Invoke-AitherPlaybook -Parallel $true -MaxConcurrency 8

    Pipes a playbook object and executes it in parallel with higher concurrency.

.EXAMPLE
    Invoke-AitherPlaybook -Name 'test-suite' -DryRun -Variables @{ TestMode = "Full" }

    Shows what would be executed with specific variables without running.

.NOTES
    Uses the OrchestrationEngine for execution, which provides:
    - Automatic dependency resolution
    - Parallel and sequential execution modes
    - Error handling and retry logic
    - Progress tracking
    - Execution history

    Playbooks are stored in library/playbooks/ directory as .psd1 files.
    Each playbook defines scripts, execution order, dependencies, and success criteria.

.LINK
    Get-AitherPlaybook
    Save-AitherPlaybook
    New-AitherPlaybook
    Get-AitherOrchestrationStatus
    Get-AitherExecutionHistory
#>

function Invoke-AitherPlaybook {
    [OutputType([PSCustomObject])]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ByName')]
    param(
        [Parameter(ParameterSetName = 'ByName', Mandatory = $false, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = "Name of the playbook to execute (e.g., 'pr-validation').")]
        [ArgumentCompleter({
                param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)
                if (Get-Command Get-AitherPlaybook -ErrorAction SilentlyContinue) {
                    Get-AitherPlaybook -List | Where-Object { $_.Name -like "$wordToComplete*" } | ForEach-Object {
                        [System.Management.Automation.CompletionResult]::new($_.Name, $_.Name, 'ParameterValue', $_.Description)
                    }
                }
            })]
        [AllowEmptyString()]
        [string]$Name,

        [Parameter(ParameterSetName = 'ByObject', Mandatory = $false, ValueFromPipeline, ValueFromPipelineByPropertyName, HelpMessage = "Playbook object or hashtable definition.")]
        [hashtable]$Playbook,

        [Parameter(HelpMessage = "Variables to pass to the playbook execution.")]
        [hashtable]$Variables = @{},

        [Parameter(HelpMessage = "Show what would be executed without running it.")]
        [switch]$DryRun,

        [Parameter(HelpMessage = "Continue execution even if a step fails.")]
        [switch]$ContinueOnError,

        [Parameter(HelpMessage = "Execute independent steps in parallel.")]
        [bool]$Parallel,

        [Parameter(HelpMessage = "Maximum number of concurrent parallel executions.")]
        [ValidateRange(1, 32)]
        [int]$MaxConcurrency,

        [Parameter(HelpMessage = "Show playbook execution output in console.")]
        [switch]$ShowOutput,

        [Parameter(HelpMessage = "Display transcript content after execution.")]
        [switch]$ShowTranscript
    )    begin {
        # Manage logging targets for this execution
        $originalLogTargets = $script:AitherLogTargets
        if ($ShowOutput) {
            if ($script:AitherLogTargets -notcontains 'Console') {
                $script:AitherLogTargets += 'Console'
            }
        }
        else {
            # Ensure Console is NOT in targets if ShowOutput is not specified
            $script:AitherLogTargets = $script:AitherLogTargets | Where-Object { $_ -ne 'Console' }
        }

        # Get scripts directory using robust discovery
        try {
            $scriptsPath = Get-AitherScriptsPath
        }
        catch {
            Write-AitherLog -Level Warning -Message "Could not resolve scripts path: $($_.Exception.Message)" -Source 'Invoke-AitherPlaybook'
            $scriptsPath = $null
        }

        $executionResults = @()
        $startTime = Get-Date
    }

    process {
        try {
            # Get playbook if name provided
            if ($Name) {
                $Playbook = Get-AitherPlaybook -Name $Name
                if (-not $Playbook) {
                    throw "Playbook not found: $Name"
                }
            }

            if (-not $Playbook -and -not $Name) {
                # During module validation, parameters may be empty - skip validation
                if ($PSCmdlet.MyInvocation.InvocationName -eq '.') {
                    return
                }
                throw "Playbook must be provided via -Name or -Playbook parameter"
            }

            # Merge playbook variables with provided variables
            $playbookVariables = if ($Playbook.Variables) { $Playbook.Variables } else { @{} }
            $mergedVariables = $playbookVariables.Clone()
            foreach ($key in $Variables.Keys) {
                $mergedVariables[$key] = $Variables[$key]
            }

            # Determine execution mode
            $executeParallel = if ($PSBoundParameters.ContainsKey('Parallel')) {
                $Parallel
            }
            elseif ($Playbook.Options -and $Playbook.Options.ContainsKey('Parallel')) {
                $Playbook.Options.Parallel
            }
            else {
                $false  # Default to sequential for safety
            }

            $maxConcurrency = if ($PSBoundParameters.ContainsKey('MaxConcurrency')) {
                $MaxConcurrency
            }
            elseif ($Playbook.Options -and $Playbook.Options.ContainsKey('MaxConcurrency')) {
                $Playbook.Options.MaxConcurrency
            }
            else {
                $config = Get-AitherConfigs -ErrorAction SilentlyContinue
                if ($config -and $config.Automation -and $config.Automation.OrchestrationEngine) {
                    $config.Automation.OrchestrationEngine.MaxConcurrency
                }
                else {
                    4  # Default
                }
            }

            $continueOnError = if ($PSBoundParameters.ContainsKey('ContinueOnError')) {
                $ContinueOnError
            }
            elseif ($Playbook.Options -and $Playbook.Options.ContainsKey('StopOnError')) {
                -not $Playbook.Options.StopOnError
            }
            else {
                $false
            }

            # Get sequence from playbook
            $sequence = if ($Playbook.Sequence) {
                $Playbook.Sequence
            }
            else {
                throw "Playbook does not contain a Sequence definition"
            }

            Write-AitherLog -Message "Executing playbook: $($Playbook.Name)" -Level Information -Source 'Invoke-AitherPlaybook'
            Write-AitherLog -Message " Scripts: $($sequence.Count)" -Level Information -Source 'Invoke-AitherPlaybook'
            Write-AitherLog -Message " Mode: $(if ($executeParallel) { 'Parallel' } else { 'Sequential' })" -Level Information -Source 'Invoke-AitherPlaybook'
            Write-AitherLog -Message " ContinueOnError: $continueOnError" -Level Information -Source 'Invoke-AitherPlaybook'

            # Dry run mode
            if ($DryRun) {
                Write-AitherLog -Level Information -Message "[DRY RUN] Playbook: $($Playbook.Name)" -Source 'Invoke-AitherPlaybook'
                Write-AitherLog -Level Information -Message ("=" * 60) -Source 'Invoke-AitherPlaybook'
                foreach ($item in $sequence) {
                    $scriptId = if ($item.Script) { $item.Script } else { $item }
                    $desc = if ($item.Description) { $item.Description } else { "Script $scriptId" }
                    Write-AitherLog -Level Information -Message " - $scriptId : $desc" -Source 'Invoke-AitherPlaybook'
                    if ($item.Parameters) {
                        Write-AitherLog -Level Information -Message " Parameters: $($item.Parameters | ConvertTo-Json -Compress)" -Source 'Invoke-AitherPlaybook'
                    }
                    elseif ($item.Params) {
                        Write-AitherLog -Level Information -Message " Parameters: $($item.Params | ConvertTo-Json -Compress)" -Source 'Invoke-AitherPlaybook'
                    }
                }
                return
            }

            # Execute sequence
            if (-not $PSCmdlet.ShouldProcess($Playbook.Name, "Execute playbook")) {
                return
            }

            # Define ModuleRoot for jobs
            $ModuleRoot = Get-AitherModuleRoot

            $scriptResults = @()
            $completed = 0
            $failed = 0
            $skipped = 0

            if ($executeParallel) {
                # Parallel execution with concurrency limit
                $jobs = @()
                $runningJobs = @{}
                $index = 0

                while ($index -lt $sequence.Count -or $runningJobs.Count -gt 0) {
                    # Start new jobs up to concurrency limit
                    while ($runningJobs.Count -lt $maxConcurrency -and $index -lt $sequence.Count) {
                        $item = $sequence[$index]
                        $scriptId = if ($item.Script) { $item.Script } else { $item }
                        $scriptParams = if ($item.Parameters) { $item.Parameters } elseif ($item.Params) { $item.Params } else { @{} }

                        # Merge variables into parameters
                        foreach ($key in $mergedVariables.Keys) {
                            if (-not $scriptParams.ContainsKey($key)) {
                                $scriptParams[$key] = $mergedVariables[$key]
                            }
                        }

                        Write-AitherLog -Message "Starting script: $scriptId" -Level Information -Source 'Invoke-AitherPlaybook'

                        $job = Start-Job -ScriptBlock {
                            param($ScriptId, $ModuleRoot, $Params, $ShowOutput, $ShowTranscript)
                            $modulePath = Join-Path $ModuleRoot 'AitherZero' 'AitherZero.psd1'
                            Import-Module $modulePath -Force
                            Invoke-AitherScript -Script $ScriptId -Parameters $Params -ErrorAction Stop -ShowOutput:$ShowOutput -ShowTranscript:$ShowTranscript
                        } -ArgumentList $scriptId, $moduleRoot, $scriptParams, $true, $ShowTranscript

                        $runningJobs[$job.Id] = @{
                            Job       = $job
                            ScriptId  = $scriptId
                            Index     = $index
                            StartTime = Get-Date
                        }
                        $index++
                    }

                    # Check for completed jobs
                    $completedJobs = @()
                    foreach ($jobId in $runningJobs.Keys) {
                        $jobInfo = $runningJobs[$jobId]
                        if ($jobInfo.Job.State -eq 'Completed' -or $jobInfo.Job.State -eq 'Failed') {
                            $result = Receive-Job -Job $jobInfo.Job

                            # Display output if requested (captured from job)
                            if ($ShowOutput) {
                                $result | ForEach-Object { Write-AitherLog -Level Information -Message $_ -Source 'Invoke-AitherPlaybook' }
                            }

                            $duration = (Get-Date) - $jobInfo.StartTime

                            $scriptResult = [PSCustomObject]@{
                                Script   = $jobInfo.ScriptId
                                Success  = $jobInfo.Job.State -eq 'Completed'
                                Duration = $duration
                                Output   = $result
                                Error    = if ($jobInfo.Job.State -eq 'Failed') { $jobInfo.Job.ChildJobs[0].Error } else { $null }
                            }

                            $scriptResults += $scriptResult

                            if ($scriptResult.Success) {
                                $completed++
                            }
                            else {
                                $failed++
                                Write-AitherLog -Message "Script failed: $($jobInfo.ScriptId)" -Level Error -Source 'Invoke-AitherPlaybook'
                                if (-not $continueOnError) {
                                    Remove-Job -Job $jobInfo.Job
                                    $completedJobs += $jobId
                                    break
                                }
                            }

                            Remove-Job -Job $jobInfo.Job
                            $completedJobs += $jobId
                        }
                    }

                    foreach ($jobId in $completedJobs) {
                        $runningJobs.Remove($jobId)
                    }

                    if ($runningJobs.Count -gt 0) {
                        Start-Sleep -Milliseconds 100
                    }
                }
            }
            else {
                # Sequential execution
                foreach ($item in $sequence) {
                    $scriptId = if ($item.Script) { $item.Script } else { $item }
                    $scriptParams = if ($item.Parameters) { $item.Parameters } elseif ($item.Params) { $item.Params } else { @{} }

                    # Merge variables into parameters
                    foreach ($key in $mergedVariables.Keys) {
                        if (-not $scriptParams.ContainsKey($key)) {
                            $scriptParams[$key] = $mergedVariables[$key]
                        }
                    }

                    Write-AitherLog -Message "Executing script: $scriptId" -Level Information -Source 'Invoke-AitherPlaybook'

                    $scriptStartTime = Get-Date
                    try {
                        # Pass Verbose preference explicitly
                        $result = Invoke-AitherScript -Script $scriptId -Parameters $scriptParams -ErrorAction Stop -ShowOutput:$ShowOutput -ShowTranscript:$ShowTranscript -Verbose:$VerbosePreference
                        $duration = (Get-Date) - $scriptStartTime

                        $scriptResult = [PSCustomObject]@{
                            Script   = $scriptId
                            Success  = $true
                            Duration = $duration
                            Output   = $result
                            Error    = $null
                        }

                        $scriptResults += $scriptResult
                        $completed++
                    }
                    catch {
                        $duration = (Get-Date) - $scriptStartTime

                        $scriptResult = [PSCustomObject]@{
                            Script   = $scriptId
                            Success  = $false
                            Duration = $duration
                            Output   = $null
                            Error    = $_.Exception.Message
                        }

                        $scriptResults += $scriptResult
                        $failed++

                        Write-AitherLog -Message "Script failed: $scriptId - $($_.Exception.Message)" -Level Error -Source 'Invoke-AitherPlaybook'

                        if (-not $continueOnError) {
                            break
                        }
                    }
                }
            }

            $endTime = Get-Date
            $totalDuration = $endTime - $startTime

            # Build result object
            $result = [PSCustomObject]@{
                PSTypeName   = 'AitherZero.PlaybookExecutionResult'
                PlaybookName = $Playbook.Name
                Success      = $failed -eq 0
                Total        = $sequence.Count
                Completed    = $completed
                Failed       = $failed
                Skipped      = $skipped
                Duration     = $totalDuration
                Results      = $scriptResults
            }

            Write-AitherLog -Message "Playbook execution completed: $completed/$($sequence.Count) succeeded, $failed failed" -Level Information -Source 'Invoke-AitherPlaybook'

            return $result
        }
        catch {
            Invoke-AitherErrorHandler -ErrorRecord $_ -Operation "Executing playbook: $($Name ?? $Playbook.Name)" -Parameters $PSBoundParameters -ThrowOnError
        }
        finally {
            # Restore original log targets
            $script:AitherLogTargets = $originalLogTargets
        }
    }

}