"""Base search picker classes with common patterns.

Provides abstract base classes for building search pickers that fetch data
from the server API on each keystroke. Two variants are provided:

- SingleSelectPicker: For selecting a single item (agents, schemas, templates)
- MultiSelectPicker: For selecting multiple items (models, tags)

Both handle the common patterns of:
- Input field with search
- OptionList displaying results
- Status line showing search state
- Selected display showing current selection(s)
- Debounced API calls via @work(exclusive=True)
- Error handling for API failures
"""

from abc import abstractmethod
from typing import Any, Generic, TypeVar

from textual import on, work
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.message import Message
from textual.widget import Widget
from textual.widgets import Input, OptionList, Static
from textual.widgets.option_list import Option

from alloy_runtime_sdk.api_client.client import ApiClient
from alloy_runtime_sdk.exceptions.errors import RequestError

# Type variable for the item type stored in search results
TItem = TypeVar("TItem")


class BaseSearchPicker(Widget, Generic[TItem]):
    """Abstract base class for search pickers.

    Subclasses must implement:
    - _fetch_items(query): Fetch items from API
    - _format_item(item): Format item for display in option list
    - _get_item_id(item): Get unique identifier for an item
    - _is_item_selected(item): Check if item is currently selected
    - _handle_item_selection(index): Handle selection of item at index
    - _format_selection_display(): Format current selection for display
    - _get_result_count_message(count): Get status message for result count
    """

    # All pickers use the shared .picker CSS class from forms.tcss
    DEFAULT_CLASSES = "picker"

    def __init__(
        self,
        client: ApiClient,
        placeholder: str = "Type to search...",
        empty_message: str = "No selection",
        fetch_on_mount: bool = False,
        **kwargs: Any,
    ) -> None:
        """Initialize the picker.

        Args:
            client: ApiClient instance for API calls
            placeholder: Placeholder text for search input
            empty_message: Message shown when nothing is selected
            fetch_on_mount: If True, fetch initial results on mount (e.g., recent items)
        """
        super().__init__(**kwargs)
        self._client = client
        self._placeholder = placeholder
        self._empty_message = empty_message
        self._search_results: list[TItem] = []
        self._last_query: str = ""
        self._fetch_on_mount = fetch_on_mount

    def compose(self) -> ComposeResult:
        with Vertical():
            yield Input(placeholder=self._placeholder, id="search-input")
            yield OptionList(id="search-options")
            yield Static("Type to search...", classes="status-line", id="status")
            yield Static(self._empty_message, classes="selected-display", id="selected")

    def on_mount(self) -> None:
        """Optionally fetch initial results on mount."""
        if self._fetch_on_mount:
            self._fetch_initial()

    @on(Input.Changed, "#search-input")
    def on_search_changed(self, event: Input.Changed) -> None:
        """Fetch results from API on each keystroke."""
        query = event.value.strip()
        if query != self._last_query:
            self._last_query = query
            self._search(query)

    @work(exclusive=True, group="picker_search")
    async def _fetch_initial(self) -> None:
        """Fetch initial results on mount (e.g., recent items)."""
        status = self.query_one("#status", Static)
        status.update("Loading...")

        try:
            self._search_results = await self._fetch_items("")
            self._update_options()

            count = len(self._search_results)
            if count > 0:
                status.update(f"{count} recent - type to filter")
            else:
                status.update("Type to search...")

        except Exception as e:
            status.update(f"[red]Error: {e}[/]")
            self._search_results = []

    @work(exclusive=True, group="picker_search")
    async def _search(self, query: str) -> None:
        """Fetch items matching query from the API."""
        status = self.query_one("#status", Static)
        options = self.query_one("#search-options", OptionList)

        if not query:
            if self._fetch_on_mount:
                # Re-fetch initial results when query is cleared
                await self._do_search("")
                return
            else:
                status.update("Type to search...")
                options.clear_options()
                self._search_results = []
                return

        await self._do_search(query)

    async def _do_search(self, query: str) -> None:
        """Perform the actual search."""
        status = self.query_one("#status", Static)
        options = self.query_one("#search-options", OptionList)

        status.update("Searching...")

        try:
            self._search_results = await self._fetch_items(query)
            self._update_options()

            count = len(self._search_results)
            if query:
                status.update(self._get_result_count_message(count))
            elif count > 0:
                status.update(f"{count} recent - type to filter")
            else:
                status.update("Type to search...")

        except RequestError as e:
            status.update(f"[red]Error: {e}[/]")
            self._search_results = []
            options.clear_options()
        except Exception as e:
            status.update(f"[red]Error: {e}[/]")
            self._search_results = []
            options.clear_options()

    def _update_options(self) -> None:
        """Update the option list with current search results."""
        options = self.query_one("#search-options", OptionList)
        options.clear_options()

        for i, item in enumerate(self._search_results):
            display_text = self._format_item(item)
            if self._is_item_selected(item):
                display_text = f"[green]✓[/] {display_text}"
            else:
                display_text = f"  {display_text}"
            options.add_option(Option(display_text, id=f"opt-{i}"))

        if self._search_results:
            options.highlighted = 0

    def _update_selected_display(self) -> None:
        """Update the display of selected item(s)."""
        display = self.query_one("#selected", Static)
        selection_text = self._format_selection_display()
        if selection_text:
            display.update(selection_text)
            display.add_class("has-selection")
        else:
            display.update(self._empty_message)
            display.remove_class("has-selection")

    @on(OptionList.OptionSelected, "#search-options")
    def on_option_selected(self, event: OptionList.OptionSelected) -> None:
        """Handle option selection via click or enter."""
        self._handle_item_selection(event.option_index)

    @abstractmethod
    async def _fetch_items(self, query: str) -> list[TItem]:
        """Fetch items from API matching query.

        Args:
            query: Search query string

        Returns:
            List of items matching the query
        """
        ...

    @abstractmethod
    def _format_item(self, item: TItem) -> str:
        """Format item for display in option list.

        Args:
            item: Item to format

        Returns:
            Formatted string for display
        """
        ...

    @abstractmethod
    def _get_item_id(self, item: TItem) -> str:
        """Get unique identifier for an item.

        Args:
            item: Item to get ID for

        Returns:
            Unique identifier string
        """
        ...

    @abstractmethod
    def _is_item_selected(self, item: TItem) -> bool:
        """Check if an item is currently selected.

        Args:
            item: Item to check

        Returns:
            True if item is selected
        """
        ...

    @abstractmethod
    def _handle_item_selection(self, index: int) -> None:
        """Handle selection of item at the given index.

        Args:
            index: Index of item in search results
        """
        ...

    @abstractmethod
    def _format_selection_display(self) -> str | None:
        """Format current selection for display.

        Returns:
            Formatted string for display, or None if nothing selected
        """
        ...

    def _get_result_count_message(self, count: int) -> str:
        """Get status message for result count.

        Args:
            count: Number of results found

        Returns:
            Status message string
        """
        if count == 0:
            return "No results found"
        return f"{count} result{'s' if count != 1 else ''} - Enter to select"

    @abstractmethod
    def clear(self) -> None:
        """Clear the current selection."""
        ...


class SingleSelectPicker(BaseSearchPicker[TItem], Generic[TItem]):
    """Base class for single-selection pickers.

    Handles the common pattern of selecting one item from search results
    with toggle behavior (selecting same item deselects it).

    Subclasses must implement:
    - _fetch_items(query): Fetch items from API
    - _format_item(item): Format item for display
    - _get_item_id(item): Get unique identifier for item
    """

    class SelectionChanged(Message):
        """Posted when selection changes."""

        def __init__(self, selected_id: str | None, selected_item: Any | None) -> None:
            self.selected_id = selected_id
            self.selected_item = selected_item
            super().__init__()

    def __init__(
        self,
        client: ApiClient,
        placeholder: str = "Type to search...",
        empty_message: str = "No selection",
        **kwargs: Any,
    ) -> None:
        super().__init__(
            client=client,
            placeholder=placeholder,
            empty_message=empty_message,
            **kwargs,
        )
        self._selected_item: TItem | None = None

    @property
    def selected_id(self) -> str | None:
        """Get the ID of the selected item, or None if nothing selected."""
        if self._selected_item is None:
            return None
        return self._get_item_id(self._selected_item)

    @property
    def selected_item(self) -> TItem | None:
        """Get the full selected item, or None if nothing selected."""
        return self._selected_item

    def _is_item_selected(self, item: TItem) -> bool:
        """Check if an item is currently selected."""
        if self._selected_item is None:
            return False
        return self._get_item_id(item) == self._get_item_id(self._selected_item)

    def _handle_item_selection(self, index: int) -> None:
        """Handle selection of item at the given index with toggle behavior."""
        if index < 0 or index >= len(self._search_results):
            return

        item = self._search_results[index]

        # Toggle selection if clicking the same item
        if self._selected_item is not None and self._get_item_id(
            item
        ) == self._get_item_id(self._selected_item):
            self._selected_item = None
        else:
            self._selected_item = item

        self._update_selected_display()
        self._update_options()
        self.post_message(self.SelectionChanged(self.selected_id, self._selected_item))

    def _format_selection_display(self) -> str | None:
        """Format current selection for display."""
        if self._selected_item is None:
            return None
        display_text = self._format_item(self._selected_item)
        return f"[bold green]Selected:[/] {display_text}"

    def clear(self) -> None:
        """Clear the selection."""
        self._selected_item = None
        self._update_selected_display()
        self.post_message(self.SelectionChanged(None, None))

    def set_initial_selection(self, item: TItem) -> None:
        """Set initial selection without triggering search.

        Use this to pre-populate the picker with an existing value.
        Should be called after mount.

        Args:
            item: Item to set as initial selection
        """
        self._selected_item = item
        self._update_selected_display()


class MultiSelectPicker(BaseSearchPicker[TItem], Generic[TItem]):
    """Base class for multi-selection pickers.

    Handles the common pattern of selecting multiple items from search results.
    Items can be toggled on/off independently.

    Subclasses must implement:
    - _fetch_items(query): Fetch items from API
    - _format_item(item): Format item for display
    - _get_item_id(item): Get unique identifier for item
    - _format_selection_display(): Format the multi-selection display
    """

    class SelectionChanged(Message):
        """Posted when selection changes."""

        def __init__(self, selected_items: list[Any]) -> None:
            self.selected_items = selected_items
            super().__init__()

    def __init__(
        self,
        client: ApiClient,
        placeholder: str = "Type to search...",
        empty_message: str = "No selection",
        **kwargs: Any,
    ) -> None:
        super().__init__(
            client=client,
            placeholder=placeholder,
            empty_message=empty_message,
            **kwargs,
        )
        # Store selected items as dict for O(1) lookup by ID
        self._selected_items: dict[str, TItem] = {}

    @property
    def selected_ids(self) -> list[str]:
        """Get the IDs of all selected items."""
        return list(self._selected_items.keys())

    @property
    def selected_items(self) -> list[TItem]:
        """Get all selected items."""
        return list(self._selected_items.values())

    def _is_item_selected(self, item: TItem) -> bool:
        """Check if an item is currently selected."""
        return self._get_item_id(item) in self._selected_items

    def _handle_item_selection(self, index: int) -> None:
        """Handle selection of item at the given index with toggle behavior."""
        if index < 0 or index >= len(self._search_results):
            return

        item = self._search_results[index]
        item_id = self._get_item_id(item)

        # Toggle selection
        if item_id in self._selected_items:
            del self._selected_items[item_id]
        else:
            self._selected_items[item_id] = item

        self._update_selected_display()
        self._update_options()
        self._post_selection_changed()

    def _post_selection_changed(self) -> None:
        """Post selection changed message. Override for custom message types."""
        self.post_message(self.SelectionChanged(self.selected_items))

    def clear(self) -> None:
        """Clear all selections."""
        self._selected_items.clear()
        self._update_selected_display()
        self._post_selection_changed()

    def set_initial_selections(self, items: list[TItem]) -> None:
        """Set initial selections without triggering search.

        Use this to pre-populate the picker with existing values.
        Should be called after mount.

        Args:
            items: Items to set as initial selections (order is preserved)
        """
        self._selected_items.clear()
        for item in items:
            item_id = self._get_item_id(item)
            self._selected_items[item_id] = item
        self._update_selected_display()
