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 -PricingCachePath ./azure-pricing.json
# ---------------------------------------------------------------------------
#Requires -Version 7.0

$ErrorActionPreference = "Stop"

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

# ============================================================================
# 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
  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 = [DateTime]::Parse($raw.lastUpdated)
        $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.
.OUTPUTS
    A hashtable with: IsFree, EstimatedMonthlyCost, MatchedMeters, Notes
#>

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

        [string]$Sku,

        [string]$Tier,

        [hashtable]$PricingCache
    )

    $resources = $PricingCache.resources

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

        # If marked as free in the curated map and confirmed by API
        if ($entry.isFree) {
            return @{
                IsFree              = $true
                EstimatedMonthlyCost = 0.0
                MatchedMeters       = @()
                Notes               = $entry.notes ?? "Free resource"
                Source              = 'cache'
            }
        }

        # Paid resource — try to find matching meters
        $matchedMeters = @()
        $estimatedCost = 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 -like "*$Sku*" })
                if ($skuMatches.Count -gt 0) {
                    $matchedMeters = $skuMatches
                }
            }

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

            # Rough monthly estimate: cheapest meter × 730 hours (or per-unit for non-hourly)
            if ($matchedMeters.Count -gt 0) {
                $cheapest = $matchedMeters | Sort-Object { $_.retailPrice } | Select-Object -First 1
                $unit = $cheapest.unitOfMeasure ?? ''

                $estimatedCost = switch -Wildcard ($unit) {
                    '*Hour*'      { [math]::Round($cheapest.retailPrice * 730, 2) }
                    '*Day*'       { [math]::Round($cheapest.retailPrice * 30, 2) }
                    '*Month*'     { [math]::Round($cheapest.retailPrice, 2) }
                    '*GB*'        { [math]::Round($cheapest.retailPrice, 4) }   # per GB — shown as unit
                    '*10K*'       { [math]::Round($cheapest.retailPrice, 4) }   # per 10K ops
                    '*10,000*'    { [math]::Round($cheapest.retailPrice, 4) }
                    default       { [math]::Round($cheapest.retailPrice, 4) }
                }
            }
        }

        return @{
            IsFree              = $false
            EstimatedMonthlyCost = $estimatedCost
            MatchedMeters       = $matchedMeters
            Notes               = $entry.notes ?? "Paid resource"
            Source              = 'cache'
        }
    }

    # Not found in cache — fall back to ARM pricing map
    $armMap = $null
    try { $armMap = Get-ArmPricingMap } catch {}

    if ($armMap -and $armMap.ContainsKey($ArmResourceType)) {
        $mapEntry = $armMap[$ArmResourceType]
        return @{
            IsFree              = [bool]$mapEntry.defaultFree
            EstimatedMonthlyCost = 0.0
            MatchedMeters       = @()
            Notes               = "$($mapEntry.notes) (no live pricing in cache — run Update-AxeonPricing)"
            Source              = 'map-only'
        }
    }

    # Completely unknown resource type
    return @{
        IsFree              = $false
        EstimatedMonthlyCost = 0.0
        MatchedMeters       = @()
        Notes               = "Unknown resource type. Run Update-AxeonPricing -DeploymentFile to discover pricing."
        Source              = 'unknown'
    }
}

# ============================================================================
# PUBLIC FUNCTION
# ============================================================================

<#
.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 PlatformSpecPath
    Path to platform-spec.json (used for naming resolution in deployment YAML).
.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]$PlatformSpecPath,

        [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
    }

    # Resolve Bicep root
    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 = @()
    $totalEstimate = 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
        $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
                TotalCost  = 0.0
                Status     = 'NoResources'
            }
            continue
        }

        $moduleResources = @()
        $moduleCost = 0.0
        $moduleFree = $true

        foreach ($r in $resources) {
            $classification = Get-ResourceCostClassification `
                -ArmResourceType $r.Type `
                -Sku $r.Sku `
                -Tier $r.Tier `
                -PricingCache $cache

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

            $moduleResources += $resourceResult

            if (-not $classification.IsFree) {
                $moduleFree = $false
                $allFree = $false
                $moduleCost += $classification.EstimatedMonthlyCost
            }

            # Display resource line
            $freeLabel = if ($classification.IsFree) { "FREE" } else { "PAID" }
            $color = if ($classification.IsFree) { 'Green' } else { 'Yellow' }
            $costStr = if ($classification.IsFree) { "" } else { " (~$($classification.EstimatedMonthlyCost)/mo)" }
            $skuStr = if ($r.Sku) { " [$($r.Sku)]" } 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) {
                    Write-Host " $($meter.meterName): $($cache.currency ?? 'USD') $($meter.retailPrice)/$($meter.unitOfMeasure)" -ForegroundColor DarkGray
                }
            }
        }

        $totalEstimate += $moduleCost

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

        Write-Host ""
    }

    # Summary
    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.TotalCost -gt 0) { " — ~$($cache.currency ?? 'USD') $($mr.TotalCost)/mo" } else { "" }
        Write-Host " [$($mr.Status)] $($mr.Module)$costStr" -ForegroundColor $statusColor
    }

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

    if ($totalEstimate -eq 0 -and $allFree) {
        Write-Host " All resources in this deployment are FREE." -ForegroundColor Green
    }
    else {
        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
        TotalMonthlyCost = [math]::Round($totalEstimate, 2)
        AllFree          = $allFree
        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 pricing cache.
.PARAMETER TemplatePath
    Absolute path to the Bicep template file.
.PARAMETER PricingCachePath
    Path to the pricing cache JSON.
.OUTPUTS
    Hashtable with: IsFree, PaidResources (list of paid ARM types), AllResources
#>

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

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

    $cache = Read-PricingCache -CachePath $PricingCachePath

    $resources = Get-BicepResourceTypes -TemplatePath $TemplatePath

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

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

    foreach ($r in $resources) {
        $classification = Get-ResourceCostClassification `
            -ArmResourceType $r.Type `
            -Sku $r.Sku `
            -Tier $r.Tier `
            -PricingCache $cache

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

        if (-not $classification.IsFree) {
            $paidResources += @{
                Type  = $r.Type
                Sku   = $r.Sku
                Cost  = $classification.EstimatedMonthlyCost
                Notes = $classification.Notes
            }
        }
    }

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