Private/Write-AACReportPdf.ps1

function Write-AACReportPdf {
    <#
    .SYNOPSIS
        Writes a report model - the one Show-AACReportView draws and the HTML
        report is written from - as a landscape A4 PDF.
    .DESCRIPTION
        1. The title, the facts (account, tenant, scope), the headline, the
           tiles and the notices.
        2. Each table with rows, under its section: the columns marked Pdf
           (else the first eight shown), sized by type - long text widest -
           with badges in their tone's colour. Up to -MaxRows rows each; the
           CSV and HTML report have every row.
        Each section and table is a bookmark (Save-AACPdfDocument). -Path
        must be a full path.
    #>

    [CmdletBinding()]
    [OutputType([System.IO.FileInfo])]
    param(
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $Report,

        [Parameter(Mandatory)]
        [string] $Path,

        [Parameter(Mandatory)]
        [string] $Title,

        [int] $MaxRows = 2000
    )

    $pdf = New-AACPdfDocument -Title $Title -Subject $(if ($Report.Contains('Subtitle')) { [string]$Report.Subtitle } else { $Title }) -Landscape
    $section = $pdf.Section
    $colors = $pdf.Colors
    $pt = $pdf.Pt
    $toneColor = @{ good = $pdf.Tone.Good.Solid; bad = $pdf.Tone.Bad.Solid; warn = $pdf.Tone.Warn.Solid; info = $colors.Accent; neutral = $colors.Muted; violet = $colors.Accent }
    $severity = Get-AACSeverityRank
    $note = { param([string] $Text, $Color) $p = $section.AddParagraph($Text); $p.Format.Font.Size = 8; $p.Format.Font.Color = $(if ($Color) { $Color } else { $colors.Muted }); $p.Format.SpaceBefore = & $pt 3 }

    # --- 1. Summary ---------------------------------------------------------------------------------------------
    & $pdf.AddTitle "$(if ($Report.Contains('Subtitle')) { $Report.Subtitle } else { $Title }) · generated $($pdf.Generated.ToString('dddd d MMMM yyyy, HH:mm'))"
    $facts = [ordered]@{}
    if ($script:AACSession) { $facts['Azure account'] = [string]$script:AACSession.Account; $facts['Tenant'] = [string]$script:AACSession.TenantId }
    if ($Report.Contains('Facts') -and $Report.Facts) { foreach ($key in $Report.Facts.Keys) { $facts[[string]$key] = [string]$Report.Facts[$key] } }
    $factTable = & $pdf.NewTable @(4.0, ($pdf.PageWidth - 4.0))
    foreach ($key in $facts.Keys) {
        $row = & $pdf.AddBodyRow $factTable
        $row.Cells[0].AddParagraph($key).Format.Font.Color = $colors.Muted
        $row.Cells[1].AddParagraph($facts[$key]) | Out-Null
    }
    if ($Report.Contains('Headline') -and $Report.Headline) {
        $statusTone = @{ Success = $pdf.Tone.Good.Solid; Warning = $pdf.Tone.Warn.Solid; Failed = $pdf.Tone.Bad.Solid }
        $p = $section.AddParagraph([string]$Report.Headline)
        $p.Format.SpaceBefore = & $pt 8
        $p.Format.Font.Name = 'Segoe UI Semibold'
        $p.Format.Font.Size = 11
        if ($Report.Contains('Status') -and $statusTone.Contains([string]$Report.Status)) { $p.Format.Font.Color = $statusTone[[string]$Report.Status] }
    }
    $section.AddParagraph().Format.SpaceAfter = & $pt 4
    $tileData = @(if ($Report.Contains('Tiles')) { $Report.Tiles })
    if ($tileData.Count) {
        $perRow = [Math]::Min(7, $tileData.Count)
        for ($start = 0; $start -lt $tileData.Count; $start += $perRow) {
            $chunk = @($tileData[$start..([Math]::Min($start + $perRow, $tileData.Count) - 1)])
            $tiles = & $pdf.NewTable @(1..$chunk.Count | ForEach-Object { $pdf.PageWidth / $chunk.Count })
            $tiles.TopPadding = & $pt 7
            $tiles.BottomPadding = & $pt 7
            $tileRow = $tiles.AddRow()
            for ($i = 0; $i -lt $chunk.Count; $i++) {
                $cell = $tileRow.Cells[$i]
                $cell.Shading.Color = $colors.Panel
                $cell.Borders.Left.Width = $(if ($i -gt 0) { 2 } else { 0 })
                $cell.Borders.Left.Color = $colors.White
                $value = $cell.AddParagraph([string]$chunk[$i].Value)
                if ($chunk[$i].Contains('Tone') -and $toneColor.Contains([string]$chunk[$i].Tone)) { $value.Format.Font.Color = $toneColor[[string]$chunk[$i].Tone] }
                $value.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Center
                $value.Format.Font.Size = 16
                $value.Format.Font.Name = 'Segoe UI Semibold'
                $caption = $cell.AddParagraph([string]$chunk[$i].Label)
                $caption.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Center
                $caption.Format.Font.Size = 7.5
                $caption.Format.Font.Color = $colors.Muted
            }
            $section.AddParagraph().Format.SpaceAfter = & $pt 2
        }
    }
    foreach ($n in @(if ($Report.Contains('Notices')) { $Report.Notices })) {
        & $note ([string]$n.Text) $(if ($n.Contains('Status') -and $n.Status -eq 'Failed') { $pdf.Tone.Bad.Solid } elseif ($n.Contains('Status') -and $n.Status -eq 'Warning') { $pdf.Tone.Warn.Solid })
    }

    # --- 2. The tables -------------------------------------------------------------------------------------------
    $lastSection = ''
    foreach ($t in @(if ($Report.Contains('Tables')) { $Report.Tables })) {
        $rows = @($t.Rows | Where-Object { $null -ne $_ })
        if (-not $rows.Count) { continue }
        $sectionName = if ($t.Contains('Section') -and $t.Section) { [string]$t.Section } else { '' }
        if ($sectionName -and $sectionName -ne $lastSection) { $section.AddParagraph($sectionName, 'Heading1') | Out-Null; $lastSection = $sectionName }
        $section.AddParagraph("$($t.Title) ($('{0:N0}' -f $rows.Count))", 'Heading2') | Out-Null
        if ($t.Contains('Note') -and $t.Note) { & $note ([string]$t.Note) }
        $columns = @($t.Columns | Where-Object { -not ($_.Contains('Hidden') -and $_.Hidden) })
        $chosen = @($columns | Where-Object { $_.Contains('Pdf') -and $_.Pdf })
        if (-not $chosen.Count) { $chosen = @($columns | Select-Object -First 8) }
        $weight = @(foreach ($c in $chosen) { switch ($(if ($c.Contains('Type')) { $c.Type } else { 'text' })) { 'wide' { 3.2 } { $_ -in 'mono', 'resource', 'path' } { 2.2 } { $_ -in 'number', 'money', 'score' } { 1.0 } 'badge' { 1.3 } default { 1.7 } } })
        $total = [double](@($weight) | Measure-Object -Sum).Sum
        $table = & $pdf.NewTable @($weight | ForEach-Object { $pdf.PageWidth * $_ / $total })
        $right = @(for ($i = 0; $i -lt $chosen.Count; $i++) { if ($chosen[$i].Contains('Type') -and $chosen[$i].Type -in 'number', 'money', 'score') { $i } })
        & $pdf.AddHeaderRow $table @($chosen | ForEach-Object { [string]$_.Label }) $right
        foreach ($row in @($rows | Select-Object -First $MaxRows)) {
            $pdfRow = & $pdf.AddBodyRow $table
            for ($i = 0; $i -lt $chosen.Count; $i++) {
                $c = $chosen[$i]
                $value = if ($row -is [System.Collections.IDictionary]) { $row[$c.Key] } else { $property = $row.PSObject.Properties[[string]$c.Key]; if ($property) { $property.Value } }
                $text = if ($value -is [datetime]) { $value.ToString('yyyy-MM-dd HH:mm') } elseif ($value -is [double] -or $value -is [decimal]) { '{0:N2}' -f $value } elseif ($value -is [int] -or $value -is [long]) { '{0:N0}' -f $value } else { [string]$value }
                $p = $pdfRow.Cells[$i].AddParagraph($text)
                $p.Format.Font.Size = 7.5
                if ($right -contains $i) { $p.Format.Alignment = [MigraDoc.DocumentObjectModel.ParagraphAlignment]::Right }
                $tone = if ($c.Key -eq 'Severity' -and $severity.Tone.Contains($text)) { $severity.Tone[$text] } elseif ($c.Contains('Tones') -and $c.Tones -and $c.Tones.Contains($text)) { [string]$c.Tones[$text] } else { '' }
                if ($tone -and $toneColor.Contains($tone)) { $p.Format.Font.Color = $toneColor[$tone]; $p.Format.Font.Name = 'Segoe UI Semibold' }
            }
        }
        if ($rows.Count -gt $MaxRows) { & $note "... and $($rows.Count - $MaxRows) more: the CSV and HTML reports list them all." }
    }

    Save-AACPdfDocument -Pdf $pdf -Path $Path
}