content/src/scripts/Get-CostEstimate.ps1

# ---------------------------------------------------------------------------
# Get-CostEstimate.ps1
# Axeon Cost Estimation Engine
#
# Compiles Bicep templates from a deployment YAML to ARM JSON, extracts
# resource types and SKUs, then looks up costs from the local pricing cache
# generated by Update-AxeonPricing.
#
# Usage:
# . ./Get-CostEstimate.ps1
# Get-AxeonCostEstimate -DeploymentFile ./deployments/hub-networking.yaml
# Get-AxeonCostEstimate -DeploymentFile ./deployments/hub-networking.yaml -Detailed
# ---------------------------------------------------------------------------
#Requires -Version 7.0

$ErrorActionPreference = "Stop"

# Dot-source the pricing engine for Bicep compilation helpers
. (Join-Path $PSScriptRoot "Get-AzurePricing.ps1")

# ============================================================================
# AZURE RESOURCE PROPERTY DEFAULTS
# Pricing-relevant property defaults per ARM resource type.
# These mirror Azure's own defaults — when a consumer omits a property,
# the cost engine uses the same default Azure would.
# Only properties that affect meter selection are listed here.
# ============================================================================

$script:ResourcePropertyDefaults = @{
    'Microsoft.Network/publicIPAddresses' = @{
        allocationMethod = 'Dynamic'
    }
    'Microsoft.Storage/storageAccounts' = @{
        accessTier  = 'Hot'
        replication = 'LRS'
    }
    'Microsoft.Compute/virtualMachines' = @{
        priority = 'Regular'
    }
    'Microsoft.Compute/disks' = @{
        tier = 'Standard'
    }
    'Microsoft.Sql/servers/databases' = @{
        tier = 'GeneralPurpose'
    }
    'Microsoft.Web/serverfarms' = @{
        tier = 'Free'
    }
    'Microsoft.Cache/redis' = @{
        tier = 'Basic'
    }
    'Microsoft.ContainerRegistry/registries' = @{
        tier = 'Basic'
    }
    'Microsoft.KeyVault/vaults' = @{
        tier = 'Standard'
    }
    'Microsoft.Network/applicationGateways' = @{
        tier = 'Standard_v2'
    }
    'Microsoft.Network/loadBalancers' = @{
        tier = 'Basic'
    }
}

# ============================================================================
# INTERNAL HELPERS
# ============================================================================

<#
.SYNOPSIS
    Reads the pricing cache file and returns the parsed data.
#>

function Read-PricingCache {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$CachePath
    )

    if (-not (Test-Path $CachePath)) {
        throw @"
Pricing cache not found at '$CachePath'.
Run 'Update-AxeonPricing' first to fetch live pricing data.

Example:
  Update-AxeonPricing -PlatformSpecPath ./platform-spec.json -DeploymentFile ./deployments/hub-networking.yaml
  Update-AxeonPricing -Region uksouth
"@

    }

    $raw = Get-Content $CachePath -Raw | ConvertFrom-Json -AsHashtable

    # Validate cache structure
    if (-not $raw.ContainsKey('resources') -or -not $raw.ContainsKey('region')) {
        throw "Invalid pricing cache file at '$CachePath'. Re-run Update-AxeonPricing to regenerate."
    }

    # Warn if cache is old (> 30 days)
    if ($raw.ContainsKey('lastUpdated')) {
        $lastUpdated = if ($raw.lastUpdated -is [datetime]) {
            $raw.lastUpdated
        } else {
            [datetime]::Parse($raw.lastUpdated, [System.Globalization.CultureInfo]::InvariantCulture, [System.Globalization.DateTimeStyles]::RoundtripKind)
        }
        $age = (Get-Date) - $lastUpdated
        if ($age.TotalDays -gt 30) {
            Write-Warning "Pricing cache is $([math]::Round($age.TotalDays)) days old. Run Update-AxeonPricing to refresh."
        }
    }

    return $raw
}

<#
.SYNOPSIS
    Classifies a resource as Free or Paid based on the pricing cache.
.DESCRIPTION
    Looks up the ARM resource type in the cache. If found, uses the cached
    isFree flag and consumption meters. Resource properties (merged with
    Azure defaults) are used to narrow meter matching by meterName contains.
.OUTPUTS
    A hashtable with: IsFree, EstimatedDailyCost, EstimatedMonthlyCost,
    EstimatedAnnualCost, MatchedMeters, Notes, Source
#>

function Get-ResourceCostClassification {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$ArmResourceType,

        [string]$Sku,

        [string]$Tier,

        [hashtable]$PricingCache,

        [hashtable]$ResourceProperties
    )

    $resources = $PricingCache.resources

    # Direct match in cache
    if ($resources.ContainsKey($ArmResourceType)) {
        $entry = $resources[$ArmResourceType]

        if ($entry.isFree) {
            return @{
                IsFree               = $true
                EstimatedDailyCost   = 0.0
                EstimatedMonthlyCost = 0.0
                EstimatedAnnualCost  = 0.0
                MatchedMeters        = @()
                Notes                = "Free resource (all consumption meters are `$0 or none exist)"
                Source               = 'cache'
            }
        }

        # Paid resource — try to find matching meters
        $matchedMeters = @()
        $estimatedDailyCost = 0.0
        $estimatedMonthlyCost = 0.0
        $estimatedAnnualCost = 0.0

        if ($entry.consumptionMeters -and $entry.consumptionMeters.Count -gt 0) {
            $meters = $entry.consumptionMeters

            # If SKU provided, try to match
            if ($Sku) {
                $skuMatches = @($meters | Where-Object {
                    ($_.skuName -and $_.skuName -like "*$Sku*") -or
                    ($_.armSkuName -and $_.armSkuName -like "*$Sku*")
                })
                if ($skuMatches.Count -gt 0) {
                    $matchedMeters = $skuMatches
                }
            }

            # If resource properties provided, narrow by meterName contains (AND logic on all values)
            if ($ResourceProperties -and $ResourceProperties.Count -gt 0) {
                $propValues = @($ResourceProperties.Values | ForEach-Object { "$_" } | Where-Object { $_ })
                if ($propValues.Count -gt 0) {
                    $pool = if ($matchedMeters.Count -gt 0) { $matchedMeters } else { $meters }
                    $propMatches = @($pool | Where-Object {
                        $mn = $_.meterName
                        $allMatch = $true
                        foreach ($val in $propValues) {
                            if ($mn -notlike "*$val*") {
                                $allMatch = $false
                                break
                            }
                        }
                        $allMatch
                    })
                    if ($propMatches.Count -gt 0) {
                        $matchedMeters = $propMatches
                    }
                    else {
                        Write-Verbose " Resource properties [$($propValues -join ', ')] did not match any meters for $ArmResourceType"
                    }
                }
            }

            # If no SKU/property match, show cheapest non-zero meters as baseline
            if ($matchedMeters.Count -eq 0) {
                $matchedMeters = @($meters |
                    Where-Object { $_.retailPrice -gt 0 } |
                    Sort-Object { $_.retailPrice } |
                    Select-Object -First 3)
            }

            # Cost estimates from the cheapest matched meter
            if ($matchedMeters.Count -gt 0) {
                $cheapest = $matchedMeters | Sort-Object { $_.retailPrice } | Select-Object -First 1

                # Use pre-calculated prices from cache if available, otherwise derive them
                if ($null -ne $cheapest.dailyPrice) {
                    $estimatedDailyCost = [math]::Round($cheapest.dailyPrice, 2)
                    $estimatedMonthlyCost = [math]::Round($cheapest.monthlyPrice, 2)
                    $estimatedAnnualCost = [math]::Round($cheapest.annualPrice, 2)
                }
                else {
                    $unit = $cheapest.unitOfMeasure ?? ''
                    $estimatedDailyCost = switch -Wildcard ($unit) {
                        '*Hour*'   { [math]::Round($cheapest.retailPrice * 24, 2) }
                        '*Day*'    { [math]::Round($cheapest.retailPrice, 2) }
                        '*Month*'  { [math]::Round($cheapest.retailPrice / 30, 2) }
                        '*Year*'   { [math]::Round($cheapest.retailPrice / 365, 2) }
                        default    { [math]::Round($cheapest.retailPrice, 4) }
                    }
                    $estimatedMonthlyCost = switch -Wildcard ($unit) {
                        '*Hour*'   { [math]::Round($cheapest.retailPrice * 730, 2) }
                        '*Day*'    { [math]::Round($cheapest.retailPrice * 30, 2) }
                        '*Month*'  { [math]::Round($cheapest.retailPrice, 2) }
                        '*Year*'   { [math]::Round($cheapest.retailPrice / 12, 2) }
                        default    { [math]::Round($cheapest.retailPrice, 4) }
                    }
                    $estimatedAnnualCost = switch -Wildcard ($unit) {
                        '*Hour*'   { [math]::Round($cheapest.retailPrice * 8760, 2) }
                        '*Day*'    { [math]::Round($cheapest.retailPrice * 365, 2) }
                        '*Month*'  { [math]::Round($cheapest.retailPrice * 12, 2) }
                        '*Year*'   { [math]::Round($cheapest.retailPrice, 2) }
                        default    { [math]::Round($cheapest.retailPrice, 4) }
                    }
                }
            }
        }

        return @{
            IsFree               = $false
            EstimatedDailyCost   = $estimatedDailyCost
            EstimatedMonthlyCost = $estimatedMonthlyCost
            EstimatedAnnualCost  = $estimatedAnnualCost
            MatchedMeters        = $matchedMeters
            Notes                = "Paid resource"
            Source               = 'cache'
        }
    }

    # Not found in cache — unknown
    return @{
        IsFree               = $false
        EstimatedDailyCost   = 0.0
        EstimatedMonthlyCost = 0.0
        EstimatedAnnualCost  = 0.0
        MatchedMeters        = @()
        Notes                = "Not in pricing cache. Run: Update-AxeonPricing -DeploymentFile <your-deployment.yaml>"
        Source               = 'unknown'
    }
}

# ============================================================================
# PUBLIC FUNCTIONS
# ============================================================================

<#
.SYNOPSIS
    Estimates the cost of an Axeon deployment by analyzing its Bicep templates.
.DESCRIPTION
    For each module in a deployment YAML file:
      1. Compiles the Bicep template to ARM JSON (requires 'az bicep')
      2. Extracts all Azure resource types and SKUs
      3. Looks up each resource in the local pricing cache
      4. Classifies each as Free or Paid with estimated monthly cost

    The pricing cache must exist (generated by Update-AxeonPricing).
.PARAMETER DeploymentFile
    Path to the deployment YAML file (e.g., deployments/hub-networking.yaml).
.PARAMETER PricingCachePath
    Path to the pricing cache JSON (default: ./azure-pricing.json).
.PARAMETER BicepRoot
    Root path for resolving Bicep template paths. Defaults to src/bicep
    relative to the script directory.
.PARAMETER Detailed
    Show per-meter pricing details for paid resources.
.EXAMPLE
    Get-AxeonCostEstimate -DeploymentFile ./deployments/hub-networking.yaml
.EXAMPLE
    Get-AxeonCostEstimate -DeploymentFile ./deployments/hub-networking.yaml -Detailed
#>

function Get-AxeonCostEstimate {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path $_ -PathType Leaf })]
        [string]$DeploymentFile,

        [string]$PricingCachePath = './azure-pricing.json',

        [string]$BicepRoot,

        [switch]$Detailed
    )

    Write-Host ""
    Write-Host " Axeon Cost Estimate" -ForegroundColor Cyan
    Write-Host " ─────────────────────────────────────────" -ForegroundColor DarkCyan
    Write-Host ""

    # Read pricing cache
    $cache = Read-PricingCache -CachePath $PricingCachePath
    Write-Host " Pricing cache : $PricingCachePath" -ForegroundColor DarkGray
    Write-Host " Region : $($cache.region)" -ForegroundColor DarkGray
    Write-Host " Currency : $($cache.currency ?? 'USD')" -ForegroundColor DarkGray
    Write-Host " Last updated : $($cache.lastUpdated)" -ForegroundColor DarkGray
    Write-Host ""

    # Parse deployment YAML
    $yamlModule = Get-Module -ListAvailable -Name 'powershell-yaml' -ErrorAction SilentlyContinue
    if (-not $yamlModule) {
        throw "Module 'powershell-yaml' is required. Install: Install-Module powershell-yaml"
    }
    Import-Module powershell-yaml -ErrorAction SilentlyContinue

    $raw = Get-Content $DeploymentFile -Raw
    $deploy = $raw | ConvertFrom-Yaml

    if (-not $deploy.modules -or $deploy.modules.Count -eq 0) {
        Write-Warning "Deployment has no modules defined."
        return
    }

    if (-not $BicepRoot) {
        $BicepRoot = Join-Path $PSScriptRoot ".." "bicep"
    }

    Write-Host " Deployment : $($deploy.name)" -ForegroundColor White
    Write-Host " Modules : $($deploy.modules.Count)" -ForegroundColor White
    Write-Host ""

    # Analyze each module
    $moduleResults = @()
    $totalDailyEstimate = 0.0
    $totalMonthlyEstimate = 0.0
    $totalAnnualEstimate = 0.0
    $allFree = $true

    foreach ($mod in $deploy.modules) {
        $moduleName = $mod.name
        $templatePath = Join-Path $BicepRoot $mod.template

        Write-Host " Module: $moduleName" -ForegroundColor Cyan
        Write-Host " Template: $($mod.template)" -ForegroundColor DarkGray

        # Compile Bicep and extract resource types (with NameParam for parameter mapping)
        $resources = Get-BicepResourceTypes -TemplatePath $templatePath

        if ($resources.Count -eq 0) {
            Write-Host " No Azure resources found (template may not exist or failed to compile)" -ForegroundColor DarkYellow
            $moduleResults += @{
                Module     = $moduleName
                Template   = $mod.template
                Resources  = @()
                IsFree     = $true
                DailyCost  = 0.0
                MonthlyCost = 0.0
                AnnualCost = 0.0
                Status     = 'NoResources'
            }
            continue
        }

        # Build parameter-name → resource properties mapping from YAML parameters
        # Parameters that are hashtables (objects) contain resource properties
        $paramProperties = @{}
        if ($mod.ContainsKey('parameters') -and $mod.parameters) {
            foreach ($paramEntry in $mod.parameters.GetEnumerator()) {
                if ($paramEntry.Value -is [hashtable] -or $paramEntry.Value -is [System.Collections.IDictionary]) {
                    $paramProperties[$paramEntry.Key] = $paramEntry.Value
                }
            }
        }
        if ($paramProperties.Count -gt 0) {
            Write-Host " Resource properties: $($paramProperties.Count) parameter(s)" -ForegroundColor DarkGray
        }

        $moduleResources = @()
        $moduleDailyCost = 0.0
        $moduleMonthlyCost = 0.0
        $moduleAnnualCost = 0.0
        $moduleFree = $true

        foreach ($r in $resources) {
            # Resolve resource properties: defaults → overlay with consumer-declared
            $mergedProps = @{}

            # Start with defaults for this ARM type
            if ($script:ResourcePropertyDefaults.ContainsKey($r.Type)) {
                foreach ($kv in $script:ResourcePropertyDefaults[$r.Type].GetEnumerator()) {
                    $mergedProps[$kv.Key] = $kv.Value
                }
            }

            # Overlay consumer-declared properties via NameParam → YAML parameter mapping
            if ($r.NameParam -and $paramProperties.ContainsKey($r.NameParam)) {
                foreach ($kv in $paramProperties[$r.NameParam].GetEnumerator()) {
                    $mergedProps[$kv.Key] = $kv.Value
                }
            }

            $classification = Get-ResourceCostClassification `
                -ArmResourceType $r.Type `
                -Sku $r.Sku `
                -Tier $r.Tier `
                -PricingCache $cache `
                -ResourceProperties $mergedProps

            $resourceResult = @{
                Type         = $r.Type
                Sku          = $r.Sku
                IsFree       = $classification.IsFree
                DailyCost    = $classification.EstimatedDailyCost
                MonthlyCost  = $classification.EstimatedMonthlyCost
                AnnualCost   = $classification.EstimatedAnnualCost
                Notes        = $classification.Notes
                Meters       = $classification.MatchedMeters
                Source       = $classification.Source
            }

            $moduleResources += $resourceResult

            if (-not $classification.IsFree) {
                $moduleFree = $false
                $allFree = $false
                $moduleDailyCost += $classification.EstimatedDailyCost
                $moduleMonthlyCost += $classification.EstimatedMonthlyCost
                $moduleAnnualCost += $classification.EstimatedAnnualCost
            }

            # Display resource line
            $freeLabel = if ($classification.IsFree) { "FREE" } else { "PAID" }
            $color = if ($classification.IsFree) { 'Green' } else { 'Yellow' }
            $costStr = if ($classification.IsFree) { "" } else { " (~$($classification.EstimatedDailyCost)/day | $($classification.EstimatedMonthlyCost)/mo | $($classification.EstimatedAnnualCost)/yr)" }
            $skuStr = if ($r.Sku) { " [$($r.Sku)]" } else { "" }

            if ($classification.Source -eq 'unknown') {
                Write-Host " [????] $($r.Type)$skuStr — not in cache" -ForegroundColor DarkYellow
            }
            else {
                Write-Host " [$freeLabel] $($r.Type)$skuStr$costStr" -ForegroundColor $color
            }

            # Detailed meter info
            if ($Detailed -and $classification.MatchedMeters.Count -gt 0) {
                foreach ($meter in $classification.MatchedMeters) {
                    $dailyStr = if ($null -ne $meter.dailyPrice) { "$($meter.dailyPrice)/day" } else { "" }
                    $monthlyStr = if ($null -ne $meter.monthlyPrice) { "$($meter.monthlyPrice)/mo" } else { "" }
                    $annualStr = if ($null -ne $meter.annualPrice) { "$($meter.annualPrice)/yr" } else { "" }
                    $priceBreakdown = @($dailyStr, $monthlyStr, $annualStr) | Where-Object { $_ } | Join-String -Separator ' | '
                    $breakdownStr = if ($priceBreakdown) { " (~$priceBreakdown)" } else { "" }
                    Write-Host " $($meter.meterName): $($cache.currency ?? 'USD') $($meter.retailPrice)/$($meter.unitOfMeasure)$breakdownStr" -ForegroundColor DarkGray
                }
            }
        }

        $totalDailyEstimate += $moduleDailyCost
        $totalMonthlyEstimate += $moduleMonthlyCost
        $totalAnnualEstimate += $moduleAnnualCost

        $moduleResults += @{
            Module      = $moduleName
            Template    = $mod.template
            Resources   = $moduleResources
            IsFree      = $moduleFree
            DailyCost   = [math]::Round($moduleDailyCost, 2)
            MonthlyCost = [math]::Round($moduleMonthlyCost, 2)
            AnnualCost  = [math]::Round($moduleAnnualCost, 2)
            Status      = if ($moduleFree) { 'Free' } else { 'Paid' }
        }

        Write-Host ""
    }

    # Summary
    $unknownCount = 0
    foreach ($mr in $moduleResults) {
        foreach ($res in $mr.Resources) {
            if ($res.Source -eq 'unknown') { $unknownCount++ }
        }
    }

    Write-Host " ═══════════════════════════════════════════════════════" -ForegroundColor DarkCyan
    Write-Host " Cost Estimate Summary" -ForegroundColor Cyan
    Write-Host " ─────────────────────────────────────────────────────" -ForegroundColor DarkCyan
    Write-Host ""

    foreach ($mr in $moduleResults) {
        $statusColor = switch ($mr.Status) {
            'Free'        { 'Green' }
            'Paid'        { 'Yellow' }
            'NoResources' { 'DarkGray' }
            default       { 'Gray' }
        }
        $costStr = if ($mr.MonthlyCost -gt 0) { " — ~$($cache.currency ?? 'USD') $($mr.DailyCost)/day | $($mr.MonthlyCost)/mo | $($mr.AnnualCost)/yr" } else { "" }
        Write-Host " [$($mr.Status)] $($mr.Module)$costStr" -ForegroundColor $statusColor
    }

    Write-Host ""
    $totalColor = if ($totalMonthlyEstimate -eq 0) { 'Green' } else { 'Yellow' }
    Write-Host " Estimated daily total : $($cache.currency ?? 'USD') $([math]::Round($totalDailyEstimate, 2))" -ForegroundColor $totalColor
    Write-Host " Estimated monthly total : $($cache.currency ?? 'USD') $([math]::Round($totalMonthlyEstimate, 2))" -ForegroundColor $totalColor
    Write-Host " Estimated annual total : $($cache.currency ?? 'USD') $([math]::Round($totalAnnualEstimate, 2))" -ForegroundColor $totalColor

    if ($unknownCount -gt 0) {
        Write-Host ""
        Write-Host " $unknownCount resource type(s) not in pricing cache." -ForegroundColor DarkYellow
        Write-Host " Run: Update-AxeonPricing -DeploymentFile $DeploymentFile" -ForegroundColor DarkYellow
    }

    Write-Host ""

    if ($totalMonthlyEstimate -eq 0 -and $allFree -and $unknownCount -eq 0) {
        Write-Host " All resources in this deployment are FREE." -ForegroundColor Green
    }
    elseif ($totalMonthlyEstimate -gt 0) {
        Write-Host " Note: Estimates are based on base pricing (lowest consumption meter)." -ForegroundColor DarkGray
        Write-Host " Actual costs depend on usage, reserved instances, and configuration." -ForegroundColor DarkGray
    }

    Write-Host ""

    # Return structured result for programmatic use
    return @{
        Deployment        = $deploy.name
        Region            = $cache.region
        Currency          = $cache.currency ?? 'USD'
        Modules           = $moduleResults
        TotalDailyCost    = [math]::Round($totalDailyEstimate, 2)
        TotalMonthlyCost  = [math]::Round($totalMonthlyEstimate, 2)
        TotalAnnualCost   = [math]::Round($totalAnnualEstimate, 2)
        AllFree           = $allFree
        UnknownCount      = $unknownCount
        PricingCacheDate  = $cache.lastUpdated
    }
}

<#
.SYNOPSIS
    Tests whether a specific deployment module contains only free resources.
.DESCRIPTION
    Used internally by the -FreeOnly flag in the bootstrap engine.
    Compiles a single Bicep template and checks all resources against the
    pricing cache.

    Resource properties from YAML parameters (hashtable values) are merged
    with Azure defaults to accurately match pricing meters.

    Resources not found in the cache are treated as potentially paid
    (unknown = not free, for safety).
.PARAMETER TemplatePath
    Absolute path to the Bicep template file.
.PARAMETER PricingCachePath
    Path to the pricing cache JSON.
.PARAMETER ModuleParameters
    The full parameters hashtable from the deployment YAML module.
    Object-valued parameters are treated as resource property declarations.
.OUTPUTS
    Hashtable with: IsFree, PaidResources, UnknownResources, AllResources
#>

function Test-ModuleFreeStatus {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$TemplatePath,

        [Parameter(Mandatory)]
        [string]$PricingCachePath,

        [hashtable]$ModuleParameters
    )

    $cache = Read-PricingCache -CachePath $PricingCachePath

    $resources = Get-BicepResourceTypes -TemplatePath $TemplatePath

    if ($resources.Count -eq 0) {
        return @{
            IsFree           = $true
            PaidResources    = @()
            UnknownResources = @()
            AllResources     = @()
        }
    }

    # Extract object-valued parameters (resource property declarations)
    $paramProperties = @{}
    if ($ModuleParameters) {
        foreach ($paramEntry in $ModuleParameters.GetEnumerator()) {
            if ($paramEntry.Value -is [hashtable] -or $paramEntry.Value -is [System.Collections.IDictionary]) {
                $paramProperties[$paramEntry.Key] = $paramEntry.Value
            }
        }
    }

    $paidResources    = @()
    $unknownResources = @()
    $allResources     = @()

    foreach ($r in $resources) {
        # Resolve resource properties: defaults → overlay with consumer-declared
        $mergedProps = @{}
        if ($script:ResourcePropertyDefaults.ContainsKey($r.Type)) {
            foreach ($kv in $script:ResourcePropertyDefaults[$r.Type].GetEnumerator()) {
                $mergedProps[$kv.Key] = $kv.Value
            }
        }
        if ($r.NameParam -and $paramProperties.ContainsKey($r.NameParam)) {
            foreach ($kv in $paramProperties[$r.NameParam].GetEnumerator()) {
                $mergedProps[$kv.Key] = $kv.Value
            }
        }

        $classification = Get-ResourceCostClassification `
            -ArmResourceType $r.Type `
            -Sku $r.Sku `
            -Tier $r.Tier `
            -PricingCache $cache `
            -ResourceProperties $mergedProps

        $allResources += @{
            Type    = $r.Type
            Sku     = $r.Sku
            IsFree  = $classification.IsFree
            Cost    = $classification.EstimatedMonthlyCost
            Notes   = $classification.Notes
            Source  = $classification.Source
        }

        if ($classification.Source -eq 'unknown') {
            $unknownResources += @{
                Type  = $r.Type
                Sku   = $r.Sku
                Notes = $classification.Notes
            }
        }
        elseif (-not $classification.IsFree) {
            $paidResources += @{
                Type  = $r.Type
                Sku   = $r.Sku
                Cost  = $classification.EstimatedMonthlyCost
                Notes = $classification.Notes
            }
        }
    }

    return @{
        IsFree           = ($paidResources.Count -eq 0 -and $unknownResources.Count -eq 0)
        PaidResources    = $paidResources
        UnknownResources = $unknownResources
        AllResources     = $allResources
    }
}