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]$vSphereEnvironment
    )

    $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.vSphereEnvironments -or -not ($config.vSphereEnvironments -is [System.Array])) {
        throw "Invalid configuration: 'vSphereEnvironments' is required and must be an array."
    }

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

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

    Validate-Configuration -Config $config -EnvironmentConfig $envConfig

    $typedEnvConfig = [VSphereEnvironmentConfiguration]::new()
    $typedEnvConfig.Name = $envConfig.Name
    $typedEnvConfig.vCenterFQDN = $envConfig.vCenterFQDN
    $typedEnvConfig.WindowsCredentialEntry = $envConfig.WindowsCredentialEntry

    $typedNexthinkApi = [VSphereNexthinkApiConfiguration]::new()
    $typedNexthinkApi.HostFQDN = $config.NexthinkAPI.HostFQDN
    $typedNexthinkApi.LoginFQDN = $config.NexthinkAPI.LoginFQDN
    $typedNexthinkApi.WindowsCredentialEntry = $config.NexthinkAPI.WindowsCredentialEntry
    $typedNexthinkApi.RequestBatchSize = $config.NexthinkAPI.RequestBatchSize

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

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

    return $cfg
}

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

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

    function Throw-ConfigError {
        param([string]$Message)
        Write-Error -Message $Message
        throw $Message
    }

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

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

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

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

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

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

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

    # Validate selected environment config
    Assert-NonEmptyString -Value $EnvironmentConfig.Name -FieldName 'vSphereEnvironments[].Name'
    Assert-NonEmptyString -Value $EnvironmentConfig.vCenterFQDN -FieldName 'vSphereEnvironments[].vCenterFQDN'
    Assert-NonEmptyString -Value $EnvironmentConfig.WindowsCredentialEntry -FieldName 'vSphereEnvironments[].WindowsCredentialEntry'

    # Validate Nexthink API settings (fail fast, even if not used yet by metrics)
    Assert-NonEmptyString -Value $Config.NexthinkAPI.HostFQDN -FieldName 'NexthinkAPI.HostFQDN'
    Assert-NonEmptyString -Value $Config.NexthinkAPI.LoginFQDN -FieldName 'NexthinkAPI.LoginFQDN'
    Assert-NonEmptyString -Value $Config.NexthinkAPI.WindowsCredentialEntry -FieldName 'NexthinkAPI.WindowsCredentialEntry'

    $batchSizeRaw = $Config.NexthinkAPI.RequestBatchSize
    if ($null -eq $batchSizeRaw -or [string]::IsNullOrWhiteSpace([string]$batchSizeRaw)) {
        Throw-ConfigError "Invalid configuration: 'NexthinkAPI.RequestBatchSize' is required and must be a positive integer."
    }

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

    # Normalize type (config.json currently stores it as a string)
    $Config.NexthinkAPI.RequestBatchSize = $batchSize

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

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

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