Public/Plugins/Deploy-DataversePlugins.ps1

# Design Decision: Sequential Step Deployment
# ============================================
# Steps are deployed sequentially rather than in parallel (ForEach-Object -Parallel).
# Rationale:
# 1. Simpler error handling and debugging
# 2. Clear progress output for users
# 3. Avoids Dataverse API rate limiting issues
# 4. Maintains transactional semantics (partial failures are clear)
# 5. Most registrations have <50 steps, making parallelism unnecessary
# Future consideration: Add -Parallel switch for large registrations (100+ steps)

function Deploy-DataversePlugins {
    <#
    .SYNOPSIS
        Deploys plugin assemblies and registers steps to Dataverse.

    .DESCRIPTION
        Deploys plugin assemblies using Web API and registers/updates SDK message
        processing steps and images using the Dataverse Web API.
        Supports both classic plugin assemblies and plugin packages (NuGet).

    .PARAMETER RegistrationFile
        Path to the registrations.json file.

    .PARAMETER Connection
        DataverseConnection object from Connect-DataverseEnvironment.

    .PARAMETER Force
        Remove orphaned steps that exist in Dataverse but not in configuration.

    .PARAMETER SkipAssembly
        Skip deploying the assembly, only register/update steps.

    .PARAMETER DetectDrift
        Run drift detection after deployment.

    .PARAMETER WhatIf
        Show what would be deployed without making changes.

    .EXAMPLE
        $conn = Connect-DataverseEnvironment -Interactive
        Deploy-DataversePlugins -RegistrationFile "./registrations.json" -Connection $conn

    .EXAMPLE
        Deploy-DataversePlugins -RegistrationFile "./registrations.json" -Connection $conn -Force
        Deploys and removes orphaned steps.

    .OUTPUTS
        Deployment summary object.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)]
        [string]$RegistrationFile,

        [Parameter(Mandatory = $true)]
        [DataverseConnection]$Connection,

        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [switch]$SkipAssembly,

        [Parameter()]
        [switch]$DetectDrift
    )

    $isWhatIf = $WhatIfPreference -or $PSCmdlet.MyInvocation.BoundParameters["WhatIf"]

    if (-not (Test-Path $RegistrationFile)) {
        throw "Registration file not found: $RegistrationFile"
    }

    $registrations = Read-RegistrationJson -Path $RegistrationFile
    if (-not $registrations -or -not $registrations.assemblies) {
        throw "Invalid or empty registrations.json"
    }

    Write-Log "Plugin Deployment Tool"
    if ($isWhatIf) {
        Write-LogWarning "Running in WhatIf mode - no changes will be made"
    }

    $apiUrl = Get-WebApiBaseUrl -Connection $Connection
    $authHeaders = Get-AuthHeaders -Connection $Connection

    Write-LogSuccess "Connected to: $($Connection.ConnectedOrgFriendlyName)"

    $totalStepsCreated = 0
    $totalStepsUpdated = 0
    $totalImagesCreated = 0
    $totalImagesUpdated = 0
    $totalOrphansWarned = 0
    $totalOrphansDeleted = 0

    foreach ($asmReg in $registrations.assemblies) {
        Write-Log ""
        Write-Log ("=" * 60)
        Write-Log "Deploying: $($asmReg.name) ($($asmReg.type))"
        Write-Log ("=" * 60)

        $solutionUniqueName = $asmReg.solution
        $solution = $null
        $publisherPrefix = $null

        if ($asmReg.type -eq "Nuget" -and -not $solutionUniqueName) {
            Write-LogError "Solution is required for plugin packages (Nuget type)"
            continue
        }

        if ($solutionUniqueName -and -not $isWhatIf) {
            Write-Log "Looking up solution: $solutionUniqueName"
            $solution = Get-Solution -ApiUrl $apiUrl -AuthHeaders $authHeaders -UniqueName $solutionUniqueName
            if (-not $solution) {
                Write-LogError "Solution not found: $solutionUniqueName"
                continue
            }
            $publisherPrefix = $solution.publisherprefix
            Write-LogSuccess "Solution found: $($solution.friendlyname) (prefix: $publisherPrefix)"
        }

        # Deploy assembly
        if (-not $SkipAssembly) {
            $registrationDir = Split-Path $RegistrationFile -Parent
            $rawPath = if ($asmReg.type -eq "Nuget" -and $asmReg.packagePath) {
                $asmReg.packagePath
            } else {
                $asmReg.path
            }

            # Resolve path based on prefix:
            # - "./" or ".\" = relative to current working directory
            # - absolute path = use as-is
            # - other relative paths (including "../") = relative to registrations.json location
            if ($rawPath -match '^\.[\\/]') {
                # Starts with "./" - relative to CWD, strip the "./"
                $deployPath = Join-Path (Get-Location) ($rawPath -replace '^\.[\\/]', '')
            }
            elseif ([System.IO.Path]::IsPathRooted($rawPath)) {
                # Absolute path - use as-is
                $deployPath = $rawPath
            }
            else {
                # Relative path (including "../") - relative to registrations.json
                $deployPath = Join-Path $registrationDir $rawPath
            }

            if (-not (Test-Path $deployPath)) {
                Write-LogWarning "Assembly/package not found: $deployPath"
                continue
            }

            $deploySuccess = Deploy-PluginAssembly -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                -Path $deployPath -AssemblyName $asmReg.name -Type $asmReg.type `
                -SolutionUniqueName $solutionUniqueName -PublisherPrefix $publisherPrefix `
                -WhatIf:$isWhatIf

            if (-not $deploySuccess -and -not $isWhatIf) {
                Write-LogError "Failed to deploy assembly, skipping step registration"
                continue
            }
        }

        # Get assembly record
        $assembly = $null
        if (-not $isWhatIf) {
            $assembly = Get-PluginAssembly -ApiUrl $apiUrl -AuthHeaders $authHeaders -Name $asmReg.name
            if (-not $assembly) {
                Write-LogError "Assembly not found in Dataverse: $($asmReg.name)"
                continue
            }
            Write-Log "Assembly ID: $($assembly.pluginassemblyid)"

            # Only add assembly to solution for classic assemblies, not NuGet packages
            # (NuGet packages are added via the plugin package, not the assembly)
            if ($solutionUniqueName -and $asmReg.type -ne "Nuget") {
                Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                    -SolutionUniqueName $solutionUniqueName `
                    -ComponentId $assembly.pluginassemblyid `
                    -ComponentType $script:ComponentType.PluginAssembly | Out-Null
            }
        }

        $configuredStepNames = @()

        # Process each plugin
        foreach ($plugin in $asmReg.plugins) {
            Write-Log ""
            Write-Log "Plugin: $($plugin.typeName)"

            $pluginType = $null
            if (-not $isWhatIf -and $assembly) {
                $pluginType = Get-PluginType -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                    -AssemblyId $assembly.pluginassemblyid -TypeName $plugin.typeName

                if (-not $pluginType -and $plugin.steps.Count -gt 0 -and $asmReg.type -eq "Assembly") {
                    Write-Log " Registering new plugin type: $($plugin.typeName)"
                    $pluginType = New-PluginType -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                        -AssemblyId $assembly.pluginassemblyid -TypeName $plugin.typeName `
                        -SolutionUniqueName $solutionUniqueName
                    if ($pluginType) {
                        Write-LogSuccess " Plugin type created"
                    }
                }
                elseif (-not $pluginType -and $plugin.steps.Count -gt 0) {
                    Write-LogWarning " Plugin type not found: $($plugin.typeName)"
                    continue
                }
            }

            # Process steps
            foreach ($step in $plugin.steps) {
                Write-Log " Step: $($step.name)"
                $configuredStepNames += $step.name

                $message = $null
                $filter = $null

                if (-not $isWhatIf) {
                    $message = Get-SdkMessage -ApiUrl $apiUrl -AuthHeaders $authHeaders -MessageName $step.message
                    if (-not $message) {
                        Write-LogError " SDK Message not found: $($step.message)"
                        continue
                    }

                    $filterParams = @{
                        ApiUrl = $apiUrl
                        AuthHeaders = $authHeaders
                        MessageId = $message.sdkmessageid
                        EntityLogicalName = $step.entity
                    }
                    if ($step.secondaryEntity) {
                        $filterParams["SecondaryEntityLogicalName"] = $step.secondaryEntity
                    }
                    $filter = Get-SdkMessageFilter @filterParams
                    if (-not $filter) {
                        Write-LogError " SDK Message Filter not found"
                        continue
                    }
                }

                $stageValue = $script:DataverseStageValues[$step.stage]
                $modeValue = $script:DataverseModeValues[$step.mode]

                $existingStep = $null
                if (-not $isWhatIf) {
                    try {
                        $existingStep = Get-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders -StepName $step.name
                    } catch {
                        Write-LogDebug "Step not found (expected for new steps): $($_.Exception.Message)"
                    }
                }

                $stepData = @{
                    Name = $step.name
                    Stage = $stageValue
                    Mode = $modeValue
                    ExecutionOrder = $step.executionOrder
                    FilteringAttributes = $step.filteringAttributes
                    Configuration = $step.configuration
                    PluginTypeId = $pluginType.plugintypeid
                    MessageId = $message.sdkmessageid
                    FilterId = $filter.sdkmessagefilterid
                }

                $stepId = $null
                if ($existingStep) {
                    Write-Log " Updating existing step..."
                    if (-not $isWhatIf) {
                        Update-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                            -StepId $existingStep.sdkmessageprocessingstepid -StepData $stepData
                        $stepId = $existingStep.sdkmessageprocessingstepid
                        $totalStepsUpdated++
                        Write-LogSuccess " Step updated"

                        if ($solutionUniqueName) {
                            $addResult = Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                                -SolutionUniqueName $solutionUniqueName `
                                -ComponentId $stepId `
                                -ComponentType $script:ComponentType.SdkMessageProcessingStep
                            if ($addResult) {
                                Write-LogDebug " Step added to solution"
                            }
                        }
                    } else {
                        Write-Log " [WhatIf] Would update step"
                        $totalStepsUpdated++
                    }
                } else {
                    Write-Log " Creating new step..."
                    if (-not $isWhatIf) {
                        $newStep = New-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders -StepData $stepData
                        $stepId = $newStep.sdkmessageprocessingstepid
                        $totalStepsCreated++
                        Write-LogSuccess " Step created"

                        if ($solutionUniqueName) {
                            $addResult = Add-SolutionComponent -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                                -SolutionUniqueName $solutionUniqueName `
                                -ComponentId $stepId `
                                -ComponentType $script:ComponentType.SdkMessageProcessingStep
                            if ($addResult) {
                                Write-LogDebug " Step added to solution"
                            }
                        }
                    } else {
                        Write-Log " [WhatIf] Would create step"
                        $totalStepsCreated++
                    }
                }

                # Process images
                foreach ($image in $step.images) {
                    Write-Log " Image: $($image.name) ($($image.imageType))"

                    $imageTypeValue = $script:DataverseImageTypeValues[$image.imageType]

                    $existingImages = @()
                    if (-not $isWhatIf -and $stepId) {
                        try {
                            $existingImages = Get-StepImages -ApiUrl $apiUrl -AuthHeaders $authHeaders -StepId $stepId
                        } catch {
                            Write-LogDebug "Could not retrieve existing images: $($_.Exception.Message)"
                        }
                    }

                    $existingImage = $existingImages | Where-Object { $_.name -eq $image.name } | Select-Object -First 1

                    $imageData = @{
                        Name = $image.name
                        EntityAlias = if ($image.entityAlias) { $image.entityAlias } else { $image.name }
                        ImageType = $imageTypeValue
                        Attributes = $image.attributes
                        StepId = $stepId
                    }

                    if ($existingImage) {
                        if (-not $isWhatIf) {
                            Write-Log " Updating existing image..."
                            try {
                                Update-StepImage -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                                    -ImageId $existingImage.sdkmessageprocessingstepimageid -ImageData $imageData
                                $totalImagesUpdated++
                                Write-LogSuccess " Image updated"
                            }
                            catch {
                                Write-LogWarning " Failed to update image: $($_.Exception.Message)"
                            }
                        } else {
                            Write-Log " [WhatIf] Would update image"
                            $totalImagesUpdated++
                        }
                    } else {
                        if (-not $isWhatIf) {
                            Write-Log " Creating new image..."
                            try {
                                $null = New-StepImage -ApiUrl $apiUrl -AuthHeaders $authHeaders -ImageData $imageData
                                $totalImagesCreated++
                                Write-LogSuccess " Image created"
                                # Note: Step images are subcomponents - they're automatically included
                                # with their parent step, so we don't add them to the solution separately
                            }
                            catch {
                                Write-LogWarning " Failed to create image: $($_.Exception.Message)"
                            }
                        } else {
                            Write-Log " [WhatIf] Would create image"
                            $totalImagesCreated++
                        }
                    }
                }
            }
        }

        # Check for orphaned steps
        if (-not $isWhatIf -and $assembly) {
            Write-Log ""
            Write-Log "Checking for orphaned steps..."

            $existingSteps = Get-ProcessingStepsForAssembly -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                -AssemblyId $assembly.pluginassemblyid

            foreach ($existingStep in $existingSteps) {
                if ($configuredStepNames -notcontains $existingStep.name) {
                    if ($Force) {
                        Write-LogWarning "Deleting orphaned step: $($existingStep.name)"
                        try {
                            Remove-ProcessingStep -ApiUrl $apiUrl -AuthHeaders $authHeaders `
                                -StepId $existingStep.sdkmessageprocessingstepid
                            $totalOrphansDeleted++
                            Write-LogSuccess " Deleted"
                        } catch {
                            Write-LogError " Failed to delete: $($_.Exception.Message)"
                        }
                    } else {
                        Write-LogWarning "Orphaned step found: $($existingStep.name)"
                        Write-Log " Use -Force to delete orphaned steps"
                        $totalOrphansWarned++
                    }
                }
            }
        }
    }

    # Summary
    Write-Log ""
    Write-Log ("=" * 60)
    Write-Log "Deployment Summary"
    Write-Log ("=" * 60)
    Write-LogSuccess "Steps created: $totalStepsCreated"
    Write-LogSuccess "Steps updated: $totalStepsUpdated"
    Write-LogSuccess "Images created: $totalImagesCreated"
    Write-LogSuccess "Images updated: $totalImagesUpdated"

    if ($totalOrphansWarned -gt 0) {
        Write-LogWarning "Orphaned steps (not deleted): $totalOrphansWarned"
    }
    if ($totalOrphansDeleted -gt 0) {
        Write-LogWarning "Orphaned steps deleted: $totalOrphansDeleted"
    }

    if ($isWhatIf) {
        Write-Log ""
        Write-LogWarning "WhatIf mode: No actual changes were made"
    }

    # Run drift detection if requested
    if ($DetectDrift -and -not $isWhatIf) {
        Write-Log ""
        Write-Log "Running post-deployment drift detection..."
        Get-DataversePluginDrift -RegistrationFile $RegistrationFile -Connection $Connection
    }

    return [PSCustomObject]@{
        StepsCreated = $totalStepsCreated
        StepsUpdated = $totalStepsUpdated
        ImagesCreated = $totalImagesCreated
        ImagesUpdated = $totalImagesUpdated
        OrphansWarned = $totalOrphansWarned
        OrphansDeleted = $totalOrphansDeleted
    }
}