src/public/Infrastructure/Invoke-AitherNodeDeploy.ps1

#Requires -Version 7.0

<#
.SYNOPSIS
    Deploy or manage an AitherNode on a remote host via the Elysium pipeline.

.DESCRIPTION
    Wrapper for the AitherZero agent and MCP tools to deploy AitherNode instances
    to remote hosts. Supports the full lifecycle: bootstrap, deploy, verify,
    configure replication, and start watchdog.

    This is the "quick deploy" command — for full customization, use
    Invoke-AitherElysiumDeploy directly.

.PARAMETER ComputerName
    Target hostname or IP address.

.PARAMETER Action
    Deployment action: Deploy (full pipeline), Status, Update, Restart, Remove.

.PARAMETER Credential
    PSCredential for authentication.

.PARAMETER CredentialName
    Stored credential name from AitherZero vault.

.PARAMETER Profile
    Service profile: minimal, core (default), gpu, dashboard, all.

.PARAMETER SkipBootstrap
    Skip OS-level setup (host already prepared).

.PARAMETER SkipReplication
    Skip database replication configuration.

.PARAMETER GPU
    Enable GPU passthrough.

.PARAMETER FailoverPriority
    Failover priority (1=highest). Default: 10.

.PARAMETER StartWatchdog
    Start failover watchdog after deployment.

.PARAMETER DryRun
    Preview mode — show what would be done.

.PARAMETER PassThru
    Return result objects.

.INPUTS
    System.String — Computer names via pipeline.

.OUTPUTS
    PSCustomObject — Deployment result.

.EXAMPLE
    Invoke-AitherNodeDeploy -ComputerName "lab-server" -Credential (Get-Credential)
    Full end-to-end deployment.

.EXAMPLE
    Invoke-AitherNodeDeploy -ComputerName "192.168.1.50" -Action Status
    Check deployment status on a remote host.

.EXAMPLE
    Invoke-AitherNodeDeploy -ComputerName "lab" -SkipBootstrap -GPU -StartWatchdog
    Deploy to a pre-configured host with GPU support and watchdog.

.NOTES
    Part of AitherZero module — Infrastructure category.
    Delegates to: Invoke-AitherElysiumDeploy, 3101, 3103, 3104 automation scripts.
#>

function Invoke-AitherNodeDeploy {
    [OutputType([PSCustomObject])]
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [string[]]$ComputerName,

        [ValidateSet("Deploy", "Status", "Update", "Restart", "Remove")]
        [string]$Action = "Deploy",

        [PSCredential]$Credential,
        [string]$CredentialName,

        [ValidateSet("minimal", "core", "gpu", "dashboard", "all")]
        [string]$Profile = "core",

        [switch]$SkipBootstrap,
        [switch]$SkipReplication,
        [switch]$GPU,

        [ValidateRange(1, 100)]
        [int]$FailoverPriority = 10,

        [switch]$StartWatchdog,
        [switch]$DryRun,
        [switch]$Force,
        [switch]$PassThru
    )

    begin {
        $results = @()
        $projectRoot = $null

        # Locate project root
        $searchDir = $PSScriptRoot
        while ($searchDir) {
            if (Test-Path (Join-Path $searchDir ".." ".." "AitherZero.psd1")) {
                $projectRoot = (Resolve-Path (Join-Path $searchDir ".." "..")).Path
                break
            }
            $parent = Split-Path $searchDir -Parent
            if ($parent -eq $searchDir) { break }
            $searchDir = $parent
        }
        if (-not $projectRoot -and $env:AITHERZERO_ROOT) {
            $projectRoot = $env:AITHERZERO_ROOT
        }
        if (-not $projectRoot) {
            $projectRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
        }

        # Resolve credential
        if ($CredentialName -and -not $Credential) {
            try {
                $Credential = Get-AitherCredential -Name $CredentialName -ErrorAction Stop
            }
            catch {
                Write-Warning "Could not retrieve credential '$CredentialName': $($_.Exception.Message)"
            }
        }
    }

    process {
        foreach ($target in $ComputerName) {
            if (-not $PSCmdlet.ShouldProcess($target, "$Action AitherNode")) { continue }

            switch ($Action) {
                "Deploy" {
                    # Full deployment via Elysium pipeline
                    $deployParams = @{
                        ComputerName     = $target
                        Profile          = $Profile
                        FailoverPriority = $FailoverPriority
                        GPU              = $GPU
                        StartWatchdog    = $StartWatchdog
                        PassThru         = $true
                    }
                    if ($Credential)     { $deployParams.Credential = $Credential }
                    if ($SkipBootstrap)  { $deployParams.SkipBootstrap = $true }
                    if ($DryRun)         { $deployParams.DryRun = $true }
                    if ($Force)          { $deployParams.Force = $true }

                    try {
                        $result = Invoke-AitherElysiumDeploy @deployParams
                    }
                    catch {
                        $result = [PSCustomObject]@{
                            ComputerName = $target
                            Action       = 'Deploy'
                            Status       = 'Failed'
                            Error        = $_.Exception.Message
                        }
                    }

                    # Configure replication if not skipped
                    if (-not $SkipReplication -and -not $DryRun -and $result.Status -ne 'Failed') {
                        Write-Host " Configuring database replication for $target..." -ForegroundColor Cyan
                        $replScript = Join-Path $projectRoot "AitherZero" "library" "automation-scripts" "31-remote" "3104_Setup-DatabaseReplication.ps1"
                        if (Test-Path $replScript) {
                            try {
                                $replParams = @{
                                    NodeHost  = $target
                                    Force     = $true
                                }
                                if ($Credential) { $replParams.Credential = $Credential }
                                & $replScript @replParams
                            }
                            catch {
                                Write-Warning "Replication setup failed: $($_.Exception.Message)"
                            }
                        }
                    }

                    $results += $result
                }

                "Status" {
                    # Check status on remote host
                    $result = [PSCustomObject]@{
                        ComputerName = $target
                        Action       = 'Status'
                        Status       = 'Unknown'
                        Containers   = @()
                        MeshJoined   = $false
                    }

                    try {
                        if ($Credential) {
                            $containers = Invoke-Command -ComputerName $target -Credential $Credential -ScriptBlock {
                                docker ps --filter "name=aitheros" --format "{{.Names}}|{{.Status}}" 2>&1
                            }
                        }
                        else {
                            $containers = Invoke-Command -ComputerName $target -ScriptBlock {
                                docker ps --filter "name=aitheros" --format "{{.Names}}|{{.Status}}" 2>&1
                            }
                        }

                        $result.Containers = $containers | ForEach-Object {
                            $parts = $_ -split '\|'
                            [PSCustomObject]@{ Name = $parts[0]; Status = $parts[1] }
                        }
                        $result.Status = if ($result.Containers.Count -gt 0) { 'Running' } else { 'Stopped' }

                        # Check mesh
                        try {
                            $meshCheck = Invoke-RestMethod -Uri "http://${target}:8125/mesh/status" -TimeoutSec 5 -ErrorAction Stop
                            $result.MeshJoined = $true
                        }
                        catch { $result.MeshJoined = $false }
                    }
                    catch {
                        $result.Status = 'Unreachable'
                        $result | Add-Member -NotePropertyName Error -NotePropertyValue $_.Exception.Message
                    }

                    $results += $result
                }

                "Update" {
                    # Rolling update via fleet manager
                    $fleetScript = Join-Path $projectRoot "AitherZero" "library" "automation-scripts" "31-remote" "3103_Manage-NodeFleet.ps1"
                    if (Test-Path $fleetScript) {
                        $fleetParams = @{
                            Action       = "Update"
                            ComputerName = $target
                            PassThru     = $true
                        }
                        if ($Credential) { $fleetParams.Credential = $Credential }
                        if ($Force)      { $fleetParams.Force = $true }
                        $results += & $fleetScript @fleetParams
                    }
                    else {
                        $results += [PSCustomObject]@{ ComputerName = $target; Action = 'Update'; Status = 'ScriptNotFound' }
                    }
                }

                "Restart" {
                    # Restart all containers on remote node
                    try {
                        $sb = { docker compose -f /opt/aitheros/docker-compose.node.yml restart 2>&1 }
                        if ($Credential) {
                            Invoke-Command -ComputerName $target -Credential $Credential -ScriptBlock $sb
                        }
                        else {
                            Invoke-Command -ComputerName $target -ScriptBlock $sb
                        }
                        $results += [PSCustomObject]@{ ComputerName = $target; Action = 'Restart'; Status = 'Success' }
                    }
                    catch {
                        $results += [PSCustomObject]@{ ComputerName = $target; Action = 'Restart'; Status = 'Failed'; Error = $_.Exception.Message }
                    }
                }

                "Remove" {
                    # Remove node from mesh and stop containers
                    try {
                        # Leave mesh first
                        try {
                            Invoke-RestMethod -Uri "http://localhost:8125/mesh/nodes/$target" -Method DELETE -TimeoutSec 10 -ErrorAction Stop
                        }
                        catch { Write-Verbose "Mesh removal: $($_.Exception.Message)" }

                        # Stop containers on remote
                        $sb = { docker compose -f /opt/aitheros/docker-compose.node.yml down 2>&1 }
                        if ($Credential) {
                            Invoke-Command -ComputerName $target -Credential $Credential -ScriptBlock $sb
                        }
                        else {
                            Invoke-Command -ComputerName $target -ScriptBlock $sb
                        }
                        $results += [PSCustomObject]@{ ComputerName = $target; Action = 'Remove'; Status = 'Removed' }
                    }
                    catch {
                        $results += [PSCustomObject]@{ ComputerName = $target; Action = 'Remove'; Status = 'Failed'; Error = $_.Exception.Message }
                    }
                }
            }
        }
    }

    end {
        if ($PassThru) {
            if ($results.Count -eq 1) { return $results[0] }
            return $results
        }

        # Display summary
        foreach ($r in $results) {
            $icon = switch -Wildcard ($r.Status) {
                'Success*'  { '✓' }
                'Running'   { '✓' }
                'Failed'    { '✗' }
                'DryRun'    { '→' }
                default     { '○' }
            }
            $color = switch -Wildcard ($r.Status) {
                'Success*'  { 'Green' }
                'Running'   { 'Green' }
                'Failed'    { 'Red' }
                'DryRun'    { 'DarkGray' }
                default     { 'Yellow' }
            }
            Write-Host " $icon $($r.ComputerName) — $($r.Action): $($r.Status)" -ForegroundColor $color
        }
    }
}