Private/Configuration.ps1

<#
This is the first function called in the connector workflow.

At this stage, the logger is not initialized yet, so this function must not rely on
`Write-CustomLog` (or any logging target setup). It should fail fast with clear
exceptions when configuration is missing or invalid.
#>

function Get-Configuration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string]$ScriptRootPath,

        [Parameter(Mandatory = $true)]
        [string]$NutanixEnvironment
    )

    $configPath = Join-Path -Path $ScriptRootPath -ChildPath (Join-Path -Path "Config" -ChildPath "config.json")
    if (-not (Test-Path -Path $configPath)) {
        # The repository uses lowercase config/, while packaged Windows bundles use Config/.
        $configPath = Join-Path -Path $ScriptRootPath -ChildPath (Join-Path -Path "config" -ChildPath "config.json")
    }

    if (-not (Test-Path -Path $configPath)) {
        throw "Configuration file not found at: $configPath"
    }

    $raw = Get-Content -Path $configPath -Raw -ErrorAction Stop
    if ([string]::IsNullOrWhiteSpace($raw)) {
        throw "Invalid configuration: configuration file is empty. Path: $configPath"
    }

    try {
        $config = $raw | ConvertFrom-Json -ErrorAction Stop
    }
    catch {
        throw "Invalid configuration: configuration file is not valid JSON. Path: $configPath. Error: $($_.Exception.Message)"
    }

    if ($null -eq $config) {
        throw "Invalid configuration: config object is null. Path: $configPath"
    }

    # Fail fast on required high-level structure so later environment selection doesn't produce misleading errors.
    if ($null -eq $config.nutanixEnvironments -or -not ($config.nutanixEnvironments -is [System.Array])) {
        throw "Invalid configuration: 'nutanixEnvironments' is required and must be an array."
    }

    if ($config.nutanixEnvironments.Count -eq 0) {
        throw "Invalid configuration: 'nutanixEnvironments' must not be empty."
    }

    $envConfig = $config.nutanixEnvironments | Where-Object { $_.Name -eq $NutanixEnvironment }
    if ($null -eq $envConfig) {
        throw "Environment '$NutanixEnvironment' not found in configuration"
    }

    Test-ConnectorConfiguration -Config $config -EnvironmentConfig $envConfig

    $typedEnvConfig = [NutanixEnvironmentConfiguration]::new()
    $typedEnvConfig.Name = $envConfig.Name
    $typedEnvConfig.PrismCentralFQDN = $envConfig.PrismCentralFQDN
    if ($null -ne $envConfig.PSObject.Properties['PrismCentralPort']) {
        $typedEnvConfig.PrismCentralPort = [int]$envConfig.PrismCentralPort
    }
    $typedEnvConfig.WindowsCredentialEntry = $envConfig.WindowsCredentialEntry
    $typedEnvConfig.SkipCertificateCheck = $false
    if ($null -ne $envConfig.PSObject.Properties['SkipCertificateCheck']) {
        $typedEnvConfig.SkipCertificateCheck = [bool]$envConfig.SkipCertificateCheck
    }

    $typedNexthinkApi = [NutanixNexthinkApiConfiguration]::new()
    $typedNexthinkApi.HostFQDN = $config.NexthinkAPI.HostFQDN
    $typedNexthinkApi.LoginFQDN = $config.NexthinkAPI.LoginFQDN
    $typedNexthinkApi.WindowsCredentialEntry = $config.NexthinkAPI.WindowsCredentialEntry
    $typedNexthinkApi.RequestBatchSize = [int]$config.NexthinkAPI.RequestBatchSize
    $typedNexthinkApi.VmCacheExpiration = $script:VM_CACHE_EXPIRATION
    if (Test-ConfigurationCacheExpiration -Value $config.NexthinkAPI.VmCacheExpiration) {
        $typedNexthinkApi.VmCacheExpiration = $config.NexthinkAPI.VmCacheExpiration
    }

    $typedLogging = $null
    if ($null -ne $config.Logging) {
        $typedLogging = [NutanixLoggingConfiguration]::new()
        $typedLogging.LogLevel = $config.Logging.LogLevel
        $typedLogging.LogRetentionDays = $config.Logging.LogRetentionDays
    }

    $cfg = [NutanixConnectorConfiguration]::new()
    $cfg.ScriptRootPath = $ScriptRootPath
    $cfg.EnvironmentConfig = $typedEnvConfig
    $cfg.NexthinkAPI = $typedNexthinkApi
    $cfg.Logging = $typedLogging

    return $cfg
}

function Stop-ConfigError {
    param([string]$Message)

    Write-Error -Message $Message
    throw $Message
}

function Assert-ConfigurationNonEmptyString {
    param(
        [string]$Value,
        [string]$FieldName
    )

    if ([string]::IsNullOrWhiteSpace($Value)) {
        Stop-ConfigError "Invalid configuration: '$FieldName' is required and must be a non-empty string."
    }
}

function Test-ConfigurationPort {
    param([object]$EnvironmentConfig)

    if ($null -eq $EnvironmentConfig.PSObject.Properties['PrismCentralPort']) {
        return
    }

    $port = 0
    if (-not [int]::TryParse([string]$EnvironmentConfig.PrismCentralPort, [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
        Stop-ConfigError "Invalid configuration: 'nutanixEnvironments[].PrismCentralPort' must be between 1 and 65535."
    }
}

function Test-ConfigurationSkipCertificateCheck {
    param([object]$EnvironmentConfig)

    if ($null -ne $EnvironmentConfig.PSObject.Properties['SkipCertificateCheck'] -and
        $EnvironmentConfig.SkipCertificateCheck -isnot [bool]) {
        Stop-ConfigError "Invalid configuration: 'nutanixEnvironments[].SkipCertificateCheck' must be a JSON boolean."
    }
}

function Test-ConfigurationLogging {
    param([object]$Logging)

    if ($null -eq $Logging) {
        return
    }

    if ($null -ne $Logging.LogLevel -and -not [string]::IsNullOrWhiteSpace([string]$Logging.LogLevel)) {
        $allowed = @('DEBUG', 'INFO', 'WARNING', 'ERROR')
        if ($allowed -notcontains ([string]$Logging.LogLevel).ToUpperInvariant()) {
            Stop-ConfigError "Invalid configuration: 'Logging.LogLevel' must be one of: $($allowed -join ', '). Value: '$($Logging.LogLevel)'"
        }
    }

    if ($null -ne $Logging.LogRetentionDays -and -not [string]::IsNullOrWhiteSpace([string]$Logging.LogRetentionDays)) {
        $retention = 0
        if (-not [int]::TryParse([string]$Logging.LogRetentionDays, [ref]$retention) -or $retention -le 0) {
            Stop-ConfigError "Invalid configuration: 'Logging.LogRetentionDays' must be a positive integer. Value: '$($Logging.LogRetentionDays)'"
        }
        $Logging.LogRetentionDays = $retention
    }
}

function Test-ConfigurationNexthinkApi {
    param([object]$NexthinkApi)

    if ($null -eq $NexthinkApi) {
        Stop-ConfigError "Invalid configuration: 'NexthinkAPI' section is required."
    }

    Assert-ConfigurationNonEmptyString -Value $NexthinkApi.HostFQDN -FieldName 'NexthinkAPI.HostFQDN'
    Assert-ConfigurationNonEmptyString -Value $NexthinkApi.LoginFQDN -FieldName 'NexthinkAPI.LoginFQDN'
    Assert-ConfigurationNonEmptyString -Value $NexthinkApi.WindowsCredentialEntry -FieldName 'NexthinkAPI.WindowsCredentialEntry'

    $batchSize = 0
    if ($null -eq $NexthinkApi.RequestBatchSize -or
        -not [int]::TryParse([string]$NexthinkApi.RequestBatchSize, [ref]$batchSize) -or
        $batchSize -le 0) {
        Stop-ConfigError "Invalid configuration: 'NexthinkAPI.RequestBatchSize' must be a positive integer."
    }
    $NexthinkApi.RequestBatchSize = $batchSize

    $null = Test-ConfigurationCacheExpiration -Value $NexthinkApi.VmCacheExpiration
}

function Test-ConnectorConfiguration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [object]$Config,

        [Parameter(Mandatory = $true)]
        [object]$EnvironmentConfig
    )

    Write-CustomLog -Message "Validating configuration..." -Severity 'DEBUG'

    if ($null -eq $Config) {
        Stop-ConfigError "Invalid configuration: config object is null."
    }

    if ($null -eq $Config.nutanixEnvironments -or -not ($Config.nutanixEnvironments -is [System.Array])) {
        Stop-ConfigError "Invalid configuration: 'nutanixEnvironments' is required and must be an array."
    }

    if ($Config.nutanixEnvironments.Count -eq 0) {
        Stop-ConfigError "Invalid configuration: 'nutanixEnvironments' must not be empty."
    }

    # Validate selected environment config
    Assert-ConfigurationNonEmptyString -Value $EnvironmentConfig.Name -FieldName 'nutanixEnvironments[].Name'
    Assert-ConfigurationNonEmptyString -Value $EnvironmentConfig.PrismCentralFQDN -FieldName 'nutanixEnvironments[].PrismCentralFQDN'
    Assert-ConfigurationNonEmptyString -Value $EnvironmentConfig.WindowsCredentialEntry -FieldName 'nutanixEnvironments[].WindowsCredentialEntry'
    Test-ConfigurationPort -EnvironmentConfig $EnvironmentConfig
    Test-ConfigurationSkipCertificateCheck -EnvironmentConfig $EnvironmentConfig

    Test-ConfigurationNexthinkApi -NexthinkApi $Config.NexthinkAPI

    # Optional logging settings validation
    Test-ConfigurationLogging -Logging $Config.Logging

    Write-CustomLog -Message "Configuration validated successfully." -Severity 'DEBUG'
}

function Test-ConfigurationCacheExpiration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        $Value
    )

    if ($null -eq $Value -or [string]::IsNullOrWhiteSpace([string]$Value)) {
        return $false
    }

    $valueStr = [string]$Value

    try {
        $null = [System.Xml.XmlConvert]::ToTimeSpan($valueStr)
        return $true
    }
    catch {
        throw "Invalid configuration: 'VmCacheExpiration' must be an ISO 8601 duration (e.g. 'P1D', 'PT1H', 'PT30M'). Value: '$valueStr'"
    }
}