"""Shared retry utility for data providers."""
from __future__ import annotations

import logging
import random
import time
from dataclasses import dataclass, field
from datetime import datetime
from typing import Optional

import requests

logger = logging.getLogger(__name__)


@dataclass
class RetryConfig:
    """Configuration for request retry behaviour."""
    max_retries: int = 3
    base_delay: float = 3.0
    max_delay: float = 60.0
    jitter: float = 0.25  # ±25% random jitter
    timeout: int = 60
    retry_on_status: list[int] = field(
        default_factory=lambda: [429, 500, 502, 503, 504]
    )


def calculate_backoff(attempt: int, config: RetryConfig,
                      retry_after: float | None = None) -> float:
    """Calculate delay with exponential backoff + jitter.

    Args:
        attempt: 0-based attempt number (0 = first retry)
        config: Retry configuration
        retry_after: Optional Retry-After value from server (seconds)

    Returns:
        Delay in seconds
    """
    if retry_after is not None and retry_after > 0:
        base = retry_after
    else:
        base = config.base_delay * (2 ** attempt)

    # Cap at max_delay
    base = min(base, config.max_delay)

    # Apply jitter: ± (base * jitter_factor)
    jitter_range = base * config.jitter
    delay = base + random.uniform(-jitter_range, jitter_range)

    return max(0.1, delay)


def parse_retry_after(response: requests.Response) -> float | None:
    """Parse Retry-After header from response.

    Supports:
    - Integer seconds: "120"
    - HTTP-date: "Thu, 01 Dec 2025 16:00:00 GMT"

    Returns:
        Seconds to wait, or None if header not present/parseable
    """
    header = response.headers.get('Retry-After')
    if not header:
        return None

    # Try integer seconds first
    try:
        return float(header)
    except ValueError:
        pass

    # Try HTTP-date
    try:
        from email.utils import parsedate_to_datetime
        target = parsedate_to_datetime(header)
        delta = (target - datetime.now(target.tzinfo)).total_seconds()
        return max(0, delta)
    except Exception:
        pass

    return None


def request_with_retry(
    session: requests.Session,
    url: str,
    params: dict | None = None,
    config: RetryConfig | None = None,
    rate_delay: float = 0.0,
    provider_name: str = 'unknown',
) -> dict | list | None:
    """Make an HTTP GET request with retry logic.

    Args:
        session: requests.Session instance
        url: URL to request
        params: Query parameters
        config: Retry configuration (uses defaults if None)
        rate_delay: Seconds to sleep after a successful request
        provider_name: Name for logging (e.g. 'coingecko', 'indodax')

    Returns:
        Parsed JSON response, or None if all retries exhausted
    """
    if config is None:
        config = RetryConfig()

    last_exception = None

    for attempt in range(config.max_retries + 1):  # 0 = initial, 1..N = retries
        try:
            resp = session.get(url, params=params, timeout=config.timeout)

            # Success
            if resp.status_code < 400:
                if rate_delay > 0:
                    time.sleep(rate_delay)
                return resp.json()

            # Retryable status
            if resp.status_code in config.retry_on_status:
                retry_after = parse_retry_after(resp) if resp.status_code == 429 else None
                delay = calculate_backoff(attempt, config, retry_after)

                if attempt < config.max_retries:
                    ra_info = f' (Retry-After: {retry_after:.0f}s)' if retry_after else ''
                    logger.warning(
                        '[%s] HTTP %d on %s%s — retry %d/%d in %.1fs',
                        provider_name, resp.status_code, _short_url(url),
                        ra_info, attempt + 1, config.max_retries, delay,
                    )
                    time.sleep(delay)
                    continue
                else:
                    logger.error(
                        '[%s] HTTP %d on %s — all %d retries exhausted',
                        provider_name, resp.status_code, _short_url(url),
                        config.max_retries,
                    )
                    return None

            # Non-retryable HTTP error
            logger.error(
                '[%s] HTTP %d on %s — not retryable',
                provider_name, resp.status_code, _short_url(url),
            )
            return None

        except requests.exceptions.RequestException as e:
            last_exception = e
            if attempt < config.max_retries:
                delay = calculate_backoff(attempt, config)
                logger.warning(
                    '[%s] Network error on %s: %s — retry %d/%d in %.1fs',
                    provider_name, _short_url(url), str(e)[:80],
                    attempt + 1, config.max_retries, delay,
                )
                time.sleep(delay)
            else:
                logger.error(
                    '[%s] Network error on %s: %s — all %d retries exhausted',
                    provider_name, _short_url(url), str(e)[:80],
                    config.max_retries,
                )
                return None

    return None


def _short_url(url: str) -> str:
    """Shorten URL for logging (remove base domain, keep path)."""
    try:
        from urllib.parse import urlparse
        parsed = urlparse(url)
        return parsed.path[:60]
    except Exception:
        return url[:60]
