"""Base classes for TUI screens.

This module provides base classes and common patterns for building
consistent screens across the application.
"""

import logging
import os
import subprocess
import tempfile
from abc import abstractmethod
from typing import TYPE_CHECKING, Any, Generic, TypeVar, cast

from textual import on, work
from textual.app import ComposeResult
from textual.containers import Horizontal, Vertical, VerticalScroll
from textual.widget import Widget
from textual.widgets import DataTable, Input, Static

from alloy_runtime_sdk.api_client.client import ApiClient

if TYPE_CHECKING:
    from cli.tui.app import AlloyRuntimeApp

logger = logging.getLogger(__name__)

# Type variable for the item type that screens display
ItemT = TypeVar("ItemT")


class BrowserWidget(Widget, Generic[ItemT]):
    """Base widget for browser-style views with search, list, and detail panels.

    This is a Widget (not a Screen) that provides a consistent layout with:
    - Search input at top
    - Status bar showing result count
    - Split view with list on left, detail/preview on right

    This widget is meant to be composed inside a NavScreen subclass which
    provides the navigation bindings and delegates actions to this widget.

    Subclasses must implement:
    - TITLE: Display title
    - TABLE_COLUMNS: Column headers for the data table
    - _fetch_items(): Async method to fetch items from API, returns (items, total)
    - _format_row(): Format an item as a table row tuple
    - _update_preview(): Update the preview panel for selected item

    State Management:
        Widgets access the shared store via self.app.store (Textual pattern).
        - Uses shared ApiClient from store
        - Can access store.agent_cache for cross-screen data
    """

    app: "AlloyRuntimeApp"

    TITLE = "Browser"
    TABLE_COLUMNS: list[str] = ["Name"]

    # Use a shared CSS class so all browser screens get proper sizing
    DEFAULT_CLASSES = "browser-widget"

    # No bindings on widget - parent Screen handles all keybindings
    # and delegates to public methods on this widget

    def __init__(self, **kwargs: Any) -> None:
        """Initialize the browser widget."""
        super().__init__(**kwargs)
        self._items: list[ItemT] = []
        self._last_search: str = ""

    async def _ensure_client(self) -> ApiClient:
        """Get the shared server client from the store.

        Returns:
            The initialized ApiClient instance.
        """
        return await self.app.store.get_client()

    def compose(self) -> ComposeResult:
        """Compose the browser layout."""
        with Vertical(id="browser-root"):
            with Vertical(id="search-container"):
                yield Input(
                    placeholder=f"Search {self.TITLE.lower()}...",
                    id="search-input",
                )
                yield Static("Loading...", id="status-bar")

            with Horizontal(id="main-container"):
                with Vertical(id="list-container"):
                    yield DataTable(id="data-table", cursor_type="row")

                with Vertical(id="detail-container"):
                    yield Static("Preview", id="preview-title")
                    with VerticalScroll(id="preview-scroll"):
                        yield Static(
                            "Select an item to view details",
                            id="preview-content",
                        )

    def on_mount(self) -> None:
        """Initialize the table and load data on mount."""
        table = self._table()
        for column in self.TABLE_COLUMNS:
            table.add_column(column)

        # Load initial data
        self._do_search("")

    @on(Input.Changed, "#search-input")
    def _on_search_input_changed(self, event: Input.Changed) -> None:
        """Handle search input changes - triggers search with debouncing."""
        query = event.value.strip()
        if query != self._last_search:
            self._last_search = query
            self._do_search(query)

    @on(DataTable.RowHighlighted)
    def _on_row_highlighted(self, event: DataTable.RowHighlighted) -> None:
        """Update preview when cursor moves."""
        row_index = event.cursor_row
        if 0 <= row_index < len(self._items):
            self._update_preview(self._items[row_index])

    @work(exclusive=True)
    async def _do_search(self, query: str) -> None:
        """Execute the search and update the UI.

        This method handles the common search flow:
        1. Update status to "Searching..."
        2. Call subclass _fetch_items() to get data
        3. Update table with formatted rows
        4. Update status and preview
        """
        status = self.query_one("#status-bar", Static)
        table = self._table()

        status.update("Searching...")

        try:
            await self._ensure_client()
            items, total_count = await self._fetch_items(query)

            self._items = items

            # Update table
            table.clear()
            for item in self._items:
                table.add_row(*self._format_row(item))

            # Update status and preview
            if self._items:
                status.update(f"Showing {len(self._items)} of {total_count} results")
                self._update_preview(self._items[0])
            else:
                status.update("No results found")
                self.query_one("#preview-content", Static).update(
                    f"No {self.TITLE.lower()} selected"
                )

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

    @abstractmethod
    async def _fetch_items(self, query: str) -> tuple[list[ItemT], int]:
        """Fetch items from the API.

        Args:
            query: Search query string (empty for initial load)

        Returns:
            Tuple of (items list, total count)
        """
        ...

    @abstractmethod
    def _format_row(self, item: ItemT) -> tuple[str, ...]:
        """Format an item as a table row.

        Args:
            item: The item to format

        Returns:
            Tuple of column values
        """
        ...

    @abstractmethod
    def _update_preview(self, item: ItemT) -> None:
        """Update the preview panel for the selected item.

        Args:
            item: The selected item
        """
        ...

    # =========================================================================
    # Public methods for parent Screen to call
    # =========================================================================

    def focus_search(self) -> None:
        """Focus the search input."""
        self.query_one("#search-input", Input).focus()

    def focus_table(self) -> None:
        """Focus the data table."""
        self._table().focus()

    def refresh_data(self) -> None:
        """Refresh the current search results."""
        self._do_search(self._last_search)

    def copy_item_id(self) -> None:
        """Copy the selected item's ID to clipboard."""
        from cli.infrastructure.tui.clipboard import copy_to_clipboard

        table = self._table()
        if table.row_count == 0:
            self.app.notify("No item to copy", severity="warning")
            return

        row_index = table.cursor_row
        if 0 <= row_index < len(self._items):
            item_id = self._get_item_id(self._items[row_index])
            if item_id:
                copy_to_clipboard(self.app, str(item_id), "ID")

    def _get_item_id(self, item: ItemT) -> str | None:
        """Get the ID from an item. Override for custom ID extraction."""
        if hasattr(item, "id"):
            return str(item.id)  # type: ignore[attr-defined]
        elif isinstance(item, dict) and "id" in item:
            item_dict = cast(dict[str, Any], item)
            return str(item_dict["id"])
        return None

    def get_selected_item(self) -> ItemT | None:
        """Get the currently selected item from the data table.

        Returns:
            The selected item, or None if no item is selected.
        """
        table = self._table()
        if table.row_count == 0:
            return None
        row_index = table.cursor_row
        if 0 <= row_index < len(self._items):
            return self._items[row_index]
        return None

    def _table(self) -> DataTable[Any]:
        return cast(DataTable[Any], self.query_one("#data-table", DataTable))


def open_in_editor(
    content: str,
    suffix: str = ".txt",
    prefix: str = "content_",
) -> str | None:
    """Open content in an external editor.

    Args:
        content: The content to edit
        suffix: File suffix for syntax highlighting (e.g., ".json", ".jinja2")
        prefix: Temporary file prefix

    Returns:
        The edited content if changes were made, None if unchanged or error
    """
    editor = os.environ.get("EDITOR") or os.environ.get("VISUAL") or "vi"

    try:
        # Write content to a temporary file
        with tempfile.NamedTemporaryFile(
            mode="w",
            suffix=suffix,
            prefix=prefix,
            delete=False,
            encoding="utf-8",
        ) as f:
            f.write(content)
            temp_path = f.name

        # Run the editor (caller should suspend TUI)
        subprocess.run([editor, temp_path], check=False)

        # Read the edited content
        with open(temp_path, encoding="utf-8") as f:
            new_content = f.read()

        # Clean up
        try:
            os.unlink(temp_path)
        except OSError:
            pass

        return new_content

    except Exception:
        logger.exception("Failed to open content in editor")
        return None
