modules/Azure/Discovery/Private/GetCIEMAzureDiscoveryCoverageReportData.ps1

function GetCIEMAzureDiscoveryCoverageReportData {
    [CmdletBinding()]
    param(
        [Parameter()]
        [int]$RunId
    )

    $ErrorActionPreference = 'Stop'

    $coverageAreaPath = Join-Path $script:AzureDiscoveryRoot 'Data/discovery_coverage_areas.json'
    if (-not (Test-Path $coverageAreaPath)) {
        throw "Discovery coverage area map was not found at $coverageAreaPath."
    }

    $coverageAreas = @(Get-Content $coverageAreaPath -Raw | ConvertFrom-Json -ErrorAction Stop)
    if ($coverageAreas.Count -eq 0) {
        throw "Discovery coverage area map contains no areas."
    }

    if ($PSBoundParameters.ContainsKey('RunId')) {
        $runRows = @(Invoke-CIEMQuery -Query 'SELECT * FROM azure_discovery_runs WHERE id = @id' -Parameters @{ id = $RunId })
        if ($runRows.Count -eq 0) {
            throw "Discovery run $RunId was not found."
        }
    }
    else {
        $runRows = @(Invoke-CIEMQuery -Query "SELECT * FROM azure_discovery_runs WHERE completed_at IS NOT NULL AND completed_at <> '' ORDER BY started_at DESC LIMIT 1")
        if ($runRows.Count -eq 0) {
            throw "No completed discovery run was found."
        }
    }

    $run = $runRows[0]
    $errorEntries = @()
    if ($run.error_message) {
        $errorEntries = @($run.error_message -split ';\s*' | Where-Object { $_.Trim() } | ForEach-Object { $_.Trim() })
    }

    $rows = @(foreach ($area in $coverageAreas) {
        foreach ($requiredProperty in @('area', 'sourceApi', 'runScopes', 'errorPrefixes')) {
            if (-not $area.PSObject.Properties[$requiredProperty]) {
                throw "Discovery coverage area map entry is missing required property '$requiredProperty'."
            }
        }

        $typeCount = 0
        $rowCount = 0
        $evidence = $null
        $missingReason = $null

        if ($area.PSObject.Properties['runTypeCountColumn']) {
            $typeCountProperty = $run.PSObject.Properties[$area.runTypeCountColumn]
            if (-not $typeCountProperty) {
                throw "Discovery run row is missing count column '$($area.runTypeCountColumn)'."
            }
            $typeCount = [int]$typeCountProperty.Value
        }

        if ($area.PSObject.Properties['runRowCountColumn']) {
            $rowCountProperty = $run.PSObject.Properties[$area.runRowCountColumn]
            if (-not $rowCountProperty) {
                throw "Discovery run row is missing count column '$($area.runRowCountColumn)'."
            }
            $rowCount = [int]$rowCountProperty.Value
        }

        if ($area.PSObject.Properties['countTable']) {
            if ($area.countTable -notmatch '^[a-z_]+$') {
                throw "Discovery coverage area '$($area.area)' has invalid count table '$($area.countTable)'."
            }
            $countRows = @(Invoke-CIEMQuery -Query "SELECT COUNT(*) AS row_count FROM $($area.countTable)")
            $rowCount = [int]$countRows[0].row_count
        }

        if ($area.PSObject.Properties['resourceTypeApiSource']) {
            $typeRows = @(Invoke-CIEMQuery -Query 'SELECT type, resource_count FROM azure_resource_types WHERE api_source = @api_source ORDER BY type' -Parameters @{ api_source = $area.resourceTypeApiSource })
            if ($typeRows.Count -gt 0) {
                $evidence = (($typeRows | ForEach-Object { "$($_.type)=$($_.resource_count)" }) -join '; ')
            }
        }
        elseif ($area.PSObject.Properties['evidenceLabel']) {
            $evidence = "$($area.evidenceLabel)=$rowCount"
        }

        $inScope = @($area.runScopes) -contains $run.scope
        if (-not $inScope) {
            $status = 'Skipped'
            $missingReason = "Run scope $($run.scope) did not include this area"
        }
        else {
            $matchedErrors = @(
                foreach ($entry in $errorEntries) {
                    foreach ($prefix in @($area.errorPrefixes)) {
                        if ($entry.StartsWith($prefix, [System.StringComparison]::Ordinal)) {
                            $entry
                        }
                    }
                }
            )

            if ($matchedErrors.Count -gt 0) {
                $missingReason = $matchedErrors[0]
                $status = if ($rowCount -gt 0) { 'Partial' } else { 'Missing' }
            }
            else {
                $status = 'Collected'
            }
        }

        $report = [CIEMAzureDiscoveryCoverageReport]::new()
        $report.Provider = 'Azure'
        $report.RunId = [int]$run.id
        $report.Scope = $run.scope
        $report.Area = $area.area
        $report.SourceApi = $area.sourceApi
        $report.Status = $status
        $report.TypeCount = $typeCount
        $report.RowCount = $rowCount
        $report.MissingReason = $missingReason
        $report.Evidence = $evidence
        $report
    })

    [pscustomobject]@{
        Rows = $rows
        Context = @{
            RunId = [int]$run.id
            Scope = $run.scope
            Status = $run.status
        }
    }
}