Modules/Private/Get-S2DExpansionHeadroom.ps1

# Get-S2DExpansionHeadroom — calculates remaining room to expand or create volumes
# at 70 / 80 / 90 / 100% fill thresholds against AvailableForVolumes (footprint basis).
#
# CANONICAL MATH (docs/capacity-model.md):
# A = CapacityWaterfall.AvailableForVolumes.Bytes (excludes pool meta, reserve, infra)
# U = sum of FootprintOnPool.Bytes for non-infra volumes
# copies = prevailing NumberOfDataCopies among non-infra volumes (assumed for new volumes)
# currentUtilizationPct = U / A * 100
# For X in {0.70, 0.80, 0.90, 1.00}:
# footprintBudgetBytes = X * A
# remainingFootprintBytes = max(0, X*A - U)
# remainingNewUsableBytes = remainingFootprintBytes / copies
# isPastLine = (U > X*A)

function Get-S2DExpansionHeadroom {
    <#
    .SYNOPSIS
        Calculates expansion headroom at 70/80/90/100% fill thresholds.

    .DESCRIPTION
        Given a completed capacity waterfall and the current volume map, computes
        how much footprint space and new usable data capacity remain before each
        fill threshold is crossed. Uses the prevailing (most-common)
        NumberOfDataCopies among non-infrastructure volumes to estimate new-volume
        usable data from remaining footprint.

        All capacity values are returned as [S2DCapacity] objects carrying both
        TiB (binary) and TB (decimal) representations.

    .PARAMETER Waterfall
        The S2DCapacityWaterfall object from Get-S2DCapacityWaterfall.
        AvailableForVolumes.Bytes is the denominator (A).

    .PARAMETER Volumes
        Array of S2DVolume objects from Get-S2DVolumeMap.
        Only non-infrastructure volumes contribute to the used footprint (U).

    .OUTPUTS
        PSCustomObject with CurrentUtilizationPct, PrevalentDataCopies,
        AvailableForVolumesBytes, UsedFootprintBytes, and a Thresholds array.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object] $Waterfall,

        [Parameter(Mandatory)]
        [AllowEmptyCollection()]
        [object[]] $Volumes
    )

    # A = available for volumes (footprint basis, excludes meta + reserve + infra)
    $availBytes = if ($Waterfall -and $Waterfall.AvailableForVolumes -and
                      $Waterfall.AvailableForVolumes.Bytes -gt 0) {
        [int64]$Waterfall.AvailableForVolumes.Bytes
    } else {
        [int64]0
    }

    # Non-infrastructure volumes only
    $workloadVols = @($Volumes | Where-Object { -not $_.IsInfrastructureVolume })

    # U = sum of footprint bytes for non-infra volumes
    $usedBytes = [int64](
        ($workloadVols | ForEach-Object {
            if ($_.FootprintOnPool -and $_.FootprintOnPool.Bytes -gt 0) {
                [int64]$_.FootprintOnPool.Bytes
            } else { [int64]0 }
        } | Measure-Object -Sum).Sum
    )

    # Prevailing data copies — most common NumberOfDataCopies among non-infra volumes.
    # Defaults to 2 (two-way mirror) when volumes list is empty.
    $prevalentCopies = if ($workloadVols.Count -gt 0) {
        $grouped = $workloadVols |
            Group-Object -Property NumberOfDataCopies |
            Sort-Object -Property Count -Descending
        [int]$grouped[0].Name
    } else {
        2
    }

    $currentPct = if ($availBytes -gt 0) {
        [math]::Round($usedBytes / $availBytes * 100, 1)
    } else { 0.0 }

    $thresholds = foreach ($pct in @(70, 80, 90, 100)) {
        $fraction              = $pct / 100.0
        $budgetBytes           = [int64][math]::Round($fraction * $availBytes)
        $remainingFootprint    = [int64][math]::Max([int64]0, $budgetBytes - $usedBytes)
        $remainingUsable       = if ($prevalentCopies -gt 0) {
            [int64][math]::Round($remainingFootprint / $prevalentCopies)
        } else { [int64]0 }
        $isPast                = ($usedBytes -gt $budgetBytes)

        [PSCustomObject]@{
            FillTargetPct               = $pct
            FootprintBudget             = [S2DCapacity]::new($budgetBytes)
            RemainingFootprint          = [S2DCapacity]::new($remainingFootprint)
            NewUsableData               = [S2DCapacity]::new($remainingUsable)
            IsPastLine                  = $isPast
            IsRecommendedPlanningLine   = ($pct -eq 70)
        }
    }

    [PSCustomObject]@{
        CurrentUtilizationPct     = $currentPct
        PrevalentDataCopies       = $prevalentCopies
        PrevalentCopiesNote       = "Assumed for new-volume estimates. Actual resiliency can differ."
        AvailableForVolumesBytes  = $availBytes
        UsedFootprintBytes        = $usedBytes
        Thresholds                = @($thresholds)
    }
}