src/Private/Invoke-LicenseReclaimScanCore.ps1

function Invoke-LicenseReclaimScanCore {
    <#
    .SYNOPSIS
        Runs the full scan pipeline and returns the assembled report object (no rendering).
    .DESCRIPTION
        Shared engine behind Invoke-OpteraLicenseScan and Get-OpteraReclaimableLicense. Collects AD +
        Graph data (live, or from an offline fixture directory), correlates, prices, evaluates the
        license gate, and returns one report object. Pure data - the public cmdlets decide how to
        present it and what the Free/Full gate is allowed to reveal.
    .OUTPUTS
        Report PSCustomObject: GeneratedAt, Mode, TenantId, StaleDays, Summary, Inventory, Reclaimable, License, Disclaimer
    #>

    [CmdletBinding()]
    param(
        [string]   $SearchBase,
        [string]   $Server,
        [switch]   $Forest,
        [int]      $StaleDays = 90,
        [string[]] $ExcludeOu,
        [string]   $PriceListPath,
        [string]   $OfflineFixturePath,
        [string]   $LicenseKey,
        [string]   $TenantId,
        [string]   $ClientId,
        [string]   $CertificateThumbprint,
        [datetime] $ReferenceDate = (Get-Date)
    )

    $priceList = Get-OpteraSkuPriceList -PriceListPath $PriceListPath

    if ($OfflineFixturePath) {
        if ($Forest) { Write-Warning '-Forest has no effect in offline fixture mode (the fixture already represents the whole environment); ignoring it.' }
        Write-Verbose "Offline mode: loading fixtures from $OfflineFixturePath"
        $adAccounts = Get-Content -LiteralPath (Join-Path $OfflineFixturePath 'ad-accounts.sample.json') -Raw | ConvertFrom-Json
        $graphUsers = Get-Content -LiteralPath (Join-Path $OfflineFixturePath 'graph-users.sample.json') -Raw | ConvertFrom-Json
        $skusFixture = Join-Path $OfflineFixturePath 'graph-skus.sample.json'
        $subscribedSkus = if (Test-Path -LiteralPath $skusFixture) {
            @(Get-Content -LiteralPath $skusFixture -Raw | ConvertFrom-Json)
        } else { @() }
        $tenantLabel = 'offline-fixture'
    }
    else {
        $context = Connect-ReclaimGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint
        $tenantLabel = if ($context) { $context.TenantId } else { 'unknown' }
        if ($Forest) {
            if ($SearchBase) { Write-Warning '-SearchBase is ignored in -Forest mode; each discovered domain is scanned from its own naming-context root.' }
            Write-Verbose 'Collecting on-prem AD account signals across the whole forest...'
            $adAccounts = @(Get-AdForestAccountSignals -Server $Server)
        }
        else {
            Write-Verbose 'Collecting on-prem AD account signals...'
            $adAccounts = @(Get-AdAccountSignals -SearchBase $SearchBase -Server $Server)
        }
        Write-Verbose 'Collecting cloud licensed users from Microsoft Graph...'
        $graphUsers = @(Get-GraphLicensedUsers)
        # Tenant license inventory (prepaid vs consumed) powers the owned-vs-reclaimable view. It is
        # enrichment, not the core finding, so a failure here (e.g. Organization.Read.All not
        # consented) must not sink the scan - degrade to an empty inventory and carry on.
        Write-Verbose 'Collecting tenant license inventory (subscribedSkus) from Microsoft Graph...'
        $subscribedSkus = @()
        try {
            $subscribedSkus = @(Get-GraphSubscribedSkus)
        }
        catch {
            Write-Warning ("Could not read tenant license inventory (subscribedSkus): {0}. The owned-vs-reclaimable view will be omitted; ensure Organization.Read.All or Directory.Read.All is consented." -f $_.Exception.Message)
        }
    }

    $reclaimable = @(Join-AccountLicense -AdAccounts @($adAccounts) -GraphUsers @($graphUsers) -StaleDays $StaleDays -ReferenceDate $ReferenceDate -ExcludeOu $ExcludeOu)
    $measured = Measure-WastedSpend -Reclaimable $reclaimable -PriceList $priceList
    $inventory = @(Get-SkuReclaimInventory -BySku @($measured.Summary.BySku) -SubscribedSkus @($subscribedSkus))

    # Offline fixture runs validate by format only (no tenant, no network); live runs validate the
    # key against the connected tenant via the Optera AI endpoint.
    if ($OfflineFixturePath) {
        $license = Test-OpteraLicenseKey -LicenseKey $LicenseKey -Offline
    }
    else {
        $license = Test-OpteraLicenseKey -LicenseKey $LicenseKey -TenantId $tenantLabel
    }

    [pscustomobject]@{
        GeneratedAt = $ReferenceDate
        Mode        = $license.Mode
        TenantId    = $tenantLabel
        StaleDays   = $StaleDays
        Summary     = $measured.Summary
        Inventory   = $inventory
        Reclaimable = $measured.PricedReclaimable
        License     = $license
        Disclaimer  = 'Figures use Microsoft list prices and may differ from your EA/CSP/negotiated rate. Verify before acting. Read-only scan; no directory contents leave your network.'
    }
}