internal/functions/Out-DocumentationForPolicyAssignments.ps1

function Out-DocumentationForPolicyAssignments {
    [CmdletBinding()]
    param (
        [string] $OutputPath,
        [switch] $WindowsNewLineCells,
        $DocumentationSpecification,
        [hashtable] $AssignmentsByEnvironment,
        [switch] $IncludeManualPolicies,
        [hashtable] $PacEnvironments
    )

    [string] $fileNameStem = $DocumentationSpecification.fileNameStem
    [string[]] $environmentCategories = $DocumentationSpecification.environmentCategories
    [string] $title = $DocumentationSpecification.title

    Write-Information "Generating Policy Assignment documentation for '$title', files '$fileNameStem'."

    # Checking parameters
    if ($null -eq $fileNameStem -or $fileNameStem -eq "") {
        Write-Error "fileNameStem not specified" -ErrorAction Stop
    }
    if ($null -eq $title -or $title -eq "") {
        Write-Error "title not specified" -ErrorAction Stop
    }
    $environmentCategoriesAreValid = $null -ne $environmentCategories -and $environmentCategories.Length -gt 0
    if (-not $environmentCategoriesAreValid) {
        Write-Error "No environmentCategories '$environmentCategories' specified." -ErrorAction Stop
    }

    #region Combine per environment flat lists into a single flat list ($flatPolicyListAcrossEnvironments)

    $flatPolicyListAcrossEnvironments = @{}
    foreach ($environmentCategory in $environmentCategories) {
        if (-not $AssignmentsByEnvironment.ContainsKey($environmentCategory)) {
            # Should never happen (programing bug)
            Write-Error "Unknown environmentCategory '$environmentCategory' encountered - bug in EPAC PowerShell code" -ErrorAction Stop
        }

        # Collate Policies
        $perEnvironment = $AssignmentsByEnvironment.$environmentCategory
        $flatPolicyList = $perEnvironment.flatPolicyList
        foreach ($policyTableId in $flatPolicyList.Keys) {

            $flatPolicyEntry = $flatPolicyList.$policyTableId
            $isEffectParameterized = $flatPolicyEntry.isEffectParameterized
            $policyDisplayName = $flatPolicyEntry.displayName -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
            $policyDescription = $flatPolicyEntry.description -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
            $effectValue = "Unknown"
            if ($null -ne $flatPolicyEntry.effectValue) {
                $effectValue = $flatPolicyEntry.effectValue
            }
            else {
                $effectValue = $flatPolicyEntry.effectDefault
            }

            if ($effectValue -ne "Manual" -or $IncludeManualPolicies) {

                $flatPolicyEntryAcrossEnvironments = @{}
                if ($flatPolicyListAcrossEnvironments.ContainsKey($policyTableId)) {
                    $flatPolicyEntryAcrossEnvironments = $flatPolicyListAcrossEnvironments.$policyTableId
                    if ($isEffectParameterized) {
                        $flatPolicyEntry.isEffectParameterized = $true
                    }
                }
                else {
                    $flatPolicyEntryAcrossEnvironments = @{
                        policyTableId          = $policyTableId
                        name                   = $flatPolicyEntry.name
                        referencePath          = $flatPolicyEntry.ReferencePath
                        displayName            = $policyDisplayName
                        description            = $policyDescription
                        policyType             = $flatPolicyEntry.policyType
                        category               = $flatPolicyEntry.category
                        isEffectParameterized  = $isEffectParameterized
                        ordinal                = 99
                        effectAllowedValues    = @{}
                        effectAllowedOverrides = $flatPolicyEntry.effectAllowedOverrides
                        environmentList        = @{}
                        groupNames             = [System.Collections.ArrayList]::new()
                        policySetList          = @{}
                        policySetEffectStrings = $flatPolicyEntry.policySetEffectStrings
                        isReferencePathMatch   = $false
                    }
                    $null = $flatPolicyListAcrossEnvironments.Add($policyTableId, $flatPolicyEntryAcrossEnvironments)
                }

                # Find out lowest ordinal for grouping (optional)
                if ($flatPolicyEntry.ordinal -lt $flatPolicyEntryAcrossEnvironments.ordinal) {
                    $flatPolicyEntryAcrossEnvironments.ordinal = $flatPolicyEntry.ordinal
                }

                # Collect union of all effect parameter allowed values
                $effectAllowedValues = $flatPolicyEntryAcrossEnvironments.effectAllowedValues
                foreach ($allowedValue in $flatPolicyEntry.effectAllowedValues.Keys) {
                    if (-not $effectAllowedValues.ContainsKey($allowedValue)) {
                        $null = $effectAllowedValues.Add($allowedValue, $allowedValue)
                    }
                }

                # Collect union of all group names
                $groupNamesList = $flatPolicyEntry.groupNamesList
                if ($null -ne $groupNamesList -and $groupNamesList.Count -gt 0) {
                    $existingGroupNames = $flatPolicyEntryAcrossEnvironments.groupNames
                    $existingGroupNames.AddRange($groupNamesList)
                }

                # Collect environment category specific items
                $environmentList = $flatPolicyEntryAcrossEnvironments.environmentList
                if ($environmentList.ContainsKey($environmentCategory)) {
                    Write-Error "Duplicate environmentCategory '$environmentCategory' encountered - bug in EPAC PowerShell code" -ErrorAction Stop
                }
                $environmentCategoryInfo = @{
                    environmentCategory = $environmentCategory
                    effectValue         = $effectValue
                    parameters          = $flatPolicyEntry.parameters

                    policySetList       = $flatPolicyEntry.policySetList
                }
                $null = $environmentList.Add($environmentCategory, $environmentCategoryInfo)

                # Collect policySet specific items
                $policySetList = $flatPolicyEntryAcrossEnvironments.policySetList
                $flatPolicyEntryPolicySetList = $flatPolicyEntry.policySetList
                foreach ($shortName in $flatPolicyEntryPolicySetList.Keys) {
                    $policySetInfo = $flatPolicyEntryPolicySetList.$shortName
                    if (-not $policySetList.ContainsKey($shortName)) {
                        $policySetDisplayName = $policySetInfo.displayName -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
                        $policySetDescription = $policySetInfo.description -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
                        $policySetEntry = @{
                            shortName              = $shortName
                            id                     = $policySetInfo.id
                            name                   = $policySetInfo.name
                            displayName            = $policySetDisplayName
                            description            = $policySetDescription
                            policyType             = $policySetInfo.policyType
                            effectParameterName    = $policySetInfo.effectParameterName
                            effectDefault          = $policySetInfo.effectDefault
                            effectAllowedValues    = $policySetInfo.effectAllowedValues
                            effectAllowedOverrides = $policySetInfo.effectAllowedOverrides
                            effectReason           = $policySetInfo.effectReason
                            isEffectParameterized  = $policySetInfo.isEffectParameterized
                            parameters             = $policySetInfo.parameters
                        }
                        $null = $policySetList.Add($shortName, $policySetEntry)
                    }
                }
            }
            else {
                Write-Verbose "Skipping Manual effect Policy '$($flatPolicyEntry.displayName)'"
            }
        }
    }

    #region Process Deprecated
    $deprecatedHash = @{}
    foreach ($key in $policyResourceDetails.policies.keys) {
        if ($true -eq $policyResourceDetails.policies.$key.isDeprecated) {
            $deprecatedHash[$policyResourceDetails.policies.$key.name] = $policyResourceDetails.policies.$key
        }
    }
    #region Review Duplicates

    # Iterate over each key-value pair in the hashtable
    foreach ($policyDef in $flatPolicyListAcrossEnvironments.Keys) {
        # Skip if policy is BuiltIn
        if ($flatPolicyListAcrossEnvironments[$policyDef].policyType -ne "BuiltIn") {
            # Compare the current key's value with every other key's value
            foreach ($policyDefCompare in $flatPolicyListAcrossEnvironments.Keys) {
                # Skip the comparison if it's the same key or the compare def is a BuiltIn
                if ($policyDef -ne $policyDefCompare -and $flatPolicyListAcrossEnvironments[$policyDefCompare].policyType -ne "BuiltIn") {
                    # Check if already been tagged as a match to another referencePathId
                    if ($flatPolicyListAcrossEnvironments[$policyDef].isReferencePathMatch -eq $false) {
                        # Check if the referencePath values match
                        if ($flatPolicyListAcrossEnvironments[$policyDef].referencePath -eq $flatPolicyListAcrossEnvironments[$policyDefCompare].referencePath) {
                            # Check if the Policy Assignment Display Name values match
                            if ($flatPolicyListAcrossEnvironments[$policyDef].displayName -eq $flatPolicyListAcrossEnvironments[$policyDefCompare].displayName) {
                                # Set variable for isReferencePatMatch to true
                                $flatPolicyListAcrossEnvironments[$policyDefCompare].isReferencePathMatch = $true
                                # Find which env category is missing, add it to $policyDef
                                foreach ($env in $flatPolicyListAcrossEnvironments[$policyDefCompare].environmentList.keys) {
                                    if (-not $flatPolicyListAcrossEnvironments[$policyDef].environmentList.ContainsKey($env)) {
                                        # Copy envrionemnt from match to original key
                                        $flatPolicyListAcrossEnvironments[$policyDef].environmentList[$env] = $flatPolicyListAcrossEnvironments[$policyDefCompare].environmentList[$env]
                                    }
                                }
                                break
                            }
                        }
                    }
                }
            }
        }
    }

    #endregion Combine per environment flat lists into a single flat list ($flatPolicyListAcrossEnvironments)

    #region Markdown

    [System.Collections.Generic.List[string]] $allLines = [System.Collections.Generic.List[string]]::new()
    $leadingHeadingHashtag = "#"
    if ($DocumentationSpecification.markdownAdoWiki) {
        $leadingHeadingHashtag = ""
        $null = $allLines.Add("[[_TOC_]]`n")
    }
    else {
        $null = $allLines.Add("# $title`n")
        if ($DocumentationSpecification.markdownAddToc) {
            $null = $allLines.Add("[[_TOC_]]`n")
        }
    }
    $null = $allLines.Add("Auto-generated Policy effect documentation across environments '$($environmentCategories -join "', '")' sorted by Policy category and Policy display name.")

    $inTableAfterDisplayNameBreak = "<br/>"
    $inTableBreak = "<br/>"
    if ($DocumentationSpecification.markdownNoEmbeddedHtml) {
        $inTableAfterDisplayNameBreak = ": "
        $inTableBreak = ", "
    }



    #region Environment Categories

    foreach ($environmentCategory in $environmentCategories) {
        $perEnvironment = $AssignmentsByEnvironment.$environmentCategory
        $itemList = $perEnvironment.itemList
        $assignmentsDetails = $perEnvironment.assignmentsDetails
        $scopes = $perEnvironment.scopes
        $null = $allLines.Add("`n$leadingHeadingHashtag# Environment Category ``$environmentCategory``")

        $null = $allLines.Add("`n$leadingHeadingHashtag## Scopes`n")
        foreach ($scope in $scopes) {
            $null = $allLines.Add("- $scope")
        }

        foreach ($item in $itemList) {
            $assignmentId = $item.assignmentId
            if ($assignmentsDetails.ContainsKey($assignmentId)) {
                # should always be true
                $assignmentsDetail = $assignmentsDetails.$assignmentId
                $null = $allLines.Add("`n$leadingHeadingHashtag## Assignment: ``$($assignmentsDetail.assignment.properties.displayName)```n")
                $null = $allLines.Add("| Property | Value |")
                $null = $allLines.Add("| :------- | :---- |")
                $null = $allLines.Add("| Assignment Id | $($assignmentId) |")
                $null = $allLines.Add("| Policy Set | ``$($assignmentsDetail.displayName)`` |")
                $null = $allLines.Add("| Policy Set Id | $($assignmentsDetail.policySetId) |")
                $null = $allLines.Add("| Type | $($assignmentsDetail.policyType) |")
                $null = $allLines.Add("| Category | ``$($assignmentsDetail.category)`` |")
                $null = $allLines.Add("| Description | $($assignmentsDetail.description) |")
            }
        }
    }

    #endregion Environment Categories

    #region Policy Effects

    $addedTableHeader = ""
    $addedTableDivider = ""
    $addedTableDividerParameters = ""
    foreach ($environmentCategory in $environmentCategories) {
        # Calculate environment columns
        $addedTableHeader += " $environmentCategory |"
        $addedTableDivider += " :-----: |"
        $addedTableDividerParameters += " :----- |"
    }

    if ($DocumentationSpecification.markdownIncludeComplianceGroupNames) {
        $null = $allLines.Add("`n$leadingHeadingHashtag# Policy Effects by Policy`n")
        $null = $allLines.Add("| Category | Policy | Group Names |$addedTableHeader")
        $null = $allLines.Add("| :------- | :----- | :---------- |$addedTableDivider")
    }
    else {
        $null = $allLines.Add("`n$leadingHeadingHashtag# Policy Effects by Policy`n")
        $null = $allLines.Add("| Category | Policy |$addedTableHeader")
        $null = $allLines.Add("| :------- | :----- |$addedTableDivider")
    }
    
    $flatPolicyListAcrossEnvironments.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
        # If statement to skip over duplicates
        if ( $true -ne $_.isReferencePathMatch) {
            # Build additional columns
            $addedEffectColumns = ""
            $environmentList = $_.environmentList
            foreach ($environmentCategory in $environmentCategories) {
                if ($environmentList.ContainsKey($environmentCategory)) {
                    $environmentCategoryValues = $environmentList.$environmentCategory
                    $effectValue = $environmentCategoryValues.effectValue
                    if ($effectValue.StartsWith("[if(contains(parameters('resourceTypeList')")) {
                        $effectValue = "SetByParameter"
                    }
                    $effectAllowedValues = $_.effectAllowedValues
                    $text = Convert-EffectToMarkdownString `
                        -Effect $effectValue `
                        -AllowedValues $effectAllowedValues.Keys `
                        -InTableBreak $inTableBreak
                    $addedEffectColumns += " $text |"
                }
                else {
                    $addedEffectColumns += " |"
                }

            }
            $groupNamesText = ""
            if ($DocumentationSpecification.markdownIncludeComplianceGroupNames) {
                $groupNames = $_.groupNames
                if ($groupNames.Count -gt 0) {
                    $sortedGroupNames = $groupNames | Sort-Object -Unique
                    $groupNamesText = "| $($sortedGroupNames -join $inTableBreak) "
                }
                else {
                    $groupNamesText = "| "
                }
            }
            $null = $allLines.Add("| $($_.category) | **$($_.displayName)**$($inTableAfterDisplayNameBreak)$($_.description) $($groupNamesText)|$($addedEffectColumns)")
        }
    }

    #endregion Policy Effects

    #region Parameters

    if ($DocumentationSpecification.markdownSuppressParameterSection) {
        Write-Verbose "Suppressing Parameters section in Markdown"
    }
    else {
        $null = $allLines.Add("`n$leadingHeadingHashtag# Policy Parameters by Policy`n")
        $null = $allLines.Add("| Category | Policy |$addedTableHeader")
        $null = $allLines.Add("| :------- | :----- |$addedTableDividerParameters")

        $flatPolicyListAcrossEnvironments.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
            # If statement to skip over duplicates
            if ( $true -ne $_.isReferencePathMatch) {
                # Build additional columns
                $addedParametersColumns = ""
                $environmentList = $_.environmentList
                $hasParameters = $false
                foreach ($environmentCategory in $environmentCategories) {
                    if ($environmentList.ContainsKey($environmentCategory)) {
                        $environmentCategoryValues = $environmentList.$environmentCategory
                        $text = ""
                        $parameters = $environmentCategoryValues.parameters
                        $notFirst = $false
                        foreach ($parameterName in $parameters.Keys) {
                            $parameter = $parameters.$parameterName
                            if (-not $parameter.isEffect) {
                                $hasParameters = $true
                                $markdownMaxParameterLength = 42
                                if ($DocumentationSpecification.markdownMaxParameterLength) {
                                    $markdownMaxParameterLength = $DocumentationSpecification.markdownMaxParameterLength
                                    if ($markdownMaxParameterLength -lt 16) {
                                        Write-Error "markdownMaxParameterLength must be at least 16; it is $markdownMaxParameterLength" -ErrorAction Stop
                                    }
                                }
                                if ($parameterName.length -gt $markdownMaxParameterLength) {
                                    $parameterName = $parameterName.substring(0, $markdownMaxParameterLength - 3) + "..."
                                }
                                # Check for null parameter
                                if ($null -eq $parameter.value) {
                                    # Parse through all assignments
                                    foreach ($assignment in $AssignmentsByEnvironment[$environmentCategory]["assignmentsDetails"].keys) {
                                        # For each policy definitions, look to see if it matches the current policy definition that has a parameter set to null
                                        foreach ($definition in $AssignmentsByEnvironment[$environmentCategory]["assignmentsDetails"][$assignment].policyDefinitions) {
                                            if ($definition.id -eq $_.policyTableId) {
                                                # Once a match is found, search all keys (which are parameter names) and see it matches the parameter we are looking for
                                                foreach ($key in $AssignmentsByEnvironment[$environmentCategory]["assignmentsDetails"][$assignment]["parameters"].keys) {
                                                    if ($key -eq $parameterName) {
                                                        # Use the key value over the $parameterName value due to possible case-sensitivity issues.
                                                        $value = $AssignmentsByEnvironment[$environmentCategory]["assignmentsDetails"][$assignment]["parameters"][$key].defaultValue
                                                        break
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                                else {
                                    $value = $parameter.value
                                }
                                if ($notFirst) {
                                    $text += $inTableBreak
                                }
                                else {
                                    $notFirst = $true
                                }
                                if ($null -eq $value) {
                                    $value = $parameter.defaultValue
                                    if ($null -eq $value) {
                                        $value = "null"
                                    }
                                }
                                $valueString = ""
                                if ($value -is [string]) {
                                    $valueString = $value
                                }
                                else {
                                    $valueString = ConvertTo-Json $value -Depth 100 -Compress
                                }
                                if ($valueString.length -gt $markdownMaxParameterLength) {
                                    $valueString = $valueString.substring(0, $markdownMaxParameterLength - 3) + "..."
                                }
                                $text += "$($parameterName) = **``$valueString``**"
                            }
                        }
                        $addedParametersColumns += " $text |"
                    }
                    else {
                        $addedParametersColumns += " |"
                    }
                }
                if ($hasParameters) {
                    $null = $allLines.Add("| $($_.category) | **$($_.displayName)**$($inTableAfterDisplayNameBreak)$($_.description) |$($addedParametersColumns)")
                }
            }
        }
    }

    #endregion Parameters

    # Output file
    $outputFilePath = "$($OutputPath -replace '[/\\]$', '')/$($fileNameStem).md"
    $allLines | Out-File $outputFilePath -Force

    #endregion Markdown

    #region csv

    [System.Collections.ArrayList] $allRows = [System.Collections.ArrayList]::new()
    [System.Collections.ArrayList] $columnHeaders = [System.Collections.ArrayList]::new()

    # Create header rows for CSV
    $null = $columnHeaders.AddRange(@("name", "referencePath", "policyType", "category", "displayName", "description", "groupNames", "policySets", "allowedEffects" ))
    foreach ($environmentCategory in $environmentCategories) {
        $null = $columnHeaders.Add("$($environmentCategory)Effect")
    }
    foreach ($environmentCategory in $environmentCategories) {
        $null = $columnHeaders.Add("$($environmentCategory)Parameters")
    }

    # deal with multi value cells
    $inCellSeparator1 = ": "
    $inCellSeparator2 = ","
    $inCellSeparator3 = ","
    if ($WindowsNewLineCells) {
        $inCellSeparator1 = ":`n "
        $inCellSeparator2 = ",`n "
        $inCellSeparator3 = ",`n"
    }


    $allRows.Clear()

    # Process the table
    $flatPolicyListAcrossEnvironments.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
        # If statement to skip over duplicates and ensure not to include Deprecated Policies
        if ( $true -ne $_.isReferencePathMatch) {
            # Initialize row - with empty strings
            $rowObj = [ordered]@{}
            foreach ($key in $columnHeaders) {
                $null = $rowObj.Add($key, "")
            }

            # Cache loop values
            # $effectAllowedValues = $_.effectAllowedValues
            # $groupNames = $_.groupNames
            # $policySetEffectStrings = $_.policySetEffectStrings
            $effectAllowedValues = $_.effectAllowedValues
            $isEffectParameterized = $_.isEffectParameterized
            $effectAllowedOverrides = $_.effectAllowedOverrides
            $groupNames = $_.groupNames
            $effectDefault = $_.effectDefault
            $policySetEffectStrings = $_.policySetEffectStrings

            # Build common columns
            $rowObj.name = $_.name
            $rowObj.referencePath = $_.referencePath
            $rowObj.policyType = $_.policyType
            $rowObj.category = $_.category
            $rowObj.displayName = $_.displayName
            $rowObj.description = $_.description
            $groupNames = $_.groupNames
            if ($groupNames.Count -gt 0) {
                $sortedGroupNameList = $groupNames | Sort-Object -Unique
                $rowObj.groupNames = $sortedGroupNameList -join $inCellSeparator3
            }
            if ($policySetEffectStrings.Count -gt 0) {
                $rowObj.policySets = $policySetEffectStrings -join $inCellSeparator3
            }
            $rowObj.allowedEffects = Convert-AllowedEffectsToCsvString `
                -DefaultEffect $effectDefault `
                -IsEffectParameterized $isEffectParameterized `
                -EffectAllowedValues $effectAllowedValues.Keys `
                -EffectAllowedOverrides $effectAllowedOverrides `
                -InCellSeparator1 $inCellSeparator1 `
                -InCellSeparator2 $inCellSeparator2

            $environmentList = $_.environmentList
            # Build environmentCategory columns
            $doNotSkip = $false
            foreach ($environmentCategory in $environmentCategories) {
                if ($environmentList.ContainsKey($environmentCategory)) {
                    $perEnvironment = $environmentList.$environmentCategory

                    # Valide doNotDisableDeprecatedPolicies for env
                    $envPacSelector = $AssignmentsByEnvironment."$($perEnvironment.environmentCategory)".pacEnvironmentSelector
                    $doNotDisableDeprecatedPolicies = $PacEnvironments.$envPacSelector.doNotDisableDeprecatedPolicies

                    if (!$deprecatedHash.ContainsKey($_.name) -or $doNotDisableDeprecatedPolicies) {
                        if ($null -ne $perEnvironment.effectValue) {
                            $rowObj["$($environmentCategory)Effect"] = Convert-EffectToCsvString $perEnvironment.effectValue
                        }
                        else {
                            $rowObj["$($environmentCategory)Effect"] = Convert-EffectToCsvString $_.effectDefault
                        }

                        $text = Convert-ParametersToString -Parameters $perEnvironment.parameters -OutputType "csvValues"
                        $rowObj["$($environmentCategory)Parameters"] = $text
                        $doNotSkip = $true
                    }
                }
            }

            # Add row to spreadsheet
            if ($doNotSkip) {
                $null = $allRows.Add($rowObj)
            }
        }
    }

    # Output file
    $outputFilePath = "$($OutputPath -replace '[/\\]$','')/$($fileNameStem).csv"
    if ($WindowsNewLineCells) {
        $allRows | ConvertTo-Csv | Out-File $outputFilePath -Force -Encoding utf8BOM
    }
    else {
        # Mac or Linux
        $allRows | ConvertTo-Csv | Out-File $outputFilePath -Force -Encoding utf8NoBOM
    }

    #endregion csv

}