"""Retry patterns implementation using tenacity library.

This module implements 33 retry patterns, providing async-first retry capabilities
with exponential backoff, circuit breakers, and configurable retry strategies.

Based on Phase 0 discovery: 33 retry patterns found in Go code need Python equivalents.
"""

import contextlib
import logging
import re
from collections.abc import Callable
from functools import wraps
from typing import Any, TypeVar, cast

import structlog
from tenacity import (
    AsyncRetrying,
    RetryCallState,
    retry,
    retry_if_exception_type,
    retry_if_result,
    stop_after_attempt,
    stop_after_delay,
    wait_exponential,
    wait_exponential_jitter,
    wait_fixed,
    wait_random,
)

from cleveragents.core.circuit_breaker import CircuitBreaker as CircuitBreaker
from cleveragents.core.circuit_breaker import CircuitBreakerOpen as CircuitBreakerOpen
from cleveragents.core.circuit_breaker import CircuitBreakerState as CircuitBreakerState
from cleveragents.core.exceptions import (
    DatabaseError,
    NetworkError,
    ProviderError,
    RateLimitError,
)

# Type variables for generic retry decorators
T = TypeVar("T")
P = TypeVar("P")
R = TypeVar("R")

# Sentinel for distinguishing "no result yet" from a legitimate None return.
_UNSET: Any = object()

# Logger
logger = structlog.get_logger(__name__)


# Custom callbacks for structlog-compatible logging
def log_before_retry(retry_state: RetryCallState) -> None:
    """Custom before callback for tenacity that uses structlog."""
    try:
        # Try to use structured logging with keyword arguments
        logger.debug(
            "Starting attempt",
            attempt_number=retry_state.attempt_number,
            outcome=type(retry_state.outcome).__name__
            if retry_state.outcome is not None
            else None,
        )
    except TypeError:
        # Fallback for when structlog is configured with empty processors
        # or using PrintLogger which doesn't accept keyword arguments
        msg = f"Starting attempt {retry_state.attempt_number}"
        with contextlib.suppress(Exception):
            # Silently ignore if logging is completely disabled
            logger.debug(msg)
    return None


def log_after_retry(retry_state: RetryCallState) -> None:
    """Custom after callback for tenacity that uses structlog."""
    outcome = retry_state.outcome
    if outcome is not None and outcome.failed:
        try:
            next_action = retry_state.next_action
            # Try to use structured logging with keyword arguments
            logger.debug(
                "Attempt failed, will retry",
                attempt_number=retry_state.attempt_number,
                wait_time=next_action.sleep if next_action is not None else 0,
                exception=_sanitize_error_message(outcome.exception())
                if outcome.failed
                else None,
            )
        except TypeError:
            # Fallback for when structlog is configured with empty processors
            msg = f"Attempt {retry_state.attempt_number} failed, will retry"
            with contextlib.suppress(Exception):
                # Silently ignore if logging is completely disabled
                logger.debug(msg)
    elif retry_state.attempt_number > 1:
        # Only log success if there were previous retries
        try:
            logger.debug(
                "Attempt succeeded",
                attempt_number=retry_state.attempt_number,
            )
        except TypeError:
            msg = f"Attempt {retry_state.attempt_number} succeeded"
            with contextlib.suppress(Exception):
                logger.debug(msg)
    return None


# Default retry configurations based on discovery patterns
DEFAULT_MAX_ATTEMPTS = 3
DEFAULT_MIN_WAIT = 1  # seconds
DEFAULT_MAX_WAIT = 60  # seconds
DEFAULT_EXPONENTIAL_BASE = 2

# Categories of retry patterns from discovery
RETRY_CATEGORIES: dict[str, dict[str, Any]] = {
    "network": {
        "max_attempts": 5,
        "wait": wait_exponential(multiplier=1, min=1, max=30),
        "exceptions": (NetworkError, ConnectionError, TimeoutError),
    },
    "provider": {
        "max_attempts": 3,
        "wait": wait_exponential_jitter(max=60),
        "exceptions": (ProviderError, RateLimitError),
    },
    "database": {
        "max_attempts": 3,
        "wait": wait_fixed(0.5),
        "exceptions": (DatabaseError,),
    },
    "file_operation": {
        "max_attempts": 3,
        "wait": wait_random(min=0.1, max=1),
        "exceptions": (IOError, OSError),
    },
}


def should_retry_result(result: Any) -> bool:
    """Determine if a result should trigger a retry."""
    if result is None:
        return False
    if isinstance(result, dict):
        # Check for error indicators in response
        error_val: Any = result.get("error")
        retry_val: Any = result.get("retry")
        if error_val or retry_val:
            return True
        status_code: Any = result.get("status_code", 200)
        if isinstance(status_code, int) and status_code >= 500:
            return True
    return False


# Basic retry decorator with exponential backoff
def retry_with_exponential_backoff(
    max_attempts: int = DEFAULT_MAX_ATTEMPTS,
    min_wait: float = DEFAULT_MIN_WAIT,
    max_wait: float = DEFAULT_MAX_WAIT,
    multiplier: float = DEFAULT_EXPONENTIAL_BASE,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Retry decorator with exponential backoff.

    Implements the most common retry pattern found in discovery:
    - 33 instances of exponential backoff patterns
    - Used for API calls, network operations, and provider interactions
    """
    return cast(
        Callable[[Callable[..., T]], Callable[..., T]],
        retry(
            stop=stop_after_attempt(max_attempts),
            wait=wait_exponential(multiplier=multiplier, min=min_wait, max=max_wait),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        ),
    )


# Async retry decorator with exponential backoff
def async_retry_with_exponential_backoff(
    max_attempts: int = DEFAULT_MAX_ATTEMPTS,
    min_wait: float = DEFAULT_MIN_WAIT,
    max_wait: float = DEFAULT_MAX_WAIT,
    multiplier: float = DEFAULT_EXPONENTIAL_BASE,
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
    """Async retry decorator with exponential backoff."""

    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        @wraps(func)
        async def wrapper(*args: Any, **kwargs: Any) -> Any:
            async for attempt in AsyncRetrying(
                stop=stop_after_attempt(max_attempts),
                wait=wait_exponential(
                    multiplier=multiplier, min=min_wait, max=max_wait
                ),
                before=log_before_retry,
                after=log_after_retry,
                reraise=True,
            ):
                with attempt:
                    return await func(*args, **kwargs)

        return wrapper

    return decorator


# Category-specific retry decorators
def retry_network_operation[T](func: Callable[..., T]) -> Callable[..., T]:
    """Retry decorator for network operations."""
    config = RETRY_CATEGORIES["network"]
    decorated: Callable[..., T] = cast(
        Callable[..., T],
        retry(
            stop=stop_after_attempt(cast(int, config["max_attempts"])),
            wait=config["wait"],
            retry=retry_if_exception_type(
                cast(tuple[type[BaseException], ...], config["exceptions"])
            ),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        )(func),
    )
    return decorated


def retry_provider_operation[T](func: Callable[..., T]) -> Callable[..., T]:
    """Retry decorator for AI provider operations."""
    config = RETRY_CATEGORIES["provider"]
    decorated: Callable[..., T] = cast(
        Callable[..., T],
        retry(
            stop=stop_after_attempt(cast(int, config["max_attempts"])),
            wait=config["wait"],
            retry=retry_if_exception_type(
                cast(tuple[type[BaseException], ...], config["exceptions"])
            ),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        )(func),
    )
    return decorated


def retry_database_operation[T](func: Callable[..., T]) -> Callable[..., T]:
    """Retry decorator for database operations."""
    config = RETRY_CATEGORIES["database"]
    decorated: Callable[..., T] = cast(
        Callable[..., T],
        retry(
            stop=stop_after_attempt(cast(int, config["max_attempts"])),
            wait=config["wait"],
            retry=retry_if_exception_type(
                cast(tuple[type[BaseException], ...], config["exceptions"])
            ),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        )(func),
    )
    return decorated


def retry_file_operation[T](func: Callable[..., T]) -> Callable[..., T]:
    """Retry decorator for file operations."""
    config = RETRY_CATEGORIES["file_operation"]
    decorated: Callable[..., T] = cast(
        Callable[..., T],
        retry(
            stop=stop_after_attempt(cast(int, config["max_attempts"])),
            wait=config["wait"],
            retry=retry_if_exception_type(
                cast(tuple[type[BaseException], ...], config["exceptions"])
            ),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        )(func),
    )
    return decorated


# Retry with jitter for distributed systems
def retry_with_jitter(
    max_attempts: int = DEFAULT_MAX_ATTEMPTS,
    base_delay: float = 1.0,
    max_delay: float = 60.0,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Retry with exponential backoff and jitter.

    Adds random jitter to prevent thundering herd problem
    in distributed systems.
    """
    return cast(
        Callable[[Callable[..., T]], Callable[..., T]],
        retry(
            stop=stop_after_attempt(max_attempts),
            wait=wait_exponential_jitter(
                initial=base_delay, max=max_delay, jitter=base_delay
            ),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        ),
    )


# Specialized retry patterns from discovery
def retry_with_timeout(
    max_attempts: int = DEFAULT_MAX_ATTEMPTS,
    timeout_seconds: float = 300.0,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Retry with overall timeout limit."""
    return cast(
        Callable[[Callable[..., T]], Callable[..., T]],
        retry(
            stop=(stop_after_attempt(max_attempts) | stop_after_delay(timeout_seconds)),
            wait=wait_exponential(),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        ),
    )


def retry_on_result(
    predicate: Callable[[Any], bool],
    max_attempts: int = DEFAULT_MAX_ATTEMPTS,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Retry based on result value."""
    return cast(
        Callable[[Callable[..., T]], Callable[..., T]],
        retry(
            stop=stop_after_attempt(max_attempts),
            retry=retry_if_result(predicate),
            wait=wait_exponential(),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        ),
    )


_CRED_URL_RE = re.compile(r"://[^@\s]+@")
_SECRET_RE = re.compile(
    r"(?i)"
    r"(api[_-]?key|token|secret|password|credential|private[_-]?key"
    r"|connection[_-]?string|access[_-]?key|bearer|session[_-]?id"
    r"|auth[_-]?token|refresh[_-]?token|client[_-]?secret)"
    r"\s*[=:]\s*"
    r"(?:"
    r"'[^']*'"  # single-quoted value
    r"|\"[^\"]*\""  # double-quoted value
    r"|[^\s,;'\"})\]]+"  # unquoted value (stop at delimiters)
    r")"
)
_AUTH_HEADER_RE = re.compile(
    r"(?i)(authorization|x-api-key)\s*[:=]\s*"
    r"(?:'[^']*'|\"[^\"]*\"|[^\s,;'\"})\]]+)"
    r"(?:\s+(?:'[^']*'|\"[^\"]*\"|[^\s,;'\"})\]]+))?"
)
_DICT_SECRET_RE = re.compile(
    r"(?i)"
    r"(['\"](?:api[_-]?key|token|secret|password|credential|private[_-]?key"
    r"|connection[_-]?string|access[_-]?key|bearer|session[_-]?id"
    r"|auth[_-]?token|refresh[_-]?token|client[_-]?secret)['\"])"
    r"\s*:\s*"
    r"(?:'[^']*'|\"[^\"]*\"|[^\s,;'\"})\]]+)"
)


def _sanitize_error_message(error: BaseException | None) -> str | None:
    """Return a sanitized error description safe for logging.

    Redacts patterns that commonly contain secrets (URLs with
    embedded credentials, API key assignments) and truncates overly
    long messages.
    """
    if error is None:
        return None
    msg = str(error)
    # Pre-truncate before running regexes to bound CPU time (P2-26).
    _MAX_SANITIZE_INPUT = 2000
    if len(msg) > _MAX_SANITIZE_INPUT:
        msg = msg[:_MAX_SANITIZE_INPUT]
    msg = _CRED_URL_RE.sub("://***@", msg)
    msg = _SECRET_RE.sub(r"\1=***", msg)
    msg = _AUTH_HEADER_RE.sub(r"\1: ***", msg)
    msg = _DICT_SECRET_RE.sub(r"\1: '***'", msg)
    if len(msg) > 200:
        msg = msg[:200] + "..."
    return msg


# Utility functions
def get_retry_decorator(
    category: str,
) -> Callable[[Callable[..., T]], Callable[..., T]]:
    """Get appropriate retry decorator for a category."""
    if category not in RETRY_CATEGORIES:
        return retry_with_exponential_backoff()

    config = RETRY_CATEGORIES[category]
    return cast(
        Callable[[Callable[..., T]], Callable[..., T]],
        retry(
            stop=stop_after_attempt(cast(int, config["max_attempts"])),
            wait=config["wait"],
            retry=retry_if_exception_type(
                cast(tuple[type[BaseException], ...], config["exceptions"])
            ),
            before=log_before_retry,
            after=log_after_retry,
            reraise=True,
        ),
    )


def configure_retry_logging(log_level: int = logging.INFO) -> None:
    """Configure retry logging globally."""
    logging.getLogger("tenacity.before").setLevel(log_level)
    logging.getLogger("tenacity.after").setLevel(log_level)


# Service-level retry wiring (M6 — #313) — extracted to
# ``retry_service_patterns`` for the 500-line file limit.
# Re-export all public names so existing call-sites keep working.
from cleveragents.core.retry_service_patterns import (
    _MAX_RETRY_NESTING_DEPTH as _MAX_RETRY_NESTING_DEPTH,
)
from cleveragents.core.retry_service_patterns import (
    RetryContext as RetryContext,
)
from cleveragents.core.retry_service_patterns import (
    _build_wait_strategy as _build_wait_strategy,
)
from cleveragents.core.retry_service_patterns import (
    _is_async_callable as _is_async_callable,
)
from cleveragents.core.retry_service_patterns import (
    _log_circuit_open as _log_circuit_open,
)
from cleveragents.core.retry_service_patterns import (
    _log_service_retry_attempt as _log_service_retry_attempt,
)
from cleveragents.core.retry_service_patterns import (
    _retry_depth as _retry_depth,
)
from cleveragents.core.retry_service_patterns import (
    is_read_only_plan_operation as is_read_only_plan_operation,
)
from cleveragents.core.retry_service_patterns import (
    retry_auto_debug as retry_auto_debug,
)
from cleveragents.core.retry_service_patterns import (
    retry_service_operation as retry_service_operation,
)

__all__ = [
    "DEFAULT_MAX_ATTEMPTS",
    "DEFAULT_MAX_WAIT",
    "DEFAULT_MIN_WAIT",
    "RETRY_CATEGORIES",
    "CircuitBreaker",
    "CircuitBreakerOpen",
    "CircuitBreakerState",
    "RetryContext",
    "async_retry_with_exponential_backoff",
    "configure_retry_logging",
    "get_retry_decorator",
    "is_read_only_plan_operation",
    "retry_auto_debug",
    "retry_database_operation",
    "retry_file_operation",
    "retry_network_operation",
    "retry_on_result",
    "retry_provider_operation",
    "retry_service_operation",
    "retry_with_exponential_backoff",
    "retry_with_jitter",
    "retry_with_timeout",
    "should_retry_result",
]
