FinOpsToolkitExt.BenefitRecommendations.psm1

function Get-BenefitRecommendation {
    <#
    .SYNOPSIS
        Queries the Azure Cost Management Benefit Recommendations API.
 
    .DESCRIPTION
        Retrieves savings plan purchase recommendations for a billing account.
        Returns the raw API response value array as-is.
 
    .PARAMETER BillingAccountId
        The billing account ID (e.g., "7249999" or the full GUID format).
 
    .PARAMETER LookBackPeriod
        The look-back period for usage analysis. Default: Last60Days.
 
    .PARAMETER Term
        The commitment term. Default: P3Y.
 
    .PARAMETER Scope
        Recommendation scope. Default: Shared.
 
    .PARAMETER ExpandUsage
        Include hourly usage data in the response.
 
    .PARAMETER ExpandAllRecommendationDetails
        Include all recommendation detail tiers in the response.
 
    .EXAMPLE
        Get-BenefitRecommendation -BillingAccountId "7249999" -LookBackPeriod Last30Days -Term P3Y -ExpandUsage -ExpandAllRecommendationDetails
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$BillingAccountId,

        [ValidateSet('Last7Days', 'Last30Days', 'Last60Days')]
        [string]$LookBackPeriod = 'Last60Days',

        [ValidateSet('P1Y', 'P3Y')]
        [string]$Term = 'P3Y',

        [ValidateSet('Single', 'Shared')]
        [string]$Scope = 'Shared',

        [switch]$ExpandUsage,

        [switch]$ExpandAllRecommendationDetails
    )

    $token = (Get-AzAccessToken -ResourceUrl "https://management.azure.com").Token

    $billingScope = "/providers/Microsoft.Billing/billingAccounts/$BillingAccountId"
    $baseUri = "https://management.azure.com$billingScope/providers/Microsoft.CostManagement/benefitRecommendations"

    $filter = "properties/lookBackPeriod eq '$LookBackPeriod' AND properties/term eq '$Term' AND properties/scope eq '$Scope'"

    $expandParts = @()
    if ($ExpandUsage) { $expandParts += 'properties/usage' }
    if ($ExpandAllRecommendationDetails) { $expandParts += 'properties/allRecommendationDetails' }

    $queryParams = @{
        'api-version' = '2025-03-01'
        '$filter'     = $filter
    }
    if ($expandParts.Count -gt 0) {
        $queryParams['$expand'] = $expandParts -join ','
    }

    $queryString = ($queryParams.GetEnumerator() | ForEach-Object {
        "$([Uri]::EscapeDataString($_.Key))=$([Uri]::EscapeDataString($_.Value))"
    }) -join '&'

    $uri = "$baseUri?$queryString"

    Write-Verbose "Calling: $uri"

    $response = Invoke-RestMethod -Uri $uri -Headers @{
        Authorization  = "Bearer $token"
        'Content-Type' = 'application/json'
    } -Method Get

    $count = ($response.value | Measure-Object).Count
    Write-Host "Retrieved $count benefit recommendation(s) for billing account $BillingAccountId" -ForegroundColor Green

    return $response.value
}

function Publish-BenefitRecommendationToKusto {
    <#
    .SYNOPSIS
        Ingests benefit recommendations into an Azure Data Explorer table.
 
    .DESCRIPTION
        Takes benefit recommendation objects (from Get-BenefitRecommendation) and
        ingests them into the SavingsPlanRecommendations_raw table as a single row
        containing the full array in the Recommendation column.
 
    .PARAMETER Recommendations
        The recommendation array from Get-BenefitRecommendation. Accepts pipeline input.
 
    .PARAMETER ClusterUri
        The ADX cluster URI (e.g., "https://str-finops-hub.israelcentral.kusto.windows.net").
 
    .PARAMETER Database
        The database name. Default: Hub.
 
    .PARAMETER Table
        The target table name. Default: SavingsPlanRecommendations_raw.
 
    .EXAMPLE
        Get-BenefitRecommendation -BillingAccountId "7249999" | Publish-BenefitRecommendationToKusto -ClusterUri "https://str-finops-hub.israelcentral.kusto.windows.net"
 
    .EXAMPLE
        $recs = Get-BenefitRecommendation -BillingAccountId "7249999"
        Publish-BenefitRecommendationToKusto -Recommendations $recs -ClusterUri "https://str-finops-hub.israelcentral.kusto.windows.net"
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [object]$Recommendations,

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

        [string]$Database = 'Hub',

        [string]$Table = 'SavingsPlanRecommendations_raw'
    )

    begin {
        $collected = [System.Collections.Generic.List[object]]::new()
    }

    process {
        if ($Recommendations -is [System.Collections.IEnumerable] -and $Recommendations -isnot [string]) {
            foreach ($item in $Recommendations) {
                $collected.Add($item)
            }
        }
        else {
            $collected.Add($Recommendations)
        }
    }

    end {
        if ($collected.Count -eq 0) {
            Write-Warning "No recommendations to publish."
            return
        }

        $token = (Get-AzAccessToken -ResourceUrl $ClusterUri).Token

        # Build the JSON payload - single row with the full array as the Recommendation column
        $jsonArray = $collected | ConvertTo-Json -Depth 100 -Compress
        # Wrap in a single-row array for ingestion: [{"Recommendation": [...]}]
        $payload = "[{`"Recommendation`":$jsonArray}]"

        $ingestUri = "$($ClusterUri.TrimEnd('/'))/v1/rest/ingest/$Database/$($Table)?streamFormat=JSON"

        Write-Verbose "Ingesting $($collected.Count) recommendation(s) into $Table via streaming ingestion"
        Write-Verbose "URI: $ingestUri"

        $response = Invoke-RestMethod -Uri $ingestUri -Method Post -Headers @{
            Authorization  = "Bearer $token"
            'Content-Type' = 'application/json; charset=utf-8'
        } -Body ([System.Text.Encoding]::UTF8.GetBytes($payload))

        Write-Host "Published $($collected.Count) recommendation(s) to $Database.$Table" -ForegroundColor Green
        return $response
    }
}