"""
Retry utilities for groknroll.

Provides retry logic for LLM API calls with exponential backoff.
"""

import asyncio
import re
from dataclasses import dataclass
from datetime import datetime
from email.utils import parsedate_to_datetime
from typing import Any, Optional


@dataclass
class RetryableError:
    """An error that can be retried."""

    message: str
    is_retryable: bool = True
    response_headers: Optional[dict[str, str]] = None
    metadata: Optional[dict[str, Any]] = None


class SessionRetry:
    """
    Session retry logic with exponential backoff.

    Handles retry delays with support for:
    - Exponential backoff
    - retry-after headers (seconds and HTTP-date formats)
    - retry-after-ms headers
    - Maximum delay caps

    Example:
        error = RetryableError("Rate limited", response_headers={"retry-after": "30"})
        delay = SessionRetry.delay(attempt=3, error=error)
        await SessionRetry.sleep(delay)
    """

    # Maximum delay without retry-after header (30 seconds)
    MAX_DELAY_DEFAULT = 30_000

    # Base delay for exponential backoff (2 seconds)
    BASE_DELAY = 2_000

    # Maximum safe delay (avoid 32-bit overflow in setTimeout)
    MAX_SAFE_DELAY = 2_147_483_647  # Max 32-bit signed int

    @classmethod
    def delay(cls, attempt: int, error: Optional[RetryableError] = None) -> int:
        """
        Calculate retry delay in milliseconds.

        Uses exponential backoff with optional retry-after header hints.

        Args:
            attempt: The retry attempt number (1-indexed)
            error: The error containing optional response headers

        Returns:
            Delay in milliseconds before next retry
        """
        # Calculate exponential backoff: 2^attempt * 1000ms
        exponential_delay = cls.BASE_DELAY * (2 ** (attempt - 1))

        # Cap at maximum default delay
        exponential_delay = min(exponential_delay, cls.MAX_DELAY_DEFAULT)

        # Check for retry-after hints in headers
        if error and error.response_headers:
            headers = error.response_headers

            # Check retry-after-ms (milliseconds)
            # OpenCode uses parseFloat, so we support decimal values
            if "retry-after-ms" in headers:
                try:
                    ms_delay = int(float(headers["retry-after-ms"]))
                    if ms_delay > 0:
                        return min(ms_delay, cls.MAX_SAFE_DELAY)
                except ValueError:
                    pass

            # Check retry-after (seconds or HTTP-date)
            # OpenCode uses parseFloat for seconds, supports decimals
            if "retry-after" in headers:
                retry_after = headers["retry-after"]

                # Try parsing as seconds (float for OpenCode compatibility)
                try:
                    seconds_delay = float(retry_after)
                    if seconds_delay > 0:
                        return min(int(seconds_delay * 1000), cls.MAX_SAFE_DELAY)
                except ValueError:
                    pass

                # Try parsing as HTTP-date
                try:
                    target_time = parsedate_to_datetime(retry_after)
                    now = datetime.now(target_time.tzinfo)
                    delta_ms = int((target_time - now).total_seconds() * 1000)
                    if delta_ms > 0:
                        return min(delta_ms, cls.MAX_SAFE_DELAY)
                except (ValueError, TypeError):
                    pass

        return exponential_delay

    @classmethod
    async def sleep(cls, delay_ms: int, signal: Optional[asyncio.Event] = None) -> bool:
        """
        Sleep for the specified delay with optional cancellation.

        Args:
            delay_ms: Delay in milliseconds
            signal: Optional event to abort the sleep

        Returns:
            True if sleep completed, False if aborted
        """
        # Cap delay to prevent overflow
        delay_ms = min(delay_ms, cls.MAX_SAFE_DELAY)
        delay_seconds = delay_ms / 1000

        if signal is None:
            await asyncio.sleep(delay_seconds)
            return True

        try:
            # Wait for either the delay or the signal
            done, pending = await asyncio.wait(
                [
                    asyncio.create_task(asyncio.sleep(delay_seconds)),
                    asyncio.create_task(signal.wait()),
                ],
                return_when=asyncio.FIRST_COMPLETED,
            )

            # Cancel pending tasks
            for task in pending:
                task.cancel()

            # Check if we completed the sleep or were aborted
            return not signal.is_set()

        except asyncio.CancelledError:
            return False

    @classmethod
    def retryable(cls, error: RetryableError) -> Optional[str]:
        """
        Check if an error is retryable and return its message.

        Args:
            error: The error to check

        Returns:
            Error message if retryable, None otherwise
        """
        if not error.is_retryable:
            return None

        # Check for connection reset
        if error.metadata and error.metadata.get("code") == "ECONNRESET":
            return error.message

        # Default retryable check
        if error.is_retryable:
            return error.message

        return None

    @classmethod
    def is_rate_limit_error(cls, error: RetryableError) -> bool:
        """
        Check if error is a rate limit error.

        Args:
            error: The error to check

        Returns:
            True if this is a rate limit error
        """
        message_lower = error.message.lower()
        return (
            "rate limit" in message_lower
            or "too many requests" in message_lower
            or "429" in error.message
        )

    @classmethod
    def is_overloaded_error(cls, error: RetryableError) -> bool:
        """
        Check if error indicates server overload.

        Args:
            error: The error to check

        Returns:
            True if server is overloaded
        """
        message_lower = error.message.lower()
        return (
            "overloaded" in message_lower
            or "503" in error.message
            or "service unavailable" in message_lower
        )
