modules/shared/Preflight/Get-RequiredInputs.ps1

#Requires -Version 7.4

Set-StrictMode -Version Latest

if (-not (Get-Command Remove-Credentials -ErrorAction SilentlyContinue)) {
    $sanitizePath = Join-Path (Split-Path $PSScriptRoot -Parent) 'Sanitize.ps1'
    if (Test-Path $sanitizePath) { . $sanitizePath }
}
if (-not (Get-Command Remove-Credentials -ErrorAction SilentlyContinue)) {
    function Remove-Credentials { param([string]$Text) return $Text }
}

function Test-PreflightNonInteractive {
    [CmdletBinding()]
    param(
        [switch] $NonInteractive
    )

    if ($NonInteractive) { return $true }

    $ci = [string]$env:CI
    if ($ci -imatch '^(true|1|yes|on)$') { return $true }

    try {
        if (-not [Environment]::UserInteractive) { return $true }
    } catch {
        Write-Verbose ("Test-NonInteractive: [Environment]::UserInteractive probe failed; defaulting to interactive. Reason: {0}" -f $_.Exception.Message)
    }

    try {
        if ([Console]::IsInputRedirected) { return $true }
    } catch {
        Write-Verbose ("Test-NonInteractive: [Console]::IsInputRedirected probe failed; defaulting to interactive. Reason: {0}" -f $_.Exception.Message)
    }

    return $false
}

function Test-PreflightConditional {
    [CmdletBinding()]
    param(
        [object] $Conditional,
        [hashtable] $KnownValues
    )

    if ($null -eq $Conditional) { return $true }
    if ($Conditional -isnot [System.Collections.IDictionary] -and -not $Conditional.PSObject) { return $true }
    if (-not $Conditional.PSObject.Properties['param']) { return $true }

    $paramName = [string]$Conditional.param
    if ([string]::IsNullOrWhiteSpace($paramName)) { return $true }
    $current = if ($KnownValues.ContainsKey($paramName)) { [string]$KnownValues[$paramName] } else { '' }

    if ($Conditional.PSObject.Properties['equals']) {
        return ($current -ieq [string]$Conditional.equals)
    }
    if ($Conditional.PSObject.Properties['notEquals']) {
        return ($current -ine [string]$Conditional.notEquals)
    }

    return $true
}

function Test-IsSensitiveInputName {
    [CmdletBinding()]
    param([string] $Name)
    return ($Name -match '(?i)(token|pat|secret|password|credential|key)')
}

function Test-PreflightValue {
    [CmdletBinding()]
    param(
        [AllowNull()]
        [object] $Value,
        [string] $Type = 'string',
        [string] $Validator,
        [string[]] $EnumValues
    )

    $text = if ($null -eq $Value) { '' } else { [string]$Value }
    if ([string]::IsNullOrWhiteSpace($text)) { return $false }

    $ok = $true
    switch (($Type ?? 'string').ToLowerInvariant()) {
        'guid' {
            $guid = [Guid]::Empty
            $ok = [Guid]::TryParse($text, [ref]$guid)
        }
        'url' {
            $uri = $null
            $ok = [Uri]::TryCreate($text, [UriKind]::Absolute, [ref]$uri)
        }
        'bool' {
            $ok = $text -match '^(?i:true|false|1|0|yes|no|on|off)$'
        }
        'enum' {
            if ($EnumValues -and $EnumValues.Count -gt 0) {
                $ok = $text -in $EnumValues
            } else {
                $ok = $true
            }
        }
        default {
            $ok = $true
        }
    }

    if (-not $ok) { return $false }
    if (-not [string]::IsNullOrWhiteSpace($Validator)) {
        return ($text -match $Validator)
    }
    return $true
}

function Get-RequiredInputs {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]] $Tools,

        [hashtable] $CliValues = @{},

        [switch] $NonInteractive
    )

    $isNonInteractive = Test-PreflightNonInteractive -NonInteractive:$NonInteractive
    $requirements = [System.Collections.Specialized.OrderedDictionary]::new()

    # Pre-resolve a merged value map (CLI > env) across every candidate input across every
    # tool, regardless of whether a `conditional` would gate it. This lets
    # Test-PreflightConditional honor values supplied via env vars - not just CLI - so a
    # downstream input is not falsely flagged as required when its prerequisite is satisfied
    # by an env var. CLI > env > prompt precedence is preserved (prompt happens later, only
    # for inputs that survive the conditional pass).
    $knownValues = @{}
    foreach ($k in $CliValues.Keys) {
        if (-not [string]::IsNullOrWhiteSpace([string]$CliValues[$k])) {
            $knownValues[$k] = $CliValues[$k]
        }
    }
    foreach ($tool in @($Tools)) {
        $allInputs = if ($tool.PSObject.Properties['required_inputs']) { @($tool.required_inputs) } else { @() }
        foreach ($inputDef in $allInputs) {
            if ($null -eq $inputDef) { continue }
            $nameProp = $inputDef.PSObject.Properties['name']
            $name = if ($nameProp) { [string]$nameProp.Value } else { '' }
            if ([string]::IsNullOrWhiteSpace($name)) { continue }
            if ($knownValues.ContainsKey($name)) { continue }
            $envName = if ($inputDef.PSObject.Properties['envVar']) { [string]$inputDef.envVar } else { '' }
            if (-not [string]::IsNullOrWhiteSpace($envName)) {
                $envValue = [Environment]::GetEnvironmentVariable($envName)
                if (-not [string]::IsNullOrWhiteSpace([string]$envValue)) {
                    $knownValues[$name] = $envValue
                }
            }
        }
    }

    foreach ($tool in @($Tools)) {
        $toolRequiredInputs = if ($tool.PSObject.Properties['required_inputs']) { @($tool.required_inputs) } else { @() }
        foreach ($inputDef in $toolRequiredInputs) {
            if ($null -eq $inputDef) { continue }
            $nameProp = $inputDef.PSObject.Properties['name']
            $name = if ($nameProp) { [string]$nameProp.Value } else { '' }
            if ([string]::IsNullOrWhiteSpace($name)) { continue }
            $conditional = if ($inputDef.PSObject.Properties['conditional']) { $inputDef.conditional } else { $null }
            if (-not (Test-PreflightConditional -Conditional $conditional -KnownValues $knownValues)) { continue }
            if (-not $requirements.Contains($name)) {
                $type = if ($inputDef.PSObject.Properties['type']) { [string]$inputDef.type } else { 'string' }
                $prompt = if ($inputDef.PSObject.Properties['prompt']) { [string]$inputDef.prompt } else { '' }
                $envVar = if ($inputDef.PSObject.Properties['envVar']) { [string]$inputDef.envVar } else { '' }
                $example = if ($inputDef.PSObject.Properties['example']) { [string]$inputDef.example } else { '' }
                $validator = if ($inputDef.PSObject.Properties['validator']) { [string]$inputDef.validator } else { '' }
                $enumValues = if ($inputDef.PSObject.Properties['enumValues']) { @($inputDef.enumValues) } else { @() }
                $requirements[$name] = [PSCustomObject]@{
                    Name      = $name
                    Type      = $type
                    Prompt    = $prompt
                    EnvVar    = $envVar
                    Example   = $example
                    Validator = $validator
                    EnumValues = $enumValues
                    Sensitive = (Test-IsSensitiveInputName -Name $name)
                }
            }
        }
    }

    $resolved = @{}
    $missing  = [System.Collections.Generic.List[object]]::new()

    foreach ($entry in $requirements.Values) {
        $name = $entry.Name
        $value = $null

        if ($CliValues.ContainsKey($name) -and -not [string]::IsNullOrWhiteSpace([string]$CliValues[$name])) {
            $value = $CliValues[$name]
        }

        if ([string]::IsNullOrWhiteSpace([string]$value) -and -not [string]::IsNullOrWhiteSpace($entry.EnvVar)) {
            $envValue = [Environment]::GetEnvironmentVariable($entry.EnvVar)
            if (-not [string]::IsNullOrWhiteSpace([string]$envValue)) {
                $value = $envValue
            }
        }

        # Prompt only when interactive, unresolved, and not a sensitive secret-like input.
        if ((-not $isNonInteractive) -and
            [string]::IsNullOrWhiteSpace([string]$value) -and
            -not $entry.Sensitive) {
            $prompt = if ([string]::IsNullOrWhiteSpace($entry.Prompt)) { "Enter $name" } else { $entry.Prompt }
            if (-not [string]::IsNullOrWhiteSpace($entry.Example)) {
                $prompt = "$prompt (example: $($entry.Example))"
            }
            $value = Read-Host -Prompt $prompt
        }

        if (Test-PreflightValue -Value $value -Type $entry.Type -Validator $entry.Validator -EnumValues $entry.EnumValues) {
            $resolved[$name] = [string]$value
            continue
        }

        $missing.Add($entry) | Out-Null
    }

    if ($missing.Count -gt 0) {
        $details = foreach ($item in $missing) {
            $displayName = if ([string]::IsNullOrWhiteSpace([string]$item.Name)) { '<unknown>' } else { [string]$item.Name }
            $parts = @($displayName)
            if (-not [string]::IsNullOrWhiteSpace($item.EnvVar)) { $parts += "env:$($item.EnvVar)" }
            if (-not [string]::IsNullOrWhiteSpace($item.Example)) { $parts += "example:$($item.Example)" }
            if ($item.Sensitive) { $parts += 'prompting-disabled(use-cli-or-env)' }
            $parts -join ' '
        }
        throw (Remove-Credentials -Text "Unresolved required inputs. Provide via CLI parameters, environment variables, or run without -NonInteractive: $($details -join '; ')")
    }

    return $resolved
}