"""Error recovery service for plan execution failures.

Provides the service layer for recording errors, generating recovery
hints, managing retry state, and formatting error output for the CLI.

Integrates with:
- ``PlanLifecycleService`` for plan lookups and state transitions.
- ``ErrorRecoveryPolicy`` for retry/escalation decisions.
- ``ErrorHistory`` for tracking error records across plan attempts.
"""

from __future__ import annotations

import traceback
from datetime import UTC, datetime
from typing import TYPE_CHECKING

import structlog
from ulid import ULID

from cleveragents.domain.models.core.error_recovery import (
    ErrorCategory,
    ErrorHistory,
    ErrorRecord,
    ErrorRecoveryPolicy,
    classify_error,
    get_recovery_hints,
)

if TYPE_CHECKING:
    from cleveragents.application.services.plan_lifecycle_service import (
        PlanLifecycleService,
    )

logger = structlog.get_logger(__name__)


class ErrorRecoveryService:
    """Service for error recording, recovery hints, and retry management.

    Bridges ``PlanLifecycleService`` with structured error tracking and
    recovery hint generation.

    Attributes:
        _lifecycle: The lifecycle service for plan lookups.
        _histories: In-memory store of error histories keyed by plan_id.
        _policy: The error recovery policy for retry decisions.
    """

    def __init__(
        self,
        lifecycle_service: PlanLifecycleService,
        auto_retry_threshold: float = 0.0,
        max_retries: int = 3,
    ) -> None:
        """Initialise the error recovery service.

        Args:
            lifecycle_service: For plan lookups and state transitions.
            auto_retry_threshold: Automation profile threshold for
                auto-retry (0.0 = always auto, 1.0 = always human).
            max_retries: Maximum retry attempts per error.
        """
        self._lifecycle = lifecycle_service
        self._histories: dict[str, ErrorHistory] = {}
        self._policy = ErrorRecoveryPolicy(
            auto_retry_threshold=auto_retry_threshold,
            max_retries=max_retries,
        )
        self._logger = logger.bind(service="error_recovery")

    @property
    def max_retries(self) -> int:
        """Return the maximum retry attempts per error from the policy."""
        return self._policy.max_retries

    # -- Record errors -------------------------------------------------------

    def record_error(
        self,
        plan_id: str,
        phase: str,
        message: str,
        *,
        exception: BaseException | None = None,
        exception_type: str | None = None,
        actor: str | None = None,
        tool_call: str | None = None,
        category: ErrorCategory | None = None,
    ) -> ErrorRecord:
        """Record an error event for a plan.

        Classifies the error, generates recovery hints, and appends
        the record to the plan's error history.

        Also updates the plan's ``error_details`` field with structured
        metadata via the lifecycle service.

        Args:
            plan_id: The plan ULID.
            phase: Phase when error occurred (strategize/execute/apply).
            message: Error message.
            exception: Optional exception object (for stack trace).
            exception_type: Optional exception class name (for
                classification if exception is not provided).
            actor: Optional actor name involved.
            tool_call: Optional tool call identifier.
            category: Optional explicit category override. If not
                provided, the error is classified automatically.

        Returns:
            The created ``ErrorRecord``.
        """
        # Classify
        exc_type_name = exception_type
        if exc_type_name is None and exception is not None:
            exc_type_name = type(exception).__name__
        resolved_category = category or classify_error(message, exc_type_name)

        # Stack summary
        stack_summary: str | None = None
        if exception is not None:
            frames = traceback.format_exception(
                type(exception), exception, exception.__traceback__
            )
            # Keep last 3 frames
            stack_summary = "".join(frames[-3:]).strip()

        # Get retry count from history
        history = self._get_or_create_history(plan_id)
        # Count retries for same category
        same_category_retries = sum(
            1 for r in history.records if r.category == resolved_category
        )

        # Generate recovery hints
        hints = get_recovery_hints(resolved_category, plan_id)

        # Create record
        record = ErrorRecord(
            error_id=str(ULID()),
            phase=phase,
            category=resolved_category,
            message=message,
            actor=actor,
            tool_call=tool_call,
            stack_summary=stack_summary,
            recovery_hints=hints,
            retry_count=same_category_retries,
            max_retries=self._policy.max_retries,
            timestamp=datetime.now(tz=UTC),
        )

        history.add(record)

        self._logger.info(
            "Error recorded",
            plan_id=plan_id,
            error_id=record.error_id,
            phase=phase,
            category=resolved_category.value,
            retry_count=record.retry_count,
            is_retriable=record.is_retriable,
        )

        # Persist structured metadata to plan
        self._persist_error_metadata(plan_id, record)

        return record

    # -- Query error history -------------------------------------------------

    def get_error_history(self, plan_id: str) -> ErrorHistory:
        """Return the error history for a plan.

        Args:
            plan_id: The plan ULID.

        Returns:
            The ``ErrorHistory`` for the plan (empty if no errors).
        """
        return self._get_or_create_history(plan_id)

    def get_latest_error(self, plan_id: str) -> ErrorRecord | None:
        """Return the most recent error for a plan.

        Args:
            plan_id: The plan ULID.

        Returns:
            The latest ``ErrorRecord``, or ``None`` if no errors.
        """
        history = self._get_or_create_history(plan_id)
        return history.latest

    # -- Recovery decisions ---------------------------------------------------

    def should_retry(self, plan_id: str) -> bool:
        """Check whether the latest error for a plan should be retried.

        Args:
            plan_id: The plan ULID.

        Returns:
            ``True`` if automatic retry is appropriate.
        """
        latest = self.get_latest_error(plan_id)
        if latest is None:
            return False
        return self._policy.should_retry(latest)

    def should_escalate(self, plan_id: str) -> bool:
        """Check whether the latest error requires human intervention.

        Args:
            plan_id: The plan ULID.

        Returns:
            ``True`` if human intervention is required.
        """
        latest = self.get_latest_error(plan_id)
        if latest is None:
            return False
        return self._policy.should_escalate(latest)

    # -- Formatting for CLI ---------------------------------------------------

    def format_error_output(
        self,
        plan_id: str,
        fmt: str = "rich",
    ) -> str:
        """Format the error history for CLI display.

        Args:
            plan_id: The plan ULID.
            fmt: Output format (``rich``, ``plain``, ``json``).

        Returns:
            Formatted error output string.
        """
        import json as json_mod

        history = self.get_error_history(plan_id)

        if fmt == "json":
            return json_mod.dumps(history.to_cli_dict(), indent=2, default=str)

        if history.total_errors == 0:
            return f"No errors recorded for plan {plan_id}."

        lines: list[str] = []
        lines.append(f"Error History for Plan {plan_id}")
        lines.append(
            f"Total errors: {history.total_errors} | "
            f"Total retries: {history.total_retries}"
        )
        lines.append("")

        for i, record in enumerate(history.records, 1):
            lines.append(f"--- Error #{i} ---")
            lines.append(self._policy.format_recovery_output(record))
            lines.append("")

        return "\n".join(lines).rstrip()

    def format_recovery_hints_for_cli(
        self,
        plan_id: str,
    ) -> str:
        """Format recovery hints for the latest error as CLI output.

        This is the output appended to error messages in the CLI
        to guide users on what to do next.

        Args:
            plan_id: The plan ULID.

        Returns:
            Formatted recovery hints string, or empty string if
            no errors.
        """
        latest = self.get_latest_error(plan_id)
        if latest is None:
            return ""

        return self._policy.format_recovery_output(latest)

    # -- Internal helpers ----------------------------------------------------

    def _get_or_create_history(self, plan_id: str) -> ErrorHistory:
        """Get or create an error history for a plan."""
        if plan_id not in self._histories:
            self._histories[plan_id] = ErrorHistory(plan_id=plan_id)
        return self._histories[plan_id]

    def _persist_error_metadata(
        self,
        plan_id: str,
        record: ErrorRecord,
    ) -> None:
        """Store structured error metadata in the plan's error_details.

        Updates the plan via the lifecycle service to persist the
        error classification, retry count, and latest recovery hint.
        """
        try:
            details: dict[str, str] = {
                "error_category": record.category.value,
                "error_phase": record.phase,
                "retry_count": str(record.retry_count),
                "max_retries": str(record.max_retries),
                "is_retriable": str(record.is_retriable).lower(),
            }

            if record.actor:
                details["error_actor"] = record.actor
            if record.tool_call:
                details["error_tool_call"] = record.tool_call
            if record.stack_summary:
                # Truncate to fit in dict[str, str] value
                details["stack_summary"] = record.stack_summary[:500]

            if record.recovery_hints:
                top_hint = sorted(record.recovery_hints, key=lambda h: h.priority)[0]
                details["recovery_action"] = top_hint.action.value
                details["recovery_hint"] = top_hint.message
                if top_hint.cli_command:
                    details["recovery_command"] = top_hint.cli_command

            self._lifecycle.update_error_details(plan_id, details)

            self._logger.debug(
                "Error metadata persisted to plan",
                plan_id=plan_id,
                error_category=record.category.value,
            )
        except (
            OSError,
            ValueError,
            KeyError,
            AttributeError,
            RuntimeError,
        ):
            # Best-effort: don't crash the error-recording flow if
            # persistence fails (e.g. DB down, plan already deleted).
            self._logger.error(
                "Failed to persist error metadata to plan",
                plan_id=plan_id,
                exc_info=True,
            )
