import asyncio
import functools
from bdb import BdbQuit
from typing import Callable, ParamSpec, TypeVar

import click

P = ParamSpec("P")
T = TypeVar("T")


def handle_exceptions(
    application_main: Callable[P, T],
    with_debugger: bool,
) -> Callable[P, T]:
    """Wraps a function so that it drops a user into a debugger if it raises an error.

    This is intended to be used as a wrapper for the main function of a CLI application.
    It will catch all errors and drop a user into a debugger if the error is not a
    KeyboardInterrupt. If the error is a KeyboardInterrupt, it will raise the error.
    If the error is not a KeyboardInterrupt, it will log the error and drop a user into a
    debugger if with_debugger is True. If with_debugger is False, it will raise the error.

    Parameters
    ----------
    application_main
        The function to wrap.
    with_debugger
        Whether to drop a user into a debugger if an error is raised.

    Returns
    -------
    Callable
        The wrapped function.

    """

    @functools.wraps(application_main)
    async def wrapped(*args: P.args, **kwargs: P.kwargs) -> T:
        try:
            return await application_main(*args, **kwargs)
        except (BdbQuit, KeyboardInterrupt, click.Abort):
            raise
        except Exception as e:
            if with_debugger:
                print(f"Uncaught exception {e}")
                import pdb

                pdb.post_mortem()
            else:
                raise

    return wrapped


def coroutine(f):
    @functools.wraps(f)
    def wrapper(*args, **kwargs):
        return asyncio.run(f(*args, **kwargs))

    return wrapper
