#Requires -Version 7.4

function Invoke-vSEALActionGroup {
    <#
    .SYNOPSIS
        Orchestrates execution of an action group with condition evaluation and baseline capture.

    .DESCRIPTION
        The core orchestration engine for vARC declarative plugin execution. Processes an
        action group by:
        1. Evaluating group-level conditions (all must pass, AND logic)
        2. Processing each action with optional per-action conditions
        3. Dispatching actions to appropriate executors by type
        4. Capturing baseline entries for successful actions
        5. Returning consolidated results with status counts

        This function never throws exceptions. All errors are captured in the result object.

    .PARAMETER Group
        A hashtable defining the action group with structure:
        - name: Group name (string)
        - conditions: Optional array of condition hashtables (AND logic)
        - actions: Array of action hashtables
        - on_error: Error handling strategy ('continue', 'stop', 'rollback') - default 'continue'

    .PARAMETER Phase
        The execution phase: Seal, Engage, Analyze, or Rollback.

    .PARAMETER PluginId
        The plugin identifier (folder name, e.g., "defender").
        Used for baseline entry association.

    .PARAMETER Globals
        Optional hashtable containing OS and Platform information for globals conditions.

    .OUTPUTS
        [PSCustomObject] Group result with properties:
        - Group: Group name
        - Status: Completed, CompletedWithErrors, Stopped, RolledBack, or Skipped
        - Actions: Array of executor results
        - Total: Total action count
        - Succeeded: Count of successful actions
        - Failed: Count of failed actions
        - Skipped: Count of skipped actions
        - Timeout: Count of timed out actions
        - Duration: Execution time in milliseconds
        - StoppedBy: Target that triggered stop (if applicable)

    .EXAMPLE
        $group = @{
            name = 'Telemetry'
            conditions = @(
                @{ type = 'globals'; path = 'OS.Build'; operator = '>='; value = 22000 }
            )
            actions = @(
                @{ type = 'registry'; action = 'set'; key = 'HKLM:\SOFTWARE\Policies\Test'; name = 'Value'; data = 1; datatype = 'dword' }
            )
        }
        Invoke-vSEALActionGroup -Group $group -Phase 'Seal' -PluginId 'system.telemetry'

        Executes a group with condition evaluation and baseline capture.

    .NOTES
        Part of the vARC (vSEAL Action Rollback Core) infrastructure.
        Plan 02 adds stop/rollback error handling strategies.
    #>
    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory)]
        [hashtable]$Group,

        [Parameter(Mandatory)]
        [ValidateSet('Seal', 'Engage', 'Rollback')]
        [string]$Phase,

        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$PluginId,

        [Parameter()]
        [hashtable]$Globals
    )

    # Start timing
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

    $groupName = $Group.name ?? 'unnamed'

    # Initialize result tracking
    $actionResults = [System.Collections.ArrayList]::new()

    # ===== EVALUATE GROUP CONDITIONS (GRPR-02) =====
    # All conditions must pass (AND logic)
    if ($Group.ContainsKey('conditions') -and $Group.conditions.Count -gt 0) {
        foreach ($condition in $Group.conditions) {
            $conditionResult = Test-vSEALCondition -Condition $condition -Globals $Globals
            if (-not $conditionResult) {
                Write-vSEALLog -Message "Group '$groupName' skipped: condition not met (type=$($condition.type))" -Level VERBOSE
                $stopwatch.Stop()
                return [PSCustomObject]@{
                    Group     = $groupName
                    Status    = 'Skipped'
                    Actions   = @()
                    Total     = 0
                    Succeeded = 0
                    Failed    = 0
                    Skipped   = 0
                    Timeout   = 0
                    Duration  = [int]$stopwatch.ElapsedMilliseconds
                    StoppedBy = $null
                }
            }
        }
        Write-vSEALLog -Message "Group '$groupName': all $($Group.conditions.Count) conditions passed" -Level VERBOSE
    }

    # ===== PROCESS EACH ACTION (GRPR-03) =====
    $hasErrors = $false
    $onError = $Group.on_error ?? 'continue'

    foreach ($action in $Group.actions) {
        # Evaluate per-action conditions (optional)
        if ($action.ContainsKey('conditions') -and $action.conditions.Count -gt 0) {
            $skipAction = $false
            foreach ($actionCondition in $action.conditions) {
                $condResult = Test-vSEALCondition -Condition $actionCondition -Globals $Globals
                if (-not $condResult) {
                    Write-vSEALLog -Message "Action skipped: condition not met (type=$($actionCondition.type))" -Level VERBOSE
                    # Add skipped result
                    $skippedResult = [PSCustomObject]@{
                        Action   = "$($action.type).skip"
                        Target   = Get-ActionTarget -Action $action
                        Status   = 'Skipped'
                        Message  = 'Condition not met'
                        Baseline = $null
                        Duration = 0
                    }
                    [void]$actionResults.Add($skippedResult)
                    $skipAction = $true
                    break
                }
            }
            if ($skipAction) { continue }
        }

        # Dispatch to appropriate executor based on action type
        $result = switch ($action.type) {
            'registry' {
                Invoke-RegistryDispatch -Action $action
            }
            'service' {
                Invoke-ServiceDispatch -Action $action
            }
            'task' {
                Invoke-TaskDispatch -Action $action
            }
            'command' {
                Invoke-CommandDispatch -Action $action
            }
            default {
                # Unknown action type - return Failed result
                Write-vSEALLog -Message "Unknown action type: $($action.type)" -Level WARNING
                [PSCustomObject]@{
                    Action   = "unknown.$($action.type)"
                    Target   = Get-ActionTarget -Action $action
                    Status   = 'Failed'
                    Message  = "Unknown action type: $($action.type)"
                    Baseline = $null
                    Duration = 0
                }
            }
        }

        [void]$actionResults.Add($result)

        # ===== BASELINE CAPTURE (GRPR-05) =====
        # After each successful Seal action, capture baseline entry (skip during Engage/Rollback)
        if ($Phase -eq 'Seal' -and $result.Status -eq 'Success' -and $null -ne $result.Baseline) {
            # Map action type to baseline type
            $baselineType = switch ($action.type) {
                'registry' { 'Registry' }
                'service'  { 'Service' }
                'task'     { 'Task' }
                'command'  { 'Command' }
                default    { 'Custom' }
            }

            # Determine applied value based on action type
            $appliedValue = Get-AppliedValue -Action $action -Result $result

            # Check if original value was null/missing
            $didNotExist = Test-OriginalValueMissing -Result $result -ActionType $action.type

            try {
                Add-vSEALBaselineEntry -PluginId $PluginId `
                    -Type $baselineType `
                    -Target $result.Target `
                    -OriginalValue $result.Baseline `
                    -AppliedValue $appliedValue `
                    -DidNotExist:$didNotExist
            }
            catch {
                # Log but don't fail - baseline context might not be initialized
                Write-vSEALLog -Message "Failed to add baseline entry: $($_.Exception.Message)" -Level WARNING
            }
        }

        # ===== ERROR HANDLING (GRPR-04) =====
        if ($result.Status -in @('Failed', 'Timeout')) {
            $hasErrors = $true

            switch ($onError) {
                'continue' {
                    Write-vSEALLog -Message "Action failed (continuing): $($result.Message)" -Level WARNING
                    # Continue to next action
                }
                'stop' {
                    Write-vSEALLog -Message "Action failed (stopping group): $($result.Message)" -Level ERROR
                    $stopwatch.Stop()
                    return [PSCustomObject]@{
                        Group     = $groupName
                        Status    = 'Stopped'
                        Actions   = @($actionResults.ToArray())
                        Total     = $actionResults.Count
                        Succeeded = @($actionResults | Where-Object { $_.Status -eq 'Success' }).Count
                        Failed    = @($actionResults | Where-Object { $_.Status -eq 'Failed' }).Count
                        Skipped   = @($actionResults | Where-Object { $_.Status -eq 'Skipped' }).Count
                        Timeout   = @($actionResults | Where-Object { $_.Status -eq 'Timeout' }).Count
                        Duration  = [int]$stopwatch.ElapsedMilliseconds
                        StoppedBy = $result.Target
                    }
                }
                'rollback' {
                    Write-vSEALLog -Message "Action failed (rolling back group): $($result.Message)" -Level WARNING
                    Invoke-GroupRollback -CompletedResults $actionResults -PluginId $PluginId
                    $stopwatch.Stop()
                    return [PSCustomObject]@{
                        Group     = $groupName
                        Status    = 'RolledBack'
                        Actions   = @($actionResults.ToArray())
                        Total     = $actionResults.Count
                        Succeeded = @($actionResults | Where-Object { $_.Status -eq 'Success' }).Count
                        Failed    = @($actionResults | Where-Object { $_.Status -eq 'Failed' }).Count
                        Skipped   = @($actionResults | Where-Object { $_.Status -eq 'Skipped' }).Count
                        Timeout   = @($actionResults | Where-Object { $_.Status -eq 'Timeout' }).Count
                        Duration  = [int]$stopwatch.ElapsedMilliseconds
                        StoppedBy = $result.Target
                    }
                }
            }
        }
    }

    # ===== RETURN GROUP RESULT =====
    $stopwatch.Stop()

    $successCount = @($actionResults | Where-Object { $_.Status -eq 'Success' }).Count
    $failedCount = @($actionResults | Where-Object { $_.Status -eq 'Failed' }).Count
    $skippedCount = @($actionResults | Where-Object { $_.Status -eq 'Skipped' }).Count
    $timeoutCount = @($actionResults | Where-Object { $_.Status -eq 'Timeout' }).Count

    $status = if ($hasErrors) { 'CompletedWithErrors' } else { 'Completed' }

    return [PSCustomObject]@{
        Group     = $groupName
        Status    = $status
        Actions   = @($actionResults)
        Total     = $actionResults.Count
        Succeeded = $successCount
        Failed    = $failedCount
        Skipped   = $skippedCount
        Timeout   = $timeoutCount
        Duration  = [int]$stopwatch.ElapsedMilliseconds
        StoppedBy = $null
    }
}

# ===== PRIVATE HELPER FUNCTIONS =====

function Invoke-RegistryDispatch {
    <#
    .SYNOPSIS
        Dispatches a registry action to the registry executor.
    #>
    param([hashtable]$Action)

    # TOML to executor field mapping
    $params = @{
        Action = ConvertTo-RegistryAction -ActionVerb $Action.action
        Key    = $Action.key
    }

    if ($Action.ContainsKey('name') -and -not [string]::IsNullOrEmpty($Action.name)) {
        $params.Name = $Action.name
    }
    if ($Action.ContainsKey('data')) {
        $params.Data = $Action.data
    }
    if ($Action.ContainsKey('datatype') -and -not [string]::IsNullOrEmpty($Action.datatype)) {
        $params.Type = ConvertTo-RegistryType -DataType $Action.datatype
    }

    return Invoke-vSEALRegistryAction @params
}

function Invoke-ServiceDispatch {
    <#
    .SYNOPSIS
        Dispatches a service action to the service executor.
    #>
    param([hashtable]$Action)

    $params = @{
        Name      = $Action.name
        StartMode = ConvertTo-ServiceStartMode -StartMode ($Action.startmode ?? 'Disabled')
    }

    return Invoke-vSEALServiceAction @params
}

function Invoke-TaskDispatch {
    <#
    .SYNOPSIS
        Dispatches a task action to the task executor.
    #>
    param([hashtable]$Action)

    $params = @{
        Name   = $Action.name
        Action = ConvertTo-TaskAction -ActionVerb ($Action.action ?? 'Disable')
    }

    return Invoke-vSEALTaskAction @params
}

function Invoke-CommandDispatch {
    <#
    .SYNOPSIS
        Dispatches a command action to the command executor.
    #>
    param([hashtable]$Action)

    $params = @{
        Command = $Action.command
    }

    if ($Action.ContainsKey('description') -and -not [string]::IsNullOrEmpty($Action.description)) {
        $params.Description = $Action.description
    }
    if ($Action.ContainsKey('timeout')) {
        $params.Timeout = $Action.timeout
    }
    if ($Action.ContainsKey('success_exit_codes')) {
        $params.SuccessExitCodes = $Action.success_exit_codes
    }
    if ($Action.ContainsKey('rollback_command') -and -not [string]::IsNullOrEmpty($Action.rollback_command)) {
        $params.RollbackCommand = $Action.rollback_command
    }

    return Invoke-vSEALCommandAction @params
}

function Get-ActionTarget {
    <#
    .SYNOPSIS
        Extracts target identifier from an action hashtable.
    #>
    param([hashtable]$Action)

    switch ($Action.type) {
        'registry' { return "$($Action.key)\$($Action.name)" }
        'service'  { return $Action.name }
        'task'     { return $Action.name }
        'command'  { return $Action.command }
        default    { return 'unknown' }
    }
}

function Get-AppliedValue {
    <#
    .SYNOPSIS
        Constructs the applied value based on action type and result.
    #>
    param(
        [hashtable]$Action,
        [PSCustomObject]$Result
    )

    switch ($Action.type) {
        'registry' {
            return @{
                Data = $Action.data
                Type = $Action.datatype ?? 'String'
            }
        }
        'service' {
            return @{
                StartMode = $Action.startmode ?? 'Disabled'
            }
        }
        'task' {
            $targetState = if (($Action.action ?? 'Disable') -eq 'Disable') { 'Disabled' } else { 'Ready' }
            return @{
                State = $targetState
            }
        }
        'command' {
            return @{
                Command = $Action.command
                ExitCode = 0
            }
        }
        default {
            return $null
        }
    }
}

function Test-OriginalValueMissing {
    <#
    .SYNOPSIS
        Determines if the original value indicates the resource didn't exist.
    #>
    param(
        [PSCustomObject]$Result,
        [string]$ActionType
    )

    if ($null -eq $Result.Baseline) {
        return $true
    }

    switch ($ActionType) {
        'registry' {
            return $null -eq $Result.Baseline.Data
        }
        'service' {
            return $null -eq $Result.Baseline.StartMode
        }
        'task' {
            return $null -eq $Result.Baseline.State
        }
        'command' {
            # Commands don't have "original value" concept in the same way
            return $false
        }
        default {
            return $false
        }
    }
}

# ===== VALUE CONVERSION HELPERS =====

function ConvertTo-RegistryAction {
    <#
    .SYNOPSIS
        Converts TOML action verb to executor parameter value.
    #>
    param([string]$ActionVerb)

    switch ($ActionVerb.ToLower()) {
        'set'       { return 'Set' }
        'delete'    { return 'Delete' }
        'deletekey' { return 'DeleteKey' }
        default     { return $ActionVerb }
    }
}

function ConvertTo-RegistryType {
    <#
    .SYNOPSIS
        Converts TOML datatype to executor Type parameter value.
    #>
    param([string]$DataType)

    switch ($DataType.ToLower()) {
        'dword'        { return 'DWord' }
        'qword'        { return 'QWord' }
        'string'       { return 'String' }
        'binary'       { return 'Binary' }
        'expandstring' { return 'ExpandString' }
        'multistring'  { return 'MultiString' }
        default        { return 'String' }
    }
}

function ConvertTo-ServiceStartMode {
    <#
    .SYNOPSIS
        Converts TOML startmode to executor StartMode parameter value.
    #>
    param([string]$StartMode)

    switch ($StartMode.ToLower()) {
        'automatic' { return 'Automatic' }
        'manual'    { return 'Manual' }
        'disabled'  { return 'Disabled' }
        default     { return 'Disabled' }
    }
}

function ConvertTo-TaskAction {
    <#
    .SYNOPSIS
        Converts TOML task action verb to executor Action parameter value.
    #>
    param([string]$ActionVerb)

    switch ($ActionVerb.ToLower()) {
        'enable'  { return 'Enable' }
        'disable' { return 'Disable' }
        default   { return 'Disable' }
    }
}

# ===== ROLLBACK FUNCTIONS =====

function Invoke-GroupRollback {
    <#
    .SYNOPSIS
        Rolls back completed actions in reverse order (LIFO).

    .DESCRIPTION
        Processes successful actions from the completed results list in reverse order,
        restoring each to its original state using baseline data. Failed or skipped
        actions are ignored as they made no changes.

        Rollback errors are logged but do not prevent other rollbacks from attempting.

    .PARAMETER CompletedResults
        ArrayList of action results from the group execution.

    .PARAMETER PluginId
        The plugin identifier for logging purposes.
    #>
    param(
        [Parameter(Mandatory)]
        [System.Collections.ArrayList]$CompletedResults,

        [Parameter(Mandatory)]
        [string]$PluginId
    )

    # Process in reverse order (LIFO - last in, first out)
    $reversed = @($CompletedResults.ToArray())
    [array]::Reverse($reversed)

    Write-vSEALLog -Message "Rolling back $($reversed.Count) actions for plugin $PluginId" -Level VERBOSE

    foreach ($actionResult in $reversed) {
        # Only rollback successful actions - others made no changes
        if ($actionResult.Status -ne 'Success') {
            Write-vSEALLog -Message "Skipping rollback for $($actionResult.Target) (status: $($actionResult.Status))" -Level VERBOSE
            continue
        }

        # Skip if no baseline data captured
        if ($null -eq $actionResult.Baseline) {
            Write-vSEALLog -Message "No baseline data for rollback: $($actionResult.Target)" -Level WARNING
            continue
        }

        try {
            switch -Regex ($actionResult.Action) {
                '^registry\.set' {
                    Invoke-RegistryRollback -Result $actionResult
                }
                '^registry\.delete' {
                    Invoke-RegistryDeleteRollback -Result $actionResult
                }
                '^service\.set' {
                    Invoke-ServiceRollback -Result $actionResult
                }
                '^task\.(enable|disable)' {
                    Invoke-TaskRollback -Result $actionResult
                }
                '^command\.run' {
                    Invoke-CommandRollback -Result $actionResult
                }
                default {
                    Write-vSEALLog -Message "Unknown action type for rollback: $($actionResult.Action)" -Level WARNING
                }
            }
        }
        catch {
            # Log but continue rolling back other actions
            Write-vSEALLog -Message "Rollback failed for $($actionResult.Target): $($_.Exception.Message)" -Level ERROR
        }
    }
}

function Invoke-RegistryRollback {
    <#
    .SYNOPSIS
        Rolls back a registry set operation.
    #>
    param([PSCustomObject]$Result)

    # Target format: "HKLM:\path\to\key\ValueName"
    $parts = $Result.Target -split '\\'
    $valueName = $parts[-1]
    $keyPath = ($parts[0..($parts.Count - 2)]) -join '\'

    if ($Result.Baseline.DidNotExist -or $null -eq $Result.Baseline.Data) {
        # Value didn't exist before - delete it
        $null = Invoke-vSEALRegistryAction -Action Delete -Key $keyPath -Name $valueName
        Write-vSEALLog -Message "Rolled back registry (deleted new value): $($Result.Target)" -Level VERBOSE
    }
    else {
        # Restore original value
        $null = Invoke-vSEALRegistryAction -Action Set -Key $keyPath -Name $valueName `
            -Data $Result.Baseline.Data -Type ($Result.Baseline.Type ?? 'String')
        Write-vSEALLog -Message "Rolled back registry (restored original): $($Result.Target)" -Level VERBOSE
    }
}

function Invoke-RegistryDeleteRollback {
    <#
    .SYNOPSIS
        Rolls back a registry delete operation by restoring the deleted value.
    #>
    param([PSCustomObject]$Result)

    # Value was deleted - restore it if we have baseline data
    if ($null -ne $Result.Baseline.Data) {
        $parts = $Result.Target -split '\\'
        $valueName = $parts[-1]
        $keyPath = ($parts[0..($parts.Count - 2)]) -join '\'

        $null = Invoke-vSEALRegistryAction -Action Set -Key $keyPath -Name $valueName `
            -Data $Result.Baseline.Data -Type ($Result.Baseline.Type ?? 'String')
        Write-vSEALLog -Message "Rolled back registry delete (restored): $($Result.Target)" -Level VERBOSE
    }
    else {
        Write-vSEALLog -Message "Cannot rollback registry delete - no baseline data: $($Result.Target)" -Level WARNING
    }
}

function Invoke-ServiceRollback {
    <#
    .SYNOPSIS
        Rolls back a service start mode change.
    #>
    param([PSCustomObject]$Result)

    if ($null -eq $Result.Baseline.StartMode) {
        Write-vSEALLog -Message "Cannot rollback service - no original StartMode: $($Result.Target)" -Level WARNING
        return
    }

    $null = Invoke-vSEALServiceAction -Name $Result.Target -StartMode $Result.Baseline.StartMode
    Write-vSEALLog -Message "Rolled back service to $($Result.Baseline.StartMode): $($Result.Target)" -Level VERBOSE
}

function Invoke-TaskRollback {
    <#
    .SYNOPSIS
        Rolls back a task enable/disable operation.
    #>
    param([PSCustomObject]$Result)

    if ($null -eq $Result.Baseline.State) {
        Write-vSEALLog -Message "Cannot rollback task - no original State: $($Result.Target)" -Level WARNING
        return
    }

    # Restore original state
    $originalAction = if ($Result.Baseline.State -eq 'Disabled') { 'Disable' } else { 'Enable' }
    $null = Invoke-vSEALTaskAction -Name $Result.Target -Action $originalAction
    Write-vSEALLog -Message "Rolled back task to $($Result.Baseline.State): $($Result.Target)" -Level VERBOSE
}

function Invoke-CommandRollback {
    <#
    .SYNOPSIS
        Rolls back a command by executing the rollback command if provided.
    #>
    param([PSCustomObject]$Result)

    if ([string]::IsNullOrEmpty($Result.Baseline.RollbackCommand)) {
        Write-vSEALLog -Message "No rollback command for: $($Result.Target)" -Level WARNING
        return
    }

    $null = Invoke-vSEALCommandAction -Command $Result.Baseline.RollbackCommand `
        -Description "Rollback: $($Result.Target)"
    Write-vSEALLog -Message "Rolled back command: $($Result.Target)" -Level VERBOSE
}
