content/src/scripts/Invoke-PlatformBootstrap.ps1

# ---------------------------------------------------------------------------
# Invoke-PlatformBootstrap.ps1
# Axeon Core Engine — Data-Driven Azure Landing Zone Orchestrator
#
# Reads platform-spec.json for global config (naming, auth, environment),
# loads a named deployment YAML file, and orchestrates modular Bicep
# deployments across one or more resource groups.
#
# Usage:
# ./Invoke-PlatformBootstrap.ps1 -ConfigPath "./platform-spec.json" -Deployment hub-networking
# ./Invoke-PlatformBootstrap.ps1 -ConfigPath "./platform-spec.json" -Deployment hub-networking -WhatIf
# ---------------------------------------------------------------------------
#Requires -Version 7.0
#Requires -Modules Az.Accounts, Az.Resources, powershell-yaml

[CmdletBinding(SupportsShouldProcess)]
param(
    [Parameter(Mandatory)]
    [ValidateScript({ Test-Path $_ -PathType Leaf })]
    [string]$ConfigPath,

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

    [string]$BicepRoot,

    [string]$DeploymentRoot,

    [string]$SchemaPath
)

$ErrorActionPreference = "Stop"
$InformationPreference = "Continue"

# Dot-source the naming convention engine
. (Join-Path $PSScriptRoot "Resolve-ResourceName.ps1")

# ============================================================================
# HELPER FUNCTIONS
# ============================================================================

function Write-Banner {
    param([string]$Title)
    $sep = "=" * 65
    Write-Host ""
    Write-Host $sep -ForegroundColor DarkCyan
    Write-Host " $Title" -ForegroundColor Cyan
    Write-Host $sep -ForegroundColor DarkCyan
    Write-Host ""
}

function Write-StepHeader {
    param([int]$Step, [string]$Title)
    Write-Host "[$Step] $Title" -ForegroundColor Yellow
    Write-Host ("-" * 50) -ForegroundColor DarkGray
}

function Write-Success {
    param([string]$Message)
    Write-Host " [OK] $Message" -ForegroundColor Green
}

function Write-Detail {
    param([string]$Message)
    Write-Host " $Message" -ForegroundColor Gray
}

# ============================================================================
# STEP 1 — Load & Validate Global Configuration
# ============================================================================

function Read-PlatformSpec {
    param([string]$Path)

    Write-StepHeader -Step 1 -Title "Loading platform specification"
    Write-Detail "Config: $Path"

    $raw = Get-Content $Path -Raw
    $spec = $raw | ConvertFrom-Json -AsHashtable

    # Required top-level fields
    $requiredFields = @('platformId', 'environment', 'location', 'naming')
    foreach ($field in $requiredFields) {
        if (-not $spec.ContainsKey($field) -or (-not ($spec[$field] -is [hashtable]) -and [string]::IsNullOrWhiteSpace($spec[$field]))) {
            throw "platform-spec.json is missing required field: '$field'"
        }
    }

    # Defaults
    if (-not $spec.ContainsKey('testMode')) { $spec['testMode'] = $false }

    # Validate naming configuration
    Write-Detail "Validating naming convention..."
    $namingValid = Test-NamingConfig -NamingConfig $spec.naming
    if (-not $namingValid) {
        throw "Naming configuration is invalid. See warnings above."
    }
    Write-Success "Naming convention validated"

    # Resolve all resource names and attach to spec
    $spec['_resolvedNames'] = Resolve-AllResourceNames -NamingConfig $spec.naming -Index 0

    Write-Success "Configuration loaded"
    Write-Detail "Platform ID : $($spec.platformId)"
    Write-Detail "Environment : $($spec.environment)"
    Write-Detail "Location : $($spec.location)"
    Write-Detail "Test Mode : $($spec.testMode)"
    Write-Host ""

    # Show naming preview
    Show-NamingPreview -NamingConfig $spec.naming -Index 0

    return $spec
}

function Test-PlatformSpec {
    param(
        [hashtable]$Spec,
        [string]$SchemaFile
    )

    if (-not $SchemaFile -or -not (Test-Path $SchemaFile)) {
        Write-Detail "Schema validation skipped (no schema found)"
        return
    }

    Write-Detail "Validating against schema: $SchemaFile"

    $schema = Get-Content $SchemaFile -Raw | ConvertFrom-Json -AsHashtable

    if ($schema.ContainsKey('required')) {
        foreach ($field in $schema.required) {
            if (-not $Spec.ContainsKey($field)) {
                throw "Schema validation failed: missing required field '$field'"
            }
        }
    }

    Write-Success "Schema validation passed"
    Write-Host ""
}

# ============================================================================
# STEP 1b — Load & Validate Deployment YAML
# ============================================================================

function Read-DeploymentSpec {
    param(
        [string]$DeploymentName,
        [string]$DeploymentDir,
        [hashtable]$ResolvedNames
    )

    $yamlPath = Join-Path $DeploymentDir "$DeploymentName.yaml"
    if (-not (Test-Path $yamlPath)) {
        # Also try .yml extension
        $yamlPath = Join-Path $DeploymentDir "$DeploymentName.yml"
    }
    if (-not (Test-Path $yamlPath)) {
        $available = Get-ChildItem -Path $DeploymentDir -Filter "*.yaml" -ErrorAction SilentlyContinue |
            ForEach-Object { $_.BaseName }
        $availableYml = Get-ChildItem -Path $DeploymentDir -Filter "*.yml" -ErrorAction SilentlyContinue |
            ForEach-Object { $_.BaseName }
        $all = @($available) + @($availableYml) | Sort-Object -Unique
        $list = if ($all.Count -gt 0) { $all -join ', ' } else { '(none found)' }
        throw "Deployment '$DeploymentName' not found at '$DeploymentDir'. Available deployments: $list"
    }

    Write-Detail "Deployment file: $yamlPath"

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

    # Validate required fields
    if (-not $deploy.name) {
        throw "Deployment file missing required field: 'name'"
    }
    if (-not $deploy.modules -or $deploy.modules.Count -eq 0) {
        throw "Deployment '$($deploy.name)' has no modules defined"
    }

    foreach ($mod in $deploy.modules) {
        if (-not $mod.name) { throw "Deployment module missing required field: 'name'" }
        if (-not $mod.template) { throw "Module '$($mod.name)' missing required field: 'template'" }
        if (-not $mod.resourceGroup) { throw "Module '$($mod.name)' missing required field: 'resourceGroup'" }
    }

    # Resolve resource group and parameter references through the naming engine
    foreach ($mod in $deploy.modules) {
        # Resolve resourceGroup — if it matches a naming.resources key, use resolved name
        if ($ResolvedNames.ContainsKey($mod.resourceGroup)) {
            $mod['_resolvedResourceGroup'] = $ResolvedNames[$mod.resourceGroup]
        }
        else {
            # Treat as a literal resource group name
            $mod['_resolvedResourceGroup'] = $mod.resourceGroup
        }

        # Resolve parameters — if a param value matches a naming.resources key, resolve it
        if ($mod.parameters) {
            $mod['_resolvedParameters'] = @{}
            foreach ($key in $mod.parameters.Keys) {
                $value = $mod.parameters[$key]
                if ($value -is [string] -and $ResolvedNames.ContainsKey($value)) {
                    $mod['_resolvedParameters'][$key] = $ResolvedNames[$value]
                }
                else {
                    $mod['_resolvedParameters'][$key] = $value
                }
            }
        }
        else {
            $mod['_resolvedParameters'] = @{}
        }
    }

    Write-Success "Deployment '$($deploy.name)' loaded — $($deploy.modules.Count) module(s)"
    if ($deploy.description) {
        Write-Detail "Description: $($deploy.description)"
    }

    # Show module summary
    foreach ($mod in $deploy.modules) {
        $deps = if ($mod.dependsOn) { " (depends on: $($mod.dependsOn -join ', '))" } else { "" }
        Write-Detail " - $($mod.name) -> $($mod._resolvedResourceGroup)$deps"
    }
    Write-Host ""

    return $deploy
}

# ============================================================================
# STEP 2 — Azure Authentication
# ============================================================================

function Connect-AzureContext {
    param([hashtable]$Spec)

    Write-StepHeader -Step 2 -Title "Authenticating to Azure"

    # Check if already connected
    $context = Get-AzContext -ErrorAction SilentlyContinue
    if ($context) {
        Write-Success "Already authenticated as: $($context.Account.Id)"
        Write-Detail "Subscription : $($context.Subscription.Name) ($($context.Subscription.Id))"
        Write-Host ""
        return $context
    }

    # Detect if running in a CI/cloud environment that supports Managed Identity
    $tryManagedIdentity = $false
    if ($env:IDENTITY_ENDPOINT -or $env:MSI_ENDPOINT -or $env:AZURE_FEDERATED_TOKEN_FILE) {
        $tryManagedIdentity = $true
    }
    elseif (-not $env:TERM_PROGRAM -and -not $env:WT_SESSION -and -not $env:SSH_CONNECTION) {
        try {
            Write-Detail "Probing Azure IMDS endpoint (2s timeout)..."
            Invoke-WebRequest -Uri 'http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/' `
                -Headers @{ Metadata = 'true' } `
                -TimeoutSec 2 `
                -ErrorAction Stop | Out-Null
            $tryManagedIdentity = $true
        }
        catch {
            Write-Detail "IMDS not reachable — not running on Azure"
        }
    }

    if ($tryManagedIdentity) {
        try {
            Write-Detail "Attempting Workload Identity / Managed Identity..."
            Connect-AzAccount -Identity -ErrorAction Stop | Out-Null
            $context = Get-AzContext
            Write-Success "Authenticated via Managed Identity"
            Write-Detail "Subscription : $($context.Subscription.Name) ($($context.Subscription.Id))"
            Write-Host ""
            return $context
        }
        catch {
            Write-Detail "Managed Identity authentication failed: $($_.Exception.Message)"
        }
    }

    Write-Detail "Using interactive login..."
    Connect-AzAccount -ErrorAction Stop | Out-Null
    $context = Get-AzContext
    Write-Success "Authenticated interactively as: $($context.Account.Id)"
    Write-Detail "Subscription : $($context.Subscription.Name) ($($context.Subscription.Id))"
    Write-Host ""
    return $context
}

# ============================================================================
# STEP 3 — Resource Group Provisioning
# ============================================================================

function Ensure-ResourceGroups {
    param(
        [hashtable]$Spec,
        [object]$DeploymentSpec,
        [switch]$TestMode
    )

    Write-StepHeader -Step 3 -Title "Ensuring resource groups"

    # Collect unique resource groups from all modules
    $rgMap = [ordered]@{}
    foreach ($mod in $DeploymentSpec.modules) {
        $rgName = $mod._resolvedResourceGroup
        if (-not $rgMap.ContainsKey($rgName)) {
            $rgMap[$rgName] = @()
        }
        $rgMap[$rgName] += $mod.name
    }

    $location = $Spec.location
    Write-Detail "Location: $location"
    Write-Detail "Resource groups required: $($rgMap.Count)"

    foreach ($rgName in $rgMap.Keys) {
        $modules = $rgMap[$rgName] -join ', '
        Write-Detail ""
        Write-Detail " $rgName (modules: $modules)"

        if ($TestMode) {
            Write-Detail " [Test Mode] Would create/ensure resource group '$rgName'"
            continue
        }

        $existing = Get-AzResourceGroup -Name $rgName -ErrorAction SilentlyContinue
        if ($existing) {
            Write-Success " Resource group '$rgName' already exists"
        }
        else {
            if ($PSCmdlet.ShouldProcess($rgName, "Create resource group")) {
                New-AzResourceGroup -Name $rgName -Location $location -Force | Out-Null
                Write-Success " Resource group '$rgName' created"
            }
        }
    }

    Write-Host ""
}

# ============================================================================
# STEP 4 — Deploy Modules (with dependency ordering)
# ============================================================================

function Get-TopologicalOrder {
    param([array]$Modules)

    $graph = @{}
    $inDegree = @{}
    $byName = @{}

    foreach ($mod in $Modules) {
        $name = $mod.name
        $byName[$name] = $mod
        if (-not $graph.ContainsKey($name)) { $graph[$name] = @() }
        if (-not $inDegree.ContainsKey($name)) { $inDegree[$name] = 0 }

        if ($mod.dependsOn) {
            foreach ($dep in $mod.dependsOn) {
                if (-not $graph.ContainsKey($dep)) { $graph[$dep] = @() }
                $graph[$dep] += $name
                $inDegree[$name]++
                if (-not $inDegree.ContainsKey($dep)) { $inDegree[$dep] = 0 }
            }
        }
    }

    # Kahn's algorithm
    $queue = [System.Collections.Queue]::new()
    foreach ($name in $inDegree.Keys) {
        if ($inDegree[$name] -eq 0) { $queue.Enqueue($name) }
    }

    $ordered = @()
    while ($queue.Count -gt 0) {
        $current = $queue.Dequeue()
        $ordered += $current
        foreach ($neighbor in $graph[$current]) {
            $inDegree[$neighbor]--
            if ($inDegree[$neighbor] -eq 0) { $queue.Enqueue($neighbor) }
        }
    }

    if ($ordered.Count -ne $Modules.Count) {
        throw "Circular dependency detected in deployment modules"
    }

    return $ordered | ForEach-Object { $byName[$_] }
}

function Invoke-DeploymentModules {
    param(
        [hashtable]$Spec,
        [object]$DeploymentSpec,
        [string]$BicepRoot,
        [switch]$TestMode
    )

    Write-StepHeader -Step 4 -Title "Deploying modules"

    $sortedModules = Get-TopologicalOrder -Modules $DeploymentSpec.modules
    $totalModules = $sortedModules.Count
    $currentModule = 0
    $results = @()

    foreach ($mod in $sortedModules) {
        $currentModule++
        $moduleName = $mod.name
        $rgName = $mod._resolvedResourceGroup
        $templatePath = Join-Path $BicepRoot $mod.template

        Write-Host ""
        Write-Host " Module $currentModule/$totalModules : $moduleName" -ForegroundColor Cyan
        Write-Detail " Template : $templatePath"
        Write-Detail " Resource Group : $rgName"

        if (-not (Test-Path $templatePath)) {
            Write-Host " [!] Template not found: $templatePath" -ForegroundColor DarkYellow
            Write-Detail " Skipping module '$moduleName'"
            $results += @{ Module = $moduleName; Status = 'Skipped'; Reason = 'Template not found' }
            continue
        }

        # Build parameter object — global location + module-specific resolved params
        $parameters = @{
            location = $Spec.location
        }

        foreach ($key in $mod._resolvedParameters.Keys) {
            $parameters[$key] = $mod._resolvedParameters[$key]
        }

        if ($parameters.Count -gt 0) {
            Write-Detail " Parameters:"
            foreach ($key in ($parameters.Keys | Sort-Object)) {
                Write-Detail " $key = $($parameters[$key])"
            }
        }

        $deploymentName = "axeon-$($Spec.platformId)-$moduleName-$(Get-Date -Format 'yyyyMMdd-HHmmss')"

        if ($TestMode) {
            $rgExists = Get-AzResourceGroup -Name $rgName -ErrorAction SilentlyContinue
            if (-not $rgExists) {
                Write-Detail " [Test Mode] Resource group '$rgName' does not exist yet."
                Write-Detail " Skipping What-If — the RG would be created in a real run."
                $results += @{ Module = $moduleName; Status = 'Validated'; Reason = 'RG not yet created' }
                continue
            }

            Write-Detail " [Test Mode] Running What-If..."

            try {
                $result = Test-AzResourceGroupDeployment `
                    -ResourceGroupName $rgName `
                    -TemplateFile $templatePath `
                    -TemplateParameterObject $parameters `
                    -ErrorAction Stop

                if ($result) {
                    Write-Host " [!] Validation issues:" -ForegroundColor DarkYellow
                    foreach ($err in $result) {
                        Write-Host " - $($err.Message)" -ForegroundColor DarkYellow
                    }
                    $results += @{ Module = $moduleName; Status = 'Warnings'; Reason = 'Validation issues' }
                }
                else {
                    Write-Success " Validation passed"
                    $results += @{ Module = $moduleName; Status = 'Validated'; Reason = '' }
                }
            }
            catch {
                Write-Host " [!] Validation failed: $($_.Exception.Message)" -ForegroundColor DarkYellow
                $results += @{ Module = $moduleName; Status = 'Failed'; Reason = $_.Exception.Message }
            }

            continue
        }

        # Full deployment
        if ($PSCmdlet.ShouldProcess("$moduleName -> $rgName", "Deploy Bicep template")) {
            Write-Detail " Starting deployment: $deploymentName"

            $deployment = New-AzResourceGroupDeployment `
                -Name $deploymentName `
                -ResourceGroupName $rgName `
                -TemplateFile $templatePath `
                -TemplateParameterObject $parameters `
                -ErrorAction Stop

            Write-Success " Module '$moduleName' deployed"
            Write-Detail " Provisioning State : $($deployment.ProvisioningState)"

            if ($deployment.Outputs) {
                Write-Detail " Outputs:"
                foreach ($key in $deployment.Outputs.Keys) {
                    Write-Detail " $key = $($deployment.Outputs[$key].Value)"
                }
            }

            $results += @{ Module = $moduleName; Status = 'Deployed'; Reason = '' }
        }
    }

    Write-Host ""
    return $results
}

# ============================================================================
# STEP 5 — Summary
# ============================================================================

function Write-DeploymentSummary {
    param(
        [hashtable]$Spec,
        [object]$DeploymentSpec,
        [array]$Results,
        [switch]$TestMode
    )

    Write-StepHeader -Step 5 -Title "Deployment summary"

    $mode = if ($TestMode) { "VALIDATION ONLY (Test Mode)" } else { "DEPLOYED" }

    Write-Host ""
    Write-Host " Platform ID : $($Spec.platformId)"          -ForegroundColor White
    Write-Host " Environment : $($Spec.environment)"          -ForegroundColor White
    Write-Host " Location : $($Spec.location)"             -ForegroundColor White
    Write-Host " Deployment : $($DeploymentSpec.name)"       -ForegroundColor White
    Write-Host " Status : $mode"                          -ForegroundColor $(if ($TestMode) { 'Yellow' } else { 'Green' })
    Write-Host ""

    if ($Results -and $Results.Count -gt 0) {
        Write-Host " Module Results:" -ForegroundColor White
        foreach ($r in $Results) {
            $color = switch ($r.Status) {
                'Deployed'  { 'Green' }
                'Validated' { 'Cyan' }
                'Warnings'  { 'Yellow' }
                'Failed'    { 'Red' }
                'Skipped'   { 'DarkGray' }
                default     { 'Gray' }
            }
            $detail = if ($r.Reason) { " — $($r.Reason)" } else { "" }
            Write-Host " [$($r.Status)] $($r.Module)$detail" -ForegroundColor $color
        }
        Write-Host ""
    }

    if ($TestMode) {
        Write-Host " To deploy for real, set testMode to false in platform-spec.json" -ForegroundColor DarkGray
    }
}

# ============================================================================
# MAIN EXECUTION
# ============================================================================

$stopwatch = [System.Diagnostics.Stopwatch]::StartNew()

Write-Banner "Axeon Platform Bootstrap Engine"

# Resolve paths
$scriptDir = $PSScriptRoot
if (-not $BicepRoot)      { $BicepRoot      = Join-Path $scriptDir ".." "bicep" }
if (-not $DeploymentRoot) { $DeploymentRoot = Join-Path $scriptDir ".." ".." "deployments" }
if (-not $SchemaPath)     { $SchemaPath     = Join-Path $scriptDir ".." "schemas" "platform-spec.schema.json" }

try {
    # Step 1 — Load & validate global config
    $spec = Read-PlatformSpec -Path $ConfigPath
    Test-PlatformSpec -Spec $spec -SchemaFile $SchemaPath

    # Step 1b — Load & validate deployment
    $deploySpec = Read-DeploymentSpec `
        -DeploymentName $Deployment `
        -DeploymentDir $DeploymentRoot `
        -ResolvedNames $spec._resolvedNames

    $isTestMode = [bool]$spec.testMode

    # Step 2 — Authenticate
    Connect-AzureContext -Spec $spec

    # Step 3 — Ensure resource groups
    Ensure-ResourceGroups -Spec $spec -DeploymentSpec $deploySpec -TestMode:$isTestMode

    # Step 4 — Deploy modules
    $results = Invoke-DeploymentModules `
        -Spec $spec `
        -DeploymentSpec $deploySpec `
        -BicepRoot $BicepRoot `
        -TestMode:$isTestMode

    # Step 5 — Summary
    Write-DeploymentSummary -Spec $spec -DeploymentSpec $deploySpec -Results $results -TestMode:$isTestMode

    $stopwatch.Stop()
    Write-Host " Completed in $([math]::Round($stopwatch.Elapsed.TotalSeconds, 1))s" -ForegroundColor DarkGray
    Write-Host ""
}
catch {
    $stopwatch.Stop()
    Write-Host ""
    Write-Host " BOOTSTRAP FAILED" -ForegroundColor Red
    Write-Host " $($_.Exception.Message)" -ForegroundColor Red
    Write-Host ""
    throw
}