commands.ps1


<#
    .SYNOPSIS
        Retrieves and processes work items from a source Azure DevOps project.
         
    .DESCRIPTION
        This function retrieves work items from a source Azure DevOps project using a WIQL query, splits them into batches of 200, and processes them to extract detailed information.
         
    .PARAMETER SourceOrganization
        The name of the source Azure DevOps organization.
         
    .PARAMETER SourceProjectName
        The name of the source Azure DevOps project.
         
    .PARAMETER SourceToken
        The personal access token (PAT) for the source Azure DevOps organization.
         
    .PARAMETER Fields
        (Optional) The fields to retrieve for each work item. Default is a set of common fields including ID, Title, Description, WorkItemType, State, and Parent.
         
    .PARAMETER ApiVersion
        (Optional) The API version to use. Default is `7.1`.
         
    .EXAMPLE
        # Example: Retrieve and process work items from a source project
         
        Get-ADOSourceWorkItemsList -SourceOrganization "source-org" -SourceProjectName "source-project" -SourceToken "source-token"
         
    .NOTES
        This function is part of the ADO Tools module and adheres to the conventions used in the module for logging, error handling, and API interaction.
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Get-ADOSourceWorkItemsList {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$SourceOrganization,

        [Parameter(Mandatory = $true)]
        [string]$SourceProjectName,

        [Parameter(Mandatory = $true)]
        [string]$SourceToken,

        [Parameter(Mandatory = $false)]
        [Array]$Fields = @("System.Id", "System.Title", "System.Description", "System.WorkItemType", "System.State", "System.Parent"),

        [Parameter(Mandatory = $false)]
        [string]$ApiVersion = "7.1"
    )

    begin {
        # Log the start of the operation
        Write-PSFMessage -Level Verbose -Message "Starting retrieval of work items from project '$SourceProjectName' in organization '$SourceOrganization'."
        Invoke-TimeSignal -Start
    }

    process {
        try {
            # Execute WIQL query to retrieve work items
            Write-PSFMessage -Level Verbose -Message "Executing WIQL query to retrieve work items from project '$SourceProjectName' in organization '$SourceOrganization'."
            $query = "SELECT [System.Id] FROM WorkItems WHERE [System.TeamProject] = '$SourceProjectName' AND [System.WorkItemType] NOT IN ('Test Suite', 'Test Plan','Shared Steps','Shared Parameter','Feedback Request') ORDER BY [System.ChangedDate] asc"
            $result = Invoke-ADOWiqlQueryByWiql -Organization $SourceOrganization -Token $SourceToken -Project $SourceProjectName -Query $query -ApiVersion $ApiVersion

            # Log the number of work items retrieved
            Write-PSFMessage -Level Verbose -Message "Retrieved $($result.workItems.Count) work items from the WIQL query."

            # Split the work item IDs into batches of 200
            Write-PSFMessage -Level Verbose -Message "Splitting work item IDs into batches of 200."
            $witListBatches = [System.Collections.ArrayList]::new()
            $batch = @()
            $result.workItems.id | ForEach-Object -Process {
                $batch += $_
                if ($batch.Count -eq 200) {
                    Write-PSFMessage -Level Verbose -Message "Adding a batch of 200 work item IDs."
                    $null = $witListBatches.Add($batch)
                    $batch = @()
                }
            } -End {
                if ($batch.Count -gt 0) {
                    Write-PSFMessage -Level Verbose -Message "Adding the final batch of $($batch.Count) work item IDs."
                    $null = $witListBatches.Add($batch)
                }
            }

            # Log the number of batches created
            Write-PSFMessage -Level Verbose -Message "Created $($witListBatches.Count) batches of work item IDs."

            $wiResult = @()
            # Process each batch
            foreach ($witBatch in $witListBatches) {
                if($witBatch.Count -eq 0) {
                    continue
                }
                Write-PSFMessage -Level Verbose -Message "Processing a batch of $($witBatch.Count) work item IDs."
                $wiResult += Get-ADOWorkItemsBatch -Organization $SourceOrganization -Token $SourceToken -Project $SourceProjectName -Ids $witBatch -Fields $Fields -ApiVersion $ApiVersion
            }

            # Log the number of work items retrieved in detail
            Write-PSFMessage -Level Verbose -Message "Retrieved detailed information for $($wiResult.Count) work items."

            # Format work items into a list
            $sourceWorkItemsList = $wiResult.fields | ForEach-Object {
                [PSCustomObject]@{
                    "System.Id"                 = $_."System.Id"
                    "System.WorkItemType"       = $_."System.WorkItemType"
                    "System.Description"        = $_."System.Description"
                    "System.State"              = $_."System.State"
                    "System.Title"              = $_."System.Title"
                    "Custom.SourceWorkitemId"   = $_."Custom.SourceWorkitemId"
                    "System.Parent"             = if ($_.PSObject.Properties["System.Parent"] -and $_."System.Parent") {
                                                    $_."System.Parent"
                                                } else {
                                                    0
                                                }
                }
            }

            # Log the work items retrieved
            Write-PSFMessage -Level Verbose -Message "Formatted work items into a list. Total items: $($sourceWorkItemsList.Count)."
            #$sourceWorkItemsList | Format-Table -AutoSize

            # Return the formatted work items list
            return $sourceWorkItemsList
        } catch {
            # Log the error
            Write-PSFMessage -Level Error -Message "An error occurred: $($_.Exception.Message)"
            Stop-PSFFunction -Message "Stopping because of errors."
        }
    }

    end {
        # Log the end of the operation
        Write-PSFMessage -Level Verbose -Message "Completed retrieval of work items from project '$SourceProjectName'."
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        Processes a source work item from Azure DevOps and creates or updates a corresponding work item in the target Azure DevOps project, maintaining parent-child relationships.
         
    .DESCRIPTION
        This function processes a source work item retrieved from Azure DevOps, builds the necessary JSON payload, and creates or updates a corresponding work item in the target Azure DevOps project. It also handles parent-child relationships by linking the work item to its parent if applicable. If the parent work item does not exist in the target project, it is created first.
         
    .PARAMETER SourceWorkItem
        The source work item object containing the fields to process.
         
    .PARAMETER SourceOrganization
        The name of the source Azure DevOps organization.
         
    .PARAMETER SourceProjectName
        The name of the source Azure DevOps project.
         
    .PARAMETER SourceToken
        The personal access token (PAT) for the source Azure DevOps organization.
         
    .PARAMETER TargetOrganization
        The name of the target Azure DevOps organization.
         
    .PARAMETER TargetProjectName
        The name of the target Azure DevOps project.
         
    .PARAMETER TargetToken
        The personal access token (PAT) for the target Azure DevOps organization.
         
    .PARAMETER TargetWorkItemList
        A hashtable containing mappings of source work item IDs to target work item URLs for parent-child relationships. Passed by reference.
         
    .PARAMETER ApiVersion
        (Optional) The API version to use. Default is `7.1`.
         
    .EXAMPLE
        # Example 1: Process a single work item and create it in the target project
         
        Invoke-ADOWorkItemsProcessing -SourceWorkItem $sourceWorkItem -SourceOrganization "source-org" `
            -SourceProjectName "source-project" -SourceToken "source-token" `
            -TargetOrganization "target-org" -TargetProjectName "target-project" `
            -TargetToken "target-token" -TargetWorkItemList ([ref]$targetWorkItemList)
         
    .NOTES
        This function is part of the ADO Tools module and adheres to the conventions used in the module for logging, error handling, and API interaction.
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Invoke-ADOWorkItemsProcessing { 
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [PSCustomObject]$SourceWorkItem,

        [Parameter(Mandatory = $true)]
        [string]$SourceOrganization,

        [Parameter(Mandatory = $true)]
        [string]$SourceProjectName,

        [Parameter(Mandatory = $true)]
        [string]$SourceToken,

        [Parameter(Mandatory = $true)]
        [string]$TargetOrganization,

        [Parameter(Mandatory = $true)]
        [string]$TargetProjectName,

        [Parameter(Mandatory = $true)]
        [string]$TargetToken,

        [Parameter(Mandatory = $true)]
        [ref]$TargetWorkItemList,

        [Parameter(Mandatory = $false)]
        [string]$ApiVersion = $Script:ADOApiVersion
    )

    begin {
        # Log the start of the operation
        Write-PSFMessage -Level Host -Message "Processing work item ID: $($SourceWorkItem.'System.Id'). Title: $($SourceWorkItem.'System.Title')."
    }

    process {
        try {
            # Build the JSON payload for the new work item
            $body = @(
                @{
                    op    = "add"
                    path  = "/fields/System.Title"
                    value = "$($SourceWorkItem."System.Title")"
                }
                @{
                    op    = "add"
                    path  = "/fields/System.Description"
                    value = "$($SourceWorkItem."System.Description")"
                }
                @{
                    op    = "add"
                    path  = "/fields/Custom.SourceWorkitemId"
                    value = "$($SourceWorkItem."System.ID")"
                }
                @{
                    op    = "add"
                    path  = "/fields/System.State"
                    value = "$($SourceWorkItem."System.State")"
                }
            )

            # Handle parent-child relationships
            if ($SourceWorkItem."System.Parent") {
                if (-not $TargetWorkItemList.Value[$SourceWorkItem."System.Parent"]) {
                    Write-PSFMessage -Level Verbose -Message "Parent work item ID $($SourceWorkItem.'System.Parent') not found in target work item list. Creating it..."
                    $SourceWorkItemsList = (Get-ADOSourceWorkItemsList -SourceOrganization $sourceOrganization -SourceProjectName $SourceProjectName -SourceToken $SourceToken)
                    $parentWorkItem = $SourceWorkItemsList | Where-Object { $_."System.Id" -eq $SourceWorkItem.'System.Parent' }
                    # Create the parent work item first
                    Invoke-ADOWorkItemsProcessing -SourceWorkItem $parentWorkItem -SourceOrganization $SourceOrganization -SourceProjectName $SourceProjectName -SourceToken $SourceToken -TargetOrganization $TargetOrganization `
                        -TargetProjectName $TargetProjectName -TargetToken $TargetToken `
                        -TargetWorkItemList ($TargetWorkItemList) -ApiVersion $ApiVersion                  
                } 
                
                
                $body += @{
                    op    = "add"
                    path  = "/relations/-"
                    value = @{
                        rel = "System.LinkTypes.Hierarchy-Reverse"
                        url = "$(($TargetWorkItemList).Value[$SourceWorkItem."System.Parent"])"
                        attributes = @{
                            comment = "Making a new link for the dependency"
                        }
                    }
                }
                
            }

            # Convert the payload to JSON
            $body = $body | ConvertTo-Json -Depth 10
            # Log the creation of the target work item
            Write-PSFMessage -Level Verbose -Message "Creating target work item for source work item ID: $($SourceWorkItem.'System.Id')."

            # Call the Add-ADOWorkItem function to create the work item
            $targetWorkItem = Add-ADOWorkItem -Organization $TargetOrganization `
                                              -Token $TargetToken `
                                              -Project $TargetProjectName `
                                              -Type "`$$($SourceWorkItem."System.WorkItemType")" `
                                              -Body $body `
                                              -ApiVersion $ApiVersion

            if(-not $targetWorkItem.url) {
                # Add the target work item URL to the TargetWorkItemList
                Write-PSFMessage -Level Error -Message "Error: $($targetWorkItem.url) for source work item ID: $($SourceWorkItem.'System.Id')."
            }
            # Log the successful creation of the target work items list
            $TargetWorkItemList.Value[$SourceWorkItem.'System.Id'] = $targetWorkItem.url

        } catch {
            # Log the error
            Write-PSFMessage -Level Error -Message "Failed to process work item ID: $($SourceWorkItem.'System.Id'). Error: $($_)"
        }
    }

    end {
        # Log the end of the operation
        Write-PSFMessage -Level Host -Message "Completed processing of work item ID: $($SourceWorkItem.'System.Id')."
    }
}


<#
    .SYNOPSIS
        Handle time measurement
         
    .DESCRIPTION
        Handle time measurement from when a cmdlet / function starts and ends
         
        Will write the output to the verbose stream (Write-PSFMessage -Level Verbose)
         
    .PARAMETER Start
        Switch to instruct the cmdlet that a start time registration needs to take place
         
    .PARAMETER End
        Switch to instruct the cmdlet that a time registration has come to its end and it needs to do the calculation
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -Start
         
        This will start the time measurement for any given cmdlet / function
         
    .EXAMPLE
        PS C:\> Invoke-TimeSignal -End
         
        This will end the time measurement for any given cmdlet / function.
        The output will go into the verbose stream.
         
    .NOTES
        This is refactored function from d365fo.tools
         
        Original Author: Mötz Jensen (@Splaxi)
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>

function Invoke-TimeSignal {
    [CmdletBinding(DefaultParameterSetName = 'Start')]
    param (
        [Parameter(Mandatory = $true, ParameterSetName = 'Start', Position = 1 )]
        [switch] $Start,
        
        [Parameter(Mandatory = $True, ParameterSetName = 'End', Position = 2 )]
        [switch] $End
    )

    $Time = (Get-Date)

    $Command = (Get-PSCallStack)[1].Command

    if ($Start) {
        if ($Script:TimeSignals.ContainsKey($Command)) {
            Write-PSFMessage -Level Verbose -Message "The command '$Command' was already taking part in time measurement. The entry has been update with current date and time."
            $Script:TimeSignals[$Command] = $Time
        }
        else {
            $Script:TimeSignals.Add($Command, $Time)
        }
    }
    else {
        if ($Script:TimeSignals.ContainsKey($Command)) {
            $TimeSpan = New-TimeSpan -End $Time -Start (($Script:TimeSignals)[$Command])

            Write-PSFMessage -Level Verbose -Message "Total time spent inside the function was $TimeSpan" -Target $TimeSpan -FunctionName $Command -Tag "TimeSignal"
            $null = $Script:TimeSignals.Remove($Command)
        }
        else {
            Write-PSFMessage -Level Verbose -Message "The command '$Command' was never started to take part in time measurement."
        }
    }
}


<#
    .SYNOPSIS
        Performs a code review of a Microsoft Dynamics 365 Business Central AL codebase using Azure OpenAI.
         
    .DESCRIPTION
        This function indexes a codebase, searches for relevant files, and queries Azure OpenAI to perform a detailed code review.
        It generates a report based on the provided user query and context extracted from the codebase.
         
    .PARAMETER OpenAIEndpoint
        The Azure OpenAI endpoint URL.
         
    .PARAMETER OpenAIApiKey
        The API key for authenticating with Azure OpenAI.
         
    .PARAMETER CodebasePath
        The path to the codebase to be indexed and reviewed.
         
    .PARAMETER Prompt
        The prompt to be sent to Azure OpenAI for code review.
         
    .PARAMETER Files
        (Optional) A list of specific file to search for in the codebase. Provide a paths to the files.
         
    .PARAMETER ExcludedFolders
        (Optional) A list of folder names to exclude from indexing.
         
    .PARAMETER FileExtensions
        (Optional) A list of file extensions to include in the indexing process.
         
    .EXAMPLE
        # Define the required parameters
        $openaiEndpoint = "https://YourAzureOpenApiEndpoint"
        $openaiApiKey = "your-api-key"
        $codebasePath = "C:\Projects\MyCodebase"
        $prompt = "Analyze the code for bugs and improvements."
        $filenames = @("example1.al", "example2.al")
         
        # Call the function
        Invoke-ADOAzureOpenAI -OpenAIEndpoint $openaiEndpoint `
            -OpenAIApiKey $openaiApiKey `
            -CodebasePath $codebasePath `
            -Prompt $prompt `
            -Files $filenames
         
    .NOTES
        This function uses PSFramework for logging and exception handling.
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Invoke-ADOAICodeReview {
    param (
        [Parameter(Mandatory = $true)]
        [string]$OpenAIEndpoint,
        [Parameter(Mandatory = $true)]
        [string]$OpenAIApiKey,
        [Parameter(Mandatory = $true)]
        [string]$CodebasePath,
        [Alias("UserQuery")]
        [string]$Prompt,
        [Parameter(Mandatory = $true)]
        [Alias("Filenames")]
        [array]$Files = @(),
        [array]$ExcludedFolders = @(".git", "node_modules", ".vscode"),
        [array]$FileExtensions = @(".al", ".json", ".xml", ".txt")
    )
    begin{

        $IndexPath = "c:\temp\codebase_index.json"
        $ErrorActionPreference = "Stop"

        # Validate parameters
        if (-not (Test-Path $CodebasePath)) {
            throw "The specified codebase path does not exist: $CodebasePath"
        }        
        #Invoke-TimeSignal -Start
    }
    process{
        if (Test-PSFFunctionInterrupt) { return }

        try {
            # Step 1: Index the codebase
            Write-PSFMessage -Level Host -Message "Indexing the codebase at path: $CodebasePath"
            $index = @()
    
            If(-not (Test-Path $IndexPath)) {
                $null = New-Item -Path $IndexPath -ItemType File -Force
            }
    
            function CheckPath {
                param (
                    $filePath
                )
                (($ExcludedFolders | ForEach-Object { 
                    IF($filePath -match  $_)
                    {
                        return $true
                    }
                  }
                ))
                return $false
            }
            Get-ChildItem -Path $codebasePath -Recurse -File | Where-Object {
                ($FileExtensions -contains $_.Extension) -and (-not (CheckPath $_.FullName))
            } | ForEach-Object {
                $filePath = $_.FullName
                try {
                    $content = Get-Content -Path $filePath -Raw
                    # Extract only the content inside CDATA tags
                    $cleanedContent = [regex]::Matches($content, "<!\[CDATA\[(.*?)\]\]>", [System.Text.RegularExpressions.RegexOptions]::Singleline) |
                        ForEach-Object { $_.Groups[1].Value }
            
                    # Combine all extracted CDATA content into a single string (if there are multiple CDATA sections)
                    $cleanedContent = $cleanedContent -join "`n"
            
                    # Add the cleaned content to the index if it's not empty
                    if (-not [string]::IsNullOrWhiteSpace($cleanedContent)) {
                        $index += @{
                            FilePath = "$filePath"
                            Content = $cleanedContent
                        }
                    }
                } catch {
                    Write-PSFMessage -Level Error -Message "Failed to process file: $filePath. Error: $_"
                }
            }
    
            $index | ConvertTo-Json -Depth 10 | Set-Content -Path $IndexPath
            Write-PSFMessage -Level Host -Message "Indexing completed. Output saved to: $IndexPath"
    
            # Step 2: Search the codebase
            Write-PSFMessage -Level Host -Message "Searching the codebase for relevant files."
            $index = Get-Content -Path $IndexPath | ConvertFrom-Json
            $context = @()
    
            foreach ($file in $index) {
                if ($file.Content -match [regex]::Escape("")) {
                    $context += @{
                        FilePath = $file.FilePath
                        Snippet = $file.Content -replace "(?s).{0,50}" + [regex]::Escape("") + ".{0,50}", "...$&..."
                    }
                }
            }
    
            Write-PSFMessage -Level Host -Message "Filtering results based on provided filenames."
            $context = $context | Where-Object {
                $filePath = $_.FilePath
                $Files | ForEach-Object { $filePath -like "*$_" } | Where-Object { $_ } | Measure-Object | Select-Object -ExpandProperty Count
            }
    
            Write-PSFMessage -Level Host -Message "Search completed. Found $($context.Count) matching files."
    
            # Step 3: Query Azure OpenAI
            Write-PSFMessage -Level Host -Message "Sending request to Azure OpenAI for code review."
            $fullPrompt = "You are an assistant that helps with code suggestions. Here is the context:\n"
            foreach ($snippet in $context) {
                $fullPrompt += "File: $($snippet.FilePath)\nCode:\n$($snippet.Snippet)\n\n"
            }
            $fullPrompt += "User Prompt: $Prompt"
    
            $messages = @(
                    @{ role = "system"; content = "You are a helpful assistant for code suggestions." },
                    @{ role = "user"; content = $fullPrompt }
                )

            $response = Invoke-ADOAzureOpenAI -OpenAIEndpoint $openaiEndpoint -OpenAIApiKey $openaiApiKey -Messages $messages 
            Write-PSFMessage -Level Verbose -Message "Azure OpenAI response received."
    
            # Output the response
            return $response.choices[0].message.content
        } catch {
            Write-PSFMessage -Level Error -Message "An error occurred: $($_.Exception.Message)"
            throw
        }
    }
    end{
        # Log the end of the operation
        Write-PSFMessage -Level Host -Message "Request completed."
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        Performs a code review of a Microsoft Dynamics 365 Business Central AL codebase using Azure OpenAI.
         
    .DESCRIPTION
        This function indexes a codebase, searches for relevant files, and queries Azure OpenAI to perform a detailed code review.
        It generates a report based on the provided user query and context extracted from the codebase.
         
    .PARAMETER OpenAIEndpoint
        The Azure OpenAI endpoint URL.
         
    .PARAMETER OpenAIApiKey
        The API key for authenticating with Azure OpenAI.
         
    .PARAMETER Messages
        The messages to be sent to Azure OpenAI.
         
    .EXAMPLE
        # Define the required parameters
        $openaiEndpoint = "https://YourAzureOpenApiEndpoint"
        $openaiApiKey = "your-api-key"
        $prompt = "Who are you?"
        $messages = @(
            @{ role = "system"; content = "You are a helpful assistant." },
            @{ role = "user"; content = $prompt }
        )
         
         
        # Call the function
        Invoke-ADOAzureOpenAI -OpenAIEndpoint $openaiEndpoint `
            -OpenAIApiKey $openaiApiKey `
            -CodebasePath $codebasePath `
            -Messages $messages
         
    .OUTPUTS
        Returns a hashtable containing the response from Azure OpenAI.
        The hashtable includes the following keys:
            - id
            - object
            - created
            - model
            - usage
            - choices
         
         
        #Example response
        @"
        {
            "model": "o1-2024-12-17",
            "created": 1745923901,
            "object": "chat.completion",
            "id": "chatcmpl-BRcqr2gTdJH2EeL63R3jEM5ZVOpUD",
            "choices": [
            {
                "content_filter_results": {
                    "hate": {
                        "filtered": false,
                        "severity": "safe"
                    },
                    "self_harm": {
                        "filtered": false,
                        "severity": "safe"
                    },
                    "sexual": {
                        "filtered": false,
                        "severity": "safe"
                    },
                    "violence": {
                        "filtered": false,
                        "severity": "safe"
                    }
                },
                "finish_reason": "stop",
                "index": 0,
                "logprobs": null,
                "message": {
                    "content": "I’m ChatGPT, a large language model trained by OpenAI. I’m here to help you with your questions, provide information, and engage in conversation. How can I assist you today?",
                    "refusal": null,
                    "role": "assistant"
                }
            }
            ],
            "usage": {
                "completion_tokens": 178,
                "completion_tokens_details": {
                    "accepted_prediction_tokens": 0,
                    "audio_tokens": 0,
                    "reasoning_tokens": 128,
                    "rejected_prediction_tokens": 0
                },
                "prompt_tokens": 20,
                "prompt_tokens_details": {
                    "audio_tokens": 0,
                    "cached_tokens": 0
                },
                "total_tokens": 198
            }
        }
        "@
         
         
    .NOTES
        This function uses PSFramework for logging and exception handling.
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Invoke-ADOAzureOpenAI {
    param (
        [Parameter(Mandatory = $true)]
        [string]$OpenAIEndpoint,
        [Parameter(Mandatory = $true)]
        [string]$OpenAIApiKey,
        [Array]$Messages
    )
    begin{       
        $ErrorActionPreference = "Stop"
        Invoke-TimeSignal -Start
        Write-PSFMessage -Level Verbose -Message "Starting Azure OpenAI request."
        
        # Validate messages array
        if (-not $Messages -or $Messages.Count -eq 0) {
            throw "The Messages parameter cannot be null or empty."
        }

        $body = @{
            messages = $Messages
        } | ConvertTo-Json -Depth 10

        $headers = @{
            "Content-Type" = "application/json"
            "api-key" = $OpenAIApiKey
        }
    }
    process{
        if (Test-PSFFunctionInterrupt) { return }

        try {
            $response = Invoke-RestMethod -Uri $OpenAIEndpoint -Method Post -Headers $headers -Body $body
            Write-PSFMessage -Level Verbose -Message "Azure OpenAI response received."
    
            # Output the response
            return $response | Select-PSFObject *

        } catch {
            Write-PSFMessage -Level Error -Message "An error occurred: $($_.Exception.Message)"
            throw
        }
    }
    end{
        # Log the end of the operation
        Write-PSFMessage -Level Verbose -Message "Request completed."
        Invoke-TimeSignal -End
    }
}


<#
    .SYNOPSIS
        Migrates a project from a source Azure DevOps organization to a target Azure DevOps organization.
         
    .DESCRIPTION
        This function facilitates the migration of a project from one Azure DevOps organization to another.
        It retrieves the source project details, validates its existence, and prepares for migration to the target organization.
         
    .PARAMETER SourceOrganization
        The name of the source Azure DevOps organization.
         
    .PARAMETER TargetOrganization
        The name of the target Azure DevOps organization.
         
    .PARAMETER SourceProjectName
        The name of the project in the source organization to be migrated.
         
    .PARAMETER TargetProjectName
        The name of the project in the target organization where the source project will be migrated.
         
    .PARAMETER SourceOrganizationToken
        The authentication token for accessing the source Azure DevOps organization.
         
    .PARAMETER TargetOrganizationToken
        The authentication token for accessing the target Azure DevOps organization.
         
    .PARAMETER ApiVersion
        The version of the Azure DevOps REST API to use. Default is "7.1".
         
    .EXAMPLE
        $sourceOrg = "sourceOrg"
        $targetOrg = "targetOrg"
        $sourceProjectName = "sourceProject"
        $targetProjectName = "targetProject"
        $sourceOrgToken = "sourceOrgToken"
        $targetOrgToken = "targetOrgToken"
         
        Invoke-ADOProjectMigration -SourceOrganization $sourceOrg `
            -TargetOrganization $targetOrg `
            -SourceProjectName $sourceProjectName `
            -TargetProjectName $targetProjectName `
            -SourceOrganizationToken $sourceOrgToken `
            -TargetOrganizationToken $targetOrgToken
         
        This example migrates the project "sourceProject" from the organization "sourceOrg" to the organization "targetOrg".
         
    .NOTES
        This function uses PSFramework for logging and exception handling.
         
        Author: Oleksandr Nikolaiev (@onikolaiev)
#>


function Invoke-ADOProjectMigration {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $true)]
        [string]$SourceOrganization,

        [Parameter(Mandatory = $true)]
        [string]$TargetOrganization,

        [Parameter(Mandatory = $true)]
        [string]$SourceProjectName,

        [Parameter(Mandatory = $true)]
        [string]$TargetProjectName,

        [Parameter(Mandatory = $true)]
        [string]$SourceOrganizationToken,

        [Parameter(Mandatory = $true)]
        [string]$TargetOrganizationToken,

        [Parameter(Mandatory = $false)]
        [string]$ApiVersion = $Script:ADOApiVersion
    )
    begin{
        Invoke-TimeSignal -Start
        $ErrorActionPreference = "Stop"
    }
    process{
        if (Test-PSFFunctionInterrupt) { return }

        # Log start of migration
        Write-PSFMessage -Level Host -Message "Starting migration from source organization '$sourceOrganization' to target organization '$targetOrganization'."
        Convert-FSCPSTextToAscii -Text "Start migration" -Font "Standard" 
        ## GETTING THE SOURCE PROJECT INFORMATION
        Write-PSFMessage -Level Host -Message "Fetching source project '$sourceProjectName' from organization '$sourceOrganization'."
        $sourceProjecttmp = (Get-ADOProjectList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -StateFilter All).Where({$_.name -eq $sourceProjectName})
        if (-not $sourceProjecttmp) {
            Write-PSFMessage -Level Error -Message "Source project '$sourceProjectName' not found in organization '$sourceOrganization'. Exiting."
            return
        }
        Write-PSFMessage -Level Host -Message "Source project '$sourceProjectName' found. Fetching detailed information."
        $sourceProject = Get-ADOProject -Organization $sourceOrganization -Token $sourceOrganizationtoken -ProjectId "$($sourceProjecttmp.id)" -IncludeCapabilities -ApiVersion $ApiVersion
        $sourceProjectVersionControl = $sourceProject.capabilities.versioncontrol
        $sourceProjectProcess = Get-ADOProcess -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessTypeId "$($sourceProject.capabilities.processTemplate.templateTypeId)"
        $sourceProjectProcessParentProcess = Get-ADOProcess -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessTypeId "$($sourceProjectProcess.parentProcessTypeId)"

        Write-PSFMessage -Level Host -Message "Source project process: '$($sourceProjectProcess.name)' (ID: $($sourceProjectProcess.typeId))."

        Convert-FSCPSTextToAscii -Text "Migrate processes.." -Font "Standard" 
        ### PROCESSING PROCESS
        Write-PSFMessage -Level Host -Message "Checking if target process '$($sourceProjectProcess.name)' exists in target organization '$targetOrganization'."
        $targetProjectProcess = (Get-ADOProcessList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion).Where({$_.name -eq $sourceProjectProcess.name})

        ## Check if the target process already exists. If not, create it.
        if (-not $targetProjectProcess) {
            Write-PSFMessage -Level Host -Message "Target process '$($sourceProjectProcess.name)' does not exist. Creating it in target organization '$targetOrganization'."
            $body = @{
                name = $sourceProjectProcess.name
                parentProcessTypeId = $sourceProjectProcessParentProcess.typeId
                description = $sourceProjectProcess.description
                customizationType = $sourceProjectProcess.customizationType
                isEnabled = "true"
            }
            $body = $body | ConvertTo-Json -Depth 10    
            Write-PSFMessage -Level Verbose -Message "Adding process '$($sourceProjectProcess.name)' to target organization '$($targetOrganization)' with the following details: $($body)"
            $targetProjectProcess = Add-ADOProcess -Organization $targetOrganization -Token $targetOrganizationtoken -Body $body -ApiVersion $ApiVersion
        } else {
            Write-PSFMessage -Level Host -Message "Target process '$($sourceProjectProcess.name)' already exists in target organization '$targetOrganization'."
        }
        Convert-FSCPSTextToAscii -Text "Migrate wit fields.." -Font "Standard" 
        ## PROCESSING WIT FIELDS
        Write-PSFMessage -Level Host -Message "Fetching custom work item fields from source organization '$sourceOrganization'."
        $sourceWitFields = (Get-ADOWitFieldList -Organization $sourceOrganization -Token $sourceOrganizationtoken -Expand "extensionFields" -ApiVersion $ApiVersion ).Where({$_.referenceName.startswith("Custom.")})
        Write-PSFMessage -Level Host -Message "Found $($sourceWitFields.Count) custom fields in source organization."

        Write-PSFMessage -Level Host -Message "Fetching custom work item fields from target organization '$targetOrganization'."
        $targetWitFields = (Get-ADOWitFieldList -Organization $targetOrganization -Token $targetOrganizationtoken -Expand "extensionFields" -ApiVersion $ApiVersion).Where({$_.referenceName.startswith("Custom.")})
        Write-PSFMessage -Level Host -Message "Found $($targetWitFields.Count) custom fields in target organization."

        $sourceWitFields | ForEach-Object {
            $witField = $_
            $targetWitField = $targetWitFields.Where({$_.name -eq $witField.name})
            
            if (-not $targetWitField) {
                Write-PSFMessage -Level Host -Message "Custom field '$($witField.name)' does not exist in target organization. Adding it."
                $sourceWitField = Get-ADOWitField -Organization $sourceOrganization -Token $sourceOrganizationtoken -FieldNameOrRefName "$($witField.referenceName)" -ApiVersion $ApiVersion
                $body = @{
                    name = $sourceWitField.name
                    referenceName = $sourceWitField.referenceName
                    description = $sourceWitField.description
                    type = $sourceWitField.type
                    usage = $sourceWitField.usage
                    readOnly = $sourceWitField.readOnly
                    isPicklist = $sourceWitField.isPicklist
                    isPicklistSuggested = $sourceWitField.isPicklistSuggested
                    isIdentity = $sourceWitField.isIdentity
                    isQueryable = $sourceWitField.isQueryable
                    isLocked = $sourceWitField.isLocked
                    canSortBy = $sourceWitField.canSortBy
                    supportedOperations = $sourceWitField.supportedOperations
                }

                $body = $body | ConvertTo-Json -Depth 10
                Write-PSFMessage -Level Verbose -Message "Adding custom field '$($sourceWitField.name)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                $targetWitField = Add-ADOWitField -Organization $targetOrganization -Token $targetOrganizationtoken -Body $body -ApiVersion $ApiVersion
            } else {
                Write-PSFMessage -Level Host -Message "Custom field '$($witField.name)' already exists in target organization. Skipping."
            }
        }
        ### Creating SourceWorkitemId field for the target organization
        $sourceWorkitemIdFieldName = "SourceWorkitemId"
        $sourceWorkitemIdReferenceName = "Custom.$sourceWorkitemIdFieldName"
        $body = @{
            name = $sourceWorkitemIdFieldName
            referenceName = "$sourceWorkitemIdReferenceName"
            description = ""
            type = "string"
            usage = "workItem"
            readOnly = $false
            isQueryable = $true
            isLocked = $false
            canSortBy = $true
        }
        
        $body = $body | ConvertTo-Json -Depth 10
        
        $null = Add-ADOWitField -Organization $targetOrganization -Token $targetOrganizationtoken -Body $body -ApiVersion $ApiVersion -ErrorAction SilentlyContinue -WarningAction SilentlyContinue

        Convert-FSCPSTextToAscii -Text "Migrate work item types.." -Font "Standard" 
        ## PROCESSING WORK ITEM TYPES
        Write-PSFMessage -Level Host -Message "Fetching custom work item types from source process '$($sourceProjectProcess.name)'."
        $sourceWitList = (Get-ADOWorkItemTypeList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -Expand layout).Where({$_.customization -eq 'inherited'})
        Write-PSFMessage -Level Host -Message "Found $($sourceWitList.Count) custom work item types in source process."

        Write-PSFMessage -Level Host -Message "Fetching custom work item types from target process '$($targetProjectProcess.name)'."
        $targetWitList = (Get-ADOWorkItemTypeList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -Expand layout).Where({$_.customization -eq 'inherited'})
        Write-PSFMessage -Level Host -Message "Found $($targetWitList.Count) custom work item types in target process."

        $sourceWitList | ForEach-Object {
            $wit = $_
            $targetWit = $targetWitList.Where({$_.name -eq $wit.name})
            
            if (-not $targetWit) {
                Write-PSFMessage -Level Host -Message "Work item type '$($wit.name)' does not exist in target process. Adding it."
                $sourceWit = Get-ADOWorkItemType -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName "$($wit.referenceName)"
                $body = @{
                    name = $sourceWit.name
                    description = $sourceWit.description
                    color = $sourceWit.color
                    icon = $sourceWit.icon
                    isDisabled = $sourceWit.isDisabled
                    inheritsFrom = $sourceWit.inherits
                }
                $body = $body | ConvertTo-Json -Depth 10
                Write-PSFMessage -Level Verbose -Message "Adding work item type '$($sourceWit.name)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                $targetWit = Add-ADOWorkItemType -Organization $targetOrganization -Token $targetOrganizationtoken -ProcessId "$($targetProjectProcess.typeId)" -Body $body
            } else {
                Write-PSFMessage -Level Host -Message "Work item type '$($wit.name)' already exists in target process. Skipping."
            }
        }
        $targetWitList = (Get-ADOWorkItemTypeList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -Expand layout).Where({$_.customization -eq 'inherited'})

        Convert-FSCPSTextToAscii -Text "Migrate fields.." -Font "Standard" 
        ## Process Fields
        Write-PSFMessage -Level Host -Message "Starting to process custom fields for work item types."
        $sourceWitList | ForEach-Object {
            $wit = $_
            Write-PSFMessage -Level Host -Message "Processing fields for work item type '$($wit.name)'."
            $targetWit = $targetWitList.Where({$_.name -eq $wit.name})
            $customFields = (Get-ADOWorkItemTypeFieldList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName $wit.referenceName).Where({$_.customization -ne "system"})
            $customFields | ForEach-Object {
                $field = $_
                Write-PSFMessage -Level Host -Message "Checking field '$($field.name)' in target process."
                $targetField = (Get-ADOWorkItemTypeFieldList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName $targetWit.referenceName).Where({$_.name -eq $field.name})
                
                if (-not $targetField) {
                    Write-PSFMessage -Level Host -Message "Field '$($field.name)' does not exist in target process. Adding it."
                    $sourceField = Get-ADOWorkItemTypeField -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName "$($wit.referenceName)" -FieldRefName "$($field.referenceName)" -Expand all
                    $body = @{
                        allowGroups = $sourceField.allowGroups
                        allowedValues = $sourceField.allowedValues
                        description = $sourceField.description
                        defaultValue = $sourceField.defaultValue
                        readOnly = $sourceField.readOnly
                        referenceName = $sourceField.referenceName
                        required = $sourceField.required
                    }
                    $body = $body | ConvertTo-Json -Depth 10
                    Write-PSFMessage -Level Verbose -Message "Adding field '$($sourceField.name)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                    $targetField = Add-ADOWorkItemTypeField -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName $targetWit.referenceName -Body $body
                } else {
                    Write-PSFMessage -Level Host -Message "Field '$($field.name)' already exists in target process. Skipping."
                }
            } 
        }
        Convert-FSCPSTextToAscii -Text "Migrate behaviors.." -Font "Standard" 
        ## Process Behaviors
        Write-PSFMessage -Level Host -Message "Starting to process behaviors for work item types."
        $sourceWitList | ForEach-Object {
            $wit = $_
            Write-PSFMessage -Level Host -Message "Processing behaviors for work item type '$($wit.name)'."
            #$targetWit = $targetWitList.Where({$_.name -eq $wit.name})
            $sourceBehaviors = (Get-ADOProcessBehaviorList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -Expand "fields")
            $targetBehaviors = (Get-ADOProcessBehaviorList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -Expand "fields")
            $sourceBehaviors | ForEach-Object {
                $behavior = $_
                Write-PSFMessage -Level Host -Message "Checking behavior '$($behavior.name)' in target process."
                $targetBehavior = $targetBehaviors.Where({$_.name -eq $behavior.name})
                
                if (-not $targetBehavior) {
                    Write-PSFMessage -Level Verbose -Message "Behavior '$($behavior.name)' does not exist in target process. Adding it."
                    $sourceBehavior = Get-ADOProcessBehavior -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -BehaviorRefName "$($behavior.referenceName)"  -Expand "fields"
                    $body = @{
                        color = $sourceBehavior.color
                        inherits = $sourceBehavior.inherits
                        name = $sourceBehavior.name
                        referenceName = $sourceBehavior.referenceName
                    }
                    $body = $body | ConvertTo-Json -Depth 10
                    Write-PSFMessage -Level Host -Message "Adding behavior '$($sourceBehavior.name)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                    $targetBehavior = Add-ADOProcessBehavior -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -Body $body
                } else {
                    Write-PSFMessage -Level Host -Message "Behavior '$($behavior.name)' already exists in target process. Skipping."
                }
            }
        }
        Convert-FSCPSTextToAscii -Text "Migrate picklists.." -Font "Standard" 
        ## Process Picklists
        Write-PSFMessage -Level Host -Message "Starting to process picklists."
        $sourcePicklists = (Get-ADOPickListList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion)
        $targetPicklists = (Get-ADOPickListList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion)
        $sourcePicklists | ForEach-Object {
            $picklist = $_
            Write-PSFMessage -Level Host -Message "Checking picklist '$($picklist.name)' in target process."
            $targetPicklist = $targetPicklists.Where({$_.name -eq $picklist.name})
            
            if (-not $targetPicklist) {
                Write-PSFMessage -Level Verbose -Message "Picklist '$($picklist.name)' does not exist in target process. Adding it."
                $sourcePicklist = Get-ADOPickList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ListId "$($picklist.id)"
                $body = @{
                    name = $sourcePicklist.name
                    type = $sourcePicklist.type
                    isSuggested = $sourcePicklist.isSuggested
                    items = $sourcePicklist.items
                }
                $body = $body | ConvertTo-Json -Depth 10
                Write-PSFMessage -Level Host -Message "Adding picklist '$($sourcePicklist.name)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                $targetPicklist = Add-ADOPickList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -Body $body
            } else {
                Write-PSFMessage -Level Host -Message "Picklist '$($picklist.name)' already exists in target process. Skipping."
            }
        }
        Convert-FSCPSTextToAscii -Text "Migrate states.." -Font "Standard" 
        ## Process States
        Write-PSFMessage -Level Host -Message "Starting to process states for work item types."
        $sourceWitList | ForEach-Object {
            $wit = $_
            Write-PSFMessage -Level Host -Message "Processing states for work item type '$($wit.name)'."
            $targetWit = $targetWitList.Where({$_.name -eq $wit.name})  
            $sourceStates = (Get-ADOWorkItemTypeStateList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName "$($wit.referenceName)")
            $targetStates = (Get-ADOWorkItemTypeStateList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)")
            $sourceStates | ForEach-Object {
                $state = $_
                Write-PSFMessage -Level Host -Message "Checking state '$($state.name)' in target process."
                $targetState = $targetStates.Where({$_.name -eq $state.name})
                $sourceState = Get-ADOWorkItemTypeState -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName "$($wit.referenceName)" -StateId "$($state.id)"
                    
                if (-not $targetState) {
                    Write-PSFMessage -Level Host -Message "State '$($state.name)' does not exist in target process. Adding it."
                    $body = @{
                        name = $sourceState.name
                        color = $sourceState.color
                        stateCategory = $sourceState.stateCategory
                        order = $sourceState.order
                    }
                    $body = $body | ConvertTo-Json -Depth 10
                    Write-PSFMessage -Level Verbose -Message "Adding state '$($sourceState.name)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                    $targetState = Add-ADOWorkItemTypeState -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -Body $body
                } else {
                    Write-PSFMessage -Level Host -Message "State '$($state.name)' already exists in target process. Skipping."
                }

                if ($sourceState.hidden) { 
                        try {
                            Write-PSFMessage -Level Verbose -Message "Hiding state '$($sourceState.name)' in target process."
                            $targetState = Hide-ADOWorkItemTypeState -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -StateId "$($targetState.id)" -Hidden "true" -WarningAction SilentlyContinue -ErrorAction SilentlyContinue

                        }
                        catch {
                            Write-PSFMessage -Level Warning -Message "Failed to hide state '$($sourceState.name)' in target process. The state is already hidden"    
                        }
                    }   
            }
        }
        Convert-FSCPSTextToAscii -Text "Migrate rules.." -Font "Standard" 
        ## Process Rules
        Write-PSFMessage -Level Host -Message "Starting to process rules for work item types."
        $sourceWitList | ForEach-Object {
            $wit = $_
            Write-PSFMessage -Level Host -Message "Processing rules for work item type '$($wit.name)'."
            $targetWit = $targetWitList.Where({$_.name -eq $wit.name})    
            $sourceRules = (Get-ADOWorkItemTypeRuleList -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName "$($wit.referenceName)").Where({$_.customizationType -ne 'system'})  
            $targetRules = (Get-ADOWorkItemTypeRuleList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)").Where({$_.customizationType -ne 'system'}) 
            $sourceRules | ForEach-Object {
                $rule = $_
                Write-PSFMessage -Level Host -Message "Checking rule '$($rule.name)' in target process."
                $targetRule = $targetRules.Where({$_.name -eq $rule.name})
                if (-not $targetRule) {
                    Write-PSFMessage -Level Host -Message "Rule '$($rule.name)' does not exist in target process. Adding it."
                    $sourceRule = Get-ADOWorkItemTypeRule -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName "$($wit.referenceName)" -RuleRefName "$($rule.referenceName)"
                    $body = @{
                        name = $sourceRule.name
                        conditions = $sourceRule.conditions
                        actions = $sourceRule.actions
                        isDisabled = $sourceRule.isDisabled
                    }
                    $body = $body | ConvertTo-Json -Depth 10
                    Write-PSFMessage -Level Host -Message "Adding rule '$($sourceRule.name)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                    $targetRule = Add-ADOWorkItemTypeRule -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -Body $body
                } else {
                    Write-PSFMessage -Level Host -Message "Rule '$($rule.name)' already exists in target process. Skipping."
                }
            }
        }
        Convert-FSCPSTextToAscii -Text "Migrate layouts.." -Font "Standard" 
        ## Process Layouts
        Write-PSFMessage -Level Host -Message "Starting to process layouts for work item types."
        $sourceWitList | ForEach-Object {
            $wit = $_
            Write-PSFMessage -Level Host -Message "Processing layouts for work item type '$($wit.name)'."
            $targetWit = $targetWitList.Where({$_.name -eq $wit.name})
            $sourceLayout = (Get-ADOWorkItemTypeLayout -Organization $sourceOrganization -Token $sourceOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($sourceProjectProcess.typeId)" -WitRefName "$($wit.referenceName)")    
            $targetLayout = (Get-ADOWorkItemTypeLayout -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)")
            $sourceLayout.pages | Where-Object pageType -EQ "custom" | ForEach-Object {
                $sourcePage = $_
                Write-PSFMessage -Level Host -Message "Processing page '$($sourcePage.label)' for work item type '$($wit.name)'."
                $targetPage = $targetLayout.pages.Where({$_.label -eq $sourcePage.label})   
                if (-not $targetPage) {
                    Write-PSFMessage -Level Host -Message "Page '$($sourcePage.label)' does not exist in target process. Adding it."
                    $body = @{
                        id = $sourcePage.id
                        label = $sourcePage.label
                        pageType = $sourcePage.pageType
                        visible = $sourcePage.visible
                    }
                    $body = $body | ConvertTo-Json -Depth 10
                    Write-PSFMessage -Level Verbose -Message "Adding page '$($sourcePage.label)' to target process '$($targetProjectProcess.name)' with the following details: $($body)"
                    $targetPage = Add-ADOWorkItemTypePage -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -Body $body
                } else {
                    Write-PSFMessage -Level Host -Message "Page '$($sourcePage.label)' already exists in target process. Updating it."
                    $body = @{
                        id = $targetPage.id
                        label = $sourcePage.label
                        pageType = $sourcePage.pageType
                        visible = $sourcePage.visible
                    }
                    $body = $body | ConvertTo-Json -Depth 10
                    Write-PSFMessage -Level Verbose -Message "Updating page '$($sourcePage.label)' in target process '$($targetProjectProcess.name)' with the following details: $($body)"
                    $null = Update-ADOWorkItemTypePage -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -Body $body
                }

                # Process Sections
                $sourcePageSections = $sourcePage.sections
                $targetPageSections = $targetPage.sections
                $sourcePageSections | Where-Object groups -NE $NULL | ForEach-Object {
                    $sourceSection = $_
                    Write-PSFMessage -Level Host -Message "Processing section ''$(if($sourceSection.label){$sourceSection.label}else{$sourceSection.id})' on page '$($sourcePage.label)'."
                    $targetSection = $targetPageSections.Where({$_.id -eq $sourceSection.id})
                    if (-not $targetSection) {
                        Write-PSFMessage -Level Host -Message "Section '$(if($sourceSection.label){$sourceSection.label}else{$sourceSection.id})' does not exist in target process. Adding it."
                        $body = @{
                            id = $sourceSection.id
                            label = $sourceSection.label
                            visible = $sourceSection.visible
                        }
                        $body = $body | ConvertTo-Json -Depth 10
                        Write-PSFMessage -Level Verbose -Message "Adding section '$(if($sourceSection.label){$sourceSection.label}else{$sourceSection.id})' to page '$($targetPage.label)' in target process '$($targetProjectProcess.name)' with the following details: $($body)"
                        $targetSection = Add-ADOWorkItemTypeSection -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -PageId "$($targetPage.id)" -Body $body
                    } else {
                        Write-PSFMessage -Level Host -Message "Section '$(if($sourceSection.label){$sourceSection.label}else{$sourceSection.id})' already exists in target process. Skipping."
                    }

                    # Process Groups
                    $sourceSection.groups | ForEach-Object {
                        $sourceGroup = $_
                        Write-PSFMessage -Level Host -Message "Processing group '$($sourceGroup.label)' in section '$($sourceSection.label)'."
                        $targetGroup = $targetSection.groups.Where({$_.label -eq $sourceGroup.label})
                        if (-not $targetGroup) {
                            Write-PSFMessage -Level Host -Message "Group '$($sourceGroup.label)' does not exist in target process. Adding it."
                            $body = @{
                                id = $sourceGroup.id
                                label = $sourceGroup.label
                                visible = $sourceGroup.visible
                                controls = $sourceGroup.controls
                            }
                            $body = $body | ConvertTo-Json -Depth 10
                            Write-PSFMessage -Level Verbose -Message "Adding group '$($sourceGroup.label)' to section '$($sourceSection.label)' on page '$($targetPage.label)' in target process '$($targetProjectProcess.name)' with the following details: $($body)"
                            $targetGroup = Add-ADOWorkItemTypeGroup -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -PageId "$($targetPage.id)" -SectionId "$($sourceSection.id)" -Body $body
                        } else {
                            Write-PSFMessage -Level Host -Message "Group '$($sourceGroup.label)' already exists in target process. Skipping."
                        }

                        # Process Controls
                        $sourceGroup.controls | ForEach-Object {
                            $sourceControl = $_
                            Write-PSFMessage -Level Host -Message "Processing control '$(if($sourceControl.label){$sourceControl.label}else{$sourceControl.id})' in group '$($sourceGroup.label)'."
                            $targetControl = $targetGroup.controls.Where({$_.id -eq $sourceControl.id})
                            if (-not $targetControl) {
                                Write-PSFMessage -Level Host -Message "Control '$(if($sourceControl.label){$sourceControl.label}else{$sourceControl.id})' does not exist in target process. Adding it."
                                $body = @{
                                    id = $sourceControl.id
                                    label = $sourceControl.label
                                    controlType = $sourceControl.controlType
                                    contribution = $sourceControl.contribution
                                    visible = $sourceControl.visible
                                    height = $sourceControl.height
                                    readOnly = $sourceControl.readOnly
                                }
                                $body = $body | ConvertTo-Json -Depth 10
                                Write-PSFMessage -Level Verbose -Message "Adding control '$(if($sourceControl.label){$sourceControl.label}else{$sourceControl.id})' to group '$($sourceGroup.label)' in target process '$($targetProjectProcess.name)' with the following details: $($body)"
                                $targetControl = Add-ADOWorkItemTypeGroupControl -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $ApiVersion -ProcessId "$($targetProjectProcess.typeId)" -WitRefName "$($targetWit.referenceName)" -GroupId "$($targetGroup.id)" -Body $body
                            } else {
                                Write-PSFMessage -Level Host -Message "Control '$(if($sourceControl.label){$sourceControl.label}else{$sourceControl.id})' already exists in target process. Skipping."
                            }
                        }
                    }
                }
            }
        }
        Convert-FSCPSTextToAscii -Text "Migrate project.." -Font "Standard" 
        ### PROCESSING PROJECT


        Write-PSFMessage -Level Host -Message "Fetching source project '$($sourceProjecttmp.name)' from organization '$($sourceOrganization)'."
        $sourceProject = Get-ADOProject -Organization $sourceOrganization -Token $sourceOrganizationtoken -ProjectId "$($sourceProjecttmp.id)" -IncludeCapabilities -ApiVersion $ApiVersion

        Write-PSFMessage -Level Host -Message "Checking if target project '$($sourceProjectName)' exists in organization '$($targetOrganization)'."
        $targetProject = (Get-ADOProjectList -Organization $targetOrganization -Token $targetOrganizationtoken -ApiVersion $apiVersion -StateFilter All).Where({$_.name -eq $sourceProjectName})

        if (-not $targetProject) {
            Write-PSFMessage -Level Verbose -Message "Target project '$($targetProjectName)' does not exist in organization '$($targetOrganization)'. Creating a new project."

            $body = @{
                name = $targetProjectName
                description = $sourceProject.description
                visibility = $sourceProject.visibility
                capabilities = @{
                    versioncontrol = @{
                        sourceControlType = $sourceProjectVersionControl.sourceControlType
                    }
                    processTemplate = @{
                        templateTypeId = $targetProjectProcess.typeId
                    }
                }
            }
            $body = $body | ConvertTo-Json -Depth 10

            Write-PSFMessage -Level Host -Message "Adding project '$($targetProjectName)' to target organization '$($targetOrganization)' with the following details: $($body)"
            $targetProject = Add-ADOProject -Organization $targetOrganization -Token $targetOrganizationtoken -Body $body -ApiVersion $ApiVersion

            Write-PSFMessage -Level Host -Message "Project '$($targetProjectName)' successfully created in target organization '$($targetOrganization)'."
        } else {
            Write-PSFMessage -Level Host -Message "Target project '$($targetProjectName)' already exists in organization '$($targetOrganization)'. Updating the project."

            $body = @{
                name = $targetProjectName
                description = $sourceProject.description
                visibility = $sourceProject.visibility
                capabilities = @{
                    versioncontrol = @{
                        sourceControlType = $sourceProjectVersionControl.sourceControlType
                    }
                    processTemplate = @{
                        templateTypeId = $targetProjectProcess.typeId
                    }
                }
            }
            $body = $body | ConvertTo-Json -Depth 10

            Write-PSFMessage -Level Host -Message "Updating project '$($targetProjectName)' in target organization '$($targetOrganization)' with the following details: $($body)"
            $targetProject = Update-ADOProject -Organization $targetOrganization -Token $targetOrganizationtoken -Body $body -ProjectId "$($targetProject.id)" -ApiVersion $ApiVersion

            Write-PSFMessage -Level Host -Message "Project '$($targetProjectName)' successfully updated in target organization '$($targetOrganization)'."
        }

        #PROCESSING WORK ITEM
        $sourceWorkItemsList = (Get-ADOSourceWorkItemsList -SourceOrganization $sourceOrganization -SourceProjectName $sourceProjectName -SourceToken $sourceOrganizationtoken -ApiVersion $ApiVersion)
        $targetWorkItemList = @{}
     
        $sourceWorkItemsList |  ForEach-Object {
            $sourceWorkItem = $_
            $targetWorkItemsList = (Get-ADOSourceWorkItemsList -SourceOrganization $targetOrganization -SourceProjectName $targetProjectName -SourceToken $targetOrganizationtoken -Fields @("System.Id", "System.Title", "System.Description", "System.WorkItemType", "System.State", "System.Parent", "Custom.SourceWorkitemId"))
            $workItemExists = $targetWorkItemsList | Where-Object 'Custom.SourceWorkitemId' -EQ $sourceWorkItem.'System.Id'
            if ($workItemExists) {
                continue;
            }
            Invoke-ADOWorkItemsProcessing -SourceWorkItem $sourceWorkItem -SourceOrganization $sourceOrganization -SourceProjectName $sourceProjectName -SourceToken $sourceOrganizationtoken -TargetOrganization $targetOrganization `
            -TargetProjectName $targetProjectName -TargetToken $targetOrganizationtoken `
            -TargetWorkItemList ([ref]$targetWorkItemList) -ApiVersion $ApiVersion
        }

        # Log the completion of the migration process
        Write-PSFMessage -Level Host -Message "Completed migration of work items from project '$sourceProjectName' in organization '$sourceOrganization' to project '$targetProjectName' in organization '$targetOrganization'."

    }
    end{
        # Log the end of the operation
        Write-PSFMessage -Level Host -Message "Migration from source organization '$sourceOrganization' to target organization '$targetOrganization' completed successfully."
        Invoke-TimeSignal -End
    }
}