"""Accordion interface module.

This module provides :class:`~discordcn.pycord.ui.AccordionInterface`, a
:class:`discord.ui.DesignerView` implementation that renders an "accordion"
layout using Discord's UI building blocks (text displays, sections, optional
container wrapping, and accessory buttons).

Exactly one section is expanded at a time:
- The active section shows its description and has a disabled accessory button
  using the "down" emoji.
- All other sections show only their header and have an enabled accessory
  button using the "right" emoji.

The view supports both non-persistent and persistent configurations:
- Non-persistent: configured by passing a non-``None`` ``timeout`` and leaving
  ``custom_id`` as ``None``.
- Persistent: configured by passing ``timeout=None`` and a non-empty
  ``custom_id``. In this mode, each section accessory button receives a
  deterministic ``custom_id`` derived from section content, position, and the
  view's ``custom_id``. Interactions rebuild a fresh view snapshot reflecting
  the newly expanded section.

The view can optionally wrap content in a :class:`discord.ui.Container`. When
container wrapping is disabled, the view receives the text display and sections
as direct items.

Notes:
- This interface edits the original message view on each selection via
  ``interaction.edit(view=...)``.
- In persistent mode, interactions do not mutate the original view instance;
  they construct a new :class:`discord.ui.DesignerView` containing the updated
  items.
"""

from __future__ import annotations

import hashlib
from functools import partial
from typing import TYPE_CHECKING, Any, Literal, NamedTuple, cast, overload

import discord
from typing_extensions import Self, override

from discordcn.pycord._utils import maybe_awaitable

if TYPE_CHECKING:
    from ._types import Callback, EmojiType

BLUE = discord.Color.blue()
"""Default color used for the container if not overridden."""

ARROW_DOWN = discord.PartialEmoji(name="⬇️")
"""Default emoji used for the active (expanded) section accessory."""

ARROW_RIGHT = discord.PartialEmoji(name="➡️")
"""Default emoji used for inactive (collapsed) section accessories."""


class _SectionMetadata(NamedTuple):
    """Metadata for a single accordion section.

    Attributes:
        header: The section title shown in the section text.
        description: The section body displayed only when the section is active.
    """

    header: str
    description: str


class _SectionStore(NamedTuple):
    """Internal store linking a rendered UI section to its metadata.

    Attributes:
        section: The UI section object that is inserted into the view.
        metadata: The logical content (header/description) for that section.
    """

    section: discord.ui.Section[AccordionInterface]
    metadata: _SectionMetadata


class _SectionButton(discord.ui.Button["AccordionInterface"]):
    """Accessory button used to expand a section.

    This button delegates to a callback and supports both sync and async
    callables via :func:`discordcn.pycord._utils.maybe_awaitable`.

    In non-persistent mode, the callback typically mutates the current view
    instance and edits the message using ``view=self``.

    In persistent mode, the callback typically builds a new view snapshot and
    edits the message using that snapshot.

    Args:
        emoji: The emoji shown on the accessory button.
        callback: A callable invoked with the interaction when the button is
            pressed. This may be sync or async.
        disabled: Whether the button is initially disabled.
        custom_id: Optional custom ID used for persistent components. If set,
            Discord can route interactions to this button across restarts.

    Attributes:
        _callback: Stored callback invoked when the button is pressed.
    """

    def __init__(
        self,
        *,
        emoji: EmojiType,
        callback: Callback[Any],
        disabled: bool,
        custom_id: str | None,
    ) -> None:
        super().__init__(
            emoji=emoji,
            style=discord.ButtonStyle.secondary,
            disabled=disabled,
            custom_id=custom_id,
        )
        self._callback: Callback[Any] = callback

    @override
    async def callback(self, interaction: discord.Interaction) -> None:
        """Handle a button press.

        Args:
            interaction: The interaction generated by the button press.

        Returns:
            None.
        """
        await maybe_awaitable(self._callback(interaction))


class AccordionInterface(discord.ui.DesignerView):
    """Interactive accordion view built from Discord UI primitives.

    The interface displays a header/description at the top and a list of
    collapsible sections underneath. Exactly one section is expanded at a time.

    The view can operate in two modes:
    - Non-persistent: ``timeout`` is an integer and ``custom_id`` must be
      ``None``.
    - Persistent: ``timeout`` is ``None`` and ``custom_id`` must be provided.
      Section accessory buttons will be created with deterministic ``custom_id``
      values derived from the section content and index plus the view's
      ``custom_id``. Interactions rebuild a view snapshot rather than mutating
      the original instance.

    Attributes:
        header: Title displayed at the top of the view.
        description: Description displayed under the title.
        custom_id: Persistent identifier for the view. This is ``None`` for
            non-persistent instances.
        color: Accent color used for container rendering and persistent rebuilds.
        _index: Index of the currently expanded section.
        _down_emoji: Emoji used for the expanded section's accessory button.
        _right_emoji: Emoji used for collapsed sections' accessory buttons.
        _container: Whether the view content is wrapped in a container.
        _sections: Internal list of stored sections (UI + metadata).
    """

    @overload
    def __init__(
        self,
        *args: tuple[str, str],
        header: str,
        description: str,
        custom_id: None = ...,
        color: discord.Color = ...,
        timeout: int = ...,
        down_emoji: EmojiType = ...,
        right_emoji: EmojiType = ...,
        container: bool = True,
        user_id: int | None = ...,
        auto_fold: bool = ...,
    ) -> None: ...

    @overload
    def __init__(
        self,
        *args: tuple[str, str],
        header: str,
        description: str,
        custom_id: str,
        color: discord.Color = ...,
        timeout: None = ...,
        down_emoji: EmojiType = ...,
        right_emoji: EmojiType = ...,
        container: bool = True,
        user_id: None = ...,
        auto_fold: Literal[True] = True,
    ) -> None: ...

    @override
    def __init__(
        self,
        *args: tuple[str, str],
        header: str,
        description: str,
        color: discord.Color = BLUE,
        custom_id: str | None = None,
        timeout: int | None = 900,
        down_emoji: EmojiType = ARROW_DOWN,
        right_emoji: EmojiType = ARROW_RIGHT,
        container: bool = True,
        user_id: int | None = None,
        auto_fold: bool = True,
    ) -> None:
        """Initialize an accordion interface.

        The view requires a consistent persistent configuration:
        - For non-persistent usage, set ``timeout`` to an integer and leave
          ``custom_id`` as ``None``.
        - For persistent usage, set ``timeout=None`` and provide a non-empty
          ``custom_id``. You also cannot provide a ``user_id`` in this case.

        Args:
            *args: Initial section tuples as ``(header, description)``. The first
                section (index 0) is expanded by default.
            header: Title text displayed at the top of the view.
            description: Description text displayed below the title.
            color: Accent color used when rendering the optional container and
                when rebuilding persistent snapshots.
            custom_id: Persistent identifier for the view. Must be provided when
                ``timeout=None`` and must be ``None`` otherwise.
            timeout: Timeout in seconds before the view times out. Set to
                ``None`` to create a persistent view.
            down_emoji: Emoji used for the active (expanded) section accessory.
            right_emoji: Emoji used for inactive (collapsed) section accessories.
            container: Whether to wrap content in a :class:`discord.ui.Container`.
            user_id: If provided, only interactions from this user are accepted.
                Interactions from other users are ignored.
            auto_fold: Whether to automatically fold sections when a new section is unfolded.
                Defaults to ``True`` for persistent views and ``False`` for non-persistent views.

        Raises:
            TypeError: If ``timeout`` and ``custom_id`` do not match one of the
                supported configurations:
                - ``timeout is None`` requires ``custom_id`` to be set.
                - ``timeout is not None`` requires ``custom_id`` to be ``None``.
        """
        if (custom_id is None and timeout is None) or (custom_id is not None and timeout is not None):
            msg = "custom_id must be set when timeout is None and vice versa"
            raise TypeError(msg)

        _args: list[_SectionMetadata] = [_SectionMetadata(*arg) for arg in args]

        self.header: str = header
        self.description: str = description

        self.custom_id: str | None = custom_id
        self.color: discord.Color = color
        self.user_id: int | None = user_id
        self._index: int = 0
        self._down_emoji: EmojiType = down_emoji
        self._right_emoji: EmojiType = right_emoji
        self._container: bool = container
        self._sections: list[_SectionStore] = [
            self._construct_section_store(metadata, i) for i, metadata in enumerate(_args)
        ]
        if self._persistent and not auto_fold:
            self.auto_fold: bool = True
        else:
            self.auto_fold = auto_fold

        items: tuple[discord.ui.ViewItem[AccordionInterface], ...] = (  # pyright: ignore[reportUnknownVariableType]
            discord.ui.TextDisplay(f"## {header}\n{description}"),
            *(_section.section for _section in self._sections),
        )
        if self._container:
            items = (discord.ui.Container(*items, color=color),)

        super().__init__(
            *items,  # pyright: ignore[reportUnknownArgumentType]
            timeout=timeout,
            disable_on_timeout=True,
        )

    @property
    def _persistent(self) -> bool:
        """Whether this view should behave as persistent.

        Returns:
            ``True`` if the view is persistent, ``False`` otherwise.
        """
        return self.custom_id is not None

    def _construct_section_store(
        self,
        metadata: _SectionMetadata,
        i: int,
        current_idx: int | None = None,
    ) -> _SectionStore:
        """Construct a rendered section and its store wrapper.

        The section is created expanded if ``i`` matches the resolved current
        index and collapsed otherwise.

        In persistent mode, the accessory button is assigned a deterministic
        ``custom_id`` derived from the section header/description, its index, and
        the view's ``custom_id``.

        Args:
            metadata: Section content (header and description).
            i: Section index within the view.
            current_idx: Index that should be expanded for the constructed
                section set. If ``None``, defaults to the view's current index.

        Returns:
            A :class:`_SectionStore` linking the UI section and metadata.
        """
        resolved_current_idx = self._index if current_idx is None else current_idx
        expanded = i == resolved_current_idx

        button_custom_id: str | None = None
        if self._persistent:
            digest_input = f"{metadata.header}{metadata.description}{i}{self.custom_id}"
            button_custom_id = hashlib.sha256(digest_input.encode()).hexdigest()

        return _SectionStore(
            section=discord.ui.Section(
                discord.ui.TextDisplay(
                    self._format_section(
                        metadata.header,
                        metadata.description if expanded else "",
                    )
                ),
                accessory=_SectionButton(
                    emoji=self._down_emoji if expanded else self._right_emoji,
                    callback=partial(self._persistent_callback, i=i)
                    if self._persistent
                    else partial(self._callback, i=i),
                    disabled=expanded,
                    custom_id=button_custom_id,
                ),
            ),
            metadata=metadata,
        )

    @staticmethod
    def _format_section(header: str, description: str) -> str:
        """Format a section into Markdown for a :class:`discord.ui.TextDisplay`.

        Args:
            header: Section title text.
            description: Section body text. Use an empty string to represent a
                collapsed section.

        Returns:
            A Markdown string combining header and description.
        """
        return f"### {header}\n{description}".strip()

    def _unfold_section(self, section: _SectionStore) -> None:
        """Expand a section in-place.

        Args:
            section: The section store to expand.

        Returns:
            None.
        """
        cast("discord.ui.TextDisplay[Self, Any]", section.section.items[0]).content = self._format_section(
            section.metadata.header,
            section.metadata.description,
        )
        button = cast("_SectionButton", section.section.accessory)
        button.emoji = self._down_emoji
        button.disabled = True

    def _fold_section(self, section: _SectionStore) -> None:
        """Collapse a section in-place.

        Args:
            section: The section store to collapse.

        Returns:
            None.
        """
        cast("discord.ui.TextDisplay[Self, Any]", section.section.items[0]).content = self._format_section(
            section.metadata.header,
            "",
        )
        button = cast("_SectionButton", section.section.accessory)
        button.emoji = self._right_emoji
        button.disabled = False

    async def _callback(self, interaction: discord.Interaction, i: int) -> None:
        """Handle selection of a section (non-persistent mode).

        Args:
            interaction: The interaction that triggered the selection.
            i: Index of the section to expand.

        Returns:
            None.
        """
        if interaction.user and interaction.user.id != self.user_id:
            return
        if self.auto_fold:
            self._fold_section(self._sections[self._index])
        self._unfold_section(self._sections[i])
        self._index = i

        await interaction.edit(view=self)  # pyright: ignore[reportUnknownMemberType]

    async def _persistent_callback(self, interaction: discord.Interaction, i: int) -> None:
        """Handle selection of a section (persistent mode).

        Args:
            interaction: The interaction that triggered the selection.
            i: Index of the section to expand in the rebuilt snapshot.

        Returns:
            None.
        """
        items: tuple[discord.ui.ViewItem[AccordionInterface], ...] = tuple(
            self._construct_section_store(
                metadata=section_store.metadata,
                i=section_store_i,
                current_idx=i,
            ).section
            for section_store_i, section_store in enumerate(self._sections)
        )

        if self._container:
            items = (discord.ui.Container(*items, color=self.color),)

        await interaction.edit(  # pyright: ignore[reportUnknownMemberType]
            view=discord.ui.DesignerView(
                *items,
            )
        )

    def add_section(self, header: str, description: str) -> int:
        """Add a new section to the accordion.

        Args:
            header: Section title.
            description: Section body.

        Returns:
            The index of the newly added section.
        """
        metadata = _SectionMetadata(header, description)
        self._sections.append(self._construct_section_store(metadata, len(self._sections)))
        return len(self._sections) - 1

    def remove_section(self, i: int) -> None:
        """Remove a section from the accordion.

        Args:
            i: Index of the section to remove.

        Returns:
            None.
        """
        if self._index == i:
            self._index -= 1
        del self._sections[i]

    def insert_section(self, i: int, header: str, description: str) -> None:
        """Insert a new section at a specific index.

        Args:
            i: Index to insert at.
            header: Section title.
            description: Section body.

        Returns:
            None.
        """
        metadata = _SectionMetadata(header, description)
        self._sections.insert(i, self._construct_section_store(metadata, i))

    async def finish(self) -> None:
        """Finalize the view by disabling components and stopping the view.

        Returns:
            None.
        """
        await super().on_timeout()
        self.stop()

    @override
    async def on_timeout(self) -> None:
        """Handle view timeout.

        Returns:
            None.
        """
        await self.finish()


__all__ = ("AccordionInterface",)
