Private/DataverseOperations.ps1

function New-ProcessingStep {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [hashtable]$StepData,
        [Parameter()]
        [switch]$WhatIf
    )

    $body = @{
        name = $StepData.Name
        stage = $StepData.Stage
        mode = $StepData.Mode
        rank = $StepData.ExecutionOrder
        "plugintypeid@odata.bind" = "/plugintypes($($StepData.PluginTypeId))"
        "sdkmessageid@odata.bind" = "/sdkmessages($($StepData.MessageId))"
        "sdkmessagefilterid@odata.bind" = "/sdkmessagefilters($($StepData.FilterId))"
    }

    if ($StepData.FilteringAttributes) {
        $body.filteringattributes = $StepData.FilteringAttributes
    }

    if ($StepData.Configuration) {
        $body.configuration = $StepData.Configuration
    }

    return Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingsteps" -Method POST -Body $body -WhatIf:$WhatIf
}

function Update-ProcessingStep {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$StepId,
        [Parameter(Mandatory = $true)]
        [hashtable]$StepData,
        [Parameter()]
        [switch]$WhatIf
    )

    $body = @{
        stage = $StepData.Stage
        mode = $StepData.Mode
        rank = $StepData.ExecutionOrder
    }

    if ($StepData.FilteringAttributes) {
        $body.filteringattributes = $StepData.FilteringAttributes
    } else {
        $body.filteringattributes = $null
    }

    if ($StepData.Configuration) {
        $body.configuration = $StepData.Configuration
    }

    return Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingsteps($StepId)" -Method PATCH -Body $body -WhatIf:$WhatIf
}

function Remove-ProcessingStep {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$StepId,
        [Parameter()]
        [switch]$WhatIf
    )

    return Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingsteps($StepId)" -Method DELETE -WhatIf:$WhatIf
}

function New-StepImage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [hashtable]$ImageData,
        [Parameter()]
        [switch]$WhatIf
    )

    $body = @{
        name = $ImageData.Name
        entityalias = $ImageData.EntityAlias
        imagetype = $ImageData.ImageType
        messagepropertyname = "Target"
        "sdkmessageprocessingstepid@odata.bind" = "/sdkmessageprocessingsteps($($ImageData.StepId))"
    }

    if ($ImageData.Attributes) {
        $body.attributes = $ImageData.Attributes
    }

    return Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingstepimages" -Method POST -Body $body -WhatIf:$WhatIf
}

function Update-StepImage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$ImageId,
        [Parameter(Mandatory = $true)]
        [hashtable]$ImageData,
        [Parameter()]
        [switch]$WhatIf
    )

    # Only update fields that are updatable after creation
    # imagetype, name, messagepropertyname, and sdkmessageprocessingstepid are read-only after create
    $body = @{}

    if ($ImageData.EntityAlias) {
        $body.entityalias = $ImageData.EntityAlias
    }

    if ($null -ne $ImageData.Attributes) {
        $body.attributes = $ImageData.Attributes
    }

    if ($body.Count -eq 0) {
        Write-LogDebug "No updatable fields for image, skipping update"
        return
    }

    return Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingstepimages($ImageId)" -Method PATCH -Body $body -WhatIf:$WhatIf
}

function Remove-StepImage {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$ImageId,
        [Parameter()]
        [switch]$WhatIf
    )

    return Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "sdkmessageprocessingstepimages($ImageId)" -Method DELETE -WhatIf:$WhatIf
}

function New-PluginType {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$AssemblyId,
        [Parameter(Mandatory = $true)]
        [string]$TypeName,
        [Parameter()]
        [string]$SolutionUniqueName,
        [Parameter()]
        [switch]$WhatIf
    )

    $friendlyName = $TypeName.Split('.')[-1]

    $body = @{
        "pluginassemblyid@odata.bind" = "/pluginassemblies($AssemblyId)"
        typename = $TypeName
        friendlyname = $friendlyName
        name = $TypeName
    }

    if ($WhatIf) {
        Write-Log "[WhatIf] Would create plugin type: $TypeName"
        return $null
    }

    # Add MSCRM.SolutionUniqueName header for solution association if provided
    $headersWithSolution = $AuthHeaders
    if ($SolutionUniqueName) {
        $headersWithSolution = $AuthHeaders.Clone()
        $headersWithSolution["MSCRM.SolutionUniqueName"] = $SolutionUniqueName
    }

    try {
        $response = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $headersWithSolution -Endpoint "plugintypes" -Method POST -Body $body
        return $response
    }
    catch {
        Write-LogError "Failed to create plugin type '$TypeName': $($_.Exception.Message)"
        throw
    }
}

function Deploy-PluginAssembly {
    param(
        [Parameter(Mandatory = $true)]
        [string]$ApiUrl,
        [Parameter(Mandatory = $true)]
        [hashtable]$AuthHeaders,
        [Parameter(Mandatory = $true)]
        [string]$Path,
        [Parameter(Mandatory = $true)]
        [string]$AssemblyName,
        [Parameter(Mandatory = $true)]
        [ValidateSet("Assembly", "Nuget")]
        [string]$Type,
        [Parameter()]
        [string]$SolutionUniqueName,
        [Parameter()]
        [string]$PublisherPrefix,
        [Parameter()]
        [switch]$WhatIf
    )

    if (-not (Test-Path $Path)) {
        Write-LogError "File not found: $Path"
        return $null
    }

    if ($Type -eq "Nuget") {
        $packageUniqueName = if ($PublisherPrefix) {
            "${PublisherPrefix}_$AssemblyName"
        } else {
            $AssemblyName
        }

        $existingPackage = Get-PluginPackage -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Name $AssemblyName -UniqueName $packageUniqueName

        if ($existingPackage) {
            Write-Log "Updating existing plugin package: $AssemblyName"
            $packageId = $existingPackage.pluginpackageid

            if ($WhatIf) {
                Write-Log "[WhatIf] pac plugin push --pluginId $packageId --pluginFile $Path"
                return $existingPackage
            }

            $result = pac plugin push --pluginId $packageId --pluginFile $Path 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-LogError "Failed to update plugin package: $result"
                return $null
            }
            Write-LogSuccess "Plugin package updated successfully"
            return Get-PluginAssembly -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Name $AssemblyName
        }
        else {
            Write-Log "Registering new plugin package: $AssemblyName"

            if (-not $SolutionUniqueName) {
                Write-LogError "Solution is required for registering new plugin packages"
                return $null
            }

            if ($WhatIf) {
                Write-Log "[WhatIf] Would register new plugin package via Web API to solution '$SolutionUniqueName'"
                return $null
            }

            $bytes = [System.IO.File]::ReadAllBytes($Path)
            $content = [System.Convert]::ToBase64String($bytes)

            # Parse name and version from nupkg filename (e.g., ppds_PackageName.1.0.0.nupkg)
            $filename = [System.IO.Path]::GetFileName($Path)
            $filenameWithoutExt = $filename -replace '\.nupkg$', ''
            # Find version pattern - require at least X.Y.Z (3 parts) to avoid greedy matching issues
            if ($filenameWithoutExt -match '^(.+?)\.(\d+\.\d+\.\d+.*)$') {
                $parsedName = $Matches[1]
                $parsedVersion = $Matches[2]
            } else {
                $parsedName = $filenameWithoutExt
                $parsedVersion = "1.0.0"
            }

            Write-LogDebug "Package filename: $filename"
            Write-LogDebug "Parsed name: $parsedName"
            Write-LogDebug "Parsed version: $parsedVersion"

            # Use parsed name for both name and uniquename to ensure prefix consistency
            $body = @{
                name = $parsedName
                uniquename = $parsedName
                version = $parsedVersion
                content = $content
            }

            # Add solution header for plugin package registration
            $headersWithSolution = $AuthHeaders.Clone()
            $headersWithSolution["MSCRM.SolutionUniqueName"] = $SolutionUniqueName

            try {
                $response = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $headersWithSolution -Endpoint "pluginpackages" -Method POST -Body $body
                $packageId = $response.pluginpackageid
                Write-LogSuccess "Plugin package registered: $packageId"

                return Get-PluginAssembly -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Name $AssemblyName
            }
            catch {
                Write-LogError "Failed to register plugin package: $($_.Exception.Message)"
                return $null
            }
        }
    }
    else {
        $existingAssembly = Get-PluginAssembly -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Name $AssemblyName

        if ($existingAssembly) {
            Write-Log "Updating existing assembly: $AssemblyName"
            $pluginId = $existingAssembly.pluginassemblyid

            if ($WhatIf) {
                Write-Log "[WhatIf] pac plugin push --pluginId $pluginId --pluginFile $Path"
                return $existingAssembly
            }

            $result = pac plugin push --pluginId $pluginId --pluginFile $Path 2>&1
            if ($LASTEXITCODE -ne 0) {
                Write-LogError "Failed to update assembly: $result"
                return $null
            }
            Write-LogSuccess "Assembly updated successfully"
            return $existingAssembly
        }
        else {
            Write-Log "Registering new assembly: $AssemblyName"

            if ($WhatIf) {
                Write-Log "[WhatIf] Would register new assembly via Web API"
                return $null
            }

            $bytes = [System.IO.File]::ReadAllBytes($Path)
            $content = [System.Convert]::ToBase64String($bytes)

            try {
                $assembly = [System.Reflection.Assembly]::LoadFrom($Path)
                $assemblyName = $assembly.GetName()
                $version = $assemblyName.Version.ToString()
                $culture = if ($assemblyName.CultureInfo.Name) { $assemblyName.CultureInfo.Name } else { "neutral" }
                $publicKeyToken = [System.BitConverter]::ToString($assemblyName.GetPublicKeyToken()).Replace("-", "").ToLower()
                if (-not $publicKeyToken) { $publicKeyToken = "null" }
            }
            catch {
                Write-LogWarning "Could not read assembly metadata: $($_.Exception.Message)"
                $version = "1.0.0.0"
                $culture = "neutral"
                $publicKeyToken = "null"
            }

            $body = @{
                name = $AssemblyName
                content = $content
                isolationmode = 2
                sourcetype = 0
                version = $version
                culture = $culture
                publickeytoken = $publicKeyToken
            }

            try {
                $response = Invoke-DataverseApi -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Endpoint "pluginassemblies" -Method POST -Body $body
                Write-LogSuccess "Assembly registered successfully"

                # Fetch the registered assembly to get its ID
                # If this fails, return a synthetic object with the essential info
                $registeredAssembly = Get-PluginAssembly -ApiUrl $ApiUrl -AuthHeaders $AuthHeaders -Name $AssemblyName
                if ($registeredAssembly) {
                    return $registeredAssembly
                }

                # Fallback: construct minimal object from response if query failed
                Write-LogDebug "Could not query registered assembly, using response data"
                return [PSCustomObject]@{
                    pluginassemblyid = $response.pluginassemblyid
                    name = $AssemblyName
                    version = $version
                    publickeytoken = $publicKeyToken
                }
            }
            catch {
                Write-LogError "Failed to register assembly: $($_.Exception.Message)"
                return $null
            }
        }
    }
}