Public/Select-EnvkEnvironment.ps1

function Select-EnvkEnvironment {
    <#
    .SYNOPSIS
        Determines which configured environment is active based on the current day,
        time, and schedule rules.
 
    .DESCRIPTION
        Evaluates the environments defined in the Envoke config object and
        returns the name of the environment that should be active right now. Supports
        schedule-based automatic detection, priority-based overlap resolution, optional
        fallback environments, and a manual override parameter.
 
        The function does NOT call Get-EnvkConfig -- it receives the already-loaded
        config object. This separation keeps Select-EnvkEnvironment testable without file I/O.
 
        The function does NOT call Test-BootGuard -- the boot guard is a separate concern
        managed by the orchestrator (Phase 7). Select-EnvkEnvironment is purely schedule
        evaluation.
 
        Logic flow:
          1. Environment override: if -Environment is provided, validate it exists in
             config and return it immediately (schedule detection skipped).
          2. Collect schedule matches: iterate environments; record the no-schedule
             environment as the fallback; for each scheduled environment, test day and
             hour match; collect all matches with their priorities.
          3. No matches + fallback: log and return the fallback environment name.
          4. No matches + no fallback: call Get-NextScheduledEnv, log the next activation
             time, and return $null.
          5. Single top-priority match: log and return the matched environment name.
          6. Priority conflict (tie at top): log warning; in interactive+debug mode prompt
             user; otherwise use first match by config order and log a warning.
 
        Hour boundary: endHour is exclusive. An environment with endHour=15 is NOT active
        at hour 15 (deactivates at the start of hour 15, i.e., 3:00 PM).
 
        Day matching: uses DayOfWeek.ToString() to produce the full English day name
        ("Monday", "Tuesday", etc.) matching the config schema enum values. This avoids
        implicit enum coercion issues between PS 5.1 and PS 7.
 
    .PARAMETER Config
        The parsed config object returned by Get-EnvkConfig. Mandatory.
 
    .PARAMETER Environment
        Optional environment name override. When provided, schedule detection is skipped
        and this environment is returned directly after validation. Validated dynamically
        against the config environment names at runtime -- no ValidateSet is used, which
        allows arbitrary custom environment profiles without code changes (ENV-02).
 
    .PARAMETER EnableDebug
        When set, enables the interactive PromptForChoice conflict resolution path
        (in combination with [System.Environment]::UserInteractive). In non-interactive
        sessions such as Task Scheduler, the prompt path is never reached regardless of
        this flag.
 
    .PARAMETER Now
        The reference datetime for schedule evaluation. Defaults to the current system
        time (Get-Date). Injectable for deterministic testing.
 
    .OUTPUTS
        System.String
        The name of the selected environment, or $null when no environment matches and
        no fallback is configured.
 
    .NOTES
        Author: Aaron AlAnsari
        Created: 2026-02-25
 
        Priority field: optional integer on each environment. Higher integer wins when
        multiple environments match the current schedule. Default is 0 when not specified.
 
        Overlap conflict: when two or more environments share the same top priority and
        both match the schedule, behavior depends on the session type:
          - Interactive + EnableDebug: PromptForChoice (user picks)
          - Non-interactive or not EnableDebug: first match in config order (with warning)
    #>


    [CmdletBinding()]
    [OutputType([string])]
    param (
        [Parameter(Mandatory)]
        [PSCustomObject]$Config,

        [Parameter()]
        [string]$Environment,

        [Parameter()]
        [switch]$EnableDebug,

        [Parameter()]
        [datetime]$Now = (Get-Date)
    )

    # ---------------------------------------------------------------------------
    # 1. Environment override (-Environment parameter)
    # ---------------------------------------------------------------------------

    if (-not [string]::IsNullOrEmpty($Environment)) {
        $envNames = @($Config.environments.PSObject.Properties.Name)
        if ($envNames -notcontains $Environment) {
            $validList = $envNames -join ', '
            throw "Environment '$Environment' not found in config. Valid environments: $validList"
        }
        Write-EnvkLog -Level 'INFO' -Message "Environment override: $Environment (schedule detection skipped)"
        return $Environment
    }

    # ---------------------------------------------------------------------------
    # 2. Collect schedule matches
    # ---------------------------------------------------------------------------

    $matchedEnvs = [System.Collections.Generic.List[PSCustomObject]]::new()
    $fallbackName = $null

    foreach ($prop in $Config.environments.PSObject.Properties) {
        $envName  = $prop.Name
        $envValue = $prop.Value

        if ($null -eq $envValue.schedule) {
            # No schedule = fallback environment.
            $fallbackName = $envName
            continue
        }

        $sched = $envValue.schedule

        # Day match: use .ToString() to produce the full English day name to avoid
        # implicit enum coercion differences between PS 5.1 and PS 7.
        $dayMatch = $sched.days -contains $Now.DayOfWeek.ToString()

        # Hour match: end-exclusive. Hour 15 does NOT match an environment with endHour=15.
        $hourMatch = $Now.Hour -ge $sched.startHour -and $Now.Hour -lt $sched.endHour

        if ($dayMatch -and $hourMatch) {
            $priority = if ($null -ne $envValue.priority) { $envValue.priority } else { 0 }
            $matchedEnvs.Add([PSCustomObject]@{
                Name     = $envName
                Priority = $priority
            })
        }
    }

    # ---------------------------------------------------------------------------
    # 3. No schedule matches
    # ---------------------------------------------------------------------------

    if ($matchedEnvs.Count -eq 0) {
        if ($null -ne $fallbackName) {
            Write-EnvkLog -Level 'INFO' -Message "No schedule match. Using fallback environment: $fallbackName"
            return $fallbackName
        }

        # No fallback either -- log next activation and return $null.
        $next = Get-NextScheduledEnv -Config $Config -Now $Now
        Write-EnvkLog -Level 'INFO' -Message "No environment matches current schedule. Next: $next"
        return $null
    }

    # ---------------------------------------------------------------------------
    # 4. Priority resolution
    # ---------------------------------------------------------------------------

    $topPriority = ($matchedEnvs | Sort-Object -Property Priority -Descending)[0].Priority
    $topMatches  = @($matchedEnvs | Where-Object { $_.Priority -eq $topPriority })

    # ---------------------------------------------------------------------------
    # 5. Single match at top priority
    # ---------------------------------------------------------------------------

    if ($topMatches.Count -eq 1) {
        $selected = $topMatches[0].Name
        Write-EnvkLog -Level 'INFO' -Message "Schedule matched: $selected"
        return $selected
    }

    # ---------------------------------------------------------------------------
    # 6. Priority conflict (multiple environments at same top priority)
    # ---------------------------------------------------------------------------

    $conflictNames = ($topMatches | ForEach-Object { $_.Name }) -join ', '
    Write-EnvkLog -Level 'WARNING' -Message "Schedule overlap: $conflictNames all match at priority $topPriority"

    if ([System.Environment]::UserInteractive -and $EnableDebug) {
        # Interactive + debug mode: prompt the user to choose.
        $choices = $topMatches | ForEach-Object {
            [System.Management.Automation.Host.ChoiceDescription]::new("&$($_.Name)", "Select the $($_.Name) environment")
        }
        $idx = $Host.UI.PromptForChoice(
            'Schedule Conflict',
            'Multiple environments match. Choose one:',
            $choices,
            0
        )
        $selected = $topMatches[$idx].Name
        Write-EnvkLog -Level 'INFO' -Message "User selected: $selected"
        return $selected
    }

    # Non-interactive or not debug mode: use first match by config order.
    $selected = $topMatches[0].Name
    Write-EnvkLog -Level 'WARNING' -Message "Non-interactive mode: using first match by config order: $selected"
    return $selected
}