# SPDX-License-Identifier: MIT

from __future__ import annotations

import asyncio
import os
import sys
import traceback
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union

from ..enums import TextInputStyle
from ..utils import MISSING
from .action_row import ActionRow, components_to_rows
from .text_input import TextInput

if TYPE_CHECKING:
    from ..interactions.modal import ModalInteraction
    from ..state import ConnectionState
    from ..types.components import Modal as ModalPayload
    from .action_row import Components, ModalUIComponent


__all__ = ("Modal",)


class Modal:
    """Represents a UI Modal.

    .. versionadded:: 2.4

    Parameters
    ----------
    title: :class:`str`
        The title of the modal.
    components: |components_type|
        The components to display in the modal. Up to 5 action rows.
    custom_id: :class:`str`
        The custom ID of the modal.
    timeout: :class:`float`
        The time to wait until the modal is removed from cache, if no interaction is made.
        Modals without timeouts are not supported, since there's no event for when a modal is closed.
        Defaults to 600 seconds.
    """

    __slots__ = ("title", "custom_id", "components", "timeout")

    def __init__(
        self,
        *,
        title: str,
        components: Components[ModalUIComponent],
        custom_id: str = MISSING,
        timeout: float = 600,
    ) -> None:
        if timeout is None:
            raise ValueError("Timeout may not be None")

        rows = components_to_rows(components)
        if len(rows) > 5:
            raise ValueError("Maximum number of components exceeded.")

        self.title: str = title
        self.custom_id: str = os.urandom(16).hex() if custom_id is MISSING else custom_id
        self.components: List[ActionRow] = rows
        self.timeout: float = timeout

    def __repr__(self) -> str:
        return (
            f"<Modal custom_id={self.custom_id!r} title={self.title!r} "
            f"components={self.components!r}>"
        )

    def append_component(self, component: Union[TextInput, List[TextInput]]) -> None:
        """Adds one or multiple component(s) to the modal.

        Parameters
        ----------
        component: Union[:class:`~.ui.TextInput`, List[:class:`~.ui.TextInput`]]
            The component(s) to add to the modal.
            This can be a single component or a list of components.

        Raises
        ------
        ValueError
            Maximum number of components (5) exceeded.
        TypeError
            An object of type :class:`TextInput` was not passed.
        """
        if len(self.components) >= 5:
            raise ValueError("Maximum number of components exceeded.")

        if not isinstance(component, list):
            component = [component]

        for c in component:
            if not isinstance(c, TextInput):
                raise TypeError(
                    f"component must be of type 'TextInput' or a list of 'TextInput' objects, not {type(c).__name__}."
                )
            try:
                self.components[-1].append_item(c)
            except (ValueError, IndexError):
                self.components.append(ActionRow(c))

    def add_text_input(
        self,
        *,
        label: str,
        custom_id: str,
        style: TextInputStyle = TextInputStyle.short,
        placeholder: Optional[str] = None,
        value: Optional[str] = None,
        required: bool = True,
        min_length: Optional[int] = None,
        max_length: Optional[int] = None,
    ) -> None:
        """Creates and adds a text input component to the modal.

        To append a pre-existing instance of :class:`~disnake.ui.TextInput` use the
        :meth:`append_component` method.

        Parameters
        ----------
        label: :class:`str`
            The label of the text input.
        custom_id: :class:`str`
            The ID of the text input that gets received during an interaction.
        style: :class:`.TextInputStyle`
            The style of the text input.
        placeholder: Optional[:class:`str`]
            The placeholder text that is shown if nothing is entered.
        value: Optional[:class:`str`]
            The pre-filled value of the text input.
        required: :class:`bool`
            Whether the text input is required. Defaults to ``True``.
        min_length: Optional[:class:`int`]
            The minimum length of the text input.
        max_length: Optional[:class:`int`]
            The maximum length of the text input.

        Raises
        ------
        ValueError
            Maximum number of components (5) exceeded.
        """
        self.append_component(
            TextInput(
                label=label,
                custom_id=custom_id,
                style=style,
                placeholder=placeholder,
                value=value,
                required=required,
                min_length=min_length,
                max_length=max_length,
            )
        )

    async def callback(self, interaction: ModalInteraction, /) -> None:
        """|coro|

        The callback associated with this modal.

        This can be overriden by subclasses.

        Parameters
        ----------
        interaction: :class:`.ModalInteraction`
            The interaction that triggered this modal.
        """
        pass

    async def on_error(self, error: Exception, interaction: ModalInteraction) -> None:
        """|coro|

        A callback that is called when an error occurs.

        The default implementation prints the traceback to stderr.

        Parameters
        ----------
        error: :class:`Exception`
            The exception that was raised.
        interaction: :class:`.ModalInteraction`
            The interaction that triggered this modal.
        """
        traceback.print_exception(error.__class__, error, error.__traceback__, file=sys.stderr)

    async def on_timeout(self) -> None:
        """|coro|

        A callback that is called when the modal is removed from the cache
        without an interaction being made.
        """
        pass

    def to_components(self) -> ModalPayload:
        payload: ModalPayload = {
            "title": self.title,
            "custom_id": self.custom_id,
            "components": [component.to_component_dict() for component in self.components],
        }

        return payload

    async def _scheduled_task(self, interaction: ModalInteraction) -> None:
        try:
            await self.callback(interaction)
        except Exception as e:
            await self.on_error(e, interaction)
        finally:
            # if the interaction was responded to (no matter if in the callback or error handler),
            # the modal closed for the user and therefore can be removed from the store
            if interaction.response._response_type is not None:
                interaction._state._modal_store.remove_modal(
                    interaction.author.id, interaction.custom_id
                )

    def dispatch(self, interaction: ModalInteraction) -> None:
        asyncio.create_task(
            self._scheduled_task(interaction), name=f"disnake-ui-modal-dispatch-{self.custom_id}"
        )


class ModalStore:
    def __init__(self, state: ConnectionState) -> None:
        self._state = state
        # (user_id, Modal.custom_id): Modal
        self._modals: Dict[Tuple[int, str], Modal] = {}

    def add_modal(self, user_id: int, modal: Modal) -> None:
        loop = asyncio.get_running_loop()
        self._modals[(user_id, modal.custom_id)] = modal
        loop.create_task(self.handle_timeout(user_id, modal.custom_id, modal.timeout))

    def remove_modal(self, user_id: int, modal_custom_id: str) -> Modal:
        return self._modals.pop((user_id, modal_custom_id))

    async def handle_timeout(self, user_id: int, modal_custom_id: str, timeout: float) -> None:
        # Waits for the timeout and then removes the modal from cache, this is done just in case
        # the user closed the modal, as there isn't an event for that.

        await asyncio.sleep(timeout)
        try:
            modal = self.remove_modal(user_id, modal_custom_id)
        except KeyError:
            # The modal has already been removed.
            pass
        else:
            await modal.on_timeout()

    def dispatch(self, interaction: ModalInteraction) -> None:
        key = (interaction.author.id, interaction.custom_id)
        modal = self._modals.get(key)
        if modal is not None:
            modal.dispatch(interaction)
