"""
Validation node to catch and handle exceptions from command/group functions.
"""

# pylint: disable=wrong-import-position
# pylint: disable=wrong-import-order
# pylint: disable=ungrouped-imports

import asyncio
import inspect
from functools import wraps
from typing import Any, Callable
from weakref import WeakKeyDictionary

from click_extended.core.nodes.validation_node import ValidationNode
from click_extended.core.other._tree import Tree
from click_extended.core.other.context import Context
from click_extended.types import Decorator

_catch_handlers: WeakKeyDictionary[
    Callable[..., Any],
    list[tuple[tuple[type[BaseException], ...], Callable[..., Any] | None, bool]],
] = WeakKeyDictionary()


class Catch(ValidationNode):
    """
    Catch and handle exceptions from command/group functions and
    validators.

    Wraps both the validation phase and function execution in a
    try-except block. When an exception is caught, an optional handler
    is invoked. Without a handler, exceptions are silently suppressed.

    Catches exceptions from:
    - Validation decorators (e.g., @exclusive, @requires)
    - The command/group function itself

    Handler signatures supported:
    - `handler()` - No arguments, just execute code
    - `handler(exception)` - Receive the exception object
    - `handler(exception, context)` - Receive exception and Context object

    Examples:
        ```py
        # Catch validation errors from @exclusive
        @command()
        @exclusive("--stock", "--query")
        @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
        def search(stock, query):
            pass
        ```

        ```py
        # Catch errors from command function
        @command()
        @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
        def cmd():
            raise ValueError("Count must be positive")
        ```

        ```py
        # Access context information
        @command()
        @catch(
            ValueError,
            handler=lambda e, ctx: print(f"{ctx.info_name}: {e}"),
        )
        def cmd():
            raise ValueError("Failed!")
        ```
    """

    def __init__(
        self,
        name: str,
        process_args: tuple[Any, ...] | None = None,
        process_kwargs: dict[str, Any] | None = None,
        **kwargs: Any,
    ) -> None:
        """Initialize Catch validation node with function to wrap."""
        super().__init__(name, process_args, process_kwargs, **kwargs)
        self.wrapped_func: Callable[..., Any] | None = None
        self.remaining_validations: list[ValidationNode] = []

    def on_finalize(self, context: Context, *args: Any, **kwargs: Any) -> None:
        """
        Execute remaining validations wrapped in exception handling.

        This catches exceptions from all validators that run after @catch,
        allowing it to catch validation errors like those from @exclusive.

        Args:
            context: The execution context
            *args: Contains exception types tuple at index 0
            **kwargs: Contains handler, reraise parameters
        """
        exception_types: tuple[type[BaseException], ...] = args[0]
        handler: Callable[..., Any] | None = kwargs.get("handler")
        reraise: bool = kwargs.get("reraise", False)

        for validation_node in self.remaining_validations:
            try:
                if asyncio.iscoroutinefunction(validation_node.on_finalize):
                    asyncio.run(
                        validation_node.on_finalize(
                            context,
                            *validation_node.process_args,
                            **validation_node.process_kwargs,
                        )
                    )
                else:
                    validation_node.on_finalize(
                        context,
                        *validation_node.process_args,
                        **validation_node.process_kwargs,
                    )
            except BaseException as exc:
                if isinstance(exc, exception_types):
                    if handler is not None:
                        if asyncio.iscoroutinefunction(handler):
                            asyncio.run(_call_handler_async(handler, exc))
                        else:
                            _call_handler_sync(handler, exc)

                    if not reraise:
                        return

                raise


def catch(
    *exception_types: type[BaseException],
    handler: Callable[..., Any] | None = None,
    reraise: bool = False,
) -> Decorator:
    r"""
    Catch and handle exceptions from command/group functions.

    Wraps the function in a try-except block. When exceptions occur, an optional
    handler is invoked. If no exception types are specified, catches Exception.

    Type: `ValidationNode`

    :param \*exception_types: Exception types to catch (defaults to Exception).
    :param handler: Optional function with signature ``()``, ``(exception)``, or
        ``(exception, context)`` to handle caught exceptions.
    :param reraise: If True, re-raise after handling (default: False).
    :raises TypeError: If exception_types contains non-exception classes.
    :returns: Decorator function.
    :rtype: Decorator

    Examples:
        ```python
        # Simple notification (no arguments)
        @command()
        @catch(ValueError, handler=lambda: print("Error occurred!"))
        def cmd():
            raise ValueError("Invalid input")
        ```

        ```python
        # Log exception details (exception argument)
        @command()
        @catch(ValueError, handler=lambda e: print(f"Error: {e}"))
        def cmd():
            raise ValueError("Count must be positive")
        ```

        ```python
        # Access context (exception + context arguments)
        @command()
        @catch(
            ValueError,
            handler=lambda e, ctx: print(f"{ctx.info_name}: {e}"),
        )
        def cmd():
            raise ValueError("Failed!")
        ```

        ```python
        # Catch multiple exception types
        @command()
        @catch(ValueError, TypeError, handler=lambda e: log_error(e))
        def cmd():
            raise ValueError("Something went wrong")
        ```

        ```python
        # Silent suppression (no handler)
        @command()
        @catch(RuntimeError)
        def cmd():
            raise RuntimeError("Silently suppressed")
        ```

        ```python
        # Log then re-raise
        @command()
        @catch(
            ValueError,
            handler=lambda e: print(f"Logging: {e}"),
            reraise=True,
        )
        def cmd():
            raise ValueError("This will be logged and re-raised")
        ```
    """
    if not exception_types:
        exception_types = (Exception,)

    for exc_type in exception_types:
        if not isinstance(exc_type, type) or not issubclass(exc_type, BaseException):
            raise TypeError(f"catch() requires exception types, got {exc_type!r}")

    def decorator(func: Callable[..., Any]) -> Callable[..., Any]:
        """The actual decorator that wraps the function."""

        instance = Catch(
            name="catch",
            process_args=(exception_types,),
            process_kwargs={"handler": handler, "reraise": reraise},
        )

        Tree.queue_validation(instance)

        original_func = func
        while hasattr(original_func, "__wrapped__"):
            original_func = original_func.__wrapped__  # type: ignore

        if original_func not in _catch_handlers:
            _catch_handlers[original_func] = []
        _catch_handlers[original_func].insert(0, (exception_types, handler, reraise))

        # Only wrap if this is the first @catch (check handlers dict)
        if len(_catch_handlers[original_func]) > 1:
            return func

        if asyncio.iscoroutinefunction(func):

            @wraps(func)
            async def async_wrapper(*call_args: Any, **call_kwargs: Any) -> Any:
                """Async wrapper that catches exceptions."""
                try:
                    return await func(*call_args, **call_kwargs)
                except BaseException as exc:
                    for exc_types, hdlr, reraise_flag in _catch_handlers.get(
                        original_func, []
                    ):
                        if isinstance(exc, exc_types):
                            if hdlr is not None:
                                if asyncio.iscoroutinefunction(hdlr):
                                    await _call_handler_async(hdlr, exc)
                                else:
                                    _call_handler_sync(hdlr, exc)

                            if reraise_flag:
                                raise

                            return None
                    raise

            return async_wrapper

        @wraps(func)
        def sync_wrapper(*call_args: Any, **call_kwargs: Any) -> Any:
            """Sync wrapper that catches exceptions."""
            try:
                return func(*call_args, **call_kwargs)
            except BaseException as exc:
                for exc_types, hdlr, reraise_flag in _catch_handlers.get(
                    original_func, []
                ):
                    if isinstance(exc, exc_types):
                        if hdlr is not None:
                            if asyncio.iscoroutinefunction(hdlr):
                                asyncio.run(_call_handler_async(hdlr, exc))
                            else:
                                _call_handler_sync(hdlr, exc)

                        if reraise_flag:
                            raise

                        return None
                raise

        return sync_wrapper

    return decorator


async def _call_handler_async(handler: Callable[..., Any], exc: BaseException) -> Any:
    """Call async handler with appropriate number of arguments."""
    sig = inspect.signature(handler)
    params = list(sig.parameters.values())

    if len(params) == 0:
        return await handler()
    if len(params) == 1:
        return await handler(exc)

    import click

    try:
        ctx = click.get_current_context()
        custom_context = ctx.meta.get("click_extended", {}).get("context")
        return await handler(exc, custom_context)
    except RuntimeError:
        return await handler(exc, None)


def _call_handler_sync(handler: Callable[..., Any], exc: BaseException) -> Any:
    """Call sync handler with appropriate number of arguments."""
    sig = inspect.signature(handler)
    params = list(sig.parameters.values())

    if len(params) == 0:
        return handler()
    if len(params) == 1:
        return handler(exc)

    import click

    try:
        ctx = click.get_current_context()
        custom_context = ctx.meta.get("click_extended", {}).get("context")
        return handler(exc, custom_context)
    except RuntimeError:
        return handler(exc, None)
