"""Exception hierarchy for CleverAgents.

Based on ADR-005: Error Handling Hierarchy and Exception Strategy.
All exceptions inherit from CleverAgentsError and follow fail-fast principles.
"""

import os
from typing import Any


class CleverAgentsError(Exception):
    """Base exception for all CleverAgents errors."""

    def __init__(self, message: str, details: dict[str, Any] | None = None):
        super().__init__(message)
        self.message = message
        self.details = details or {}


# Backward compatibility exception types for v2 code
class CleverAgentsException(CleverAgentsError):
    """Backward compatibility exception name for v2 code."""


class UnsafeConfigurationError(CleverAgentsError):
    """Unsafe configuration requires explicit opt-in."""


# Domain Exceptions
class DomainError(CleverAgentsError):
    """Base for domain/business logic errors."""

    pass


class ValidationError(DomainError):
    """Data validation failures."""

    pass


class ToolTypeMismatchError(DomainError):
    """Raised when a plain Tool is used where a Validation is required.

    The ``agents validation attach`` command only accepts entries whose
    ``tool_type`` discriminator is ``"validation"``.  Attempting to attach
    a plain Tool raises this error.

    Attributes:
        tool_name: The namespaced name of the offending tool.
        actual_type: The ``tool_type`` value found on the registered entry.
    """

    def __init__(
        self,
        tool_name: str,
        actual_type: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        """Initialise with the offending tool name and its actual type.

        Args:
            tool_name: Namespaced name of the tool that was rejected.
            actual_type: The ``tool_type`` value found (e.g. ``"tool"``).
            details: Optional additional context.
        """
        super().__init__(
            f"'{tool_name}' is a plain Tool (tool_type='{actual_type}'), "
            "not a Validation. Only entries with tool_type='validation' "
            "may be attached via 'agents validation attach'.",
            details,
        )
        self.tool_name = tool_name
        self.actual_type = actual_type


class BusinessRuleViolation(DomainError):
    """Business rule violations."""

    pass


class ResourceNotFoundError(DomainError):
    """Requested resource doesn't exist."""

    def __init__(
        self,
        message: str | None = None,
        resource_type: str | None = None,
        resource_id: str | None = None,
        details: dict[str, Any] | None = None,
    ):
        """Initialize with resource information.

        Args:
            message: Error message (auto-generated if not provided)
            resource_type: Type of resource (e.g., 'project', 'plan')
            resource_id: Resource identifier
            details: Additional error context
        """
        if not message and resource_type and resource_id:
            message = f"{resource_type.capitalize()} '{resource_id}' not found"
        elif not message:
            message = "Resource not found"

        super().__init__(message, details)
        self.resource_type = resource_type
        self.resource_id = resource_id


# Alias for compatibility
NotFoundError = ResourceNotFoundError


class ResourceConflictError(DomainError):
    """Resource state conflict."""

    pass


class LockConflictError(BusinessRuleViolation):
    """Raised when a lock cannot be acquired due to a conflicting owner.

    Attributes:
        resource_type: The type of resource that is locked.
        resource_id: The identifier of the locked resource.
        owner_id: The owner that currently holds the lock.
    """

    def __init__(
        self,
        resource_type: str,
        resource_id: str,
        owner_id: str,
    ) -> None:
        """Initialize lock conflict error.

        Args:
            resource_type: The type of resource (e.g. 'plan', 'project').
            resource_id: The identifier of the locked resource.
            owner_id: The owner that currently holds the lock.
        """
        super().__init__(
            f"Cannot acquire lock on {resource_type}/{resource_id}: "
            f"held by owner '{owner_id}'"
        )
        self.resource_type = resource_type
        self.resource_id = resource_id
        self.owner_id = owner_id


class LockExpiredError(DomainError):
    """Raised when a lock operation targets an expired lock.

    Attributes:
        resource_type: The type of resource.
        resource_id: The identifier of the resource.
    """

    def __init__(self, resource_type: str, resource_id: str) -> None:
        """Initialize lock expired error.

        Args:
            resource_type: The type of resource.
            resource_id: The identifier of the resource.
        """
        super().__init__(f"Lock on {resource_type}/{resource_id} has expired")
        self.resource_type = resource_type
        self.resource_id = resource_id


# Infrastructure Exceptions
class InfrastructureError(CleverAgentsError):
    """Base for infrastructure errors."""

    pass


class DatabaseError(InfrastructureError):
    """Database operation failures."""

    pass


class MigrationNotApprovedError(DatabaseError):
    """Database migrations were declined by the user."""

    pass


class NetworkError(InfrastructureError):
    """Network communication failures."""

    pass


class ExternalServiceError(InfrastructureError):
    """External service failures."""

    pass


# Provider Exceptions
class ProviderError(CleverAgentsError):
    """Base for AI provider errors."""

    pass


class StreamRoutingError(CleverAgentsError):
    """Reactive stream routing failures."""

    pass


class RateLimitError(ProviderError):
    """Rate limit exceeded."""

    def __init__(self, message: str, retry_after: int | None = None):
        """Initialize with optional retry_after seconds.

        Args:
            message: Error message
            retry_after: Number of seconds to wait before retrying
        """
        super().__init__(message)
        self.retry_after = retry_after


class ModelNotAvailableError(ProviderError):
    """Model not available or deprecated."""

    pass


class TokenLimitExceededError(ProviderError):
    """Context window exceeded."""

    pass


# Authentication/Authorization
class AuthenticationError(CleverAgentsError):
    """Authentication failures."""

    pass


class AuthorizationError(CleverAgentsError):
    """Authorization failures."""

    pass


# Configuration Errors
class ConfigurationError(CleverAgentsError):
    """Configuration issues."""

    pass


class MissingConfigurationError(ConfigurationError):
    """Required configuration missing."""

    pass


# File System Errors
class FileSystemError(CleverAgentsError):
    """File system operation failures."""

    def __init__(
        self,
        message: str,
        path: str | os.PathLike[str] | None = None,
        details: dict[str, Any] | None = None,
    ) -> None:
        """Initialize with path information.

        Args:
            message: Error message
            path: Path that caused the error
            details: Additional error context
        """
        super().__init__(message, details)
        self.path = path


# Plan Errors
class PlanError(DomainError):
    """Plan operation failures."""

    pass


class DecisionPhaseViolationError(BusinessRuleViolation):
    """Raised when a decision type is invalid for the plan's current phase.

    Attributes:
        decision_type: The :class:`str` value of the attempted decision type.
        plan_phase: The :class:`str` value of the plan's current phase.
        allowed_types: Frozenset of :class:`str` values of decision types
            permitted in the current phase.
    """

    def __init__(
        self,
        decision_type: str,
        plan_phase: str,
        allowed_types: frozenset[str],
    ) -> None:
        sorted_allowed = sorted(allowed_types)
        super().__init__(
            f"Decision type '{decision_type}' is not permitted in the "
            f"'{plan_phase}' phase. Allowed types: {sorted_allowed}"
        )
        self.decision_type = decision_type
        self.plan_phase = plan_phase
        self.allowed_types = allowed_types


class ExecutionError(CleverAgentsError):
    """Execution failures for agents or graph nodes."""

    pass


class InvariantViolationError(BusinessRuleViolation):
    """Raised when a plan action violates an active invariant.

    Attributes:
        invariant_id: The ULID of the violated invariant.
        violated_text: The text of the invariant that was violated.
        action_text: The action text that caused the violation.
    """

    def __init__(
        self,
        invariant_id: str,
        violated_text: str,
        action_text: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        """Initialize invariant violation error.

        Args:
            invariant_id: The ULID of the violated invariant.
            violated_text: The text of the invariant that was violated.
            action_text: The action text that caused the violation.
            details: Optional additional context.
        """
        message = (
            f"Invariant violation: invariant '{invariant_id}' "
            f"('{violated_text}') violated by action: {action_text}"
        )
        super().__init__(message, details)
        self.invariant_id = invariant_id
        self.violated_text = violated_text
        self.action_text = action_text


# Subplan Errors (v3.3.0)
class SubplanSpawnError(PlanError):
    """Raised when subplan spawning fails.

    Covers spawn-level failures including validation errors, upstream
    service unavailability, and plan state constraints that prevent
    child plans from being created.

    Attributes:
        message: Human-readable error description.
        details: Optional structured context (e.g. plan_id).
    """

    def __init__(
        self,
        message: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        """Initialize subplan spawn error.

        Args:
            message: Human-readable description of the failure.
            details: Optional structured context.
        """
        super().__init__(message, details)


class SubplanExecutionError(DomainError):
    """Raised when a subplan execution fails after spawning.

    Covers runtime failures during child-plan execution such as
    sandbox errors, merge conflicts (when FAIL_ON_CONFLICT), and
    per-subplan timeouts.

    Attributes:
        message: Human-readable error description.
        details: Optional structured context (e.g., subplan_id).
    """

    def __init__(
        self,
        message: str,
        details: dict[str, Any] | None = None,
    ) -> None:
        """Initialize subplan execution error.

        Args:
            message: Human-readable description of the failure.
            details: Optional structured context.
        """
        super().__init__(message, details)


class MaxParallelExceededError(BusinessRuleViolation):
    """Raised when concurrent subplans exceed the configured max limit.

    Attributes:
        plan_id: ULID of the parent plan that triggered the limit.
        requested: Number of subplans requested to execute concurrently.
        max_allowed: Configured maximum parallel subplans.
    """

    def __init__(
        self,
        plan_id: str,
        requested: int,
        max_allowed: int,
    ) -> None:
        """Initialize max-parallel-exceeded error.

        Args:
            plan_id: ULID of the parent plan.
            requested: Number of subplans attempted.
            max_allowed: The configured ``max_parallel`` value.
        """
        super().__init__(
            f"Concurrent subplan count {requested} exceeds "
            f"max_parallel limit {max_allowed} for plan {plan_id}"
        )
        self.plan_id = plan_id
        self.requested = requested
        self.max_allowed = max_allowed


class SubplanDepthLimitError(BusinessRuleViolation):
    """Raised when subplan nesting exceeds the configured depth limit.

    Attributes:
        depth_limit: Maximum allowed nesting depth.
        current_depth: The depth that was exceeded.
    """

    def __init__(
        self,
        depth_limit: int,
        current_depth: int,
    ) -> None:
        """Initialize depth-limit error.

        Args:
            depth_limit: Maximum allowed nesting depth.
            current_depth: The actual nesting depth encountered.
        """
        super().__init__(
            f"Subplan nesting depth {current_depth} exceeds limit {depth_limit}"
        )
        self.depth_limit = depth_limit
        self.current_depth = current_depth


__all__ = [
    "AuthenticationError",
    "AuthorizationError",
    "BudgetExceededError",
    "BusinessRuleViolation",
    "CleverAgentsError",
    "ConfigurationError",
    "DatabaseError",
    "DecisionPhaseViolationError",
    "DomainError",
    "ExecutionError",
    "ExternalServiceError",
    "FileSystemError",
    "InfrastructureError",
    "InvariantViolationError",
    "LockConflictError",
    "LockExpiredError",
    "MaxParallelExceededError",
    "MigrationNotApprovedError",
    "MissingConfigurationError",
    "ModelNotAvailableError",
    "NetworkError",
    "NotFoundError",
    "PlanError",
    "ProviderError",
    "RateLimitError",
    "ResourceConflictError",
    "ResourceNotFoundError",
    "StreamRoutingError",
    "SubplanDepthLimitError",
    "SubplanExecutionError",
    "SubplanSpawnError",
    "TokenLimitExceededError",
    "ToolTypeMismatchError",
    "ValidationError",
]


class BudgetExceededError(DomainError):
    """Raised when a cost budget limit is exceeded.

    Attributes:
        budget_type: The type of budget that was exceeded ('plan', 'session', or 'org').
        limit: The budget limit in USD.
        used: The amount used in USD.
        provider: The provider that triggered the exhaustion.
        model: The model that triggered the exhaustion.
    """

    def __init__(
        self,
        budget_type: str,
        limit: float,
        used: float,
        provider: str = "",
        model: str = "",
        details: dict[str, Any] | None = None,
    ) -> None:
        """Initialize budget exceeded error.

        Args:
            budget_type: Type of budget ('plan', 'session', or 'org').
            limit: Budget limit in USD.
            used: Amount used in USD.
            provider: Provider that triggered exhaustion.
            model: Model that triggered exhaustion.
            details: Additional error context.
        """
        message = (
            f"{budget_type.capitalize()} budget of ${limit:.2f} exceeded "
            f"(used: ${used:.2f})"
        )
        if provider:
            message += f" by {provider}"
        if model:
            message += f" ({model})"

        super().__init__(message, details)
        self.budget_type = budget_type
        self.limit = limit
        self.used = used
        self.provider = provider
        self.model = model
