plugins/docker/Jax.Plugin.Docker.psm1

# NOTE:
# Helper functions must live in module script scope (not inside Register-JaxDockerPlugin),
# because hook scriptblocks execute later and need to resolve these names.
function Get-JaxDockerIsCi {
    if (-not [string]::IsNullOrWhiteSpace($env:TEAMCITY_VERSION)) { return $true }
    if (-not [string]::IsNullOrWhiteSpace($env:TEAMCITY_BUILD_PROPERTIES_FILE)) { return $true }
    if (-not [string]::IsNullOrWhiteSpace($env:CI)) {
        $val = $env:CI.ToString().Trim().ToLowerInvariant()
        if ($val -eq '1' -or $val -eq 'true' -or $val -eq 'yes') { return $true }
    }
    return $false
}

function Get-JaxDockerDefaultsFromConfig {
    param(
        [System.Collections.IDictionary] $pluginConfig,
        [bool] $isCi
    )

    $localDefaults = @{
        push          = $false
        onlyLocalArch = $true
    }
    $ciDefaults = @{
        push          = $true
        onlyLocalArch = $false
    }

    if ($null -ne $pluginConfig -and $pluginConfig -is [System.Collections.IDictionary]) {
        if ($pluginConfig.Contains('defaults') -and $pluginConfig['defaults'] -is [System.Collections.IDictionary]) {
            $defs = $pluginConfig['defaults']
            if ($defs.Contains('local') -and $defs['local'] -is [System.Collections.IDictionary]) {
                $l = $defs['local']
                if ($l.Contains('push') -and $l['push'] -is [bool]) { $localDefaults.push = [bool]$l['push'] }
                if ($l.Contains('onlyLocalArch') -and $l['onlyLocalArch'] -is [bool]) { $localDefaults.onlyLocalArch = [bool]$l['onlyLocalArch'] }
            }
            if ($defs.Contains('ci') -and $defs['ci'] -is [System.Collections.IDictionary]) {
                $c = $defs['ci']
                if ($c.Contains('push') -and $c['push'] -is [bool]) { $ciDefaults.push = [bool]$c['push'] }
                if ($c.Contains('onlyLocalArch') -and $c['onlyLocalArch'] -is [bool]) { $ciDefaults.onlyLocalArch = [bool]$c['onlyLocalArch'] }
            }
        }
    }

    if ($isCi) { return $ciDefaults }
    return $localDefaults
}

function Get-JaxDockerStateOverrides {
    param(
        [hashtable] $context
    )

    $stateOnlyLocalArch = $null
    $stateUseLocalRegistry = $null

    if ($context.ContainsKey('State') -and $context['State'] -is [System.Collections.IDictionary]) {
        $st = $context['State']
        if ($st.Contains('plugins') -and $st['plugins'] -is [System.Collections.IDictionary]) {
            $plugins = $st['plugins']
            if ($plugins.Contains('docker') -and $plugins['docker'] -is [System.Collections.IDictionary]) {
                $prefs = $plugins['docker']
                if ($prefs.Contains('onlyLocalArch') -and $prefs['onlyLocalArch'] -is [bool]) { $stateOnlyLocalArch = [bool]$prefs['onlyLocalArch'] }
                if ($prefs.Contains('useLocalRegistry') -and $prefs['useLocalRegistry'] -is [bool]) { $stateUseLocalRegistry = [bool]$prefs['useLocalRegistry'] }
            }
        }
    }

    return @{
        # push is intentionally not persisted; require explicit CLI flag or config default.
        push             = $null
        onlyLocalArch    = $stateOnlyLocalArch
        useLocalRegistry = $stateUseLocalRegistry
    }
}

function Get-JaxDockerEffectiveSettings {
    param(
        [hashtable] $context,
        [System.Collections.IDictionary] $resolved,
        [System.Collections.IDictionary] $pluginConfig
    )

    $isCi = Get-JaxDockerIsCi
    $defaults = Get-JaxDockerDefaultsFromConfig -pluginConfig $pluginConfig -isCi $isCi
    $push = [bool]$defaults.push
    $onlyLocalArch = [bool]$defaults.onlyLocalArch
    $useLocalRegistry = $false
    $remoteBuildxK8s = $false

    # Apply persisted preferences (if present)
    $stateOverrides = Get-JaxDockerStateOverrides -context $context
    if ($stateOverrides.push -is [bool]) { $push = [bool]$stateOverrides.push }
    if ($stateOverrides.onlyLocalArch -is [bool]) { $onlyLocalArch = [bool]$stateOverrides.onlyLocalArch }
    if ($stateOverrides.useLocalRegistry -is [bool]) { $useLocalRegistry = [bool]$stateOverrides.useLocalRegistry }

    # CLI overrides always win
    $hasPushOverride = $false
    $hasLocalArchOverride = $false
    $hasUseLocalRegistryOverride = $false
    $hasRemoteBuildxOverride = $false
    if ($null -ne $resolved -and $resolved -is [System.Collections.IDictionary]) {
        if ($resolved.Contains('pushToDockerRegistry') -and [bool]$resolved['pushToDockerRegistry']) {
            $push = $true
            $useLocalRegistry = $false
            $hasPushOverride = $true
            $hasUseLocalRegistryOverride = $true
        }
        if ($resolved.Contains('pushLocal') -and [bool]$resolved['pushLocal']) {
            $push = $true
            $useLocalRegistry = $true
            $hasPushOverride = $true
            $hasUseLocalRegistryOverride = $true
        }
        if ($resolved.Contains('remoteBuildxK8s') -and [bool]$resolved['remoteBuildxK8s']) {
            $remoteBuildxK8s = $true
            $hasRemoteBuildxOverride = $true
        }
        # Arch selection:
        # - default comes from config (plugins.config.docker.defaults.*.onlyLocalArch)
        # - `-onlyLocalArch` forces single-arch
        # - `-allArch` forces multi-arch (i.e. disables onlyLocalArch) for this run
        if ($resolved.Contains('allArch') -and [bool]$resolved['allArch']) {
            $onlyLocalArch = $false
            $hasLocalArchOverride = $true
        }
        if ($resolved.Contains('onlyLocalArch') -and [bool]$resolved['onlyLocalArch']) {
            $onlyLocalArch = $true
            $hasLocalArchOverride = $true
        }
    }

    # Safety: require explicit CLI push flag to push.
    if (-not $hasPushOverride) {
        $push = $false
        $useLocalRegistry = $false
    }

    # Multi-arch without pushing is not generally useful; default to onlyLocalArch.
    # If user explicitly overrode arch selection via CLI, respect it.
    if ((-not $push) -and (-not $onlyLocalArch) -and (-not $hasLocalArchOverride)) {
        $onlyLocalArch = $true
    }

    $hasCliOverride = ($hasPushOverride -or $hasLocalArchOverride -or $hasUseLocalRegistryOverride -or $hasRemoteBuildxOverride)
    return @{
        push             = $push
        onlyLocalArch    = $onlyLocalArch
        useLocalRegistry = $useLocalRegistry
        remoteBuildxK8s  = $remoteBuildxK8s
        isCi             = $isCi
        hasCliOverride   = $hasCliOverride
    }
}

function Get-JaxDockerLocalRegistryOverrides {
    param(
        [System.Collections.IDictionary] $pluginConfig
    )

    $localRegistryName = $null
    $localThirdpartyNamespace = $null

    if ($null -ne $pluginConfig -and $pluginConfig -is [System.Collections.IDictionary]) {
        if ($pluginConfig.Contains('localRegistry') -and $pluginConfig['localRegistry'] -is [System.Collections.IDictionary]) {
            $lr = $pluginConfig['localRegistry']
            if ($lr.Contains('name') -and $lr['name'] -is [string] -and -not [string]::IsNullOrWhiteSpace([string]$lr['name'])) {
                $localRegistryName = [string]$lr['name']
            }
            if ($lr.Contains('thirdpartyNamespace') -and $lr['thirdpartyNamespace'] -is [string] -and -not [string]::IsNullOrWhiteSpace([string]$lr['thirdpartyNamespace'])) {
                $localThirdpartyNamespace = [string]$lr['thirdpartyNamespace']
            }
        }
    }

    return @{
        name                = $localRegistryName
        thirdpartyNamespace = $localThirdpartyNamespace
    }
}

function Register-JaxDockerPlugin {
    [CmdletBinding()]
    param()

    $commonParams = Get-JaxCommonParameters -BoundParameters $PSBoundParameters
    if ($null -eq $commonParams) {
        $commonParams = @{}
    }

    # Compatibility CLI flags
    Register-JaxCliParameter `
        -Name 'pushToDockerRegistry' `
        -Aliases @('push', 'pushdocker', 'pushharbor', 'pushtodocker') `
        -Type 'switch' `
        -Scope 'docker'

    Register-JaxCliParameter -Name 'onlyLocalArch' -Aliases @() -Type 'switch' -Scope 'docker'
    Register-JaxCliParameter -Name 'allArch' -Aliases @('multiArch', 'multiarch') -Type 'switch' -Scope 'docker'
    Register-JaxCliParameter -Name 'pushLocal' -Aliases @() -Type 'switch' -Scope 'docker'
    Register-JaxCliParameter -Name 'remoteBuildxK8s' -Aliases @('rbk8s', 'remoteBuild') -Type 'switch' -Scope 'docker'

    # Persisted preference defaults (stored under state.plugins.docker.*)
    Register-JaxPersistentSetting -Scope 'plugins' -Key 'docker' -Default @{
        push             = $null
        onlyLocalArch    = $null
        useLocalRegistry = $null
    }

    $hooks = @{
        # Persist and apply docker build-related preferences.
        # This runs before the header is printed so follow-up hooks can see the effective settings.
        'BeforeRunHeader'   = {
            param($hook)

            $context = $hook.Context
            $pluginConfig = $hook.PluginConfig

            $resolved = $null
            if ($hook.Data -and $hook.Data.ContainsKey('Resolved')) {
                $resolved = $hook.Data['Resolved']
            }

            $effective = Get-JaxDockerEffectiveSettings -context $context -resolved $resolved -pluginConfig $pluginConfig

            # Persist user choice only when explicitly overridden via CLI.
            $noSave = $false
            if ($context.ContainsKey('NoSavedSettings') -and [bool]$context['NoSavedSettings']) { $noSave = $true }
            if ($resolved -and $resolved.Contains('noSavedSettings') -and [bool]$resolved['noSavedSettings']) { $noSave = $true }

            if ($effective.hasCliOverride -and (-not $noSave)) {
                Update-JaxState -RepoRoot $context['RepoRoot'] -Updates @{
                    plugins = @{
                        docker = @{
                            onlyLocalArch    = [bool]$effective.onlyLocalArch
                            useLocalRegistry = [bool]$effective.useLocalRegistry
                        }
                    }
                } | Out-Null
            }

            $context['DockerBuildSettings'] = $effective
            if ($resolved -and $resolved -is [System.Collections.IDictionary]) {
                $resolved['dockerBuildSettings'] = $effective
            }
        }

        # Inject docker build/push settings into the psake/script property merge pipeline:
        # RunConfig -> PsakeProperties -> Entity.Args -> CliArgs (highest precedence)
        'BeforeRunEntities' = {
            param($hook)

            $context = $hook.Context
            $pluginConfig = $hook.PluginConfig

            $effective = $null
            if ($context.ContainsKey('DockerBuildSettings') -and $context['DockerBuildSettings'] -is [System.Collections.IDictionary]) {
                $effective = $context['DockerBuildSettings']
            } else {
                $resolved = $null
                if ($context.ContainsKey('ResolvedOptions') -and $context['ResolvedOptions'] -is [System.Collections.IDictionary]) {
                    $resolved = $context['ResolvedOptions']
                }
                $effective = Get-JaxDockerEffectiveSettings -context $context -resolved $resolved -pluginConfig $pluginConfig
            }

            $override = @{
                module = @{
                    docker = @{
                        push          = [bool]$effective.push
                        onlyLocalArch = [bool]$effective.onlyLocalArch
                        buildx        = @{
                            remoteKubernetes = [bool]$effective.remoteBuildxK8s
                        }
                    }
                }
            }

            if ([bool]$effective.remoteBuildxK8s -and $pluginConfig -and $pluginConfig.Contains('remoteBuildxK8s') -and $pluginConfig['remoteBuildxK8s'] -is [System.Collections.IDictionary]) {
                $rb = $pluginConfig['remoteBuildxK8s']
                if ($rb.Contains('builderName')) {
                    $override.module.docker.buildx['builderName'] = [string]$rb['builderName']
                }
                if ($rb.Contains('kubernetesNamespace')) {
                    $override.module.docker.buildx['kubernetesNamespace'] = [string]$rb['kubernetesNamespace']
                }
                if ($rb.Contains('kubeContext')) {
                    $override.module.docker.buildx['kubeContext'] = [string]$rb['kubeContext']
                }
            }

            if ([bool]$effective.useLocalRegistry) {
                $localReg = Get-JaxDockerLocalRegistryOverrides -pluginConfig $pluginConfig
                if (-not [string]::IsNullOrWhiteSpace($localReg.name)) {
                    $override.module.docker['registry'] = @{ name = $localReg.name }
                } else {
                    Write-Warning "[docker] -pushLocal was set but plugins.config.docker.localRegistry.name is not configured; leaving registry unchanged."
                }
                if (-not [string]::IsNullOrWhiteSpace($localReg.thirdpartyNamespace)) {
                    if (-not $override.module.docker.ContainsKey('registry')) {
                        $override.module.docker['registry'] = @{}
                    }
                    $override.module.docker.registry['thirdparty_images'] = $localReg.thirdpartyNamespace
                }
            }

            $existingCliArgs = @{}
            if ($context.ContainsKey('CliArgs') -and $context['CliArgs'] -is [System.Collections.IDictionary]) {
                $existingCliArgs = $context['CliArgs']
            }
            $context['CliArgs'] = Merge-JaxHashtable -Base $existingCliArgs -Overlay $override
        }
    }

    Register-JaxPlugin -Name 'docker' -Hooks $hooks -SourcePath $MyInvocation.MyCommand.Path @commonParams
}

# Auto-register on import
Register-JaxDockerPlugin