import discord
from typing import List
from typing import Optional
from typing import Iterable
import traceback

MAX_SELECT_PER_PAGE = 2
MAX_OPTIONS_PER_SELECT = 25
CHANNELS_PER_PAGE = MAX_SELECT_PER_PAGE * MAX_OPTIONS_PER_SELECT


# ---------- util ----------
def sort_channels_for_display(
    guild: discord.Guild, channels: list[discord.abc.GuildChannel]
) -> list[discord.abc.GuildChannel]:
    try:
        result: list[discord.abc.GuildChannel] = []

        has_category = any(isinstance(ch, discord.CategoryChannel) for ch in channels)

        # ===== カテゴリを使わないケース（VOICEのみ等） =====
        if not has_category:
            result = [ch for ch in channels if not isinstance(ch, discord.Thread)]
            result.sort(key=lambda c: c.position)
            return result

        # ===== 通常（カテゴリ構造あり） =====

        # --- カテゴリ未所属 ---
        no_category = [
            ch
            for ch in channels
            if not isinstance(ch, (discord.CategoryChannel, discord.Thread))
            and ch.category_id is None
        ]
        no_category.sort(key=lambda c: c.position)
        result.extend(no_category)

        # --- カテゴリ配下 ---
        category_ids = {
            ch.id for ch in channels if isinstance(ch, discord.CategoryChannel)
        }

        category_map: dict[int, list[discord.abc.GuildChannel]] = {}

        for ch in channels:
            if (
                not isinstance(ch, (discord.CategoryChannel, discord.Thread))
                and ch.category_id in category_ids
            ):
                category_map.setdefault(ch.category_id, []).append(ch)

        categories = [ch for ch in channels if isinstance(ch, discord.CategoryChannel)]
        categories.sort(key=lambda c: c.position)

        for category in categories:
            result.append(category)
            children = category_map.get(category.id, [])
            children.sort(key=lambda c: c.position)
            result.extend(children)

        # --- Thread ---
        threads = [ch for ch in channels if isinstance(ch, discord.Thread)]
        threads.sort(key=lambda t: (t.parent_id or 0, t.created_at or 0, t.name))
        result.extend(threads)

        return result
    except Exception:
        traceback.print_exc()
        return []


def filter_channels_by_type(
    guild: discord.Guild, channel_types: Optional[Iterable[str]]
) -> List[discord.abc.GuildChannel]:
    if not channel_types:
        channel_types = {"TEXT", "VOICE", "STAGE", "FORUM", "THREAD", "CATEGORY"}

    channel_types = set(channel_types)
    channels: list[discord.abc.GuildChannel] = []

    if "CATEGORY" in channel_types:
        channels.extend(guild.categories)

    if "TEXT" in channel_types:
        channels.extend(guild.text_channels)

    if "VOICE" in channel_types:
        channels.extend(guild.voice_channels)

    if "STAGE" in channel_types:
        channels.extend(guild.stage_channels)

    if "FORUM" in channel_types:
        channels.extend(guild.forums)

    if "THREAD" in channel_types:
        channels.extend(guild.threads)

    return channels


class ChannelSelect(discord.ui.Select):
    def __init__(
        self,
        channels: List[discord.abc.GuildChannel],
        selected_ids: set[int],
    ):
        options = []

        for ch in channels:
            label = f"#{ch.name}" if hasattr(ch, "name") else str(ch.id)
            options.append(
                discord.SelectOption(
                    label=label, value=str(ch.id), default=ch.id in selected_ids
                )
            )

        super().__init__(
            placeholder="チャンネルを選択してください",
            min_values=0,
            max_values=len(options),
            options=options,
        )

    async def callback(self, interaction: discord.Interaction):
        view: ChannelSelectView = self.view  # type: ignore

        for option in self.options:
            view.selected_channel_ids.discard(int(option.value))

        for value in self.values:
            view.selected_channel_ids.add(int(value))

        await interaction.response.defer()


class ChannelSelectView(discord.ui.View):
    def __init__(
        self,
        interaction: discord.Interaction,
        channels: List[discord.abc.GuildChannel],
        default_channels: Optional[List[discord.abc.GuildChannel]] = None,
        extra_items: Optional[List[discord.ui.Item]] = None,
    ):
        super().__init__(timeout=None)

        self.interaction = interaction
        self.channels = channels
        self.page = 0

        self.selected_channel_ids: set[int] = {ch.id for ch in (default_channels or [])}

        self.extra_items = extra_items
        self._refresh_items()

    def _refresh_items(self):
        self.clear_items()

        start = self.page * CHANNELS_PER_PAGE
        end = start + CHANNELS_PER_PAGE
        page_channels = self.channels[start:end]

        for i in range(0, len(page_channels), MAX_OPTIONS_PER_SELECT):
            self.add_item(
                ChannelSelect(
                    channels=page_channels[i : i + MAX_OPTIONS_PER_SELECT],
                    selected_ids=self.selected_channel_ids,
                )
            )

        self.add_item(PrevButton())
        self.add_item(NextButton())

        if self.extra_items:
            for item in self.extra_items:
                self.add_item(item)

    def get_selected_channels(self) -> List[discord.abc.GuildChannel]:
        guild = self.interaction.guild
        if not guild:
            return []

        return [
            guild.get_channel(ch_id)
            for ch_id in self.selected_channel_ids
            if guild.get_channel(ch_id)
        ]


class PrevButton(discord.ui.Button):
    def __init__(self):
        super().__init__(label="前へ", style=discord.ButtonStyle.secondary)

    async def callback(self, interaction: discord.Interaction):
        view: ChannelSelectView = self.view  # type: ignore
        if view.page > 0:
            view.page -= 1
            view._refresh_items()
        await interaction.response.edit_message(view=view)


class NextButton(discord.ui.Button):
    def __init__(self):
        super().__init__(label="次へ", style=discord.ButtonStyle.secondary)

    async def callback(self, interaction: discord.Interaction):
        view: ChannelSelectView = self.view  # type: ignore
        max_page = (len(view.channels) - 1) // CHANNELS_PER_PAGE
        if view.page < max_page:
            view.page += 1
            view._refresh_items()
        await interaction.response.edit_message(view=view)


class ChannelSelectPagination:
    def __init__(
        self,
        interaction: discord.Interaction,
        channel_types: Optional[List[str]] = None,
        edit: bool = False,
        default_values: Optional[List[discord.abc.GuildChannel]] = None,
        extra_items: Optional[List[discord.ui.Item]] = None,
    ):
        self.interaction = interaction
        self.edit = edit

        guild = interaction.guild
        if not guild:
            raise RuntimeError("Guild only")

        channels = filter_channels_by_type(guild=guild, channel_types=channel_types)

        self.channels = sort_channels_for_display(guild=guild, channels=channels)

        self.view = ChannelSelectView(
            interaction=interaction,
            channels=self.channels,
            default_channels=default_values,
            extra_items=extra_items,
        )

    async def send(self, embed: discord.Embed = None):
        try:
            if self.edit:
                await self.interaction.response.edit_message(
                    embed=embed, view=self.view
                )
            else:
                await self.interaction.response.send_message(
                    embed=embed, view=self.view
                )
        except Exception:
            traceback.print_exc()

    def get_selected_channels(self):
        return self.view.get_selected_channels()
