CostManagementView.psm1

function ConvertFrom-JwtPayload {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Token
    )

    $payload = $Token.Split('.')[1].Replace('-', '+').Replace('_', '/')
    $payload = $payload.PadRight($payload.Length + (4 - $payload.Length % 4) % 4, '=')

    [System.Text.Encoding]::UTF8.GetString(
        [Convert]::FromBase64String($payload)
    ) | ConvertFrom-Json
}

function Get-CurrentUserIdentity {
    param (
        [string]$TenantId
    )

    try {
        if ($TenantId) {
            $tokenObj = Get-AzAccessToken `
                -ResourceUrl "https://management.azure.com" `
                -TenantId $TenantId `
                -ErrorAction Stop
        }
        else {
            $tokenObj = Get-AzAccessToken `
                -ResourceUrl "https://management.azure.com" `
                -ErrorAction Stop
        }

        if ($tokenObj.Token -is [System.Security.SecureString]) {
            $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($tokenObj.Token)
            try {
                $token = [System.Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr)
            }
            finally {
                [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr)
            }
        }
        else {
            $token = $tokenObj.Token
        }

        $decoded = ConvertFrom-JwtPayload -Token $token

        $email = if ($decoded.unique_name) {
            $decoded.unique_name
        }
        elseif ($decoded.email) {
            $decoded.email
        }
        elseif ($decoded.upn) {
            $decoded.upn
        }
        else {
            (Get-AzContext).Account.Id
        }

        [PSCustomObject]@{
            Email    = $email
            ObjectId = $decoded.oid
            TenantId = $decoded.tid
        }
    }
    catch {
        Write-Host "Warning: Failed to decode access token. Falling back to Az context." -ForegroundColor Yellow

        $context = Get-AzContext

        [PSCustomObject]@{
            Email    = if ($context) { $context.Account.Id } else { $null }
            ObjectId = $null
            TenantId = if ($context) { $context.Tenant.Id } else { $null }
        }
    }
}

function Ensure-AzureSignIn {
    param (
        [string]$TenantId
    )

    $context = Get-AzContext

    if ($context -and (-not $TenantId -or $context.Tenant.Id -eq $TenantId)) {
        return $true
    }

    Write-Host "Launching Azure sign-in..." -ForegroundColor Yellow

    try {
        if ($TenantId) {
            Connect-AzAccount -TenantId $TenantId -ErrorAction Stop | Out-Null
        }
        else {
            Connect-AzAccount -ErrorAction Stop | Out-Null
        }

        $context = Get-AzContext

        if (-not $context) {
            Write-Host "Sign-in was not completed." -ForegroundColor Red
            return $false
        }

        Write-Host "Signed in successfully as $($context.Account.Id)" -ForegroundColor Green
        return $true
    }
    catch {
        Write-Host "Error during sign-in: $($_.Exception.Message)" -ForegroundColor Red
        return $false
    }
}

function Invoke-AzureManagementGet {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path
    )

    $response = Invoke-AzRestMethod `
        -Method GET `
        -Path $Path `
        -ErrorAction Stop

    if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 300) {
        throw "Azure REST API failed. StatusCode: $($response.StatusCode). Content: $($response.Content)"
    }

    return ($response.Content | ConvertFrom-Json)
}

function Invoke-AzureManagementPut {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Path,

        [Parameter(Mandatory = $true)]
        [object]$Body
    )

    $jsonBody = $Body | ConvertTo-Json -Depth 30

    $response = Invoke-AzRestMethod `
        -Method PUT `
        -Path $Path `
        -Payload $jsonBody `
        -ErrorAction Stop

    if ($response.StatusCode -lt 200 -or $response.StatusCode -ge 300) {
        throw "Azure REST API failed. StatusCode: $($response.StatusCode). Content: $($response.Content)"
    }

    return ($response.Content | ConvertFrom-Json)
}

function Get-BillingAccounts {
    $path = '/providers/Microsoft.Billing/billingAccounts' +
        '?$expand=billingProfiles/invoiceSections,billingProfiles,billingProfiles/customers,subscriptions,soldTo' +
        '&includeCAID=true' +
        '&includeAllOrgs=false' +
        '&api-version=2022-10-01-privatepreview'

    $result = Invoke-AzureManagementGet -Path $path
    return @($result.value)
}

function Get-McaBillingProfiles {
    param (
        [Parameter(Mandatory = $true)]
        [string]$BillingAccountName
    )

    $encodedBillingAccountName = [System.Uri]::EscapeDataString($BillingAccountName)

    $path = "/providers/Microsoft.Billing/billingAccounts/$encodedBillingAccountName/billingProfiles" +
        '?api-version=2020-11-01-privatepreview' +
        '&%24top=20' +
        '&%24orderBy=properties%2FdisplayName%20asc'

    $result = Invoke-AzureManagementGet -Path $path
    return @($result.value)
}

function Select-ItemFromList {
    param (
        [Parameter(Mandatory = $true)]
        [array]$Items,

        [Parameter(Mandatory = $true)]
        [string]$Prompt,

        [Parameter(Mandatory = $true)]
        [scriptblock]$Display,

        [bool]$AllowBack = $false
    )

    if ($Items.Count -eq 0) {
        return $null
    }

    for ($i = 0; $i -lt $Items.Count; $i++) {
        $displayText = & $Display $Items[$i]
        Write-Host " $($i + 1). $displayText"
    }

    $backSelectionNumber = $null

    if ($AllowBack) {
        $backSelectionNumber = $Items.Count + 1
        Write-Host " $backSelectionNumber. Back"
    }

    $maxSelection = if ($AllowBack) {
        $Items.Count + 1
    }
    else {
        $Items.Count
    }

    do {
        $selection = Read-Host $Prompt
    } while (
        $selection -notmatch '^\d+$' -or
        [int]$selection -lt 1 -or
        [int]$selection -gt $maxSelection
    )

    if ($AllowBack -and [int]$selection -eq $backSelectionNumber) {
        return "__BACK__"
    }

    return $Items[[int]$selection - 1]
}

function Normalize-AzureScope {
    param (
        [string]$Scope
    )

    if ([string]::IsNullOrWhiteSpace($Scope)) {
        return $null
    }

    $normalized = $Scope.Trim()

    if ($normalized -eq "/") {
        return "/"
    }

    return $normalized.TrimEnd("/")
}

function Test-RoleAssignmentAppliesToScope {
    param (
        [Parameter(Mandatory = $true)]
        [string]$AssignmentScope,

        [Parameter(Mandatory = $true)]
        [string]$TargetScope
    )

    $assignment = Normalize-AzureScope -Scope $AssignmentScope
    $target = Normalize-AzureScope -Scope $TargetScope

    if (-not $assignment -or -not $target) {
        return $false
    }

    if ($assignment -eq "/") {
        return $true
    }

    return (
        $target -ieq $assignment -or
        $target.StartsWith("$assignment/", [System.StringComparison]::OrdinalIgnoreCase)
    )
}

function Test-ActionPatternCovers {
    param (
        [Parameter(Mandatory = $true)]
        [string]$AllowedAction,

        [Parameter(Mandatory = $true)]
        [string]$RequiredAction
    )

    return (
        $AllowedAction -eq "*" -or
        $AllowedAction -ieq $RequiredAction -or
        $RequiredAction -like $AllowedAction
    )
}

function Test-ActionPatternDenied {
    param (
        [Parameter(Mandatory = $true)]
        [string]$NotAction,

        [Parameter(Mandatory = $true)]
        [string]$RequiredAction
    )

    return (
        $NotAction -eq "*" -or
        $RequiredAction -like $NotAction -or
        $NotAction -like $RequiredAction
    )
}

function Get-RolePermissionActions {
    param (
        [Parameter(Mandatory = $true)]
        $RoleDefinition
    )

    $actions = @()
    $notActions = @()

    if ($RoleDefinition.Permissions) {
        foreach ($permission in $RoleDefinition.Permissions) {
            $actions += @($permission.Actions)
            $notActions += @($permission.NotActions)
        }
    }
    else {
        $actions += @($RoleDefinition.Actions)
        $notActions += @($RoleDefinition.NotActions)
    }

    [PSCustomObject]@{
        Actions    = @($actions | Where-Object { $_ })
        NotActions = @($notActions | Where-Object { $_ })
    }
}

function Test-CostManagementContributorPermission {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Scope,

        [Parameter(Mandatory = $true)]
        [string]$TenantId
    )

    $requiredActions = @(
        "Microsoft.Consumption/*",
        "Microsoft.CostManagement/*",
        "Microsoft.Billing/billingPeriods/read",
        "Microsoft.Resources/subscriptions/read",
        "Microsoft.Resources/subscriptions/resourceGroups/read",
        "Microsoft.Support/*",
        "Microsoft.Advisor/configurations/read",
        "Microsoft.Advisor/recommendations/read",
        "Microsoft.Management/managementGroups/read",
        "Microsoft.Billing/billingProperty/read"
    )

    $context = Get-AzContext

    if (-not $context) {
        Write-Host "Access denied. No Azure context found." -ForegroundColor Red
        return $false
    }

    $identity = Get-CurrentUserIdentity -TenantId $TenantId

    if (-not $identity.ObjectId) {
        Write-Host "Access denied. Could not determine signed-in user ObjectId." -ForegroundColor Red
        return $false
    }

    $roleAssignments = @(
        Get-AzRoleAssignment `
            -ObjectId $identity.ObjectId `
            -ExpandPrincipalGroups `
            -ErrorAction SilentlyContinue
    )

    if ($roleAssignments.Count -eq 0) {
        Write-Host "Access denied. No role assignments found for $($identity.Email)." -ForegroundColor Red
        return $false
    }

    $applicableAssignments = @(
        $roleAssignments | Where-Object {
            -not [string]::IsNullOrWhiteSpace($_.Scope) -and
            (Test-RoleAssignmentAppliesToScope -AssignmentScope $_.Scope -TargetScope $Scope)
        }
    )

    if ($applicableAssignments.Count -eq 0) {
        Write-Host "Access denied. No applicable role assignments found for scope $Scope." -ForegroundColor Red
        return $false
    }

    $bypassRoles = @(
        "Owner",
        "Contributor",
        "Cost Management Contributor"
    )

    $matchingBypassRole = $applicableAssignments |
        Where-Object { $_.RoleDefinitionName -in $bypassRoles } |
        Select-Object -First 1

    if ($matchingBypassRole) {
        Write-Host "Access confirmed. User has '$($matchingBypassRole.RoleDefinitionName)' on the selected subscription scope." -ForegroundColor Green
        return $true
    }

    foreach ($requiredAction in $requiredActions) {
        $hasPermission = $false

        foreach ($assignment in $applicableAssignments) {
            $roleDefinition = Get-AzRoleDefinition `
                -Id $assignment.RoleDefinitionId `
                -ErrorAction SilentlyContinue `
                -WarningAction SilentlyContinue

            if (-not $roleDefinition) {
                continue
            }

            $permissions = Get-RolePermissionActions -RoleDefinition $roleDefinition

            $isDenied = $false

            foreach ($notAction in $permissions.NotActions) {
                if (Test-ActionPatternDenied -NotAction $notAction -RequiredAction $requiredAction) {
                    $isDenied = $true
                    break
                }
            }

            if ($isDenied) {
                continue
            }

            foreach ($allowedAction in $permissions.Actions) {
                if (Test-ActionPatternCovers -AllowedAction $allowedAction -RequiredAction $requiredAction) {
                    $hasPermission = $true
                    break
                }
            }

            if ($hasPermission) {
                break
            }
        }

        if (-not $hasPermission) {
            Write-Host "Access denied. Missing required action: $requiredAction" -ForegroundColor Red
            return $false
        }
    }

    Write-Host "Access confirmed. User has equivalent or higher permissions than Cost Management Contributor." -ForegroundColor Green
    return $true
}

function Select-CostManagementScope {
    while ($true) {
        Write-Host ""
        Write-Host "=== Select Cost Management Scope ===" -ForegroundColor Cyan
        Write-Host ""
        Write-Host " 1. Subscription"
        Write-Host " 2. Enterprise Agreement (EA)"
        Write-Host " 3. Microsoft Customer Agreement (MCA)"
        Write-Host " 4. MCA Billing Profile"
        Write-Host ""

        do {
            $scopeSelection = Read-Host "Select scope type"
        } while ($scopeSelection -notin @("1", "2", "3", "4"))

        switch ($scopeSelection) {
            "1" {
                do {
                    $subscriptionId = Read-Host "Enter Subscription GUID"
                } while (
                    [string]::IsNullOrWhiteSpace($subscriptionId) -or
                    $subscriptionId -notmatch '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$'
                )

                try {
                    Set-AzContext -SubscriptionId $subscriptionId -ErrorAction Stop | Out-Null
                }
                catch {
                    Write-Host "Could not set Azure context to subscription $subscriptionId. Error: $($_.Exception.Message)" -ForegroundColor Red
                    return $null
                }

                return [PSCustomObject]@{
                    ScopeType          = "Subscription"
                    SubscriptionId     = $subscriptionId
                    BillingAccountName = $null
                    BillingAccountId   = $null
                    BillingProfileName = $null
                    BillingProfileId   = $null
                    Scope              = "/subscriptions/$subscriptionId"
                    DisplayName        = $subscriptionId
                }
            }

            "2" {
                Write-Host ""
                Write-Host "Fetching billing accounts..." -ForegroundColor Yellow

                $billingAccounts = Get-BillingAccounts

                $eaAccounts = @(
                    $billingAccounts | Where-Object {
                        $_.properties.agreementType -eq "EnterpriseAgreement"
                    }
                )

                if ($eaAccounts.Count -eq 0) {
                    Write-Host "No Enterprise Agreement billing accounts found." -ForegroundColor Red
                    continue
                }

                Write-Host ""
                Write-Host "Enterprise Agreement billing accounts:" -ForegroundColor Cyan

                $selectedAccount = Select-ItemFromList `
                    -Items $eaAccounts `
                    -Prompt "Select an EA billing account" `
                    -AllowBack $true `
                    -Display {
                        param ($account)

                        $displayName = $account.properties.displayName
                        if ([string]::IsNullOrWhiteSpace($displayName)) {
                            $displayName = $account.name
                        }

                        "$displayName | Billing Account: $($account.name) | AgreementType: $($account.properties.agreementType)"
                    }

                if ($selectedAccount -is [string] -and $selectedAccount -eq "__BACK__") {
                    continue
                }

                return [PSCustomObject]@{
                    ScopeType          = "EnterpriseAgreement"
                    SubscriptionId     = $null
                    BillingAccountName = $selectedAccount.name
                    BillingAccountId   = $selectedAccount.id
                    BillingProfileName = $null
                    BillingProfileId   = $null
                    Scope              = $selectedAccount.id
                    DisplayName        = $selectedAccount.properties.displayName
                }
            }

            "3" {
                Write-Host ""
                Write-Host "Fetching billing accounts..." -ForegroundColor Yellow

                $billingAccounts = Get-BillingAccounts

                $mcaAccounts = @(
                    $billingAccounts | Where-Object {
                        $_.properties.agreementType -eq "MicrosoftCustomerAgreement"
                    }
                )

                if ($mcaAccounts.Count -eq 0) {
                    Write-Host "No Microsoft Customer Agreement billing accounts found." -ForegroundColor Red
                    continue
                }

                Write-Host ""
                Write-Host "Microsoft Customer Agreement billing accounts:" -ForegroundColor Cyan

                $selectedAccount = Select-ItemFromList `
                    -Items $mcaAccounts `
                    -Prompt "Select an MCA billing account" `
                    -AllowBack $true `
                    -Display {
                        param ($account)

                        $displayName = $account.properties.displayName
                        if ([string]::IsNullOrWhiteSpace($displayName)) {
                            $displayName = $account.name
                        }

                        "$displayName | Billing Account: $($account.name) | AgreementType: $($account.properties.agreementType)"
                    }

                if ($selectedAccount -is [string] -and $selectedAccount -eq "__BACK__") {
                    continue
                }

                return [PSCustomObject]@{
                    ScopeType          = "MicrosoftCustomerAgreement"
                    SubscriptionId     = $null
                    BillingAccountName = $selectedAccount.name
                    BillingAccountId   = $selectedAccount.id
                    BillingProfileName = $null
                    BillingProfileId   = $null
                    Scope              = $selectedAccount.id
                    DisplayName        = $selectedAccount.properties.displayName
                }
            }

            "4" {
                Write-Host ""
                Write-Host "Fetching billing accounts..." -ForegroundColor Yellow

                $billingAccounts = Get-BillingAccounts

                $mcaAccounts = @(
                    $billingAccounts | Where-Object {
                        $_.properties.agreementType -eq "MicrosoftCustomerAgreement"
                    }
                )

                if ($mcaAccounts.Count -eq 0) {
                    Write-Host "No Microsoft Customer Agreement billing accounts found." -ForegroundColor Red
                    continue
                }

                while ($true) {
                    Write-Host ""
                    Write-Host "Microsoft Customer Agreement billing accounts:" -ForegroundColor Cyan

                    $selectedAccount = Select-ItemFromList `
                        -Items $mcaAccounts `
                        -Prompt "Select an MCA billing account" `
                        -AllowBack $true `
                        -Display {
                            param ($account)

                            $displayName = $account.properties.displayName
                            if ([string]::IsNullOrWhiteSpace($displayName)) {
                                $displayName = $account.name
                            }

                            "$displayName | Billing Account: $($account.name) | AgreementType: $($account.properties.agreementType)"
                        }

                    if ($selectedAccount -is [string] -and $selectedAccount -eq "__BACK__") {
                        break
                    }

                    Write-Host ""
                    Write-Host "Fetching billing profiles for $($selectedAccount.properties.displayName)..." -ForegroundColor Yellow

                    $billingProfiles = Get-McaBillingProfiles -BillingAccountName $selectedAccount.name

                    if ($billingProfiles.Count -eq 0) {
                        Write-Host "No billing profiles found for this MCA billing account." -ForegroundColor Red
                        continue
                    }

                    Write-Host ""
                    Write-Host "MCA billing profiles:" -ForegroundColor Cyan

                    $selectedProfile = Select-ItemFromList `
                        -Items $billingProfiles `
                        -Prompt "Select a billing profile" `
                        -AllowBack $true `
                        -Display {
                            param ($profile)

                            $displayName = $profile.properties.displayName
                            if ([string]::IsNullOrWhiteSpace($displayName)) {
                                $displayName = $profile.name
                            }

                            "$displayName | Billing Profile: $($profile.name)"
                        }

                    if ($selectedProfile -is [string] -and $selectedProfile -eq "__BACK__") {
                        continue
                    }

                    return [PSCustomObject]@{
                        ScopeType          = "McaBillingProfile"
                        SubscriptionId     = $null
                        BillingAccountName = $selectedAccount.name
                        BillingAccountId   = $selectedAccount.id
                        BillingProfileName = $selectedProfile.name
                        BillingProfileId   = $selectedProfile.id
                        Scope              = $selectedProfile.id
                        DisplayName        = $selectedProfile.properties.displayName
                    }
                }
            }
        }
    }
}

function Read-CostManagementViewInput {
    while ($true) {
        Write-Host ""
        Write-Host "=== Create Cost Management View ===" -ForegroundColor Cyan
        Write-Host ""
        Write-Host " 1. Continue"
        Write-Host " 2. Back to scope selection"
        Write-Host ""

        do {
            $selection = Read-Host "Select an option"
        } while ($selection -notin @("1", "2"))

        if ($selection -eq "2") {
            return "__BACK__"
        }

        do {
            $viewName = Read-Host "Enter the view name"
        } while ([string]::IsNullOrWhiteSpace($viewName))

        while ($true) {
            $startDateInput = (Read-Host "Enter the start date (yyyy-MM-dd)").Trim()
            $endDateInput = (Read-Host "Enter the end date (yyyy-MM-dd)").Trim()

            try {
                $startDate = [DateTime]::ParseExact(
                    $startDateInput,
                    "yyyy-MM-dd",
                    [System.Globalization.CultureInfo]::InvariantCulture,
                    [System.Globalization.DateTimeStyles]::None
                )

                $endDate = [DateTime]::ParseExact(
                    $endDateInput,
                    "yyyy-MM-dd",
                    [System.Globalization.CultureInfo]::InvariantCulture,
                    [System.Globalization.DateTimeStyles]::None
                )
            }
            catch {
                Write-Host "Invalid date format. Please enter dates in yyyy-MM-dd format, for example 2026-01-01." -ForegroundColor Red
                continue
            }

            if ($endDate -lt $startDate) {
                Write-Host "End date cannot be earlier than start date. Please enter a valid date range." -ForegroundColor Red
                continue
            }

            if ($endDate -gt $startDate.AddMonths(12)) {
                Write-Host "The selected time period is outside the allowed 12-month window. Please enter a start date and end date within 12 months." -ForegroundColor Red
                continue
            }

            return [PSCustomObject]@{
                ViewName  = $viewName.Trim()
                StartDate = $startDate.ToString("yyyy-MM-dd")
                EndDate   = $endDate.ToString("yyyy-MM-dd")
            }
        }
    }
}

function New-CostManagementViewRequestBody {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Scope,

        [Parameter(Mandatory = $true)]
        [string]$ViewName,

        [Parameter(Mandatory = $true)]
        [string]$StartDate,

        [Parameter(Mandatory = $true)]
        [string]$EndDate
    )

    [PSCustomObject]@{
        properties = [PSCustomObject]@{
            currency    = "USD"
            dateRange   = "Custom"
            query       = [PSCustomObject]@{
                type       = "ActualCost"
                dataSet    = [PSCustomObject]@{
                    granularity = "Daily"
                    aggregation = [PSCustomObject]@{
                        totalCost    = [PSCustomObject]@{
                            name     = "Cost"
                            function = "Sum"
                        }
                        totalCostUSD = [PSCustomObject]@{
                            name     = "CostUSD"
                            function = "Sum"
                        }
                    }
                    sorting     = @(
                        [PSCustomObject]@{
                            direction = "ascending"
                            name      = "UsageDate"
                        }
                    )
                }
                timeframe  = "Custom"
                timePeriod = [PSCustomObject]@{
                    from = $StartDate
                    to   = $EndDate
                }
            }
            chart       = "Area"
            accumulated = "true"
            pivots      = @(
                [PSCustomObject]@{
                    type = "Dimension"
                    name = "ServiceName"
                },
                [PSCustomObject]@{
                    type = "Dimension"
                    name = "ResourceLocation"
                },
                [PSCustomObject]@{
                    type = "Dimension"
                    name = "MeterCategory"
                }
            )
            scope       = $Scope
            kpis        = @(
                [PSCustomObject]@{
                    type               = "Budget"
                    id                 = "COST_NAVIGATOR.BUDGET_OPTIONS.NONE"
                    enabled            = $true
                    extendedProperties = [PSCustomObject]@{
                        name = "COST_NAVIGATOR.BUDGET_OPTIONS.NONE"
                    }
                },
                [PSCustomObject]@{
                    type    = "Forecast"
                    enabled = $true
                }
            )
            displayName = $ViewName
        }
    }
}

function New-CostManagementView {
    param (
        [Parameter(Mandatory = $true)]
        [string]$Scope,

        [Parameter(Mandatory = $true)]
        [string]$ViewName,

        [Parameter(Mandatory = $true)]
        [string]$StartDate,

        [Parameter(Mandatory = $true)]
        [string]$EndDate
    )

    $encodedViewName = [System.Uri]::EscapeDataString($ViewName)
    $normalizedScope = Normalize-AzureScope -Scope $Scope

    $path = "$normalizedScope/providers/Microsoft.CostManagement/views/$encodedViewName" +
        "?api-version=2026-06-01"

    $body = New-CostManagementViewRequestBody `
        -Scope $normalizedScope `
        -ViewName $ViewName `
        -StartDate $StartDate `
        -EndDate $EndDate

    Invoke-AzureManagementPut -Path $path -Body $body | Out-Null
}


function Invoke-CostManagementViewWizard {
    [CmdletBinding()]
    param (
        [string]$TenantId
    )

    $tenantIdInput = $TenantId

    if ([string]::IsNullOrWhiteSpace($tenantIdInput)) {
        $tenantIdInput = Read-Host "Enter Tenant ID, or press Enter to use current/default tenant"
    }

    if ([string]::IsNullOrWhiteSpace($tenantIdInput)) {
        $tenantIdInput = $null
    }

    if (-not (Ensure-AzureSignIn -TenantId $tenantIdInput)) {
        return
    }

    $context = Get-AzContext

    if (-not $tenantIdInput) {
        $tenantIdInput = $context.Tenant.Id
    }

    $identity = Get-CurrentUserIdentity -TenantId $tenantIdInput

    Write-Host ""
    Write-Host "Signed-in user: $($identity.Email)" -ForegroundColor Cyan
    Write-Host "Tenant ID: $tenantIdInput" -ForegroundColor Cyan

    while ($true) {
        $targetScope = Select-CostManagementScope

        if (-not $targetScope) {
            Write-Host "No scope selected. Exiting." -ForegroundColor Red
            return
        }

        Write-Host ""
        Write-Host "Selected scope:" -ForegroundColor Green
        Write-Host "Scope Type: $($targetScope.ScopeType)"
        Write-Host "Name: $($targetScope.DisplayName)"
        Write-Host "Scope: $($targetScope.Scope)"

        if ($targetScope.BillingAccountName) {
            Write-Host "Billing Account Name: $($targetScope.BillingAccountName)"
        }

        if ($targetScope.BillingProfileName) {
            Write-Host "Billing Profile Name: $($targetScope.BillingProfileName)"
        }

        if ($targetScope.ScopeType -eq "Subscription") {
            if (-not (Test-CostManagementContributorPermission -Scope $targetScope.Scope -TenantId $tenantIdInput)) {
                Write-Host "You do not have the required permissions to create the Cost Management view." -ForegroundColor Red
                return
            }
        }

        $viewInput = Read-CostManagementViewInput

        if ($viewInput -is [string] -and $viewInput -eq "__BACK__") {
            continue
        }

        try {
            New-CostManagementView `
                -Scope $targetScope.Scope `
                -ViewName $viewInput.ViewName `
                -StartDate $viewInput.StartDate `
                -EndDate $viewInput.EndDate

            Write-Host ""
            Write-Host "Cost Management view '$($viewInput.ViewName)' was successfully created." -ForegroundColor Green
            Write-Host "Scope: $($targetScope.Scope)"
            return
        }
        catch {
            Write-Host ""
            Write-Host "Failed to create Cost Management view '$($viewInput.ViewName)'." -ForegroundColor Red
            Write-Host "Error: $($_.Exception.Message)" -ForegroundColor Red
            return
        }
    }
}

Export-ModuleMember -Function Invoke-CostManagementViewWizard, New-CostManagementView