src/public/Configuration/Sync-AitherServiceConfig.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Synchronizes AitherOS services.yaml to AitherZero services.psd1.
 
.DESCRIPTION
    Reads the canonical AitherOS/config/services.yaml and generates an updated
    AitherZero/config/services.psd1 mirror. Detects drift between the two files
    and reports added/removed/changed services.
 
    This prevents silent config drift between AitherOS (Python) and AitherZero
    (PowerShell) service registries.
 
.PARAMETER DryRun
    Show what would change without actually updating services.psd1.
 
.PARAMETER Force
    Overwrite services.psd1 even if no drift is detected.
 
.PARAMETER ServicesYamlPath
    Path to AitherOS services.yaml. Auto-discovered if not specified.
 
.PARAMETER OutputPath
    Path for the generated services.psd1. Defaults to AitherZero/config/services.psd1.
 
.EXAMPLE
    Sync-AitherServiceConfig
    # Auto-sync, report drift
 
.EXAMPLE
    Sync-AitherServiceConfig -DryRun
    # Show what would change without writing
 
.NOTES
    Category: Configuration
    Dependencies: Python (for YAML parsing helper)
    Platform: Windows, Linux, macOS
#>

function Sync-AitherServiceConfig {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter()]
        [switch]$DryRun,

        [Parameter()]
        [switch]$Force,

        [Parameter()]
        [string]$ServicesYamlPath,

        [Parameter()]
        [string]$OutputPath
    )

    # Locate services.yaml
    if (-not $ServicesYamlPath) {
        $root = Get-AitherProjectRoot -ErrorAction SilentlyContinue
        if (-not $root) { $root = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent }
        $candidates = @(
            (Join-Path $root "AitherOS/config/services.yaml"),
            (Join-Path $root "config/services.yaml")
        )
        foreach ($c in $candidates) {
            if (Test-Path $c) { $ServicesYamlPath = $c; break }
        }
        if (-not $ServicesYamlPath) {
            Write-Error "Cannot find AitherOS/config/services.yaml. Use -ServicesYamlPath."
            return
        }
    }

    # Locate output
    if (-not $OutputPath) {
        $moduleRoot = Get-AitherModuleRoot -ErrorAction SilentlyContinue
        if (-not $moduleRoot) {
            $fallbackRoot = Get-AitherProjectRoot -ErrorAction SilentlyContinue
            if ($fallbackRoot) { $moduleRoot = Join-Path $fallbackRoot "AitherZero" }
            else { $moduleRoot = Split-Path $PSScriptRoot -Parent }
        }
        $OutputPath = Join-Path $moduleRoot "config/services.psd1"
    }

    Write-Host "`n Syncing service registry" -ForegroundColor Cyan
    Write-Host " Source: $ServicesYamlPath" -ForegroundColor DarkGray
    Write-Host " Target: $OutputPath" -ForegroundColor DarkGray

    # Parse YAML using Python helper
    $helperScript = @"
import yaml, json, sys
with open(sys.argv[1], 'r') as f:
    data = yaml.safe_load(f)
services = data.get('services', data)
result = {}
for name, svc in services.items():
    result[name] = {
        'port': svc.get('port'),
        'layer': svc.get('layer'),
        'group': svc.get('group', ''),
        'description': svc.get('description', ''),
        'health_path': svc.get('health_path', '/health'),
        'boot_tier': svc.get('boot_tier', 99),
        'absorbed': svc.get('absorbed', False),
        'compound_of': svc.get('compound_of', []),
    }
json.dump(result, sys.stdout)
"@


    try {
        $jsonOutput = $helperScript | python - $ServicesYamlPath 2>&1
        if ($LASTEXITCODE -ne 0) {
            Write-Error "Python YAML parser failed: $jsonOutput"
            return
        }
        $yamlServices = $jsonOutput | ConvertFrom-Json -AsHashtable
    }
    catch {
        Write-Error "Failed to parse services.yaml: $_"
        return
    }

    # Load current psd1 for drift detection
    $currentServices = @{}
    if (Test-Path $OutputPath) {
        try {
            $currentData = Import-PowerShellDataFile $OutputPath -ErrorAction Stop
            if ($currentData.Services) { $currentServices = $currentData.Services }
        }
        catch {
            Write-Warning "Could not parse current services.psd1: $_"
        }
    }

    # Detect drift
    $added = @()
    $removed = @()
    $changed = @()

    foreach ($name in $yamlServices.Keys) {
        if (-not $currentServices.ContainsKey($name)) {
            $added += $name
        }
        elseif ($currentServices[$name].Port -ne $yamlServices[$name].port) {
            $changed += "$name (port: $($currentServices[$name].Port) -> $($yamlServices[$name].port))"
        }
    }
    foreach ($name in $currentServices.Keys) {
        if (-not $yamlServices.ContainsKey($name)) {
            $removed += $name
        }
    }

    $hasDrift = ($added.Count -gt 0) -or ($removed.Count -gt 0) -or ($changed.Count -gt 0)

    if ($hasDrift) {
        Write-Host "`n Drift detected:" -ForegroundColor Yellow
        if ($added.Count -gt 0) {
            Write-Host " + Added ($($added.Count)): $($added -join ', ')" -ForegroundColor Green
        }
        if ($removed.Count -gt 0) {
            Write-Host " - Removed ($($removed.Count)): $($removed -join ', ')" -ForegroundColor Red
        }
        if ($changed.Count -gt 0) {
            Write-Host " ~ Changed ($($changed.Count)): $($changed -join ', ')" -ForegroundColor Yellow
        }
    }
    else {
        Write-Host "`n No drift detected" -ForegroundColor Green
        if (-not $Force) { return }
    }

    if ($DryRun) {
        Write-Host "`n [DryRun] Would update $OutputPath" -ForegroundColor Yellow
        return
    }

    if (-not $PSCmdlet.ShouldProcess($OutputPath, "Update service registry from services.yaml")) {
        return
    }

    # Generate services.psd1
    $sb = [System.Text.StringBuilder]::new()
    $null = $sb.AppendLine("# ═══════════════════════════════════════════════════════════════════════════════")
    $null = $sb.AppendLine("# AITHER ECOSYSTEM SERVICE REGISTRY")
    $null = $sb.AppendLine("# ═══════════════════════════════════════════════════════════════════════════════")
    $null = $sb.AppendLine("# AUTO-GENERATED from AitherOS/config/services.yaml by Sync-AitherServiceConfig")
    $null = $sb.AppendLine("# Last sync: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
    $null = $sb.AppendLine("# Source: $ServicesYamlPath")
    $null = $sb.AppendLine("# ═══════════════════════════════════════════════════════════════════════════════")
    $null = $sb.AppendLine("")
    $null = $sb.AppendLine("@{")
    $null = $sb.AppendLine(" Version = `"1.2.0`"")
    $null = $sb.AppendLine("")
    $null = $sb.AppendLine(" Services = @{")

    # Group by layer
    $byLayer = @{}
    foreach ($name in ($yamlServices.Keys | Sort-Object)) {
        $svc = $yamlServices[$name]
        $layer = if ($svc.layer) { $svc.layer } else { 99 }
        if (-not $byLayer.ContainsKey($layer)) { $byLayer[$layer] = @() }
        $byLayer[$layer] += @{ Name = $name; Svc = $svc }
    }

    foreach ($layer in ($byLayer.Keys | Sort-Object)) {
        $null = $sb.AppendLine("")
        $null = $sb.AppendLine(" # Layer $layer")
        foreach ($entry in $byLayer[$layer]) {
            $name = $entry.Name
            $svc = $entry.Svc
            $null = $sb.AppendLine(" $name = @{")
            if ($svc.port) { $null = $sb.AppendLine(" Port = $($svc.port)") }
            $null = $sb.AppendLine(" Layer = $layer")
            if ($svc.group) { $null = $sb.AppendLine(" Group = `"$($svc.group)`"") }
            if ($svc.description) { $null = $sb.AppendLine(" Description = `"$($svc.description)`"") }
            if ($svc.health_path) { $null = $sb.AppendLine(" HealthPath = `"$($svc.health_path)`"") }
            if ($svc.boot_tier -ne $null) { $null = $sb.AppendLine(" BootTier = $($svc.boot_tier)") }
            if ($svc.absorbed) { $null = $sb.AppendLine(" Absorbed = `$true") }
            $null = $sb.AppendLine(" }")
        }
    }

    $null = $sb.AppendLine(" }")
    $null = $sb.AppendLine("}")

    $sb.ToString() | Set-Content -Path $OutputPath -Encoding UTF8
    Write-Host "`n Updated $OutputPath ($($yamlServices.Count) services)" -ForegroundColor Green

    # Report to Strata
    if (Get-Command Send-AitherStrata -ErrorAction SilentlyContinue) {
        Send-AitherStrata -EventType 'config-sync' -Data @{
            source_file = $ServicesYamlPath
            target_file = $OutputPath
            services_count = $yamlServices.Count
            added = $added.Count
            removed = $removed.Count
            changed = $changed.Count
        }
    }
}