"""Keyboard markup builders — inline keyboards, reply keyboards, and keyboard removal."""

from __future__ import annotations

import copy
from itertools import cycle
from typing import Any, Iterator, Optional

from pydantic import BaseModel, Field


class InlineKeyboardButton(BaseModel):
    """A button in an inline keyboard row.

    Style options: default, primary, secondary, destructive, success, info
    Shape options: rounded (default), pill, sharp, square, circle
    Size options: sm, md (default), lg
    """

    text: str
    callback_data: Optional[str] = Field(None, serialization_alias="callbackData")
    url: Optional[str] = None
    style: Optional[str] = None  # default|primary|secondary|destructive|success|info
    shape: Optional[str] = None  # rounded|pill|sharp|square|circle
    size: Optional[str] = None  # sm|md|lg
    copy_text: Optional[dict] = None  # {"text": "string to copy"} — frontend-only clipboard action


class InlineKeyboardMarkup(BaseModel):
    """An inline keyboard that appears attached to the message."""

    inline_keyboard: list[list[InlineKeyboardButton]]

    @classmethod
    def from_column(cls, buttons: list[InlineKeyboardButton]) -> InlineKeyboardMarkup:
        """Create a keyboard with each button on its own row."""
        return cls(inline_keyboard=[[btn] for btn in buttons])

    @classmethod
    def from_row(cls, buttons: list[InlineKeyboardButton]) -> InlineKeyboardMarkup:
        """Create a keyboard with all buttons in a single row."""
        return cls(inline_keyboard=[buttons])

    def add_row(self, *buttons: InlineKeyboardButton) -> InlineKeyboardMarkup:
        """Add a row of buttons and return self for chaining."""
        self.inline_keyboard.append(list(buttons))
        return self

    def to_dict(self) -> dict:
        return self.model_dump(exclude_none=True, by_alias=True)


class ReplyKeyboardButton(BaseModel):
    """A button in a reply keyboard row."""

    text: str


class ReplyKeyboardMarkup(BaseModel):
    """A custom reply keyboard shown below the input field."""

    keyboard: list[list[ReplyKeyboardButton]]
    resize_keyboard: bool = True
    one_time_keyboard: bool = False
    input_field_placeholder: Optional[str] = None

    @classmethod
    def from_column(cls, buttons: list[str], **kwargs: Any) -> ReplyKeyboardMarkup:
        """Create a keyboard with each button text on its own row."""
        return cls(
            keyboard=[[ReplyKeyboardButton(text=t)] for t in buttons],
            **kwargs,
        )

    @classmethod
    def from_row(cls, buttons: list[str], **kwargs: Any) -> ReplyKeyboardMarkup:
        """Create a keyboard with all button texts in a single row."""
        return cls(
            keyboard=[[ReplyKeyboardButton(text=t) for t in buttons]],
            **kwargs,
        )

    def add_row(self, *texts: str) -> ReplyKeyboardMarkup:
        """Add a row of buttons by text and return self for chaining."""
        self.keyboard.append([ReplyKeyboardButton(text=t) for t in texts])
        return self

    def to_dict(self) -> dict:
        return self.model_dump(exclude_none=True)


class ReplyKeyboardRemove(BaseModel):
    """Request to remove the current reply keyboard."""

    remove_keyboard: bool = True

    def to_dict(self) -> dict:
        return self.model_dump()


class ForceReply(BaseModel):
    """Request a force-reply from the user."""

    force_reply: bool = True
    input_field_placeholder: Optional[str] = None
    selective: Optional[bool] = None

    def to_dict(self) -> dict:
        return self.model_dump(exclude_none=True)


# ---------------------------------------------------------------------------
# Builder
# ---------------------------------------------------------------------------


class InlineKeyboardBuilder:
    """Fluent builder for :class:`InlineKeyboardMarkup`.

    Provides a chainable API inspired by ``aiogram``'s keyboard builder
    pattern.  Buttons are accumulated with :meth:`button` and flushed
    into rows either explicitly via :meth:`row` or automatically via
    :meth:`adjust`.

    Example::

        markup = (
            InlineKeyboardBuilder()
            .button(text="Yes", callback_data="yes", style="success")
            .button(text="No", callback_data="no", style="destructive")
            .row()
            .button(text="Cancel", callback_data="cancel")
            .as_markup()
        )
        # Result: 2 rows — ["Yes", "No"] and ["Cancel"]

    Redistribution with :meth:`adjust`::

        builder = InlineKeyboardBuilder()
        for i in range(6):
            builder.button(text=str(i), callback_data=str(i))
        builder.adjust(2, repeat=True)
        markup = builder.as_markup()
        # Result: 3 rows of 2 buttons each
    """

    def __init__(self) -> None:
        """Initialize an empty keyboard builder.

        Buttons are added via :meth:`button` and flushed into rows
        with :meth:`row` or :meth:`adjust`.
        """
        self._buttons: list[InlineKeyboardButton] = []
        self._rows: list[list[InlineKeyboardButton]] = []

    # -- Accumulation -------------------------------------------------------

    def button(
        self,
        text: str,
        callback_data: Optional[str] = None,
        url: Optional[str] = None,
        style: Optional[str] = None,
        shape: Optional[str] = None,
        size: Optional[str] = None,
        copy_text: Optional[dict] = None,
    ) -> InlineKeyboardBuilder:
        """Add a button to the pending buffer.

        Parameters match :class:`InlineKeyboardButton` fields exactly.
        Returns *self* for chaining.
        """
        btn = InlineKeyboardButton(
            text=text,
            callback_data=callback_data,
            url=url,
            style=style,
            shape=shape,
            size=size,
            copy_text=copy_text,
        )
        self._buttons.append(btn)
        return self

    def row(self) -> InlineKeyboardBuilder:
        """Flush pending buttons into a committed row.

        If there are no pending buttons this is a no-op.  Returns
        *self* for chaining.
        """
        if self._buttons:
            self._rows.append(self._buttons)
            self._buttons = []
        return self

    # -- Redistribution -----------------------------------------------------

    def adjust(self, *sizes: int, repeat: bool = False) -> InlineKeyboardBuilder:
        """Redistribute *all* buttons (committed + pending) into new rows.

        Each value in *sizes* specifies how many buttons go into the
        corresponding row.  When *repeat* is ``True`` the sizes sequence
        is cycled indefinitely; otherwise the last value is reused for
        any remaining buttons.

        Returns *self* for chaining.
        """
        # Collect every button
        all_buttons: list[InlineKeyboardButton] = []
        for row in self._rows:
            all_buttons.extend(row)
        all_buttons.extend(self._buttons)

        # Clear state
        self._rows = []
        self._buttons = []

        if not all_buttons or not sizes:
            self._buttons = all_buttons
            return self

        if repeat:
            size_iter = cycle(sizes)
        else:
            # Yield each explicit size, then repeat the last one forever.
            def _sizes_then_last() -> Iterator[int]:
                yield from sizes
                while True:
                    yield sizes[-1]

            size_iter = _sizes_then_last()

        idx = 0
        for chunk_size in size_iter:
            if idx >= len(all_buttons):
                break
            self._rows.append(all_buttons[idx : idx + chunk_size])
            idx += chunk_size

        return self

    # -- Terminal -----------------------------------------------------------

    def as_markup(self) -> InlineKeyboardMarkup:
        """Build and return the final :class:`InlineKeyboardMarkup`.

        Any remaining pending buttons are flushed into a last row
        before constructing the markup.
        """
        self.row()  # flush any pending buttons
        return InlineKeyboardMarkup(inline_keyboard=self._rows)

    # -- Utilities ----------------------------------------------------------

    def copy(self) -> InlineKeyboardBuilder:
        """Return a deep copy of this builder for reuse."""
        return copy.deepcopy(self)
