src/Public/Invoke-OpteraLicenseScan.ps1

function Invoke-OpteraLicenseScan {
    <#
    .SYNOPSIS
        Scans for Microsoft 365 licenses still assigned to dead/stale on-prem AD accounts and
        reports the monthly dollars wasted.
    .DESCRIPTION
        Read-only. Correlates on-premises Active Directory account state with Microsoft Graph
        license assignments for a hybrid (Entra Connect synced) tenant, prices the reclaimable licenses,
        and writes a report. The scan always runs and computes locally; the Free/Full license gate
        only controls whether the per-account remediation detail is revealed.

        Connect to Graph first (interactive) or pass app-only credentials. Use -OfflineFixturePath
        to run the whole pipeline against sample data with no tenant - handy for a demo or testing.
    .EXAMPLE
        Invoke-OpteraLicenseScan
        Interactive Graph sign-in, scans the current domain, prints the headline to the console.
    .EXAMPLE
        Invoke-OpteraLicenseScan -StaleDays 120 -OutputPath C:\Reports -Format Both
        Writes both an HTML report and (if unlocked) the remediation CSV.
    .EXAMPLE
        Invoke-OpteraLicenseScan -Forest -Verbose
        Multi-domain forest: discovers every domain, scans each, and aggregates the totals. Verbose
        output lists the per-domain account tallies.
    .EXAMPLE
        Invoke-OpteraLicenseScan -OfflineFixturePath .\tests\fixtures -OutputPath .\out -Format Html
        No tenant required - renders a sample report from the bundled fixtures.
    .OUTPUTS
        The report object (also see -PassThru). Console summary is always written.
    #>

    [CmdletBinding()]
    param(
        [string] $SearchBase,
        [string] $Server,

        # Scan every domain in the forest, not just the current one. Use this for multi-domain
        # forests so licensed dead accounts in child/sibling domains are counted. Default
        # (single-domain) behaviour is unchanged.
        [switch] $Forest,

        [ValidateRange(1, 3650)] [int] $StaleDays = 90,

        # OU(s) to suppress from the candidate list - e.g. a shared-mailbox or litigation-hold OU whose
        # licensed-but-disabled accounts are intentional. Accepts a full OU DN or a single component.
        [string[]] $ExcludeOu,

        [string] $PriceListPath,
        [string] $OfflineFixturePath,

        # Where to write file reports; defaults to the current directory when a Format is chosen.
        [string] $OutputPath,
        [ValidateSet('None', 'Html', 'Csv', 'Both')] [string] $Format = 'None',

        [string] $LicenseKey,

        # App-only (client credentials) Graph auth - omit for interactive sign-in.
        [string] $TenantId,
        [string] $ClientId,
        [string] $CertificateThumbprint,

        # Reference 'now' for staleness math; exposed mainly for deterministic testing.
        [datetime] $ReferenceDate = (Get-Date),

        [switch] $PassThru
    )

    # Call the core explicitly (do not splat $PSBoundParameters - it carries OutputPath/Format/PassThru
    # which the core does not accept).
    $report = Invoke-LicenseReclaimScanCore -SearchBase $SearchBase -Server $Server -Forest:$Forest -StaleDays $StaleDays `
        -ExcludeOu $ExcludeOu -PriceListPath $PriceListPath -OfflineFixturePath $OfflineFixturePath -LicenseKey $LicenseKey `
        -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint `
        -ReferenceDate $ReferenceDate

    Write-ReclaimReportConsole -Report $report

    if ($Format -ne 'None') {
        $dir = if ($OutputPath) { $OutputPath } else { (Get-Location).Path }
        if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null }
        $stamp = '{0:yyyyMMdd-HHmmss}' -f $report.GeneratedAt

        if ($Format -in 'Html', 'Both') {
            $htmlPath = New-ReclaimReportHtml -Report $report -Path (Join-Path $dir "license-reclaim-$stamp.html")
            Write-Host " HTML report: $htmlPath" -ForegroundColor Cyan
        }
        if ($Format -in 'Csv', 'Both') {
            $csvPath = New-ReclaimReportCsv -Report $report -Path (Join-Path $dir "license-reclaim-$stamp.csv")
            if ($csvPath) { Write-Host " CSV remediation list: $csvPath" -ForegroundColor Cyan }
        }
    }

    # Console (and any files) are the default output; return the object only when asked, so the
    # rich report isn't followed by PowerShell format-listing the whole object to the screen.
    if ($PassThru) { return $report }
}