from __future__ import annotations

import asyncio
import time
from collections.abc import AsyncIterator, Awaitable, Callable, Iterable
from typing import Any, TypeVar

from .logging import get_logger

T = TypeVar("T")
logger = get_logger(__name__)

SENSITIVE_KEYS = {"access_token", "authorization", "appsecret_proof", "password", "client_secret"}


def exponential_backoff(attempt: int, base: float = 0.5, factor: float = 2.0, max_delay: float = 30.0) -> float:
    """Return the delay (in seconds) for a given retry attempt."""

    attempt = max(attempt, 1)
    delay = base * (factor ** (attempt - 1))
    return min(delay, max_delay)


async def async_retry(
    operation: Callable[[], Awaitable[T]],
    *,
    retries: int = 3,
    retry_exceptions: tuple[type[Exception], ...] = (Exception,),
    before_sleep: Callable[[int, float, Exception], Awaitable[None]] | None = None,
) -> T:
    """Retry an awaitable operation with exponential backoff."""

    attempt = 0
    while True:
        try:
            return await operation()
        except retry_exceptions as exc:  # type: ignore[misc]
            attempt += 1
            if attempt > retries:
                raise
            delay = exponential_backoff(attempt)
            logger.warning(
                "retrying_operation attempt=%d delay=%.2f error=%s",
                attempt,
                delay,
                exc,
            )
            if before_sleep is not None:
                await before_sleep(attempt, delay, exc)
            await asyncio.sleep(delay)


def redact_value(value: Any) -> Any:
    """Redact sensitive values while preserving structure."""

    if value is None:
        return value
    if isinstance(value, str):
        if len(value) <= 4:
            return "***"
        return f"{value[:2]}***{value[-2:]}"
    if isinstance(value, (list, tuple)):
        return type(value)(redact_value(item) for item in value)
    if isinstance(value, dict):
        return {key: redact_value(val) for key, val in value.items()}
    return "***"


def redact_sensitive(data: dict[str, Any]) -> dict[str, Any]:
    """Return a shallow-copied dict with sensitive keys redacted."""

    redacted: dict[str, Any] = {}
    for key, value in data.items():
        if key.lower() in SENSITIVE_KEYS:
            redacted[key] = redact_value(value)
        elif isinstance(value, dict):
            redacted[key] = redact_sensitive(value)
        else:
            redacted[key] = value
    return redacted


def utc_now_ms() -> int:
    """Return current time in milliseconds since epoch."""

    return int(time.time() * 1000)


async def auto_paginate(
    fetch_page: Callable[[dict[str, Any] | None], Awaitable[dict[str, Any]]],
    *,
    cursor_key: str = "after",
) -> AsyncIterator[dict[str, Any]]:
    """Iterate over paginated Graph API responses."""

    cursor: dict[str, Any] | None = None
    while True:
        page = await fetch_page(cursor)
        yield page
        paging = page.get("paging") if isinstance(page, dict) else None
        if not paging or cursor_key not in paging.get("cursors", {}):
            break
        cursor = {cursor_key: paging["cursors"][cursor_key]}


def chunk(iterable: Iterable[T], size: int) -> Iterable[list[T]]:
    """Yield lists of a fixed size from an iterable."""

    batch: list[T] = []
    for item in iterable:
        batch.append(item)
        if len(batch) == size:
            yield batch
            batch = []
    if batch:
        yield batch
