modules/IW.Core.psm1

# SailPoint Connect core: definition registry, progressive disclosure, wizard driver.

Set-StrictMode -Version Latest

$script:IntegrationWizardDefinitions = @{}
$script:IntegrationWizardPlatforms = @{}

function Register-IntegrationWizardPlatform {
    param(
        [Parameter(Mandatory)]
        [string]$Name,
        [Parameter(Mandatory)]
        [string]$Title,
        [Parameter(Mandatory)]
        [string[]]$CoreOptions,
        [string]$CliName = 'aws',
        [hashtable]$EntroCompanions = @{}
    )
    $script:IntegrationWizardPlatforms[$Name] = [ordered]@{
        Name             = $Name
        Title            = $Title
        CoreOptions      = $CoreOptions
        CliName          = $CliName
        EntroCompanions  = $EntroCompanions
    }
}

function Register-IntegrationWizardDefinition {
    param(
        [Parameter(Mandatory)]
        [hashtable]$Definition
    )
    if (-not $Definition.Id) { throw 'Definition requires Id.' }
    $script:IntegrationWizardDefinitions[$Definition.Id] = $Definition
}

function Get-IntegrationWizardDefinition {
    param([Parameter(Mandatory)][string]$Id)
    $script:IntegrationWizardDefinitions[$Id]
}

function Get-IntegrationWizardDefinitionsForPlatform {
    param([Parameter(Mandatory)][string]$Platform)
    $script:IntegrationWizardDefinitions.Values | Where-Object { $_.Platform -eq $Platform }
}

function Get-IntegrationWizardWizardRoot {
    param([Parameter(Mandatory)][string]$Platform)
    Split-Path (Split-Path $PSScriptRoot -Parent) -Parent
}

function Import-IntegrationWizardConsole {
    param(
        [string]$WizardRoot,
        [switch]$NonInteractive
    )
    $mod = Join-Path $WizardRoot 'modules/ISC.OperatorConsole.psm1'
    Import-Module $mod -Force -Global -WarningAction SilentlyContinue
    Import-Module (Join-Path $WizardRoot 'modules/ISC.OperatorToolchain.psm1') -Force -Global -WarningAction SilentlyContinue
    Initialize-OperatorConsole -NonInteractive:$NonInteractive
}

function Show-IntegrationWizardPlatformHelp {
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][string]$ScriptName,
        [Parameter(Mandatory)][hashtable]$CoreOptionHelp
    )
    $meta = $script:IntegrationWizardPlatforms[$Platform]
    Write-Host ''
    Write-Host "$($meta.Title) integration wizard" -ForegroundColor Cyan
    Write-Host " ./$ScriptName -Wizard Guided setup, asks every question"
    foreach ($key in $CoreOptionHelp.Keys) {
        Write-Host " ./$ScriptName -Wizard -$key Guided setup, $($CoreOptionHelp[$key].Short) already chosen"
    }
    foreach ($key in $CoreOptionHelp.Keys) {
        Write-Host " ./$ScriptName -$key [...] $($CoreOptionHelp[$key].Long)"
    }
    Write-Host 'Then choose what to do: -Action Run | BuildScript | BuildRequirements'
    Write-Host ' Artifacts go under ~/.sailpoint-connect/<project>/<platform>/<integration>/ (-Project to skip the project menu)'
    Write-Host ' Combine ISC connector + Entro: pass both switches (e.g. -SaaSConnector -Entro on AWS)'
    $first = ($CoreOptionHelp.Keys | Select-Object -First 1)
    if ($first) {
        Write-Host "Next: ./$ScriptName -$first (lists its options)"
    }
    Write-Host ''
}

function Test-IntegrationWizardContextSwitch {
    param([hashtable]$Context, [string]$Name)
    return $Context.ContainsKey($Name) -and [bool]$Context[$Name]
}

function Get-IntegrationWizardEntroCompanionConfig {
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][string]$ConnectorCoreSwitch
    )
    $meta = $script:IntegrationWizardPlatforms[$Platform]
    if (-not $meta -or -not $meta.EntroCompanions) { return $null }
    $meta.EntroCompanions[$ConnectorCoreSwitch]
}

function Resolve-IntegrationWizardEntroAddon {
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][hashtable]$Context,
        [Parameter(Mandatory)][hashtable]$ConnectorDefinition,
        [switch]$Wizard,
        [switch]$NonInteractive
    )
    $companion = Get-IntegrationWizardEntroCompanionConfig -Platform $Platform -ConnectorCoreSwitch $ConnectorDefinition.CoreSwitch
    if (-not $companion) { return }

    if ($companion['EntroSwitch'] -and (Test-IntegrationWizardContextSwitch -Context $Context -Name $companion['EntroSwitch'])) {
        $Context[$companion['EntroSwitch']] = $true
        return
    }
    if ($companion['Variants']) {
        foreach ($v in @($companion['Variants'])) {
            if (Test-IntegrationWizardContextSwitch -Context $Context -Name $v) { return }
        }
    }

    if (-not $Wizard) { return }

    if (-not (Read-YesNo -Prompt 'Also configure Entro for this platform?' -Default $false)) { return }

    if ($companion['EntroSwitch']) {
        $Context[$companion['EntroSwitch']] = $true
        return
    }
    if ($companion['Variants']) {
        $picked = Read-Choice -Prompt 'Which Entro integration?' -Options $companion['Variants'] `
            -Labels @($companion['Variants'] | ForEach-Object {
                switch ($_) {
                    'GcpEntro' { 'Entro Google GCP organization' }
                    'WorkspaceEntro' { 'Entro Google Workspace' }
                    default { $_ }
                }
            }) -Default $companion['Variants'][0]
        $Context[$picked] = $true
    }
}

function Get-IntegrationWizardTargets {
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][hashtable]$Context
    )
    $all = @(Get-IntegrationWizardDefinitionsForPlatform -Platform $Platform)
    $active = @($all | Where-Object { Test-IntegrationWizardContextSwitch -Context $Context -Name $_.CoreSwitch })

    $isc = @($active | Where-Object { $_.Product -eq 'ISC' })
    $entro = @($active | Where-Object { $_.Product -eq 'Entro' })

    if ($isc.Count -gt 1) {
        throw 'Only one ISC connector target can be selected per run.'
    }
    if ($isc.Count -eq 1 -and $isc[0]['CoreSwitch'] -eq 'CiemConnector' -and $entro.Count -gt 0) {
        throw 'CIEM connector cannot be combined with Entro in the same run.'
    }
    if ($entro.Count -gt 1) {
        throw 'Only one Entro target can be selected per run.'
    }

    if ($isc.Count -eq 0 -and $entro.Count -eq 0) { return @() }
    if ($isc.Count -eq 0) { return @($entro[0]) }
    if ($entro.Count -eq 0) { return @($isc[0]) }
    return @($isc[0], $entro[0])
}

function Merge-IntegrationWizardDefinitions {
    param(
        [Parameter(Mandatory)][object[]]$Members
    )
    if ($Members.Count -eq 1) { return $Members[0] }

    $cli = $Members[0].CliName
    foreach ($m in $Members) {
        if ($m.CliName -ne $cli) { throw 'Combined definitions must use the same CLI.' }
    }

    $mergedInputs = @{}
    foreach ($m in $Members) {
        foreach ($input in @($m.Inputs)) {
            if (-not $input) { continue }
            $name = $input.Name
            if ($mergedInputs.ContainsKey($name)) {
                $existing = $mergedInputs[$name]
                if ($existing.Purpose -ne $input.Purpose) {
                    throw "Input name collision '$name' with different purpose; use distinct names (e.g. IscExternalId vs EntroExternalId)."
                }
                continue
            }
            $mergedInputs[$name] = $input
        }
    }

    $titles = @($Members | ForEach-Object { $_.Title })
    $ids = @($Members | ForEach-Object { $_.Id }) -join '+'

    $access = @()
    foreach ($m in $Members) {
        if ($m['AccessLevels'] -and @($m['AccessLevels']).Count -gt 0) { $access = @($m['AccessLevels']); break }
    }

    $membersCopy = @($Members)
    $folderNames = @($Members | ForEach-Object {
        if ($_['FolderName']) { [string]$_['FolderName'] } else { [string]$_.Title }
    })

    return @{
        IsComposite  = $true
        Id           = $ids
        Platform     = $Members[0].Platform
        Product      = 'Combined'
        CliName      = $cli
        Title        = ($titles -join ' + ')
        FolderName   = ($folderNames -join ' and ')
        AccessLevels = $access
        Inputs       = @($mergedInputs.Values)
        Members      = $membersCopy
        AdminRoles   = @($Members | ForEach-Object { @($_['AdminRoles']) } | ForEach-Object { $_ })
        OutputsHelp  = ($Members | ForEach-Object { if ($_['OutputsHelp']) { $_['OutputsHelp'] } }) -join "`n`n"
        Steps        = {
            param($ctx)
            $allSteps = @()
            foreach ($member in $membersCopy) {
                $allSteps += & $member.Steps $ctx
            }
            $allSteps
        }
    }
}

function Resolve-IntegrationWizardPlan {
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][hashtable]$Context
    )
    $members = @(Get-IntegrationWizardTargets -Platform $Platform -Context $Context)
    if ($members.Count -eq 0) { return $null }
    if ($members.Count -eq 1) { return $members[0] }
    Merge-IntegrationWizardDefinitions -Members $members
}

function Get-IntegrationWizardDefinitionByCoreSwitch {
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][hashtable]$BoundParameters
    )
    foreach ($def in (Get-IntegrationWizardDefinitionsForPlatform -Platform $Platform)) {
        if ($def.CoreSwitch -and $BoundParameters.ContainsKey($def.CoreSwitch)) {
            return $def
        }
    }
    $null
}

function Test-IntegrationWizardInputWhen {
    param($InputDef, [hashtable]$Context)
    if (-not $InputDef) { return $false }
    if (-not ($InputDef.ContainsKey('When') -or ($InputDef.PSObject.Properties.Name -contains 'When'))) { return $true }
    $when = if ($InputDef -is [hashtable]) { $InputDef['When'] } else { $InputDef.When }
    if (-not $when) { return $true }
    & $when $Context
}

function Test-IntegrationWizardRequiredInputs {
    param(
        [Parameter(Mandatory)][hashtable]$Definition,
        [Parameter(Mandatory)][hashtable]$Context
    )
    $missing = @()
    foreach ($input in @($Definition.Inputs)) {
        if (-not $input) { continue }
        if (-not (Test-IntegrationWizardInputWhen -InputDef $input -Context $Context)) { continue }
        $name = $input.Name
        if ($input['Required'] -and [string]::IsNullOrWhiteSpace([string]$Context[$name])) {
            $missing += $name
        }
    }
    ,$missing
}

function Show-IntegrationWizardDefinitionHelp {
    param(
        [Parameter(Mandatory)][hashtable]$Definition,
        [Parameter(Mandatory)][string]$ScriptName,
        [Parameter(Mandatory)][hashtable]$Context,
        [string[]]$Missing
    )
    Write-Host ''
    Write-Host "$($Definition.Title)" -ForegroundColor Cyan
    if ($Definition['IsComposite']) {
        Write-Host ' Combined targets:'
        foreach ($m in @($Definition.Members)) { Write-Host " -$($m['CoreSwitch']) $($m['Title'])" }
    }
    elseif ($Definition['CoreSwitch']) {
        Write-Host " Core switch: -$($Definition['CoreSwitch'])"
    }
    if ($Definition['AccessLevels'] -and @($Definition['AccessLevels']).Count -gt 1) {
        Write-Host ' -Access ReadOnly|ReadWrite'
    }
    Write-Host ' -Action Run|BuildScript|BuildRequirements'
    Write-Host ' -OutputDirectory'
    Write-Host ' -NonInteractive -OutputFormat Text|Json'
    Write-Host ''
    Write-Host 'Inputs:'
    foreach ($input in @($Definition.Inputs)) {
        if (-not $input) { continue }
        if (-not (Test-IntegrationWizardInputWhen -InputDef $input -Context $Context)) { continue }
        $req = if ($input['Required']) { ' (required)' } else { '' }
        $src = if ($input['Source'] -eq 'Discover') { ' [discover]' } else { '' }
        Write-Host " -$($input.Name)$req$src — $($input['Purpose'])"
    }
    if ($Missing -and $Missing.Count -gt 0) {
        Write-Host ''
        Write-Host 'Missing:' -ForegroundColor Yellow
        foreach ($m in $Missing) { Write-Host " -$m" -ForegroundColor Yellow }
    }
    Write-Host ''
}

function Resolve-IntegrationWizardAccessLevel {
    param(
        [Parameter(Mandatory)][hashtable]$Definition,
        [Parameter(Mandatory)][hashtable]$Context,
        [switch]$Wizard,
        [switch]$NonInteractive
    )
    $levels = @($Definition['AccessLevels'])
    if ($levels.Count -le 1) {
        if ($levels.Count -eq 1) {
            $Context['Access'] = $levels[0]
        }
        return
    }
    if (-not [string]::IsNullOrWhiteSpace([string]$Context['Access'])) { return }
    if (-not $Wizard) {
        if ($NonInteractive) { throw "Missing required -Access ($($levels -join '|'))." }
        return
    }
    $Context['Access'] = Read-Choice -Prompt 'Access level for this connector:' `
        -Options $levels `
        -Labels @($levels | ForEach-Object {
            if ($_ -eq 'ReadOnly') { 'Read only (aggregation, no provisioning writes)' }
            else { 'Read and write (includes provisioning permissions)' }
        }) -Default 'ReadOnly'
}

function Show-IntegrationWizardConnectionCheck {
    param(
        [Parameter(Mandatory)][string]$CliName,
        [Parameter(Mandatory)][hashtable]$SessionState
    )
    Write-Step 'Current CLI session'
    if ($SessionState.Installed -and $SessionState.SignedIn) {
        Write-Ok "${CliName}: $($SessionState.Summary)"
    }
    elseif ($SessionState.Installed) {
        $detail = if ($SessionState['Summary']) { " ($($SessionState['Summary']))" } else { '' }
        Write-Info "$CliName is installed but not signed in.$detail"
    }
    else {
        Write-Info "$CliName was not found on PATH."
    }
}

function Resolve-IntegrationWizardConnectionChoice {
    param(
        [Parameter(Mandatory)][string]$CliName,
        [Parameter(Mandatory)][hashtable]$SessionState,
        [switch]$Wizard,
        [switch]$NonInteractive
    )
    if (-not $Wizard) {
        if ($SessionState['Mode']) { return $SessionState['Mode'] }
        return 'UseSession'
    }
    Show-IntegrationWizardConnectionCheck -CliName $CliName -SessionState $SessionState
    # "Use this session" is only offered when a usable session exists. An installed CLI with
    # expired or missing credentials cannot introspect, so that choice is omitted.
    $options = @('SignIn', 'NoSession')
    $labels = @(
        'Install or sign in / switch account'
        'Continue without a session (type values manually)'
    )
    $default = 'SignIn'
    if ($SessionState.SignedIn) {
        $options = @('UseSession') + $options
        $labels = @('Use this session (introspect and pick lists)') + $labels
        $default = 'UseSession'
    }
    $pick = Read-Choice -Prompt 'How do you want to connect?' -Options $options -Labels $labels -Default $default
    $SessionState['Mode'] = $pick
    return $pick
}

function Collect-IntegrationWizardInputs {
    param(
        [Parameter(Mandatory)][hashtable]$Definition,
        [Parameter(Mandatory)][hashtable]$Context,
        [switch]$Wizard,
        [switch]$NonInteractive,
        [scriptblock]$Discover
    )
    foreach ($input in @($Definition.Inputs)) {
        if (-not $input) { continue }
        if (-not (Test-IntegrationWizardInputWhen -InputDef $input -Context $Context)) { continue }
        $name = $input.Name
        if ($Context.ContainsKey($name) -and -not [string]::IsNullOrWhiteSpace([string]$Context[$name])) { continue }
        if (-not $Wizard) { continue }
        $discoverValues = $null
        if ($Context['ConnectionMode'] -ne 'NoSession' -and $input['Source'] -eq 'Discover' -and $Discover) {
            $discoverValues = & $Discover $name $Context
        }
        if ($discoverValues -and @($discoverValues).Count -gt 0) {
            $typed = 'Type a different value'
            $options = @($discoverValues) + @($typed)
            $picked = Read-Choice -Prompt $input['Prompt'] -Options $options -Default $discoverValues[0]
            if ($picked -ne $typed) { $Context[$name] = $picked; continue }
        }
        if ($input['ValidateSet']) {
            $Context[$name] = Read-Choice -Prompt $input['Prompt'] -Options $input['ValidateSet'] -Default $input['Default']
        }
        else {
            $Context[$name] = Read-InputString -Prompt $input['Prompt'] -Default $input['Default'] -Required:([bool]$input['Required'])
        }
    }
}

function Show-IntegrationWizardSummary {
    param(
        [Parameter(Mandatory)][hashtable]$Definition,
        [Parameter(Mandatory)][hashtable]$Context
    )
    Write-Host ''
    Write-Host 'Plan summary' -ForegroundColor Cyan
    if ($Context['IntegrationProjectName']) {
        Write-Host " Project: $($Context['IntegrationProjectName'])"
        Write-Host " Folder: $($Context['IntegrationFolderPath'])"
    }
    Write-Host " Target: $($Definition.Title)"
    if ($Context['Access']) { Write-Host " Access: $($Context['Access'])" }
    if ($Context['Action']) { Write-Host " Action: $($Context['Action'])" }
    foreach ($input in @($Definition.Inputs)) {
        if (-not $input) { continue }
        if (-not (Test-IntegrationWizardInputWhen -InputDef $input -Context $Context)) { continue }
        $val = $Context[$input.Name]
        if ($input['Secret']) { $val = if ($val) { '***' } else { '(created at run)' } }
        Write-Host " $($input.Name): $val"
    }
    Write-Host ''
}

function Resolve-IntegrationWizardAction {
    param(
        [Parameter(Mandatory)][hashtable]$Context,
        [switch]$Wizard
    )
    if (-not [string]::IsNullOrWhiteSpace([string]$Context['Action'])) { return }
    if (-not $Wizard) { throw 'Missing required -Action (Run|BuildScript|BuildRequirements).' }
    $Context['Action'] = Read-Choice -Prompt 'What should happen next?' `
        -Options @('Run', 'BuildScript', 'BuildRequirements') `
        -Labels @(
            'Run changes (apply on the target now)'
            'Build script (PowerShell 7 + platform CLI, review before running)'
            'Build requirements (Markdown document)'
        ) -Default 'Run'
}

function Start-IntegrationWizardFlow {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory)][string]$Platform,
        [Parameter(Mandatory)][string]$ScriptName,
        [Parameter(Mandatory)][hashtable]$CoreOptionHelp,
        [Parameter(Mandatory)][hashtable]$Context,
        [scriptblock]$PickTarget,
        [scriptblock]$Discover,
        [scriptblock]$Execute
    )
    $wizard = [bool]$Context['Wizard']
    $nonInteractive = [bool]$Context['NonInteractive']

    $plan = $null
    try {
        $plan = Resolve-IntegrationWizardPlan -Platform $Platform -Context $Context
    }
    catch {
        Write-Error $_.Exception.Message
        return
    }

    if (-not $plan -and -not $wizard) {
        Show-IntegrationWizardPlatformHelp -Platform $Platform -ScriptName $ScriptName -CoreOptionHelp $CoreOptionHelp
        return
    }

    $projectMod = Join-Path $PSScriptRoot 'IW.Project.psm1'
    if (-not (Get-Module -Name 'IW.Project')) {
        Import-Module $projectMod -Force -WarningAction SilentlyContinue
    }
    if ($wizard -and (Test-IntegrationWizardNeedsProjectContext -Wizard:$wizard -Context $Context)) {
        Select-IntegrationProjectWorkspace -Context $Context
    }

    $connectorDef = $null
    if ($wizard -and -not $plan -and $PickTarget) {
        $connectorDef = & $PickTarget
        if ($connectorDef) {
            if ($connectorDef.Product -eq 'ISC') {
                Resolve-IntegrationWizardEntroAddon -Platform $Platform -Context $Context -ConnectorDefinition $connectorDef `
                    -Wizard:$wizard -NonInteractive:$nonInteractive
            }
            $plan = Resolve-IntegrationWizardPlan -Platform $Platform -Context $Context
        }
    }
    elseif ($wizard -and $plan -and -not $plan['IsComposite'] -and $plan.Product -eq 'ISC') {
        Resolve-IntegrationWizardEntroAddon -Platform $Platform -Context $Context -ConnectorDefinition $plan `
            -Wizard:$wizard -NonInteractive:$nonInteractive
        try {
            $plan = Resolve-IntegrationWizardPlan -Platform $Platform -Context $Context
        }
        catch {
            Write-Error $_.Exception.Message
            return
        }
    }

    if (-not $plan) {
        Show-IntegrationWizardPlatformHelp -Platform $Platform -ScriptName $ScriptName -CoreOptionHelp $CoreOptionHelp
        return
    }

    Resolve-IntegrationWizardAccessLevel -Definition $plan -Context $Context -Wizard:$wizard -NonInteractive:$nonInteractive
    $meta = $script:IntegrationWizardPlatforms[$Platform]
    if (Test-IntegrationWizardNeedsProjectContext -Wizard:$wizard -Context $Context) {
        Initialize-IntegrationProjectContext -Platform $Platform -PlatformTitle $meta.Title -Plan $plan -Context $Context -ScriptName $ScriptName
    }
    if ($wizard) {
        $session = Get-IntegrationWizardCliSession -CliName $meta.CliName
        $mode = Resolve-IntegrationWizardConnectionChoice -CliName $meta.CliName -SessionState $session -Wizard:$wizard -NonInteractive:$nonInteractive
        $Context['ConnectionMode'] = $mode
        if ($mode -eq 'SignIn') {
            Install-IntegrationWizardCli -CliName $meta.CliName -NonInteractive:$nonInteractive
            Invoke-IntegrationWizardCliLogin -CliName $meta.CliName -NonInteractive:$nonInteractive
        }
        Collect-IntegrationWizardInputs -Definition $plan -Context $Context -Wizard:$wizard -NonInteractive:$nonInteractive -Discover $Discover
    }
    $missing = Test-IntegrationWizardRequiredInputs -Definition $plan -Context $Context
    if ($missing.Count -gt 0 -and -not $wizard) {
        Show-IntegrationWizardDefinitionHelp -Definition $plan -ScriptName $ScriptName -Context $Context -Missing $missing
        return
    }
    if ($missing.Count -gt 0 -and $nonInteractive) {
        throw "Missing required inputs: $($missing -join ', ')"
    }
    if ($wizard) {
        Resolve-IntegrationWizardAction -Context $Context -Wizard:$wizard
        Show-IntegrationWizardSummary -Definition $plan -Context $Context
        if (-not (Read-YesNo -Prompt 'Proceed?' -Default $true)) {
            Write-Host 'Cancelled.' -ForegroundColor Yellow
            if (Get-Command Close-IntegrationProjectActivityLog -ErrorAction SilentlyContinue) {
                Close-IntegrationProjectActivityLog
            }
            return
        }
    }
    else {
        if ([string]::IsNullOrWhiteSpace([string]$Context['Action'])) {
            Show-IntegrationWizardDefinitionHelp -Definition $plan -ScriptName $ScriptName -Context $Context -Missing @('Action')
            return
        }
    }
    if ($Context['IntegrationProjectName']) {
        $record = Get-IntegrationProjectSelectionSnapshot -Context $Context -Definition $plan -Platform $Platform -ScriptName $ScriptName
        Write-IntegrationProjectSelection -Record $record
        Write-IntegrationProjectLogLine -Level 'INFO' -Message "Recorded selection for project $($Context['IntegrationProjectName'])"
    }
    try {
        & $Execute $plan $Context
    }
    finally {
        if (Get-Command Close-IntegrationProjectActivityLog -ErrorAction SilentlyContinue) {
            Close-IntegrationProjectActivityLog
        }
    }
}

function Get-IntegrationWizardCliSession {
    param([Parameter(Mandatory)][string]$CliName)
    $state = @{
        Installed = $false
        SignedIn  = $false
        Summary   = ''
    }
    $cmd = Get-Command $CliName -ErrorAction SilentlyContinue
    if (-not $cmd) { return $state }
    $state.Installed = $true
    try {
        switch ($CliName) {
            'aws' {
                $json = aws sts get-caller-identity --output json 2>$null
                if ($LASTEXITCODE -eq 0 -and $json) {
                    $o = $json | ConvertFrom-Json
                    $state.SignedIn = $true
                    $state.Summary = "account $($o.Account) ($($o.Arn))"
                }
            }
            'az' {
                $json = az account show --output json 2>$null
                if ($LASTEXITCODE -eq 0 -and $json) {
                    $o = $json | ConvertFrom-Json
                    $state.SignedIn = $true
                    $state.Summary = "subscription $($o.name) ($($o.id))"
                }
            }
            'gcloud' {
                $acct = gcloud config get-value account 2>$null
                $proj = gcloud config get-value project 2>$null
                if ($acct) {
                    # A configured account is not enough: its token may have expired. Only a token
                    # that can be minted counts as signed in.
                    gcloud auth print-access-token --quiet 2>$null | Out-Null
                    if ($LASTEXITCODE -eq 0) {
                        $state.SignedIn = $true
                        $state.Summary = "account $acct project $proj"
                    }
                    else {
                        $state.Summary = "credentials for $acct have expired"
                    }
                }
            }
            'databricks' {
                $out = databricks auth describe --output json 2>$null
                if ($LASTEXITCODE -eq 0 -and $out) {
                    $state.SignedIn = $true
                    $state.Summary = 'Databricks CLI authenticated'
                }
            }
        }
    }
    catch { }
    return $state
}

function Install-IntegrationWizardCli {
    param(
        [Parameter(Mandatory)][string]$CliName,
        [switch]$NonInteractive
    )
    switch ($CliName) {
        'aws' {
            Ensure-CliCommand -CommandNames @('aws', 'aws.exe') `
                -WingetPackageId 'Amazon.AWSCLI' -BrewPackage 'awscli' `
                -ManualHint 'Install AWS CLI v2 from https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html' | Out-Null
        }
        'az' { Ensure-AzCliCommand -ErrorAction Stop }
        'gcloud' { Ensure-GCloudCommand -ErrorAction Stop }
        'databricks' {
            if (-not (Get-Command databricks -ErrorAction SilentlyContinue)) {
                if ($NonInteractive) { throw 'Install the Databricks CLI: https://docs.databricks.com/dev-tools/cli/install.html' }
                Write-Info 'Install the Databricks CLI, then press Enter.'
                Read-Host | Out-Null
            }
        }
    }
}

function Invoke-IntegrationWizardCliLogin {
    param(
        [Parameter(Mandatory)][string]$CliName,
        [switch]$NonInteractive
    )
    if ($NonInteractive) {
        throw "Sign in with: $(Get-IntegrationWizardLoginHint -CliName $CliName)"
    }
    Write-Info (Get-IntegrationWizardLoginHint -CliName $CliName)
    switch ($CliName) {
        'aws' { aws sso login 2>$null; if ($LASTEXITCODE -ne 0) { aws configure } }
        'az' { az login | Out-Null }
        'gcloud' { gcloud auth login }
        'databricks' { databricks auth login | Out-Null }
    }
}

function Get-IntegrationWizardLoginHint {
    param([string]$CliName)
    switch ($CliName) {
        'aws' { return 'Run: aws sso login (or aws configure)' }
        'az' { return 'Run: az login' }
        'gcloud' { return 'Run: gcloud auth login' }
        'databricks' { return 'Run: databricks auth login' }
        default { return "Sign in to $CliName" }
    }
}

Export-ModuleMember -Function @(
    'Register-IntegrationWizardPlatform'
    'Register-IntegrationWizardDefinition'
    'Get-IntegrationWizardDefinition'
    'Get-IntegrationWizardDefinitionsForPlatform'
    'Get-IntegrationWizardWizardRoot'
    'Import-IntegrationWizardConsole'
    'Show-IntegrationWizardPlatformHelp'
    'Show-IntegrationWizardDefinitionHelp'
    'Get-IntegrationWizardDefinitionByCoreSwitch'
    'Get-IntegrationWizardTargets'
    'Merge-IntegrationWizardDefinitions'
    'Resolve-IntegrationWizardPlan'
    'Resolve-IntegrationWizardEntroAddon'
    'Resolve-IntegrationWizardConnectionChoice'
    'Test-IntegrationWizardRequiredInputs'
    'Test-IntegrationWizardInputWhen'
    'Start-IntegrationWizardFlow'
    'Get-IntegrationWizardCliSession'
    'Install-IntegrationWizardCli'
    'Invoke-IntegrationWizardCliLogin'
    'Get-IntegrationWizardLoginHint'
)