"""Hook system for wallet transactions."""

from __future__ import annotations

import logging
from dataclasses import dataclass, field
from decimal import Decimal
from enum import Enum
from typing import Any, Callable, Dict, List, Optional, Protocol

from .exceptions import HookExecutionError, HookRejectionError

logger = logging.getLogger(__name__)


class HookType(Enum):
    """Hook execution timing types."""

    PRE = "pre"  # Execute before transaction
    POST = "post"  # Execute after transaction


@dataclass(frozen=True)
class HookContext:
    """
    Immutable context passed to hooks containing transaction details.

    All transaction parameters are frozen to prevent hooks from modifying them.
    Use metadata dict to share data between hooks.
    """

    # Operation type
    operation: str  # 'add', 'deduct', 'transfer'

    # User and point type
    user_id: int
    point_type: str

    # Transaction details
    amount: Decimal
    remarks: str
    trans_type: Optional[int]
    iid: int  # initiator id

    # Transfer-specific fields (None for add/deduct)
    to_user_id: Optional[int] = None
    to_point_type: Optional[str] = None

    # Additional params
    params: Dict[str, Any] = field(default_factory=dict)

    # Mutable metadata bus for inter-hook communication
    metadata: Dict[str, Any] = field(default_factory=dict)

    # Current balance (fetched before transaction)
    current_balance: Optional[Decimal] = None


@dataclass
class PostHookError:
    """Represents an error that occurred in a POST hook."""

    hook_name: str
    error_code: str
    message: str
    details: Dict[str, Any]
    exception: Exception


class HookFunction(Protocol):
    """Protocol for hook functions."""

    def __call__(self, context: HookContext) -> bool | None:
        """
        Execute hook logic.

        Args:
            context: Immutable transaction context with mutable metadata

        Returns:
            - For PRE hooks: True to continue, False to reject, or raise HookRejectionError
            - For POST hooks: Return value is ignored

        Raises:
            HookRejectionError: To reject transaction with error code (PRE hooks only)
            Exception: Any other exception is wrapped in HookExecutionError
        """
        ...


@dataclass
class Hook:
    """Represents a registered hook."""

    name: str
    hook_type: HookType
    operation: str  # 'add', 'deduct', 'transfer', or '*' for all
    callback: HookFunction
    priority: int = 100  # Lower number = higher priority


class HookRegistry:
    """
    Registry for managing hooks with priority support.

    Hooks are stored per operation type and executed by priority (lower = higher priority).
    """

    def __init__(self):
        self._hooks: Dict[str, List[Hook]] = {
            "add": [],
            "deduct": [],
            "transfer": [],
            "*": [],  # Global hooks for all operations
        }

    def register(
        self,
        name: str,
        hook_type: HookType,
        callback: HookFunction,
        operation: str = "*",
        priority: int = 100,
    ) -> None:
        """
        Register a new hook.

        Args:
            name: Unique identifier for the hook
            hook_type: HookType.PRE or HookType.POST
            callback: Function to execute
            operation: 'add', 'deduct', 'transfer', or '*' for all operations
            priority: Execution priority (lower number = higher priority, default=100)

        Example:
            registry.register(
                name="check_daily_limit",
                hook_type=HookType.PRE,
                callback=check_daily_limit_hook,
                operation="deduct",
                priority=50,
            )
        """
        if operation not in self._hooks:
            raise ValueError(f"Invalid operation: {operation}. Must be 'add', 'deduct', 'transfer', or '*'")

        # Check for duplicate names
        for hooks in self._hooks.values():
            if any(h.name == name for h in hooks):
                raise ValueError(f"Hook with name '{name}' already registered")

        hook = Hook(
            name=name,
            hook_type=hook_type,
            operation=operation,
            callback=callback,
            priority=priority,
        )

        self._hooks[operation].append(hook)
        # Sort by priority (lower number = higher priority), then by registration order
        self._hooks[operation].sort(key=lambda h: (h.priority, h.name))

        logger.info(f"Registered {hook_type.value} hook '{name}' for operation '{operation}' with priority {priority}")

    def unregister(self, name: str) -> bool:
        """
        Unregister a hook by name.

        Args:
            name: Hook name to remove

        Returns:
            True if hook was found and removed, False otherwise
        """
        for operation, hooks in self._hooks.items():
            for hook in hooks:
                if hook.name == name:
                    hooks.remove(hook)
                    logger.info(f"Unregistered hook '{name}' from operation '{operation}'")
                    return True
        return False

    def get_hooks(self, operation: str, hook_type: HookType) -> List[Hook]:
        """
        Get all hooks for a specific operation and type, sorted by priority.

        Args:
            operation: 'add', 'deduct', or 'transfer'
            hook_type: HookType.PRE or HookType.POST

        Returns:
            List of hooks sorted by priority (lower number first)
        """
        # Get operation-specific hooks + global hooks
        operation_hooks = self._hooks.get(operation, [])
        global_hooks = self._hooks.get("*", [])

        # Combine and filter by hook type
        all_hooks = operation_hooks + global_hooks
        filtered = [h for h in all_hooks if h.hook_type == hook_type]

        # Sort by priority, then name
        filtered.sort(key=lambda h: (h.priority, h.name))

        return filtered

    def clear(self) -> None:
        """Clear all registered hooks (useful for testing)."""
        for operation in self._hooks:
            self._hooks[operation].clear()
        logger.info("Cleared all hooks")


class HookManager:
    """
    Manages hook execution for wallet transactions.

    Executes PRE hooks before transaction (can reject) and POST hooks after (cannot reject).
    """

    def __init__(self, registry: Optional[HookRegistry] = None):
        self.registry = registry or HookRegistry()

    def execute_pre_hooks(self, context: HookContext) -> None:
        """
        Execute all PRE hooks for the given operation.

        PRE hooks can reject the transaction by:
        1. Returning False
        2. Raising HookRejectionError with error_code and message

        Args:
            context: Transaction context

        Raises:
            HookRejectionError: If any hook rejects the transaction
            HookExecutionError: If a hook fails unexpectedly
        """
        hooks = self.registry.get_hooks(context.operation, HookType.PRE)

        for hook in hooks:
            logger.debug(f"Executing PRE hook '{hook.name}' for {context.operation}")

            try:
                result = hook.callback(context)

                # Check if hook rejected transaction
                if result is False:
                    raise HookRejectionError(
                        error_code=f"HOOK_REJECTED_{hook.name.upper()}",
                        message=f"Transaction rejected by hook: {hook.name}",
                        details={"hook_name": hook.name},
                    )

                logger.debug(f"PRE hook '{hook.name}' completed successfully")

            except HookRejectionError:
                # Re-raise rejection errors as-is
                raise

            except Exception as e:
                # Wrap unexpected errors
                logger.error(f"PRE hook '{hook.name}' failed with error: {e}", exc_info=True)
                raise HookExecutionError(
                    message=f"Hook '{hook.name}' failed to execute: {str(e)}",
                    hook_name=hook.name,
                    original_exception=e,
                ) from e

    def execute_post_hooks(self, context: HookContext) -> List[PostHookError]:
        """
        Execute all POST hooks for the given operation.

        POST hooks cannot reject the transaction. Errors are captured and returned
        so the transaction remains successful but errors can be logged/displayed.

        Args:
            context: Transaction context (with updated balance after transaction)

        Returns:
            List of errors that occurred in POST hooks (empty if all succeeded)
        """
        hooks = self.registry.get_hooks(context.operation, HookType.POST)
        errors: List[PostHookError] = []

        for hook in hooks:
            logger.debug(f"Executing POST hook '{hook.name}' for {context.operation}")

            try:
                hook.callback(context)
                logger.debug(f"POST hook '{hook.name}' completed successfully")

            except HookRejectionError as e:
                # POST hooks trying to reject - capture as error but don't fail transaction
                logger.warning(f"POST hook '{hook.name}' tried to reject transaction: {e.error_code} - {e.message}")
                errors.append(
                    PostHookError(
                        hook_name=hook.name,
                        error_code=e.error_code,
                        message=e.message,
                        details=e.details,
                        exception=e,
                    )
                )

            except Exception as e:
                # Capture all other errors
                logger.error(f"POST hook '{hook.name}' failed with error: {e}", exc_info=True)
                errors.append(
                    PostHookError(
                        hook_name=hook.name,
                        error_code="HOOK_EXECUTION_ERROR",
                        message=f"Hook '{hook.name}' failed: {str(e)}",
                        details={"exception_type": type(e).__name__},
                        exception=e,
                    )
                )

        return errors


# Global hook registry singleton
_global_registry = HookRegistry()


def get_global_registry() -> HookRegistry:
    """Get the global hook registry."""
    return _global_registry


def register_hook(
    name: str,
    hook_type: HookType,
    callback: HookFunction,
    operation: str = "*",
    priority: int = 100,
) -> None:
    """
    Register a hook in the global registry.

    Args:
        name: Unique identifier for the hook
        hook_type: HookType.PRE or HookType.POST
        callback: Function to execute
        operation: 'add', 'deduct', 'transfer', or '*' for all operations
        priority: Execution priority (lower number = higher priority, default=100)

    Example:
        def check_fraud(context: HookContext) -> bool:
            if context.amount > Decimal("10000"):
                raise HookRejectionError(
                    error_code="AMOUNT_TOO_HIGH",
                    message="Transaction amount exceeds limit",
                    details={"limit": 10000, "requested": float(context.amount)}
                )
            return True

        register_hook(
            name="fraud_check",
            hook_type=HookType.PRE,
            callback=check_fraud,
            operation="deduct",
            priority=10,
        )
    """
    _global_registry.register(name, hook_type, callback, operation, priority)


def unregister_hook(name: str) -> bool:
    """
    Unregister a hook from the global registry.

    Args:
        name: Hook name to remove

    Returns:
        True if hook was found and removed, False otherwise
    """
    return _global_registry.unregister(name)


def clear_hooks() -> None:
    """Clear all hooks from the global registry (useful for testing)."""
    _global_registry.clear()
