"""Loading throbber widget used to indicate background activity."""

from __future__ import annotations

import importlib
import random
from collections import deque
from collections.abc import Sequence
from contextlib import suppress
from typing import Any, cast

from cleveragents.tui.quotes import THROBBER_QUOTES


def _load_static_base() -> type[Any]:
    """Return the Textual Static base class or a lightweight fallback."""

    try:
        return importlib.import_module("textual.widgets").Static
    except (ImportError, AttributeError):  # pragma: no cover - textual not installed

        class _FallbackStatic:
            """Minimal stand-in mirroring the API used by the widget."""

            def __init__(self, *args: object, **kwargs: object) -> None:
                self._text = ""
                self.visible = True

            def update(self, renderable: object) -> None:  # pragma: no cover - trivial
                self._text = str(renderable)

        return _FallbackStatic


try:  # pragma: no branch - optional dependency
    _RICH_TEXT_CLS: type[Any] | None = importlib.import_module("rich.text").Text
except ImportError:  # pragma: no cover - rich unavailable when textual missing
    _RICH_TEXT_CLS = None


_StaticBase = _load_static_base()


_RAINBOW_COLORS: tuple[str, ...] = (
    "#881177",
    "#aa3355",
    "#cc6666",
    "#ee9944",
    "#eedd00",
    "#99dd55",
    "#44dd88",
    "#22ccbb",
    "#00bbcc",
    "#0099cc",
    "#3366bb",
    "#663399",
)


class LoadingThrobber(_StaticBase):
    """Animated loading indicator supporting rainbow and quote styles."""

    _RAINBOW_FPS: float = 15.0
    _QUOTES_PERIOD_SECONDS: float = 3.0
    _FALLBACK_BLOCK_WIDTH: int = 30

    def __init__(
        self,
        *,
        style: str = "rainbow",
        quotes: Sequence[str] | None = None,
        id: str | None = None,
    ) -> None:
        super().__init__("", id=id)
        self._style = "rainbow"
        self._rainbow_offset = 0
        self._running = False
        self._rainbow_timer: Any | None = None
        self._quotes_timer: Any | None = None
        self._quotes: deque[str] = deque(quotes or THROBBER_QUOTES)
        self._rng = random.Random()
        self.set_style(style)
        self._apply_visibility(False)

    # ------------------------------------------------------------------
    # Lifecycle hooks
    # ------------------------------------------------------------------
    def on_mount(self) -> None:  # pragma: no cover - relies on Textual runtime
        self._apply_visibility(False)
        if self._style == "quotes":
            self._shuffle_quotes()

    # ------------------------------------------------------------------
    # Public API
    # ------------------------------------------------------------------
    def set_style(self, style: str) -> None:
        """Change the throbber style ("rainbow" or "quotes")."""

        normalized = style.strip().lower()
        if normalized not in {"rainbow", "quotes"}:
            raise ValueError("Throbber style must be 'rainbow' or 'quotes'.")
        if normalized == self._style:
            return
        self._style = normalized
        if self._style == "quotes":
            self._shuffle_quotes()
        if self._running:
            self._restart_animation()

    def show_loading(self) -> None:
        """Display the throbber and start animation if possible."""

        if self._running:
            return
        self._running = True
        self._apply_visibility(True)
        self._start_animation()

    def hide_loading(self) -> None:
        """Hide the throbber and stop any running animation."""

        if not self._running:
            return
        self._running = False
        self._stop_animation()
        self.update("")
        self._apply_visibility(False)

    @property
    def is_running(self) -> bool:
        """Return whether the throbber is currently animating."""

        return self._running

    # ------------------------------------------------------------------
    # Animation helpers
    # ------------------------------------------------------------------
    def _restart_animation(self) -> None:
        self._stop_animation()
        if self._running:
            self._start_animation()

    def _start_animation(self) -> None:
        if self._style == "rainbow":
            self._render_rainbow_frame()
            timer_fn = getattr(self, "set_interval", None)
            if callable(timer_fn):
                interval = 1.0 / self._RAINBOW_FPS
                self._rainbow_timer = timer_fn(interval, self._render_rainbow_frame)
        else:
            self._advance_quote()
            timer_fn = getattr(self, "set_interval", None)
            if callable(timer_fn):
                self._quotes_timer = timer_fn(
                    self._QUOTES_PERIOD_SECONDS, self._advance_quote
                )

    def _stop_animation(self) -> None:
        if self._rainbow_timer is not None:
            stop = getattr(self._rainbow_timer, "stop", None)
            if callable(stop):  # pragma: no branch - textual timer exposes stop()
                stop()
            self._rainbow_timer = None
        if self._quotes_timer is not None:
            stop = getattr(self._quotes_timer, "stop", None)
            if callable(stop):
                stop()
            self._quotes_timer = None

    # ------------------------------------------------------------------
    # Rendering
    # ------------------------------------------------------------------
    def _render_rainbow_frame(self) -> None:
        """Render a single rainbow frame and update the widget."""

        frame = self._build_rainbow_frame(self._rainbow_offset)
        self.update(frame)
        self._rainbow_offset = (self._rainbow_offset + 1) % len(_RAINBOW_COLORS)

    def _advance_quote(self) -> None:
        if not self._quotes:
            return
        quote = self._quotes[0]
        self._quotes.rotate(-1)
        self.update(quote)

    def _build_rainbow_frame(self, offset: int) -> Any:
        if _RICH_TEXT_CLS is None:
            # Fallback using plain text with indicators when Rich is unavailable.
            block = "━" * self._FALLBACK_BLOCK_WIDTH
            marker = "◆"
            return f"{block} {marker} {block}"

        text = _RICH_TEXT_CLS()
        colors = list(_RAINBOW_COLORS)
        for index in range(len(colors)):
            color = colors[(index + offset) % len(colors)]
            text.append("━", style=f"bold {color}")
        text.append(" ◆ ", style=f"bold {colors[offset % len(colors)]}")
        for index in range(len(colors) - 1, -1, -1):
            color = colors[(index + offset) % len(colors)]
            text.append("━", style=f"bold {color}")
        return text

    # ------------------------------------------------------------------
    # Utilities
    # ------------------------------------------------------------------
    def _shuffle_quotes(self) -> None:
        items = list(self._quotes)
        self._rng.shuffle(items)
        self._quotes = deque(items)

    def _apply_visibility(self, is_visible: bool) -> None:
        target = cast(Any, self)
        with suppress(AttributeError):
            target.display = is_visible
        with suppress(AttributeError):
            target.visible = is_visible
        styles = getattr(self, "styles", None)
        if styles is not None:  # pragma: no cover - requires Textual runtime
            if is_visible:
                styles.height = 1
                styles.min_height = 1
                styles.padding = (0, 1)
            else:
                styles.height = 0
                styles.min_height = 0
                styles.padding = 0


__all__ = ["LoadingThrobber"]
