#Requires -Version 5.1
<#
.SYNOPSIS
  Extract and verify CCS PDF Editor project state and rolling audit log.

.DESCRIPTION
  Supports both storage layouts used by the app:

  A) Catalog (default since schema 4.0 / product 1.5.x)
     PDF catalog private dict /CCSProject:
       /Type /CCSProject
       /Ver  (4.0)
       /Fmt  /GZipHex
       /Hash (legacy integrity of gzip payload)
       /Data <hex of gzip(UTF-8 JSON schema)>

  B) Legacy trailer (still readable; dual-write is opt-in in the app)
     <PDF bytes>
     \n---CCS-SAVE-STATE---
     <sha256 hex>
     ---HASH-DELIM---
     <gzip UTF-8 JSON schema>

  Schema JSON may include:
    - pages[]   overlays / form layout (incl. signatureAttestation on form fields)
    - audit[]   rolling change insight (last ~200 high-level edits)
    - seal      layered originHash / contentHash / schemaHash (+ optional device ECDSA)

  This is NOT decryption of a QPDF/password-protected *exported* PDF.
  "Decode" means find payload + gunzip + parse. Export PDFs may still carry /CCSProject.

  Hash algorithm matches the app (legacy recipe on gzip bytes):
    SHA-256( UTF-8(  (Uint8Array.toString() of gzip bytes) + "CCS-SAVE-HASH-CHECK-S4lT!!" ) )
    where Uint8Array.toString() is comma-separated decimal bytes (JavaScript TypedArray).

.PARAMETER Path
  Path to a .ccspdf or .pdf that may contain CCS project state.

.PARAMETER OutDir
  Optional folder for written artifacts. Default: next to the input file (*_extract).

.PARAMETER FullSchema
  Also write the full decoded project JSON (pages + audit + seal), not only audit.

.PARAMETER ExtractPdf
  Write a PDF body artifact. For trailer files: bytes before the marker.
  For catalog-only files: a copy of the source (already a normal PDF).

.PARAMETER NoHashCheck
  Skip integrity verification (use only if investigating a damaged file).

.PARAMETER PreferCatalog
  If both trailer and catalog exist, use catalog (default prefers trailer, matching the app open path).

.EXAMPLE
  powershell -File scripts\extract-ccspdf-audit.ps1 -Path "C:\cases\packet.ccspdf"

.EXAMPLE
  powershell -File scripts\extract-ccspdf-audit.ps1 -Path ".\export.pdf" -FullSchema -ExtractPdf
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory = $true, Position = 0)]
    [string]$Path,

    [string]$OutDir = "",

    [switch]$FullSchema,

    [switch]$ExtractPdf,

    [switch]$NoHashCheck,

    [switch]$PreferCatalog
)

$ErrorActionPreference = "Stop"
$HashSalt = "CCS-SAVE-HASH-CHECK-S4lT!!"
$Marker = [System.Text.Encoding]::UTF8.GetBytes("`n---CCS-SAVE-STATE---")
$Delim = [System.Text.Encoding]::UTF8.GetBytes("---HASH-DELIM---")
$Latin1 = [System.Text.Encoding]::GetEncoding(28591)

function Find-BytePattern {
    param([byte[]]$Haystack, [byte[]]$Needle, [int]$Start = 0)
    if ($Needle.Length -eq 0 -or $Haystack.Length -lt $Needle.Length) { return -1 }
    $last = $Haystack.Length - $Needle.Length
    for ($i = $Start; $i -le $last; $i++) {
        $ok = $true
        for ($j = 0; $j -lt $Needle.Length; $j++) {
            if ($Haystack[$i + $j] -ne $Needle[$j]) { $ok = $false; break }
        }
        if ($ok) { return $i }
    }
    return -1
}

function Find-BytePatternFromEnd {
    param([byte[]]$Haystack, [byte[]]$Needle)
    if ($Needle.Length -eq 0 -or $Haystack.Length -lt $Needle.Length) { return -1 }
    for ($i = $Haystack.Length - $Needle.Length; $i -ge 0; $i--) {
        $ok = $true
        for ($j = 0; $j -lt $Needle.Length; $j++) {
            if ($Haystack[$i + $j] -ne $Needle[$j]) { $ok = $false; break }
        }
        if ($ok) { return $i }
    }
    return -1
}

function Get-JsCompatibleHash {
    param([byte[]]$CompressedBytes)
    # JS: TextEncoder.encode(uint8Array + salt) after ToString of TypedArray -> "1,2,3,...salt"
    $sb = New-Object System.Text.StringBuilder
    for ($i = 0; $i -lt $CompressedBytes.Length; $i++) {
        if ($i -gt 0) { [void]$sb.Append(',') }
        [void]$sb.Append([int]$CompressedBytes[$i])
    }
    [void]$sb.Append($HashSalt)
    $utf8 = [System.Text.Encoding]::UTF8.GetBytes($sb.ToString())
    $sha = [System.Security.Cryptography.SHA256]::Create()
    try {
        $hash = $sha.ComputeHash($utf8)
        return ([System.BitConverter]::ToString($hash) -replace '-', '').ToLowerInvariant()
    } finally {
        $sha.Dispose()
    }
}

function Expand-GzipBytes {
    param([byte[]]$GzipBytes)
    $inMs = New-Object System.IO.MemoryStream(,$GzipBytes)
    try {
        $gzip = New-Object System.IO.Compression.GzipStream($inMs, [System.IO.Compression.CompressionMode]::Decompress)
        try {
            $outMs = New-Object System.IO.MemoryStream
            try {
                $gzip.CopyTo($outMs)
                return [System.Text.Encoding]::UTF8.GetString($outMs.ToArray())
            } finally { $outMs.Dispose() }
        } finally { $gzip.Dispose() }
    } finally { $inMs.Dispose() }
}

function Convert-HexToBytes {
    param([string]$Hex)
    $clean = [regex]::Replace([string]$Hex, '[^0-9a-fA-F]', '')
    if ($clean.Length -eq 0) { return [byte[]]@() }
    if (($clean.Length % 2) -ne 0) {
        throw "Invalid hex length ($($clean.Length)) in /CCSProject /Data"
    }
    $out = New-Object byte[] ($clean.Length / 2)
    for ($i = 0; $i -lt $out.Length; $i++) {
        $out[$i] = [Convert]::ToByte($clean.Substring($i * 2, 2), 16)
    }
    return $out
}

function Get-PdfStringSlice {
    param([string]$Text, [int]$OpenParenIndex)
    # PDF literal string starting at '(' - handles \( \) \\
    if ($OpenParenIndex -lt 0 -or $OpenParenIndex -ge $Text.Length -or $Text[$OpenParenIndex] -ne '(') {
        return $null
    }
    $sb = New-Object System.Text.StringBuilder
    $i = $OpenParenIndex + 1
    $depth = 1
    while ($i -lt $Text.Length -and $depth -gt 0) {
        $c = $Text[$i]
        if ($c -eq '\' -and ($i + 1) -lt $Text.Length) {
            [void]$sb.Append($Text[$i + 1])
            $i += 2
            continue
        }
        if ($c -eq '(') { $depth++; [void]$sb.Append($c); $i++; continue }
        if ($c -eq ')') {
            $depth--
            if ($depth -eq 0) { break }
            [void]$sb.Append($c)
            $i++
            continue
        }
        [void]$sb.Append($c)
        $i++
    }
    return [PSCustomObject]@{
        Value     = $sb.ToString()
        EndIndex  = $i  # index of closing ')'
    }
}

function Find-CcsCatalogPayload {
    <#
      Scan PDF binary (as Latin-1) for /Type /CCSProject dict and return gzip bytes + hash.
      pdf-lib writes PDFHexString /Data <hex> and PDFString /Hash (...).
    #>
    param([byte[]]$Bytes)

    $text = $Latin1.GetString($Bytes)
    $typeMatch = [regex]::Match($text, '/Type\s*/CCSProject')
    if (-not $typeMatch.Success) {
        # Indirect catalog key only - still try Fmt marker used by CCS
        $typeMatch = [regex]::Match($text, '/Fmt\s*/GZipHex')
        if (-not $typeMatch.Success) { return $null }
    }

    $anchor = $typeMatch.Index
    # Search window: dict keys sit near Type; /Data hex can be huge so scan to EOF for /Data after anchor
    $windowStart = [Math]::Max(0, $anchor - 200)
    $searchFrom = $windowStart

    # Prefer /Data that appears after Type (same object)
    $dataKey = [regex]::Match($text.Substring($anchor), '/Data\s*<')
    if (-not $dataKey.Success) {
        # Fallback: any /Data < after window
        $dataKey = [regex]::Match($text.Substring($searchFrom), '/Data\s*<')
        if (-not $dataKey.Success) { return $null }
        $dataOpen = $searchFrom + $dataKey.Index + $dataKey.Length - 1  # points at '<'
    } else {
        $dataOpen = $anchor + $dataKey.Index + $dataKey.Length - 1
    }

    if ($dataOpen -lt 0 -or $dataOpen -ge $text.Length -or $text[$dataOpen] -ne '<') {
        return $null
    }

    $hexSb = New-Object System.Text.StringBuilder
    $i = $dataOpen + 1
    while ($i -lt $text.Length) {
        $c = $text[$i]
        if ($c -eq '>') { break }
        # PDF allows whitespace inside hex strings
        if ($c -match '[0-9a-fA-F]') {
            [void]$hexSb.Append($c)
        } elseif ($c -match '\s') {
            # skip
        } else {
            # unexpected - stop
            break
        }
        $i++
    }
    if ($i -ge $text.Length -or $text[$i] -ne '>') {
        return $null
    }

    $hex = $hexSb.ToString()
    if ($hex.Length -lt 4) { return $null }

    $compressed = Convert-HexToBytes -Hex $hex

    # Hash: search near Type for /Hash (
    $hashVal = $null
    $hashRegion = $text.Substring($windowStart, [Math]::Min(8000, $text.Length - $windowStart))
    $hashKey = [regex]::Match($hashRegion, '/Hash\s*\(')
    if ($hashKey.Success) {
        $abs = $windowStart + $hashKey.Index + $hashKey.Length - 1
        $parsed = Get-PdfStringSlice -Text $text -OpenParenIndex $abs
        if ($parsed) { $hashVal = $parsed.Value.Trim() }
    }

    $ver = $null
    $verKey = [regex]::Match($hashRegion, '/Ver\s*\(')
    if ($verKey.Success) {
        $abs = $windowStart + $verKey.Index + $verKey.Length - 1
        $parsed = Get-PdfStringSlice -Text $text -OpenParenIndex $abs
        if ($parsed) { $ver = $parsed.Value.Trim() }
    }

    return [PSCustomObject]@{
        Source     = "catalog"
        Compressed = $compressed
        SavedHash  = $hashVal
        CatalogVer = $ver
        HexChars   = $hex.Length
    }
}

function Get-TrailerPayload {
    param([byte[]]$Bytes)

    $markerIndex = Find-BytePatternFromEnd -Haystack $Bytes -Needle $Marker
    if ($markerIndex -lt 0) { return $null }

    $payload = New-Object byte[] ($Bytes.Length - $markerIndex - $Marker.Length)
    [Array]::Copy($Bytes, $markerIndex + $Marker.Length, $payload, 0, $payload.Length)

    $delimIndex = Find-BytePattern -Haystack $payload -Needle $Delim -Start 0
    $savedHash = $null
    $compressed = $null

    if ($delimIndex -ge 0) {
        $savedHash = [System.Text.Encoding]::UTF8.GetString($payload, 0, $delimIndex).Trim()
        $compLen = $payload.Length - $delimIndex - $Delim.Length
        $compressed = New-Object byte[] $compLen
        [Array]::Copy($payload, $delimIndex + $Delim.Length, $compressed, 0, $compLen)
    } else {
        $compressed = $payload
    }

    return [PSCustomObject]@{
        Source       = "trailer"
        Compressed   = $compressed
        SavedHash    = $savedHash
        MarkerIndex  = $markerIndex
        HasDelim     = ($delimIndex -ge 0)
    }
}

function Test-SchemaHasProperty {
    param($Schema, [string]$Name)
    if ($null -eq $Schema) { return $false }
    return ($Schema.PSObject.Properties.Name -contains $Name)
}

function Get-FormSignatureAttestations {
    param($Schema)
    $list = New-Object System.Collections.Generic.List[object]
    if (-not $Schema -or -not (Test-SchemaHasProperty $Schema "pages")) { return @() }
    $pageIdx = 0
    foreach ($p in @($Schema.pages)) {
        $fields = @()
        if ($p.PSObject.Properties.Name -contains "formFields" -and $p.formFields) {
            $fields += @($p.formFields)
        }
        if ($p.PSObject.Properties.Name -contains "overlays" -and $p.overlays) {
            foreach ($o in @($p.overlays)) {
                if ($o.type -eq "formField" -or $o.fieldType) { $fields += $o }
            }
        }
        # Some schemas store fields at page.fields
        if ($p.PSObject.Properties.Name -contains "fields" -and $p.fields) {
            $fields += @($p.fields)
        }
        foreach ($f in $fields) {
            $att = $null
            $attHash = $null
            if ($f.PSObject.Properties.Name -contains "signatureAttestation" -and $f.signatureAttestation) {
                $att = $f.signatureAttestation
            }
            if ($f.PSObject.Properties.Name -contains "signatureAttestationHash") {
                $attHash = $f.signatureAttestationHash
            }
            if (-not $att -and -not $attHash) { continue }
            $name = $null
            $signedAt = $null
            $originH = $null
            $attH = $attHash
            $method = $null
            $platform = $null
            if ($att) {
                if ($att.signedName) { $name = $att.signedName }
                if ($att.signedAt) { $signedAt = $att.signedAt }
                if ($att.originHash) { $originH = $att.originHash }
                if ($att.attestationHash) { $attH = $att.attestationHash }
                if ($att.method) { $method = $att.method }
                if ($att.client -and $att.client.platform) { $platform = $att.client.platform }
            }
            $list.Add([PSCustomObject]@{
                pageIndex         = $pageIdx
                fieldName         = $(if ($f.fieldName) { $f.fieldName } else { $null })
                fieldId           = $(if ($f.id) { $f.id } else { $null })
                signedName        = $name
                signedAt          = $signedAt
                originHash        = $originH
                attestationHash   = $attH
                method            = $method
                platform          = $platform
                attested          = $(if ($att -and $att.attested) { $true } elseif ($att) { $att.attested } else { $null })
            })
        }
        $pageIdx++
    }
    return @($list)
}

# --- load file ---
if (-not (Test-Path -LiteralPath $Path)) {
    throw "File not found: $Path"
}
$Path = (Resolve-Path -LiteralPath $Path).Path
$bytes = [System.IO.File]::ReadAllBytes($Path)
Write-Host ""
Write-Host "CCS PDF Editor - project audit extractor" -ForegroundColor Cyan
Write-Host "  File: $Path"
Write-Host ("  Size: {0:N0} bytes" -f $bytes.Length)

$trailer = Get-TrailerPayload -Bytes $bytes
$catalog = $null
try {
    $catalog = Find-CcsCatalogPayload -Bytes $bytes
} catch {
    Write-Host "  Warning: catalog scan error: $_" -ForegroundColor Yellow
}

if ($trailer) {
    Write-Host "  Found: legacy trailer (---CCS-SAVE-STATE---)" -ForegroundColor DarkCyan
}
if ($catalog) {
    Write-Host ("  Found: catalog /CCSProject (GZipHex, {0:N0} hex chars{1})" -f `
        $catalog.HexChars, `
        $(if ($catalog.CatalogVer) { ", Ver=$($catalog.CatalogVer)" } else { "" })) -ForegroundColor DarkCyan
}

if (-not $trailer -and -not $catalog) {
    throw @"
No CCS project state found.
  - No legacy trailer (---CCS-SAVE-STATE---)
  - No catalog /CCSProject (/Type /CCSProject + /Data <hex>)
Is this a CCS Save Project / Export PDF from 1.5.x+, or was the catalog stripped by another editor?
"@
}

# Choose payload (app open path prefers trailer when both exist)
$chosen = $null
if ($PreferCatalog -and $catalog) {
    $chosen = $catalog
} elseif ($trailer -and -not $PreferCatalog) {
    $chosen = $trailer
} elseif ($catalog) {
    $chosen = $catalog
} else {
    $chosen = $trailer
}

# If preferred source empty/broken, fall back
$sourceLabel = $chosen.Source
$compressed = $chosen.Compressed
$savedHash = $chosen.SavedHash
$pdfBodyLen = if ($trailer -and $sourceLabel -eq "trailer") { $trailer.MarkerIndex } else { $bytes.Length }

Write-Host "  Using: $sourceLabel payload" -ForegroundColor Cyan

if ($sourceLabel -eq "trailer" -and -not $chosen.HasDelim) {
    Write-Host "  Warning: no hash delimiter - legacy trailer without integrity check." -ForegroundColor Yellow
}

$integrity = "skipped"
if ($savedHash -and -not $NoHashCheck) {
    $actual = Get-JsCompatibleHash -CompressedBytes $compressed
    if ($actual -eq $savedHash.ToLowerInvariant()) {
        $integrity = "OK"
        Write-Host "  Integrity: OK (hash matches app algorithm)" -ForegroundColor Green
    } else {
        $integrity = "FAIL"
        Write-Host "  Integrity: FAIL" -ForegroundColor Red
        Write-Host "    saved : $savedHash"
        Write-Host "    actual: $actual"
        Write-Host "  Continuing extract for investigation; treat content as possibly tampered." -ForegroundColor Yellow
    }
} elseif ($savedHash) {
    Write-Host "  Integrity: not checked (-NoHashCheck)" -ForegroundColor Yellow
} else {
    Write-Host "  Integrity: no /Hash or trailer hash present" -ForegroundColor Yellow
    $integrity = "none"
}

# --- gunzip schema ---
try {
    $jsonText = Expand-GzipBytes -GzipBytes $compressed
} catch {
    try {
        $jsonText = [System.Text.Encoding]::UTF8.GetString($compressed)
        Write-Host "  Note: payload was not gzip; treated as raw JSON." -ForegroundColor Yellow
    } catch {
        throw "Failed to decompress/decode payload: $_"
    }
}

$schema = $null
try {
    $schema = $jsonText | ConvertFrom-Json
} catch {
    throw "Schema JSON parse failed: $_. First 200 chars: $($jsonText.Substring(0, [Math]::Min(200, $jsonText.Length)))"
}

# Optional: also report the other storage if both present and preferred one failed integrity
if ($integrity -eq "FAIL" -and $trailer -and $catalog -and $sourceLabel -eq "trailer") {
    Write-Host "  Tip: re-run with -PreferCatalog if dual-write catalog may be intact." -ForegroundColor Yellow
}

# --- outputs ---
if (-not $OutDir) {
    $OutDir = Join-Path (Split-Path -Parent $Path) (([IO.Path]::GetFileNameWithoutExtension($Path)) + "_extract")
}
New-Item -ItemType Directory -Path $OutDir -Force | Out-Null
$base = [IO.Path]::GetFileNameWithoutExtension($Path)

$audit = @()
if (Test-SchemaHasProperty $schema "audit" -and $null -ne $schema.audit) {
    $audit = @($schema.audit)
}

$seal = $null
if (Test-SchemaHasProperty $schema "seal" -and $null -ne $schema.seal) {
    $seal = $schema.seal
}

$sigAttestations = Get-FormSignatureAttestations -Schema $schema

$summaryPath = Join-Path $OutDir "${base}_summary.txt"
$auditJsonPath = Join-Path $OutDir "${base}_audit.json"
$auditCsvPath = Join-Path $OutDir "${base}_audit.csv"
$sealPath = Join-Path $OutDir "${base}_seal.json"
$sigPath = Join-Path $OutDir "${base}_signatures.json"

$pageCount = 0
$overlayCount = 0
$formFieldCount = 0
if (Test-SchemaHasProperty $schema "pages" -and $schema.pages) {
    $pageCount = @($schema.pages).Count
    foreach ($p in @($schema.pages)) {
        if ($p.overlays) { $overlayCount += @($p.overlays).Count }
        if ($p.formFields) { $formFieldCount += @($p.formFields).Count }
        if ($p.fields) { $formFieldCount += @($p.fields).Count }
        if ($p.overlays) {
            foreach ($o in @($p.overlays)) {
                if ($o.type -eq "formField" -or $o.fieldType) { $formFieldCount++ }
            }
        }
    }
}

$summary = @(
    "CCS PDF Editor - project investigation extract",
    "Generated: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')",
    "Source:    $Path",
    "Storage:   $sourceLabel$(if ($trailer -and $catalog) { ' (both trailer + catalog present)' } elseif ($trailer) { ' (trailer only)' } else { ' (catalog only)' })",
    "Integrity: $integrity",
    "Schema version: $(if (Test-SchemaHasProperty $schema 'version') { $schema.version } else { '(none)' })",
    "pdfName:        $(if (Test-SchemaHasProperty $schema 'pdfName') { $schema.pdfName } else { '(none)' })",
    "storage field:  $(if (Test-SchemaHasProperty $schema 'storage') { $schema.storage } else { '(none)' })",
    "purpose:        $(if (Test-SchemaHasProperty $schema 'purpose') { $schema.purpose } else { '(none)' })",
    "flattenedExport:$(if (Test-SchemaHasProperty $schema 'flattenedExport') { $schema.flattenedExport } else { '(none)' })",
    "Pages in schema: $pageCount",
    "Overlays total:  $overlayCount",
    "Form fields ~:   $formFieldCount",
    "Audit entries:   $($audit.Count)",
    "Form signatures: $($sigAttestations.Count)",
    "Payload gzip:    $($compressed.Length) bytes",
    "PDF body bytes:  $pdfBodyLen",
    ""
)

if ($seal) {
    $summary += "=== Seal (layered hashes - not a circular file hash) ==="
    $summary += "  purpose:     $(if ($seal.purpose) { $seal.purpose } else { '?' })"
    $summary += "  sealedAt:    $(if ($seal.sealedAt) { $seal.sealedAt } else { '?' })"
    $summary += "  originHash:  $(if ($seal.originHash) { $seal.originHash } else { '(none)' })"
    $summary += "  contentHash: $(if ($seal.contentHash) { $seal.contentHash } else { '(none)' })"
    $summary += "  schemaHash:  $(if ($seal.schemaHash) { $seal.schemaHash } else { '(none)' })"
    $summary += "  flattened:   $(if ($null -ne $seal.flattened) { $seal.flattened } else { '?' })"
    $hasDevSig = $false
    if ($seal.sig -and $seal.sig.signature) { $hasDevSig = $true }
    $summary += "  device ECDSA:$(if ($hasDevSig) { 'present' } else { 'none' })"
    if ($seal.covers) { $summary += "  covers:      $($seal.covers)" }
    $summary += ""
} else {
    $summary += "No .seal object in schema (older project or seal skipped)."
    $summary += ""
}

$summary += "NOTE: audit[] is a rolling insight log (max ~200 entries in-app), not a legal e-sign trail."
$summary += "Form signature attestation is device-local (name/time/browser meta + hashes) - not TSA/identity-proofed e-sign."
$summary += "File-level history may also exist via OneDrive / share versioning where the file was saved."
$summary += ""

if ($sigAttestations.Count -gt 0) {
    $summary += "=== Form signature attestations ==="
    foreach ($s in $sigAttestations) {
        $summary += ("  page {0}  field={1}  name={2}  at={3}" -f `
            $s.pageIndex, `
            $(if ($s.fieldName) { $s.fieldName } else { '?' }), `
            $(if ($s.signedName) { $s.signedName } else { '?' }), `
            $(if ($s.signedAt) { $s.signedAt } else { '?' }))
        if ($s.attestationHash) { $summary += "    attestationHash: $($s.attestationHash)" }
        if ($s.originHash) { $summary += "    originHash:      $($s.originHash)" }
        if ($s.platform) { $summary += "    platform:        $($s.platform)" }
    }
    $summary += ""
}

if ($audit.Count -eq 0) {
    $summary += "No audit[] array found (project saved before audit feature, or no logged edits)."
} else {
    $summary += "=== Audit log (chronological) ==="
    foreach ($e in $audit) {
        $id = if ($null -ne $e.id) { $e.id } else { "?" }
        $t = if ($e.t) { $e.t } else { "?" }
        $act = if ($e.action) { $e.action } else { "?" }
        $det = ""
        if ($e.detail) {
            try { $det = ($e.detail | ConvertTo-Json -Compress -Depth 4) } catch { $det = "$($e.detail)" }
        }
        $summary += ("[{0}] {1}  {2}  {3}" -f $id, $t, $act, $det)
    }
}

$summary | Set-Content -Path $summaryPath -Encoding UTF8
$audit | ConvertTo-Json -Depth 8 | Set-Content -Path $auditJsonPath -Encoding UTF8

if ($seal) {
    $seal | ConvertTo-Json -Depth 8 | Set-Content -Path $sealPath -Encoding UTF8
    Write-Host "  Seal:        $sealPath"
}
if ($sigAttestations.Count -gt 0) {
    $sigAttestations | ConvertTo-Json -Depth 8 | Set-Content -Path $sigPath -Encoding UTF8
    Write-Host "  Signatures:  $sigPath"
}

# CSV for Excel
$csvRows = foreach ($e in $audit) {
    $det = ""
    if ($e.detail) {
        try { $det = ($e.detail | ConvertTo-Json -Compress -Depth 4) } catch { $det = "$($e.detail)" }
    }
    [PSCustomObject]@{
        id     = $e.id
        t      = $e.t
        action = $e.action
        detail = $det
    }
}
if ($csvRows) {
    $csvRows | Export-Csv -Path $auditCsvPath -NoTypeInformation -Encoding UTF8
} else {
    "id,t,action,detail" | Set-Content -Path $auditCsvPath -Encoding UTF8
}

if ($FullSchema) {
    $schemaPath = Join-Path $OutDir "${base}_schema.json"
    $jsonText | Set-Content -Path $schemaPath -Encoding UTF8
    Write-Host "  Full schema: $schemaPath"
}

if ($ExtractPdf) {
    $pdfPath = Join-Path $OutDir "${base}_body.pdf"
    if ($sourceLabel -eq "trailer" -and $trailer) {
        $pdfBytes = New-Object byte[] $trailer.MarkerIndex
        [Array]::Copy($bytes, 0, $pdfBytes, 0, $trailer.MarkerIndex)
        [System.IO.File]::WriteAllBytes($pdfPath, $pdfBytes)
        Write-Host "  PDF body:    $pdfPath (trailer stripped)"
    } else {
        # Catalog-only: file is already a valid PDF; still copy for a stable artifact name
        [System.IO.File]::Copy($Path, $pdfPath, $true)
        Write-Host "  PDF body:    $pdfPath (catalog-only source copy; /CCSProject still inside)"
    }
}

Write-Host ""
Write-Host "Wrote:" -ForegroundColor Green
Write-Host "  $summaryPath"
Write-Host "  $auditJsonPath"
Write-Host "  $auditCsvPath"
Write-Host ""
Write-Host ("Storage: {0} | Audit entries: {1} | Signatures: {2} | Integrity: {3}" -f `
    $sourceLabel, $audit.Count, $sigAttestations.Count, $integrity)
if ($integrity -eq "FAIL") { exit 2 }
exit 0
