"""Process a batch of items with per-item exponential backoff retry."""

import time
import random
from typing import Callable, TypeVar

T = TypeVar("T")
R = TypeVar("R")


def call_batch_with_retry(
    items: list,
    process_func: Callable,
    max_retries: int = 3,
    initial_delay: float = 1.0,
    max_delay: float = 30.0,
    backoff_factor: float = 2.0,
    exceptions: tuple = (Exception,),
    continue_on_failure: bool = True,
) -> tuple:
    """Process a list of items with independent per-item retry and exponential backoff.

    Each item is processed by process_func. If it raises one of the specified
    exceptions, it is retried up to max_retries times with exponential backoff.
    If all retries are exhausted, the item is recorded as a failure.

    Args:
        items: List of items to process.
        process_func: Callable that takes a single item and returns a result.
        max_retries: Maximum number of retry attempts per item after first failure.
        initial_delay: Initial delay in seconds before the first retry.
        max_delay: Maximum delay cap in seconds between retries.
        backoff_factor: Multiplier applied to delay on each successive retry.
        exceptions: Tuple of exception types to catch and retry on.
        continue_on_failure: If True, continue processing remaining items when an
            item exhausts all retries. If False, re-raise the exception immediately.

    Returns:
        A tuple (results, failures) where:
        - results is a list of successful return values from process_func.
        - failures is a list of dicts with keys "index", "item", and "error"
          for each item that failed after all retries.

    Raises:
        Exception: The last exception for a failed item when continue_on_failure
            is False.
    """
    results = []
    failures = []

    for index, item in enumerate(items):
        last_exc = None
        succeeded = False

        for attempt in range(max_retries + 1):
            try:
                result = process_func(item)
                results.append(result)
                succeeded = True
                break
            except exceptions as exc:
                last_exc = exc
                if attempt < max_retries:
                    delay = min(
                        initial_delay * (backoff_factor ** attempt),
                        max_delay,
                    )
                    # Add small jitter (up to 10% of delay) to avoid thundering herd
                    delay += random.uniform(0, delay * 0.1)
                    time.sleep(delay)

        if not succeeded:
            if not continue_on_failure:
                raise last_exc
            failures.append({
                "index": index,
                "item": item,
                "error": str(last_exc),
            })

    return results, failures
