"""
Error Recovery Pipeline — Self-healing LLM error handling for report_agent.

Adapted from hermes-agent-repo/agent/error_classifier.py. Provides automatic
retry with exponential backoff, context compression triggers, and structured
logging for the report generation pipeline.

Usage:
    from agent.report_agent.error_recovery import with_recovery, RetryPolicy

    # Simple usage — wraps any async LLM call
    result = await with_recovery(
        call_fn=lambda: call_llm(messages),
        provider="codex",
    )

    # Custom policy
    policy = RetryPolicy(max_retries=5, compress_on_overflow=True)
    result = await with_recovery(call_fn, policy=policy)
"""

from __future__ import annotations

import asyncio
import logging
import random
import time
from dataclasses import dataclass, field
from typing import Any, Awaitable, Callable, Dict, List, Optional

from agent.report_agent.hermes_bridge import (
    ClassifiedError,
    FailoverReason,
    classify_error,
)

logger = logging.getLogger(__name__)


@dataclass
class RetryPolicy:
    """Configuration for the retry/recovery pipeline."""
    max_retries: int = 3
    base_backoff_seconds: float = 1.0
    max_backoff_seconds: float = 30.0
    jitter_fraction: float = 0.3
    compress_on_overflow: bool = True
    log_recoveries: bool = True


@dataclass
class RecoveryResult:
    """Outcome of a recovery-wrapped call."""
    success: bool
    value: Any = None
    attempts: int = 1
    errors: List[ClassifiedError] = field(default_factory=list)
    compressions_triggered: int = 0
    total_backoff_seconds: float = 0.0


def _compute_backoff(
    attempt: int,
    base: float,
    max_backoff: float,
    jitter: float,
) -> float:
    """Exponential backoff with jitter to avoid thundering herd."""
    delay = min(base * (2 ** attempt), max_backoff)
    jitter_range = delay * jitter
    return delay + random.uniform(-jitter_range, jitter_range)


async def with_recovery(
    call_fn: Callable[[], Awaitable[Any]],
    *,
    provider: str = "codex",
    policy: RetryPolicy = None,
    compress_fn: Optional[Callable[[], Awaitable[None]]] = None,
    on_error: Optional[Callable[[ClassifiedError, int], None]] = None,
) -> RecoveryResult:
    """Execute an async LLM call with automatic error recovery.

    Args:
        call_fn: Async callable that performs the LLM call.
        provider: API provider name for error classification.
        policy: Retry configuration (defaults to sensible values).
        compress_fn: Optional async callable to compress context when
                     context_overflow is detected.
        on_error: Optional callback for each error (for SSE events).

    Returns:
        RecoveryResult with success flag, value, and error history.
    """
    if policy is None:
        policy = RetryPolicy()

    result = RecoveryResult(success=False)

    for attempt in range(policy.max_retries + 1):
        try:
            value = await call_fn()
            result.success = True
            result.value = value
            result.attempts = attempt + 1

            if policy.log_recoveries and attempt > 0:
                logger.info(
                    "Report LLM call recovered after %d retries (errors: %s)",
                    attempt,
                    [e.reason.value for e in result.errors],
                )
            return result

        except Exception as exc:
            classified = classify_error(exc, provider=provider)
            result.errors.append(classified)
            result.attempts = attempt + 1

            if on_error:
                try:
                    on_error(classified, attempt)
                except Exception:
                    pass

            # Handle non-retryable errors immediately
            if not classified.retryable:
                logger.warning(
                    "Non-retryable error on attempt %d: %s (%s)",
                    attempt + 1, classified.reason.value, classified.message[:200],
                )
                return result

            # Handle context overflow with compression
            if (
                classified.should_compress
                and compress_fn
                and policy.compress_on_overflow
            ):
                try:
                    logger.info(
                        "Context overflow detected — triggering compression "
                        "(attempt %d)", attempt + 1,
                    )
                    await compress_fn()
                    result.compressions_triggered += 1
                    # Retry immediately after compression (no backoff)
                    continue
                except Exception as ce:
                    logger.warning("Compression failed: %s", ce)

            # Compute backoff for retryable errors
            if attempt < policy.max_retries:
                backoff = _compute_backoff(
                    attempt,
                    classified.backoff_seconds or policy.base_backoff_seconds,
                    policy.max_backoff_seconds,
                    policy.jitter_fraction,
                )
                result.total_backoff_seconds += backoff

                logger.info(
                    "Retrying after %.1fs (attempt %d/%d, reason: %s)",
                    backoff, attempt + 1, policy.max_retries + 1,
                    classified.reason.value,
                )
                await asyncio.sleep(backoff)

    # All retries exhausted
    logger.error(
        "Report LLM call failed after %d attempts. Errors: %s",
        result.attempts,
        [(e.reason.value, e.message[:100]) for e in result.errors],
    )
    return result


# ─── Convenience: synchronous wrapper ────────────────────────────────────

def classify_and_log(
    exc: Exception,
    *,
    context: str = "",
    provider: str = "codex",
) -> ClassifiedError:
    """Classify an error, log it, and return the classification.

    Useful for synchronous code paths that need structured error info
    without the full retry pipeline.
    """
    classified = classify_error(exc, provider=provider)
    level = logging.ERROR if not classified.retryable else logging.WARNING

    logger.log(
        level,
        "[%s] %s error: %s — retryable=%s, compress=%s%s",
        classified.reason.value,
        provider,
        classified.message[:200],
        classified.retryable,
        classified.should_compress,
        f" (context: {context})" if context else "",
    )
    return classified
