import typing
import os
import asyncio
import functools
import types
import weakref
import inspect

from . import helpers as _helpers
from . import model   as _model
from . import client  as _client
from . import events  as _events


__all__ = ('Info', 'callback', 'interact', 'load', 'drop')


_V = typing.TypeVar('_V')
_P = typing.ParamSpec('_P')


class Info(typing.NamedTuple):

    """
    Holds global information.
    """

    client : _client.Client
    """
    The client for which this widget has been loaded.
    """
    widgets: dict[str, types.ModuleType]
    """
    All currently loaded widgets.
    """


def _load_module(name, path, intermediary = '_m_'):

    module_name = f'{__name__}.{intermediary}.{name}'

    module = _helpers.load_module_via_path(module_name, path)

    return module


_latent_callback_assets = []


def _callback(Event):
    def decorator(function):
        _latent_callback_assets.append((Event, function))
        return function
    return decorator


def callback(Event: _V) -> typing.Callable[[typing.Callable[_P, _V]], typing.Callable[[Info, typing.Unpack[tuple[_V, ...]]], None]]:

    """
    Create a callback for an core_event.

    :param Event:
        Any event dispatchable by :class:`.client.Client`.
    """

    return _callback(Event)


_latent_interact_assets = []


def _interact(path):
    def decorator(function):
        _latent_interact_assets.append((path, function))
        return function
    return decorator


def interact(*path: typing.Unpack[tuple[str, ...]]) -> typing.Callable[[typing.Callable[_P, _V]], typing.Callable[[Info, _events.CreateIntegration], typing.Awaitable[_model.protocols.InteractionResponse | None]]]:

    """
    Create a callback for an interaction.

    If a :code:`.model.protocols.InteractionResponse` is returned, it is used to respond via :meth:`.client.create_interaction_response`.

    :param *path:
        The ``command-name`` to ``sub[command/group]-name`` leading to the desired command.
    """

    return _interact(path)


_modules_group = weakref.WeakKeyDictionary()


_assets = weakref.WeakKeyDictionary()


def _find_caller_widget_asset():

    stack = inspect.stack()

    for index, frame in enumerate(stack):
        if index < 2:
            continue
        module = inspect.getmodule(frame)
        try:
            asset = _assets[module]
        except KeyError:
            continue
        return asset
    
    return None


async def _load(client, name, path):

    if path is None:
        asset = _find_caller_widget_asset()
        path = '.' if asset is None else asset.path

    loop = asyncio.get_event_loop()

    try:
        modules = _modules_group[client]
    except KeyError:
        modules = _modules_group[client] = weakref.WeakValueDictionary()

    try:
        await _drop(client, name, pop = False)
    except KeyError:
        pass

    if not (application := client.cache.application):
        application = await client.get_self_application_information()

    commands = await client.get_global_application_commands(application.id)

    module_path = os.path.join(path, name)
    
    load_module = functools.partial(_load_module, name, module_path, intermediary = id(client))

    module = modules[name] = await loop.run_in_executor(None, load_module)

    # module.__name__, name = name, module.__name__

    info = Info(client, types.MappingProxyType(modules))

    callbacks = []

    Events = set()

    def load_callback(Event, function):
        Events.add(Event)
        async def callback(event, *args, **kwargs):
            if not isinstance(event, Event):
                return
            await function(info, event, *args, **kwargs)
        return callback

    for asset in _latent_callback_assets:
        callback = load_callback(*asset)
        callbacks.append(callback)

    _latent_callback_assets.clear()

    def get_command(path, commands = commands):
        name, *path = path
        for command in commands:
            if not command.name == name:
                continue
            break
        if not path:
            return command
        return get_command(path, commands = command.options)

    def load_interact_callback(path, function):
        command = get_command(path)
        async def callback(info, core_event, *args, **kwargs):
            if not core_event.interaction.type == _model.enums.InteractionType.application_command:
                return
            if not core_event.interaction.data.id == command.id:
                return
            response = await function(info, core_event, *args, **kwargs)
            if not response is None:
                await client.create_interaction_response(core_event.interaction.id, core_event.interaction.token, **response)
        return callback
    
    for asset in _latent_interact_assets:
        callback = load_interact_callback(*asset)
        callback = load_callback(_events.CreateInteraction, callback)
        callbacks.append(callback)

    _latent_interact_assets.clear()

    try:
        load = getattr(module, '__load__')
    except AttributeError:
        pass
    else:
        await load(info)

    client.callbacks.extend(callbacks)

    _assets[module] = types.SimpleNamespace(
        name = name, 
        path = path, 
        info = info, 
        Events = Events,
        callbacks = callbacks
    )

    return module


def load(client: _client.Client, 
         name  : str, 
         path  : str = None) -> typing.Awaitable[types.ModuleType]:
    
    """
    Load a widget by creating and attaching events to the client.
    
    :param client:
        The client to load the widget for.
    :param name:
        The name of the widget, used for identifying in :attr:`.Info.widgets` and :func:`.drop`\ing.
    :param path:
        The location of the module's parent directory. If not specified, the caller widget's parent directory is used.

    The widget may define a ``__load__(info)`` function, which will be called before callbacks are attached.
    """

    return _load(client, name, path)


def _drop_module(name):

    _helpers.drop_module(name)


async def _drop(client, name, *, pop = True):

    loop = asyncio.get_event_loop()

    modules = _modules_group[client]

    getter = modules.pop if pop else modules.__getitem__

    try:
        module = getter(name)
    except KeyError:
        return

    asset = _assets.pop(module)

    for callback in asset.callbacks:
        client.callbacks.remove(callback)

    try:
        drop = getattr(module, '__drop__')
    except AttributeError:
        pass
    else:
        await drop(asset.info)

    drop_module = functools.partial(_drop_module, asset.name)

    await loop.run_in_executor(None, drop_module)


def drop(client: _client.Client, 
         name: str) -> typing.Awaitable[None]:

    """
    Unload a widget by detaching all the related callbles from the client.

    :param client:
        The client to unload the widget for.
    :param name:
        The name of the widget, as specified in :func:`.load`.

    The widget may define a ``__drop__`` function which will be called after callbacks are detached.
    """

    return _drop(client, name)
