"""Prompt widget with mode-aware symbol handling."""

from __future__ import annotations

import importlib
from collections.abc import Iterable
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Protocol, cast

from cleveragents.tui.input.modes import InputMode, InputModeRouter


class _InputChangedEvent(Protocol):
    input: Any
    value: str


class _MutableValueInput(Protocol):
    value: str

    def focus(self) -> None: ...


class _StaticWidget(Protocol):
    def __init__(self, text: str = "", *args: object, **kwargs: object) -> None: ...

    def update(self, text: str) -> None: ...


class _HorizontalWidget(Protocol):
    def __init__(
        self,
        *,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
    ) -> None: ...

    def focus(self) -> None: ...


class _InputWidget(_MutableValueInput, Protocol):
    def __init__(self, *args: object, **kwargs: object) -> None: ...


def _load_input_base() -> type[Any]:
    try:
        return importlib.import_module("textual.widgets").Input
    except Exception:  # pragma: no cover - optional dependency

        class _FallbackInput:
            value = ""

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

            def focus(self) -> None:  # pragma: no cover - API parity
                return None

        return _FallbackInput


def _load_static_base() -> type[Any]:
    try:
        return importlib.import_module("textual.widgets").Static
    except Exception:  # pragma: no cover - optional dependency

        class _FallbackStatic:
            def __init__(self, text: str = "", *args: object, **kwargs: object) -> None:
                self._text = text

            def update(self, text: str) -> None:
                self._text = text

        return _FallbackStatic


def _load_horizontal_base() -> type[Any]:
    try:
        return importlib.import_module("textual.containers").Horizontal
    except Exception:  # pragma: no cover - optional dependency

        class _FallbackHorizontal:
            def __init__(self, *args: object, **kwargs: object) -> None:
                del args, kwargs
                self._children: list[Any] = []

            def compose(self) -> Iterable[Any]:
                yield from self._children

            def mount(self, widget: Any) -> None:
                self._children.append(widget)

            def focus(self) -> None:  # pragma: no cover - API parity
                return None

        return _FallbackHorizontal


_InputBase = cast(type[_InputWidget], _load_input_base())
_StaticBase = cast(type[_StaticWidget], _load_static_base())
_HorizontalBase = cast(type[_HorizontalWidget], _load_horizontal_base())
_TEXTUAL_AVAILABLE = _InputBase.__module__.startswith("textual.")


if _TEXTUAL_AVAILABLE:
    from textual.geometry import Size as _TextualSize  # type: ignore[import]
    from textual.reactive import reactive as _textual_reactive  # type: ignore[import]
    from textual.widgets import Input as _TextualInput  # type: ignore[import]

    class _PromptTextInput(_TextualInput):
        """``Input`` subclass that prevents ``layout=True`` refreshes per keystroke.

        ``textual.widgets.Input._watch_value`` sets ``self.virtual_size`` on
        every keystroke.  ``virtual_size`` is defined as
        ``Reactive(layout=True)`` on ``Widget``, so each change calls
        ``refresh(layout=True)`` which enqueues ANSI escape sequences in
        Textual's ``WriterThread``.  The thread only flushes stdout when its
        queue is empty (``qsize() == 0``); with continuous layout writes the
        queue never drains between keystrokes, so characters are invisible
        until Enter produces a large output burst that finally drains it.

        Overriding ``virtual_size`` with ``layout=False`` neutralises this:
        the attribute can still be set by ``_watch_value`` without triggering
        a full layout recalculation.  The prompt is single-line and never
        needs horizontal scrolling, so this is safe.
        """

        virtual_size: _textual_reactive[_TextualSize] = _textual_reactive(
            _TextualSize(0, 0),
            repaint=False,
            layout=False,
        )

    _PromptInputBase: type[Any] = _PromptTextInput
else:
    _PromptInputBase = _InputBase

if TYPE_CHECKING:  # pragma: no cover - typing only
    _ComposeResult = Iterable[Any]
else:
    _ComposeResult = Iterable[Any]


@dataclass(slots=True, frozen=True)
class PromptSubmitted:
    """Submitted prompt payload."""

    text: str


_PROMPT_NORMAL = chr(0x276F)
_PROMPT_COMMAND = "/"
_PROMPT_SHELL = "$"
_PROMPT_MULTILINE = chr(0x2630)

_PROMPT_SYMBOLS: dict[InputMode, str] = {
    InputMode.NORMAL: _PROMPT_NORMAL,
    InputMode.COMMAND: _PROMPT_COMMAND,
    InputMode.SHELL: _PROMPT_SHELL,
    InputMode.MULTILINE: _PROMPT_MULTILINE,
}


class _PromptSymbolMixin:
    """Mixin providing mode-aware prompt symbol updates.

    Subclasses must define a ``value`` property (str) and implement
    ``_apply_symbol(symbol: str) -> None``.

    .. note::

        The spec (§29257) refers to "PromptTextArea" — implying a
        multi-line ``TextArea`` widget.  However, ``TextArea.__init__()``
        dropped the ``placeholder`` keyword in textual >=1.0, so this
        implementation uses ``textual.widgets.Input`` (single-line)
        instead.  The ``MULTILINE`` mode is detected by content
        (``\\n`` or triple-backtick) and the symbol updates correctly,
        but the underlying ``Input`` widget cannot display multiple
        lines.  A future migration to ``TextArea`` (once placeholder
        support is restored or replaced) would enable true multi-line
        editing per spec §30209-30218.
    """

    _current_symbol: str

    def _apply_symbol(self, symbol: str) -> None:
        raise NotImplementedError

    def _update_symbol(self, raw_text: str) -> None:
        symbol = _PROMPT_SYMBOLS[InputModeRouter.detect_mode(raw_text)]
        self._current_symbol = symbol
        self._apply_symbol(symbol)

    @property
    def prompt_symbol(self) -> str:
        return self._current_symbol

    def consume_text(self) -> PromptSubmitted:
        text = self.value
        self.value = ""
        return PromptSubmitted(text=text)


class _TextualPromptInput(_PromptSymbolMixin, _HorizontalBase):
    """Composite widget that displays a mode-aware prompt symbol."""

    def __init__(
        self,
        placeholder: str = "",
        *,
        name: str | None = None,
        id: str | None = None,
        classes: str | None = None,
    ) -> None:
        horizontal = cast(Any, super())
        horizontal.__init__(name=name, id=id, classes=classes)
        self._symbol_widget = _StaticBase("", classes="prompt-symbol")
        input_id = f"{id}--input" if id else None
        self._input = cast(
            _MutableValueInput, _PromptInputBase(placeholder=placeholder, id=input_id)
        )
        self._current_symbol = _PROMPT_SYMBOLS[InputMode.NORMAL]
        self._update_symbol(self._input.value)

    def compose(self) -> _ComposeResult:
        yield self._symbol_widget
        yield self._input

    @property
    def value(self) -> str:
        return self._input.value

    @value.setter
    def value(self, new_value: str) -> None:
        self._input.value = new_value
        self._update_symbol(new_value)

    def focus(self) -> None:  # pragma: no cover - delegating to inner input
        self._input.focus()

    def on_input_changed(self, event: _InputChangedEvent) -> None:
        if event.input is self._input:
            self._update_symbol(event.value)

    def _apply_symbol(self, symbol: str) -> None:
        self._symbol_widget.update(symbol)


class _FallbackPromptInput(_PromptSymbolMixin):
    """Fallback prompt input used when Textual is unavailable."""

    def __init__(self, placeholder: str = "", **_: object) -> None:
        self.placeholder = placeholder
        self._input = cast(_MutableValueInput, _InputBase())
        self._current_symbol = _PROMPT_SYMBOLS[InputMode.NORMAL]
        self._update_symbol(self._input.value)

    @property
    def value(self) -> str:
        return getattr(self._input, "value", "")

    @value.setter
    def value(self, new_value: str) -> None:
        self._input.value = new_value
        self._update_symbol(new_value)

    def focus(self) -> None:  # pragma: no cover - API parity
        focus = getattr(self._input, "focus", None)
        if callable(focus):
            focus()

    def _apply_symbol(self, symbol: str) -> None:
        self._current_symbol = symbol


PromptInput = _TextualPromptInput if _TEXTUAL_AVAILABLE else _FallbackPromptInput
