Private/RoleManagement/Resolve-PIMActivationSchedule.ps1

function Resolve-PIMActivationSchedule {
    <#
    .SYNOPSIS
        Resolves and validates a PIM activation start time.
 
    .DESCRIPTION
        Converts a requested local activation start time to UTC and calculates the scheduling
        window for the selected roles when eligibility start and end metadata is available.
        The returned object is used by the activation dialog and request builders so regular
        activations and activation-profile launches use the same scheduling rules.
 
    .PARAMETER RoleItems
        The selected role ListView items or role objects. When a ListView item is supplied, its
        Tag property is used as the role data source.
 
    .PARAMETER RequestedDuration
        Hashtable containing the requested activation duration. Supports Hours, Minutes, and
        TotalMinutes keys.
 
    .PARAMETER ScheduleStartTime
        The local date and time selected for the activation start.
 
    .PARAMETER Scheduled
        When specified, validates ScheduleStartTime as a future scheduled activation. When omitted,
        the function returns the current schedule window without requiring a future start.
 
    .EXAMPLE
        Resolve-PIMActivationSchedule -RoleItems $checkedItems -RequestedDuration $duration -ScheduleStartTime $start -Scheduled
 
        Validates a requested scheduled start time and returns UTC request metadata.
 
    .OUTPUTS
        PSCustomObject
        Returns IsValid, ErrorMessage, StartLocal, StartUtc, StartUtcString, MinStartLocal,
        MaxStartLocal, and MaxStartRoleName properties.
    #>

    [CmdletBinding()]
    param(
        [object[]]$RoleItems = @(),

        [hashtable]$RequestedDuration,

        [datetime]$ScheduleStartTime,

        [switch]$Scheduled
    )

    $nowLocal = Get-Date
    $nowUtc = $nowLocal.ToUniversalTime()
    $requestedTotalMinutes = 480

    if ($RequestedDuration) {
        if ($RequestedDuration.ContainsKey('TotalMinutes') -and $null -ne $RequestedDuration.TotalMinutes) {
            $requestedTotalMinutes = [int]$RequestedDuration.TotalMinutes
        }
        else {
            $hours = if ($RequestedDuration.ContainsKey('Hours') -and $null -ne $RequestedDuration.Hours) { [int]$RequestedDuration.Hours } else { 8 }
            $minutes = if ($RequestedDuration.ContainsKey('Minutes') -and $null -ne $RequestedDuration.Minutes) { [int]$RequestedDuration.Minutes } else { 0 }
            $requestedTotalMinutes = ($hours * 60) + $minutes
        }
    }

    $toUtc = {
        param($Value)

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

        try {
            if ($Value -is [System.DateTimeOffset]) {
                return $Value.UtcDateTime
            }

            if ($Value -is [datetime]) {
                $dateTimeValue = [datetime]$Value
                if ($dateTimeValue.Kind -eq [System.DateTimeKind]::Unspecified) {
                    $dateTimeValue = [datetime]::SpecifyKind($dateTimeValue, [System.DateTimeKind]::Local)
                }
                return $dateTimeValue.ToUniversalTime()
            }

            $offsetValue = [System.DateTimeOffset]::Parse(
                [string]$Value,
                [System.Globalization.CultureInfo]::InvariantCulture,
                [System.Globalization.DateTimeStyles]::AssumeUniversal
            )
            return $offsetValue.UtcDateTime
        }
        catch {
            return $null
        }
    }

    $getRoleData = {
        param($Item)

        if (-not $Item) { return $null }
        if ($Item.GetType().FullName -eq 'System.Windows.Forms.ListViewItem' -and $Item.PSObject.Properties['Tag']) {
            return $Item.Tag
        }
        if ($Item.PSObject.Properties['Tag'] -and $Item.Tag) {
            return $Item.Tag
        }
        return $Item
    }

    $getRoleName = {
        param($RoleData)

        if (-not $RoleData) { return 'Selected role' }
        if ($RoleData.PSObject.Properties['DisplayName'] -and $RoleData.DisplayName) { return [string]$RoleData.DisplayName }
        if ($RoleData.PSObject.Properties['Name'] -and $RoleData.Name) { return [string]$RoleData.Name }
        if ($RoleData.PSObject.Properties['RoleName'] -and $RoleData.RoleName) { return [string]$RoleData.RoleName }
        return 'Selected role'
    }

    $getDateCandidate = {
        param(
            $RoleData,
            [string]$PropertyName,
            [string]$NestedPropertyName
        )

        if (-not $RoleData) { return $null }
        if ($RoleData.PSObject.Properties[$PropertyName] -and $RoleData.$PropertyName) {
            return $RoleData.$PropertyName
        }

        if ($RoleData.PSObject.Properties['Assignment'] -and $RoleData.Assignment) {
            $assignment = $RoleData.Assignment
            if ($assignment.PSObject.Properties[$PropertyName] -and $assignment.$PropertyName) {
                return $assignment.$PropertyName
            }
            if ($assignment.PSObject.Properties['properties'] -and $assignment.properties) {
                $properties = $assignment.properties
                if ($properties.PSObject.Properties[$NestedPropertyName] -and $properties.$NestedPropertyName) {
                    return $properties.$NestedPropertyName
                }
            }
        }

        return $null
    }

    $minStartUtc = $nowUtc
    $maxStartUtc = $null
    $maxStartRoleName = $null

    foreach ($item in @($RoleItems)) {
        $roleData = & $getRoleData $item
        if (-not $roleData) { continue }

        $roleName = & $getRoleName $roleData
        $roleStartUtc = & $toUtc (& $getDateCandidate $roleData 'StartDateTime' 'startDateTime')
        $roleEndUtc = & $toUtc (& $getDateCandidate $roleData 'EndDateTime' 'endDateTime')

        if ($roleStartUtc -and $roleStartUtc -gt $minStartUtc) {
            $minStartUtc = $roleStartUtc
        }

        if ($roleEndUtc) {
            $maxDurationHours = 8
            if ($roleData.PSObject.Properties['PolicyInfo'] -and $roleData.PolicyInfo -and $roleData.PolicyInfo.PSObject.Properties['MaxDuration'] -and $roleData.PolicyInfo.MaxDuration) {
                try { $maxDurationHours = [int]$roleData.PolicyInfo.MaxDuration } catch { $maxDurationHours = 8 }
            }

            $effectiveDuration = Get-EffectiveDuration -RequestedMinutes $requestedTotalMinutes -MaxDurationHours $maxDurationHours
            $effectiveMinutes = if ($effectiveDuration.ContainsKey('TotalMinutes')) { [int]$effectiveDuration.TotalMinutes } else { ([int]$effectiveDuration.Hours * 60) + [int]$effectiveDuration.Minutes }
            $roleMaxStartUtc = $roleEndUtc.AddMinutes(-1 * $effectiveMinutes)

            if (-not $maxStartUtc -or $roleMaxStartUtc -lt $maxStartUtc) {
                $maxStartUtc = $roleMaxStartUtc
                $maxStartRoleName = $roleName
            }
        }
    }

    $selectedLocal = if ($PSBoundParameters.ContainsKey('ScheduleStartTime')) { [datetime]$ScheduleStartTime } else { $nowLocal }
    if ($selectedLocal.Kind -eq [System.DateTimeKind]::Unspecified) {
        $selectedLocal = [datetime]::SpecifyKind($selectedLocal, [System.DateTimeKind]::Local)
    }
    else {
        $selectedLocal = $selectedLocal.ToLocalTime()
    }

    $selectedUtc = $selectedLocal.ToUniversalTime()
    $isValid = $true
    $errorMessage = ''

    if ($Scheduled) {
        if ($selectedUtc -lt $nowUtc.AddMinutes(-1)) {
            $isValid = $false
            $errorMessage = 'Scheduled activation start time cannot be in the past.'
        }
        elseif ($selectedUtc -lt $minStartUtc.AddMinutes(-1)) {
            $isValid = $false
            $errorMessage = "Scheduled activation start time is before the selected role eligibility window opens. Earliest start: $($minStartUtc.ToLocalTime().ToString('yyyy-MM-dd HH:mm'))."
        }
        elseif ($maxStartUtc -and $selectedUtc -gt $maxStartUtc) {
            $isValid = $false
            $roleSuffix = if ($maxStartRoleName) { " for '$maxStartRoleName'" } else { '' }
            $errorMessage = "Scheduled activation start time is outside the selected role eligibility window$roleSuffix. Latest start: $($maxStartUtc.ToLocalTime().ToString('yyyy-MM-dd HH:mm'))."
        }
    }

    [PSCustomObject]@{
        IsScheduled      = [bool]$Scheduled
        IsValid          = $isValid
        ErrorMessage     = $errorMessage
        StartLocal       = $selectedLocal
        StartUtc         = $selectedUtc
        StartUtcString   = $selectedUtc.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'")
        MinStartLocal    = $minStartUtc.ToLocalTime()
        MaxStartLocal    = if ($maxStartUtc) { $maxStartUtc.ToLocalTime() } else { $null }
        MaxStartRoleName = $maxStartRoleName
    }
}