internal/functions/Out-DocumentationForPolicySets.ps1

function Out-DocumentationForPolicySets {
    [CmdletBinding()]
    param (
        [string] $OutputPath,
        [switch] $WindowsNewLineCells,
        $DocumentationSpecification,
        [array] $ItemList,
        [array] $EnvironmentColumnsInCsv,
        [hashtable] $PolicySetDetails,
        [hashtable] $FlatPolicyList,
        [switch] $IncludeManualPolicies
    )

    $fileNameStem = $DocumentationSpecification.fileNameStem
    $title = $DocumentationSpecification.title
    $environmentColumnsInCsv = $DocumentationSpecification.environmentColumnsInCsv


    Write-Information "Generating Policy Set documentation for '$title', files '$FileNameStem'."

    #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 for PolicySets grouped by Effect and sorted by Policy category and Policy display name.")
    $inTableAfterDisplayNameBreak = "<br/>"
    $inTableBreak = "<br/>"
    if ($DocumentationSpecification.markdownNoEmbeddedHtml) {
        $inTableAfterDisplayNameBreak = ": "
        $inTableBreak = ", "
    }

    #region Policy Set List
    $addedTableHeader = ""
    $addedTableDivider = ""
    $null = $allLines.Add("`n$leadingHeadingHashtag# Policy Set (Initiative) List`n")
    foreach ($item in $ItemList) {
        $shortName = $item.shortName
        $policySetId = $item.policySetId
        $policySetDetail = $PolicySetDetails.$policySetId
        $policySetDisplayName = $policySetDetail.displayName -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
        $policySetDescription = $policySetDetail.description -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
        $null = $allLines.Add("$leadingHeadingHashtag## $($shortName)`n")
        $null = $allLines.Add("- Display name: $($policySetDisplayName)`n")
        $null = $allLines.Add("- Type: $($policySetDetail.policyType)")
        $null = $allLines.Add("- Category: $($policySetDetail.category)`n")
        $null = $allLines.Add("$($policySetDescription)`n")

        $addedTableHeader += " $shortName |"
        $addedTableDivider += " :-------- |"
    }
    #endregion Policy Set List

    #region Policy Effects
    if ($DocumentationSpecification.markdownIncludeComplianceGroupNames) {
        $null = $allLines.Add("`n$leadingHeadingHashtag# Policy Effects by Policy`n")
        $null = $allLines.Add("| Category | Policy | Compliance |$addedTableHeader")
        $null = $allLines.Add("| :------- | :----- | :----------|$addedTableDivider")
    }
    else {
        $null = $allLines.Add("`n$leadingHeadingHashtag# Policy Effects`n")
        $null = $allLines.Add("| Category | Policy |$addedTableHeader")
        $null = $allLines.Add("| :------- | :----- |$addedTableDivider")
    }

    $FlatPolicyList.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
        $policySetList = $_.policySetList
        $addedEffectColumns = ""
        $effectValue = "Unknown"
        if ($null -ne $_.effectValue) {
            $effectValue = $_.effectValue
        }
        else {
            $effectValue = $_.effectDefault
        }

        if ($effectValue -ne "Manual" -or $IncludeManualPolicies) {
            $groupNamesList = [System.Collections.ArrayList]::new()
            foreach ($item in $ItemList) {
                $shortName = $item.shortName
                if ($policySetList.ContainsKey($shortName)) {
                    $perPolicySet = $policySetList.$shortName
                    $effectValue = $perPolicySet.effectValue
                    $effectAllowedValues = $perPolicySet.effectAllowedValues
                    $text = Convert-EffectToMarkdownString `
                        -Effect $effectValue `
                        -AllowedValues $effectAllowedValues 1 `
                        -inTableBreak $inTableBreak
                    $addedEffectColumns += " $text |"

                    [array] $groupNames = $perPolicySet.groupNames
                    if ($groupNames.Count -gt 0) {
                        $groupNamesList.AddRange($groupNames)
                    }
                }
                else {
                    $addedEffectColumns += " |"
                }
            }
            $complianceText = ""
            if ($DocumentationSpecification.markdownIncludeComplianceGroupNames) {
                if ($groupNamesList.Count -gt 0) {
                    $groupNamesList = $groupNamesList | Sort-Object -Unique
                    $complianceText = "| $($groupNamesList -join $inTableBreak) "
                }
                else {
                    $complianceText = "| "
                }
            }
            $policyDisplayName = $_.displayName -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
            $policyDescription = $_.description -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
            $null = $allLines.Add("| $($_.category) | **$($policyDisplayName)**$($inTableAfterDisplayNameBreak)$($policyDescription) $complianceText|$addedEffectColumns")
        }
        else {
            Write-Verbose "Skipping manual policy: $($_.name)"
        }
    }
    #endregion Policy Effects

    #region Policy 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("| :------- | :----- |$addedTableDivider")

        $FlatPolicyList.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
            $policySetList = $_.policySetList
            $addedParametersColumns = ""
            $effectValue = "Unknown"
            if ($null -ne $_.effectValue) {
                $effectValue = $_.effectValue
            }
            else {
                $effectValue = $_.effectDefault
            }

            if ($effectValue -ne "Manual" -or $IncludeManualPolicies) {
                $hasParameters = $false
                foreach ($item in $ItemList) {
                    $shortName = $item.shortName
                    if ($policySetList.ContainsKey($shortName)) {
                        $perPolicySet = $policySetList.$shortName
                        $parameters = $perPolicySet.parameters
                        $text = ""
                        $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) + "..."
                                }
                                $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) {
                    $policyDisplayName = $_.displayName -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
                    $policyDescription = $_.description -replace "\n\r", " " -replace "\n", " " -replace "\r", " "
                    $null = $allLines.Add("| $($_.category) | **$($policyDisplayName)**$($inTableAfterDisplayNameBreak)$($policyDescription) |$addedParametersColumns")
                }
            }
            else {
                Write-Verbose "Skipping manual policy: $($_.name)"
            }
        }
    }
    #endregion Policy Parameters
    
    # Output file
    $outputFilePath = "$($OutputPath -replace '[/\\]$','')/$fileNameStem.md"
    $allLines | Out-File $outputFilePath -Force

    #endregion Markdown

    #region CSV

    $outputEnvironmentColumns = $null -ne $EnvironmentColumnsInCsv -and $EnvironmentColumnsInCsv.Length -gt 0
    if (!$outputEnvironmentColumns) {
        $EnvironmentColumnsInCsv = @( "default" )
    }

    [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 $EnvironmentColumnsInCsv) {
        $null = $columnHeaders.Add("$($environmentCategory)Effect")
    }
    foreach ($environmentCategory in $EnvironmentColumnsInCsv) {
        $null = $columnHeaders.Add("$($environmentCategory)Parameters")
    }

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

    $allRows.Clear()

    # Content rows
    $FlatPolicyList.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
        # Initialize row - with empty strings
        $rowObj = [ordered]@{}
        foreach ($key in $columnHeaders) {
            $null = $rowObj.Add($key, "")
        }

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

        $effectValue = "Unknown"
        if ($null -ne $_.effectValue) {
            $effectValue = $_.effectValue
        }
        else {
            $effectValue = $_.effectDefault
        }

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

            # Build common columns
            $rowObj.name = $_.name
            $rowObj.referencePath = $_.referencePath
            $rowObj.policyType = $_.policyType
            $rowObj.category = $_.category
            $rowObj.displayName = $_.displayName
            $rowObj.description = $_.description
            if ($groupNamesList.Count -gt 0) {
                $rowObj.groupNames = $groupNamesList -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

            # Per environment columns
            $parameters = $_.parameters
            $parametersValueString = Convert-ParametersToString -Parameters $parameters -OutputType "csvValues"
            $normalizedEffectDefault = Convert-EffectToCsvString -Effect $effectDefault
            foreach ($environmentCategory in $EnvironmentColumnsInCsv) {
                $rowObj["$($environmentCategory)Effect"] = $normalizedEffectDefault
                $rowObj["$($environmentCategory)Parameters"] = $parametersValueString
            }

            # Add row to spreadsheet
            $null = $allRows.Add($rowObj)
        }
        else {
            Write-Verbose "Skipping manual policy: $($_.name)"
        }
    }

    # 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

    #region Compliance CSV

    # Pivot the data by group name
    [hashtable] $perGroupNamePolicies = @{}
    $FlatPolicyList.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
        $groupNamesList = $_.groupNamesList
        foreach ($groupName in $groupNamesList) {
            if (!$perGroupNamePolicies.ContainsKey($groupName)) {
                $perGroupNamePolicies.Add($groupName, [System.Collections.ArrayList]::new())
            }
            $null = $perGroupNamePolicies.$groupName.Add($_)
        }
    }

    # Sort by groupName
    $complianceColumnHeaders = @( "groupName", "category", "policyDisplayName", "allowedEffects", "defaultEffect", "policyId" )
    $allRows.Clear()
    $perGroupNamePolicies.Keys | Sort-Object | ForEach-Object -Process {

        # Initialize row in te correct order - with empty strings
        $rowObj = [ordered]@{}
        foreach ($key in $complianceColumnHeaders) {
            $null = $rowObj.Add($key, "")
        }

        # Cache loop values
        $groupName = $_
        $policies = $perGroupNamePolicies.$groupName
        $categoryList = [System.Collections.ArrayList]::new()
        $displayNameList = [System.Collections.ArrayList]::new()
        $effectsList = [System.Collections.ArrayList]::new()
        $defaultEffectList = [System.Collections.ArrayList]::new()
        $policyIdList = [System.Collections.ArrayList]::new()
        foreach ($policy in $policies) {

            # collect Policy information
            $null = $categoryList.Add($policy.category)
            $null = $displayNameList.Add($policy.displayName)
            $null = $policyIdList.Add($policy.name)

            # Collect effects values
            $effectAllowedValues = $policy.effectAllowedValues
            $isEffectParameterized = $policy.isEffectParameterized
            $effectAllowedOverrides = $policy.effectAllowedOverrides
            $effectDefault = $policy.effectDefault
            $allowedEffects = $effectDefault
            if ($isEffectParameterized -and $effectAllowedValues.Count -gt 1) {
                $allowedEffects = "param:$($effectAllowedValues.Keys -join '|')"
            }
            elseif ($effectAllowedOverrides.Count -gt 0) {
                $allowedEffects = "overr:$($effectAllowedOverrides -join '|')"
            }
            $null = $effectsList.Add($allowedEffects)
            $null = $defaultEffectList.Add($effectDefault)
        }

        # Build a row
        $rowObj.groupName = $groupName
        $rowObj.category = $categoryList -join $inCellSeparator3
        $rowObj.policyDisplayName = $displayNameList -join $inCellSeparator3
        $rowObj.allowedEffects = $effectsList -join $inCellSeparator3
        $rowObj.defaultEffect = $defaultEffectList -join $inCellSeparator3
        $rowObj.policyId = $policyIdList -join $inCellSeparator3

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

    # Output file
    $outputFilePath = "$($OutputPath -replace '[/\\]$','')/$($FileNameStem)-compliance.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 Compliance CSV

    #region Parameters JSON

    $sb = [System.Text.StringBuilder]::new()
    $null = $sb.Append("{")
    $null = $sb.Append("`n `"parameters`": {")
    $FlatPolicyList.Values | Sort-Object -Property { $_.category }, { $_.displayName } | ForEach-Object -Process {
        if ($_.isEffectParameterized) {

            $policySetList = $_.policySetList
            $referencePath = $_.referencePath
            $displayName = $_.displayName
            $category = $_.category

            $null = $sb.Append("`n // ")
            $null = $sb.Append("`n // -----------------------------------------------------------------------------------------------------------------------------")
            $null = $sb.Append("`n // $($category) -- $($displayName)")
            if ($referencePath -ne "") {
                $null = $sb.Append("`n // referencePath: $($referencePath)")
            }
            foreach ($item in $ItemList) {
                $shortName = $item.shortName
                if ($policySetList.ContainsKey($shortName)) {
                    $perPolicySet = $policySetList.$shortName
                    $policySetDisplayName = $perPolicySet.displayName
                    if ($perPolicySet.isEffectParameterized) {
                        $null = $sb.Append("`n // $($policySetDisplayName): $($perPolicySet.effectDefault) ($($perPolicySet.effectParameterName))")
                    }
                    else {
                        $null = $sb.Append("`n // $($policySetDisplayName): $($perPolicySet.effectDefault) ($($perPolicySet.effectReason))")
                    }
                }
            }
            $null = $sb.Append("`n // -----------------------------------------------------------------------------------------------------------------------------")
            $parameterText = Convert-ParametersToString -Parameters $_.parameters -OutputType "jsonc"
            $null = $sb.Append($parameterText)
        }
    }
    $null = $sb.Append("`n }")
    $null = $sb.Append("`n}")

    # Output file
    $outputFilePath = "$($OutputPath -replace '[/\\]$', '')/$FileNameStem.jsonc"
    $sb.ToString() | Out-File $outputFilePath -Force

    #endregion

}