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.
        Uses Invoke-AzRestMethod for authentication (requires Connect-AzAccount).
 
    .PARAMETER BillingAccountId
        The billing account ID (e.g., "90846601").
 
    .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 "90846601" -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
    )

    $path = "/providers/Microsoft.Billing/billingAccounts/$BillingAccountId/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' }

    $queryString = "api-version=2025-03-01&`$filter=$filter"
    if ($expandParts.Count -gt 0) {
        $queryString += "&`$expand=$($expandParts -join ',')"
    }

    $fullPath = "${path}?${queryString}"
    Write-Verbose "Calling: $fullPath"

    $result = Invoke-AzRestMethod -Method GET -Path $fullPath -ErrorAction Stop

    if ($result.StatusCode -ne 200) {
        throw "API returned $($result.StatusCode): $($result.Content)"
    }

    $response = $result.Content | ConvertFrom-Json
    $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 "90846601" | Publish-BenefitRecommendationToKusto -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
        }

        $tokenResponse = Get-AzAccessToken -ResourceUrl "https://kusto.kusto.windows.net" -ErrorAction Stop
        $t = $tokenResponse.Token
        if ($t -is [securestring]) {
            $token = [System.Net.NetworkCredential]::new('', $t).Password
        } else {
            $token = [string]$t
        }

        $jsonArray = $collected | ConvertTo-Json -Depth 100 -Compress
        $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
    }
}