workflows/default/systems/runtime/modules/rate-limit-handler.ps1

function Get-RateLimitResetTime {
    <#
    .SYNOPSIS
    Parses a rate limit message and extracts the reset time
    
    .PARAMETER Message
    The rate limit message to parse (e.g., "You've hit your limit · resets 10pm (Europe/Berlin)")
    
    .OUTPUTS
    Hashtable with parsed info or $null if not a rate limit message
    #>

    param(
        [Parameter(Mandatory = $true)]
        [string]$Message
    )
    
    # Pattern: "resets 10pm (Europe/Berlin)" or "resets 10:30pm (Europe/Berlin)"
    if ($Message -match "resets?\s+(\d{1,2}):?(\d{2})?\s*(am|pm)\s*\(([^)]+)\)") {
        $hour = [int]$matches[1]
        $minute = if ($matches[2]) { [int]$matches[2] } else { 0 }
        $ampm = $matches[3].ToLower()
        $timezone = $matches[4]
        
        # Convert to 24-hour format
        if ($ampm -eq "pm" -and $hour -ne 12) {
            $hour += 12
        } elseif ($ampm -eq "am" -and $hour -eq 12) {
            $hour = 0
        }
        
        # Try to map timezone to .NET timezone
        $tzMap = @{
            "Europe/Berlin" = "Central European Standard Time"
            "Europe/London" = "GMT Standard Time"
            "America/New_York" = "Eastern Standard Time"
            "America/Los_Angeles" = "Pacific Standard Time"
            "UTC" = "UTC"
        }
        
        $dotnetTz = $tzMap[$timezone]
        if (-not $dotnetTz) {
            # Fallback: assume local timezone
            $dotnetTz = [TimeZoneInfo]::Local.Id
        }
        
        try {
            $tz = [TimeZoneInfo]::FindSystemTimeZoneById($dotnetTz)
            $now = [DateTimeOffset]::Now
            $nowInTz = [TimeZoneInfo]::ConvertTime($now, $tz)
            
            # Build reset time in the target timezone for today
            $resetInTz = [DateTime]::new($nowInTz.Year, $nowInTz.Month, $nowInTz.Day, $hour, $minute, 0)
            
            # If reset time is in the past, it's for tomorrow
            if ($resetInTz -lt $nowInTz.DateTime) {
                $resetInTz = $resetInTz.AddDays(1)
            }
            
            # Convert back to local time
            $resetOffset = [DateTimeOffset]::new($resetInTz, $tz.GetUtcOffset($resetInTz))
            $resetLocal = $resetOffset.ToLocalTime()
            
            # Calculate wait seconds + 1 minute buffer
            $waitSeconds = [int]($resetLocal - [DateTimeOffset]::Now).TotalSeconds + 60
            
            if ($waitSeconds -lt 0) {
                $waitSeconds = 60  # Minimum 1 minute wait
            }
            
            return @{
                reset_time = $resetLocal.DateTime
                wait_seconds = $waitSeconds
                timezone = $timezone
                original_message = $Message
            }
        } catch {
            # If timezone conversion fails, wait 15 minutes as fallback
            return @{
                reset_time = (Get-Date).AddMinutes(15)
                wait_seconds = 900
                timezone = $timezone
                original_message = $Message
                parse_error = $_.Exception.Message
            }
        }
    }
    
    # Check for simpler "hit your limit" patterns without specific time
    if ($Message -match "hit your limit|rate.?limit|too many requests|quota exceeded") {
        return @{
            reset_time = (Get-Date).AddMinutes(15)
            wait_seconds = 900
            timezone = "unknown"
            original_message = $Message
            fallback = $true
        }
    }
    
    return $null
}