from __future__ import annotations

from collections import defaultdict

from textual.app import ComposeResult
from textual.binding import Binding
from textual.widgets import DataTable, Static
from textual.widget import Widget

from django.apps import apps

from django_shreck.messages import ModelCursorChanged, ShowTable


class ModelBrowserWidget(Widget):
    """Displays all registered Django models with one column per app and class names as rows.

    Typing letters while the table is focused filters models by name prefix.
    Navigating the cursor automatically fills the shell input with Model.objects.all().
    """

    def __init__(self, focus_model=None, **kwargs) -> None:
        super().__init__(**kwargs)
        self._focus_model = focus_model
        self._filter: str = ""
        self._app_labels: list[str] = []
        self._by_app: dict[str, list[str]] = {}
        self._last_cursor: tuple[int, int] = (-1, -1)
        self._suppress_next_emit: bool = False

    DEFAULT_CSS = """
    ModelBrowserWidget {
        height: 100%;
        layout: vertical;
    }
    ModelBrowserWidget #browser-header {
        height: 1;
        content-align: center middle;
        background: $primary;
        color: $text;
        text-style: bold;
    }
    ModelBrowserWidget #browser-table {
        height: 1fr;
    }
    """

    BINDINGS = [
        Binding("enter", "open_model", "List Records", show=True),
    ]

    def compose(self) -> ComposeResult:
        yield Static("Django Models — Enter to list records", id="browser-header")
        yield DataTable(id="browser-table", cursor_type="cell")

    # ── helpers ────────────────────────────────────────────────────────────

    def _build_display_data(self) -> tuple[list[str], dict[str, list[str]]]:
        prefix = self._filter.lower()
        by_app: dict[str, list[str]] = defaultdict(list)
        for model in apps.get_models():
            name = model.__name__
            if not prefix or name.lower().startswith(prefix):
                by_app[model._meta.app_label].append(name)
        for names in by_app.values():
            names.sort()
        app_labels = sorted(app for app in by_app if by_app[app])
        return app_labels, {a: by_app[a] for a in app_labels}

    def set_filter(self, text: str, suppress_emit: bool = False) -> None:
        """Filter the model list to names starting with *text* (case-insensitive).

        Pass suppress_emit=True when the call originates from the shell input so
        we don't fire ModelCursorChanged back and create a feedback loop.
        """
        self._filter = text
        self._suppress_next_emit = suppress_emit
        self._reload_table()
        self.call_after_refresh(self._emit_cursor_model)

    # ── mount / reload ─────────────────────────────────────────────────────

    def on_mount(self) -> None:
        self._reload_table()

        def _focus_and_position() -> None:
            self.query_one("#browser-table", DataTable).focus()
            self._move_cursor_to_model()
            self._emit_cursor_model()

        self.call_after_refresh(_focus_and_position)

    def _reload_table(self) -> None:
        table = self.query_one("#browser-table", DataTable)
        table.clear(columns=True)

        self._app_labels, self._by_app = self._build_display_data()
        self._last_cursor = (-1, -1)

        if not self._app_labels:
            return

        for app_label in self._app_labels:
            table.add_column(app_label, key=app_label)

        max_rows = max(len(names) for names in self._by_app.values())
        for row_idx in range(max_rows):
            row = [
                self._by_app[app_label][row_idx] if row_idx < len(self._by_app[app_label]) else ""
                for app_label in self._app_labels
            ]
            table.add_row(*row)

    def _move_cursor_to_model(self) -> None:
        if self._focus_model is None:
            return
        table = self.query_one("#browser-table", DataTable)
        app_label = self._focus_model._meta.app_label
        class_name = self._focus_model.__name__
        if app_label not in self._app_labels:
            return
        col_idx = self._app_labels.index(app_label)
        models_in_app = self._by_app[app_label]
        if class_name not in models_in_app:
            return
        row_idx = models_in_app.index(class_name)
        table.move_cursor(row=row_idx, column=col_idx)

    # ── model under cursor ─────────────────────────────────────────────────

    def _model_at_cursor(self):
        table = self.query_one("#browser-table", DataTable)
        col_idx = table.cursor_column
        row_idx = table.cursor_row
        if col_idx < 0 or row_idx < 0 or not self._app_labels:
            return None
        app_label = self._app_labels[col_idx]
        models_in_app = self._by_app.get(app_label, [])
        if row_idx >= len(models_in_app):
            return None
        class_name = models_in_app[row_idx]
        try:
            return apps.get_model(app_label, class_name)
        except Exception:
            return None

    def _emit_cursor_model(self) -> None:
        """Emit ModelCursorChanged if the cursor has moved to a new cell."""
        if self._suppress_next_emit:
            self._suppress_next_emit = False
            return
        table = self.query_one("#browser-table", DataTable)
        pos = (table.cursor_row, table.cursor_column)
        if pos == self._last_cursor:
            return
        self._last_cursor = pos
        model = self._model_at_cursor()
        if model is not None:
            self.post_message(ModelCursorChanged(model=model))

    # ── key handling ───────────────────────────────────────────────────────

    def on_key(self, event) -> None:
        """After navigation keys, sync the shell input via ModelCursorChanged."""
        nav_keys = {"up", "down", "left", "right", "home", "end", "pageup", "pagedown"}
        if event.key in nav_keys:
            self.call_after_refresh(self._emit_cursor_model)

    # ── actions ────────────────────────────────────────────────────────────

    def action_open_model(self) -> None:
        model = self._model_at_cursor()
        if model is not None:
            self.post_message(ShowTable(queryset=model.objects.all()))

    def on_data_table_cell_selected(self, event: DataTable.CellSelected) -> None:
        self.action_open_model()


