from __future__ import annotations

from textual.app import ComposeResult
from textual.binding import Binding
from textual.containers import Vertical, Horizontal
from textual.reactive import reactive
from textual.screen import ModalScreen
from textual.widgets import DataTable, Static, Input, Button, Label, ListView, ListItem
from textual.widget import Widget
from textual import on

from django.db import models
from django.db.models import QuerySet

from django_shreck.messages import GoBack
from django_shreck.services.clipboard import copy_to_clipboard


# ─────────────────────────────────────────────────────────────────────────────
# Popup screen for editing a single field
# ─────────────────────────────────────────────────────────────────────────────

class FieldEditScreen(ModalScreen):
    """Modal popup to edit a single field value.

    For FK fields:
      - Single query input; typing triggers live search.
      - Results always shown in a list (even for a single hit).
      - User highlights a row and presses Enter or clicks Save to confirm.
    For non-FK fields:
      - Simple value input, Enter/Ctrl+S saves.
    """

    DEFAULT_CSS = """
    FieldEditScreen {
        align: center middle;
    }
    FieldEditScreen #popup-container {
        width: 70%;
        max-height: 80%;
        background: $surface;
        border: tall $primary;
        padding: 1 2;
    }
    FieldEditScreen #popup-title {
        text-style: bold;
        margin-bottom: 1;
        content-align: center middle;
        width: 100%;
    }
    FieldEditScreen #popup-hint-value {
        color: $text-muted;
        margin-bottom: 1;
        text-wrap: wrap;
    }
    FieldEditScreen #popup-hint-query {
        color: $text-muted;
        margin-bottom: 1;
        text-wrap: wrap;
    }
    FieldEditScreen #field-input {
        margin-bottom: 1;
    }
    FieldEditScreen #query-input {
        margin-bottom: 1;
    }
    FieldEditScreen #qs-list {
        height: 10;
        border: solid $primary-darken-1;
        margin-bottom: 1;
    }
    FieldEditScreen #popup-buttons {
        height: 3;
        align: right middle;
    }
    FieldEditScreen Button {
        margin-left: 1;
    }
    FieldEditScreen #error-label {
        color: $error;
        margin-bottom: 1;
    }
    FieldEditScreen #selection-label {
        color: $success;
        margin-bottom: 1;
        text-style: bold;
    }
    """

    BINDINGS = [
        Binding("ctrl+s", "confirm", "Save", show=True),
        Binding("escape", "cancel", "Cancel", show=True),
    ]

    def __init__(
        self,
        field_name: str,
        field_type: str,
        current_value: str,
        field_obj,  # Django field object
        instance,   # current model instance (for FK namespace)
    ) -> None:
        super().__init__()
        self._field_name = field_name
        self._field_type = field_type
        self._current_value = current_value
        self._field_obj = field_obj
        self._instance = instance
        self._is_fk = isinstance(field_obj, models.ForeignKey) if field_obj else False
        self._qs_objects: list = []
        self._selected_obj = None  # resolved FK object chosen from list

    def compose(self) -> ComposeResult:
        with Vertical(id="popup-container"):
            if self._is_fk:
                related_name = self._field_obj.related_model.__name__
                title = f"Edit [bold]{self._field_name}[/bold] (FK → {related_name})"
            else:
                title = f"Edit [bold]{self._field_name}[/bold] ({self._field_type})"
            yield Static(title, id="popup-title")

            if self._is_fk:
                yield Static(
                    "Type a pk, a string, or a Django ORM expression — results appear below:",
                    id="popup-hint-query",
                )
                yield Input(
                    value=self._current_value,
                    placeholder="pk / str(obj) / Model.objects.filter(…)",
                    id="query-input",
                )
                yield Static("", id="error-label")
                yield ListView(id="qs-list")
                yield Static("", id="selection-label")
            else:
                yield Static(
                    f"New value for {self._field_name}:",
                    id="popup-hint-value",
                )
                yield Input(
                    value=self._current_value,
                    placeholder=f"New value for {self._field_name}",
                    id="field-input",
                )
                yield Static("", id="error-label")

            with Horizontal(id="popup-buttons"):
                yield Button("Save", variant="primary", id="btn-save")
                yield Button("Cancel", variant="default", id="btn-cancel")

    def on_mount(self) -> None:
        if self._is_fk:
            lv = self.query_one("#qs-list", ListView)
            lv.display = False
            qi = self.query_one("#query-input", Input)
            qi.focus()
            # If there's a pre-filled value, run an initial search
            if self._current_value.strip():
                self._run_orm_query(self._current_value.strip())
        else:
            self.query_one("#field-input", Input).focus()

    # ── Live search ──────────────────────────────────────────────

    @on(Input.Changed, "#query-input")
    def _on_query_changed(self, event: Input.Changed) -> None:
        """Trigger live search on every keystroke."""
        self._run_orm_query(event.value.strip())

    def _run_orm_query(self, expr: str) -> None:
        """Execute expr and populate the results list."""
        lv = self.query_one("#qs-list", ListView)

        if not expr:
            lv.clear()
            lv.display = False
            self._qs_objects = []
            self._selected_obj = None
            self._set_error("")
            self._set_selection("")
            return

        from django.apps import apps as django_apps
        from django.db.models import Q, F, Count, Sum, Avg, Max, Min
        namespace: dict = {
            "Q": Q, "F": F,
            "Count": Count, "Sum": Sum, "Avg": Avg, "Max": Max, "Min": Min,
        }
        for m in django_apps.get_models():
            namespace[m.__name__] = m

        # Try ORM expression first
        result = None
        try:
            result = eval(expr, {"__builtins__": {}}, namespace)
        except Exception:
            pass

        # Fallback: pk lookup or __str__ match on the related model
        if result is None and self._is_fk:
            related_model = self._field_obj.related_model
            try:
                pk_val = int(expr)
                result = related_model.objects.filter(pk=pk_val)
            except ValueError:
                result = related_model.objects.all()
                result = [o for o in result[:500] if expr.lower() in str(o).lower()]

        if result is None:
            self._set_error("No results.")
            lv.display = False
            return

        # Normalise to a list
        if isinstance(result, QuerySet):
            objects = list(result[:100])
        elif isinstance(result, models.Model):
            objects = [result]
        elif isinstance(result, list):
            objects = result[:100]
        else:
            # Treat as a direct FK value (e.g. a plain pk integer/string)
            if self._is_fk:
                related_model = self._field_obj.related_model
                try:
                    obj = related_model.objects.get(pk=result)
                    objects = [obj]
                except Exception:
                    self._set_error(f"No {related_model.__name__} with pk={result!r}.")
                    lv.display = False
                    return
            else:
                self._set_error(f"Expression returned {type(result).__name__}, expected model instance or queryset.")
                lv.display = False
                return

        if not objects:
            self._set_error("No results.")
            lv.clear()
            lv.display = False
            self._qs_objects = []
            self._selected_obj = None
            self._set_selection("")
            return

        self._set_error("")
        self._qs_objects = objects
        self._selected_obj = None
        self._set_selection("")

        lv.clear()
        for obj in objects:
            lv.append(ListItem(Label(f"[dim]{obj.pk}[/dim]  {obj}")))
        lv.display = True

        # Move cursor to first item after the DOM has settled; this fires
        # ListView.Highlighted which sets _selected_obj correctly.
        def _highlight_first():
            try:
                lv.move_cursor(0)
            except Exception:
                pass
        self.call_after_refresh(_highlight_first)

    # ── List interaction ─────────────────────────────────────────

    @on(ListView.Highlighted, "#qs-list")
    def _on_list_highlighted(self, event: ListView.Highlighted) -> None:
        idx = event.list_view.index
        if idx is not None and 0 <= idx < len(self._qs_objects):
            obj = self._qs_objects[idx]
            self._selected_obj = obj
            self._set_error("")
            self._set_selection(f"✔ {obj}  [dim](pk={obj.pk})[/dim]")

    @on(ListView.Selected, "#qs-list")
    def _on_list_selected(self, event: ListView.Selected) -> None:
        """Enter on a list item confirms immediately."""
        self.action_confirm()

    # ── Button / key handlers ────────────────────────────────────

    def on_button_pressed(self, event: Button.Pressed) -> None:
        if event.button.id == "btn-save":
            self.action_confirm()
        elif event.button.id == "btn-cancel":
            self.action_cancel()

    def on_key(self, event) -> None:
        if event.key == "ctrl+s":
            event.stop()
            event.prevent_default()
            self.action_confirm()
        elif event.key == "escape":
            event.stop()
            event.prevent_default()
            if self._is_fk:
                try:
                    lv = self.query_one("#qs-list", ListView)
                    qi = self.query_one("#query-input", Input)
                    if self.focused == lv:
                        # Escape from list → back to query input to refine search
                        qi.focus()
                        return
                except Exception:
                    pass
            self.action_cancel()
        elif event.key == "tab" and self._is_fk:
            # Tab moves focus from query input → list
            try:
                lv = self.query_one("#qs-list", ListView)
                if lv.display and self.focused == self.query_one("#query-input", Input):
                    event.stop()
                    event.prevent_default()
                    lv.focus()
            except Exception:
                pass
        elif event.key == "enter" and self._is_fk:
            # Enter in query input → move focus to list if results available
            try:
                qi = self.query_one("#query-input", Input)
                lv = self.query_one("#qs-list", ListView)
                if self.focused == qi and lv.display:
                    event.stop()
                    event.prevent_default()
                    lv.focus()
            except Exception:
                pass
        elif event.key == "enter" and not self._is_fk:
            event.stop()
            event.prevent_default()
            self.action_confirm()

    @on(Input.Submitted, "#field-input")
    def _on_field_submitted(self, event: Input.Submitted) -> None:
        self.action_confirm()

    def _set_error(self, text: str) -> None:
        try:
            self.query_one("#error-label", Static).update(text)
        except Exception:
            pass

    def _set_selection(self, text: str) -> None:
        try:
            self.query_one("#selection-label", Static).update(text)
        except Exception:
            pass

    # ── Actions ──────────────────────────────────────────────────

    def action_confirm(self) -> None:
        if self._is_fk:
            if self._selected_obj is None:
                self._set_error("Please select a result from the list first.")
                return
            self.dismiss(("__resolved__", self._selected_obj))
        else:
            value = self.query_one("#field-input", Input).value
            self.dismiss(value)

    def action_cancel(self) -> None:
        self.dismiss(None)


# ─────────────────────────────────────────────────────────────────────────────
# Main detail widget
# ─────────────────────────────────────────────────────────────────────────────

class DetailWidget(Widget):
    """Displays a single Django model instance as a navigable key-value detail view.

    Read mode   — compact DataTable; E enters edit mode.
    Edit mode   — same compact DataTable with pending edits shown inline;
                  Enter on an editable row opens a FieldEditScreen popup.
    Ctrl+S saves; Escape cancels/goes back.
    If is_new=True, opens directly in edit mode for creating a new record.
    """

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

    BINDINGS = [
        Binding("e", "start_edit", "Edit", show=True),
        Binding("ctrl+c", "copy_cell", "Copy Cell", show=True),
        Binding("ctrl+shift+c", "copy_row", "Copy Row", show=True),
    ]

    # Reactive flag drives recompose automatically
    _edit_mode: reactive[bool] = reactive(False, recompose=True)

    def __init__(
        self,
        instance: models.Model,
        editable: bool = False,
        is_new: bool = False,
        start_editing: bool = False,
        **kwargs,
    ) -> None:
        super().__init__(**kwargs)
        self._instance = instance
        self._editable = editable or is_new or start_editing
        self._is_new = is_new
        self._start_editing = start_editing
        self._initial_edit_mode = is_new or start_editing
        # Pending edits: field_name → new display string (or ("__resolved__", obj))
        self._pending: dict[str, object] = {}
        # Original display values (set when edit mode is first entered)
        self._original_display: dict[str, str] = {}

    def on_mount(self) -> None:
        self._edit_mode = self._initial_edit_mode
        self.call_after_refresh(self._focus_table)

    def watch__edit_mode(self, value: bool) -> None:
        self.call_after_refresh(self._focus_table)

    def _focus_table(self) -> None:
        try:
            self.query_one(DataTable).focus()
        except Exception:
            pass

    # ── Header ───────────────────────────────────────────────────

    def _header_text(self) -> str:
        meta = self._instance._meta
        if self._is_new:
            return f"New {meta.app_label}.{meta.model_name}  [bold](Ctrl+S = save  Esc = cancel  Enter = edit field)[/bold]"
        if self._edit_mode:
            return (
                f"{meta.app_label}.{meta.model_name} (pk={self._instance.pk})"
                "  [bold](Ctrl+S = save  Esc = cancel  Enter = edit field)[/bold]"
            )
        hint = "  [dim](E = edit  Esc = back)[/dim]" if self._editable else "  [dim](Esc = back)[/dim]"
        return f"{meta.app_label}.{meta.model_name} (pk={self._instance.pk}){hint}"

    # ── Compose ──────────────────────────────────────────────────

    def compose(self) -> ComposeResult:
        header = Static(self._header_text(), id="detail-header")
        if self._edit_mode:
            header.add_class("editing")
        yield header
        yield self._build_table()

    def _get_field_display_data(self) -> list[tuple[str, str, str, bool]]:
        """Return (field_name, field_type, display_value, is_editable) for each field."""
        result = []
        for field in self._instance._meta.get_fields():
            if not (hasattr(field, "column") or hasattr(field, "attname")):
                continue
            field_name = field.name
            field_type = type(field).__name__
            editable_flag = self._edit_mode and not getattr(field, "primary_key", False) and getattr(field, "editable", True)

            # Determine display value
            if field_name in self._pending:
                pv = self._pending[field_name]
                if isinstance(pv, tuple) and len(pv) == 2 and pv[0] == "__resolved__":
                    display = f"[yellow]→ {pv[1]}[/yellow]"
                else:
                    display = f"[yellow]{pv}[/yellow]"
            else:
                raw = getattr(self._instance, field_name, "N/A")
                display = repr(raw)

            result.append((field_name, field_type, display, editable_flag))
        return result

    def _build_table(self) -> DataTable:
        table = DataTable(id="detail-table")
        if self._edit_mode:
            table.add_columns("✎", "Field", "Type", "Value")
        else:
            table.add_columns("Field", "Type", "Value")
        data = self._get_field_display_data()
        for field_name, field_type, display_value, is_editable in data:
            if self._edit_mode:
                edit_marker = "✎" if is_editable else " "
                table.add_row(edit_marker, field_name, field_type, display_value, key=field_name)
            else:
                table.add_row(field_name, field_type, display_value, key=field_name)
        return table

    def _refresh_table(self) -> None:
        """Rebuild the table in-place after a pending edit changes."""
        try:
            table = self.query_one("#detail-table", DataTable)
            old_row = table.cursor_row
        except Exception:
            old_row = 0

        # Update header
        try:
            self.query_one("#detail-header", Static).update(self._header_text())
        except Exception:
            pass

        # Update the existing table rows without removing/re-mounting the widget
        # (remove() is async; mounting immediately after causes DuplicateIds)
        try:
            table = self.query_one("#detail-table", DataTable)
            table.clear(columns=True)
            if self._edit_mode:
                table.add_columns("✎", "Field", "Type", "Value")
            else:
                table.add_columns("Field", "Type", "Value")
            data = self._get_field_display_data()
            for field_name, field_type, display_value, is_editable in data:
                if self._edit_mode:
                    edit_marker = "✎" if is_editable else " "
                    table.add_row(edit_marker, field_name, field_type, display_value, key=field_name)
                else:
                    table.add_row(field_name, field_type, display_value, key=field_name)

            def _restore_cursor():
                try:
                    t = self.query_one("#detail-table", DataTable)
                    t.move_cursor(row=old_row)
                    t.focus()
                except Exception:
                    pass
            self.call_after_refresh(_restore_cursor)
        except Exception:
            pass

    # ── Key handling ─────────────────────────────────────────────

    def on_key(self, event) -> None:
        if event.key == "ctrl+s":
            event.stop()
            event.prevent_default()
            if self._edit_mode:
                self._save_instance()
        elif event.key == "escape":
            event.stop()
            event.prevent_default()
            if self._edit_mode and not self._is_new and not self._start_editing:
                self._pending.clear()
                self._edit_mode = False
            else:
                self.post_message(GoBack())
        elif event.key == "enter":
            event.stop()
            event.prevent_default()
            if self._edit_mode:
                self._open_field_editor()

    # ── Actions ──────────────────────────────────────────────────

    def action_start_edit(self) -> None:
        if not self._editable or self._edit_mode:
            return
        # Snapshot original values when entering edit mode
        self._original_display = {}
        for field in self._instance._meta.get_fields():
            if not (hasattr(field, "column") or hasattr(field, "attname")):
                continue
            if getattr(field, "primary_key", False) or not getattr(field, "editable", True):
                continue
            field_name = field.name
            if isinstance(field, models.ForeignKey):
                related_obj = getattr(self._instance, field_name, None)
                self._original_display[field_name] = str(related_obj) if related_obj is not None else ""
            else:
                raw = getattr(self._instance, field_name, "") or ""
                self._original_display[field_name] = str(raw)
        self._edit_mode = True

    def _get_current_row_field(self) -> tuple[str, str, str] | None:
        """Return (field_name, field_type, display_value) for the cursor row."""
        try:
            table = self.query_one("#detail-table", DataTable)
            row_index = table.cursor_row
            data = self._get_field_display_data()
            if 0 <= row_index < len(data):
                field_name, field_type, display_value, _editable = data[row_index]
                return field_name, field_type, display_value
        except Exception:
            pass
        return None

    def _open_field_editor(self) -> None:
        """Open the FieldEditScreen popup for the currently selected row."""
        result = self._get_current_row_field()
        if result is None:
            return
        field_name, field_type, _ = result
        field_obj = self._get_field_object(field_name)

        # Skip non-editable fields
        if field_obj and (getattr(field_obj, "primary_key", False) or not getattr(field_obj, "editable", True)):
            self.notify(f"'{field_name}' is not editable.", severity="warning")
            return

        # Determine current display value for the popup
        if field_name in self._pending:
            pv = self._pending[field_name]
            if isinstance(pv, tuple) and pv[0] == "__resolved__":
                current_val = str(pv[1])
            else:
                current_val = str(pv)
        elif isinstance(field_obj, models.ForeignKey):
            related_obj = getattr(self._instance, field_name, None)
            current_val = str(related_obj) if related_obj is not None else ""
        else:
            raw = getattr(self._instance, field_name, "") or ""
            current_val = str(raw)

        def _handle_result(new_value) -> None:
            if new_value is None:
                return  # cancelled
            self._pending[field_name] = new_value
            self._refresh_table()

        self.app.push_screen(
            FieldEditScreen(
                field_name=field_name,
                field_type=field_type,
                current_value=current_val,
                field_obj=field_obj,
                instance=self._instance,
            ),
            _handle_result,
        )

    def _get_field_object(self, field_name: str):
        try:
            return self._instance._meta.get_field(field_name)
        except Exception:
            return None

    # ── Copy actions (read mode) ─────────────────────────────────

    def _get_current_field_data(self) -> tuple[str, str] | None:
        if self._edit_mode:
            return None
        result = self._get_current_row_field()
        if result:
            return result[0], result[2]
        return None

    def action_copy_row(self) -> None:
        result = self._get_current_field_data()
        if result is None:
            return
        field_name, value = result
        copy_to_clipboard(f"{field_name}: {value}")
        self.notify(f"'{field_name}' copied to clipboard.", severity="information")

    def action_copy_cell(self) -> None:
        result = self._get_current_field_data()
        if result is None:
            return
        field_name, value = result
        copy_to_clipboard(value)
        self.notify(f"Value of '{field_name}' copied to clipboard.", severity="information")

    # ── Save logic ───────────────────────────────────────────────

    def _save_instance(self) -> None:
        update_fields = []

        for field_name, new_value in self._pending.items():
            field_obj = self._get_field_object(field_name)
            if not field_obj:
                continue

            if isinstance(field_obj, models.ForeignKey):
                if isinstance(new_value, tuple) and new_value[0] == "__resolved__":
                    resolved = new_value[1]
                    setattr(self._instance, field_obj.attname, resolved.pk)
                    update_fields.append(field_obj.attname)
                elif not new_value or (isinstance(new_value, str) and not new_value.strip()):
                    setattr(self._instance, field_obj.attname, None)
                    update_fields.append(field_obj.attname)
                else:
                    # Try to resolve from string
                    text = str(new_value)
                    related_model = field_obj.related_model
                    resolved = self._resolve_fk(related_model, text)
                    if resolved is None:
                        self.notify(
                            f"'{text}' did not match any {related_model.__name__}",
                            severity="error",
                        )
                        return
                    setattr(self._instance, field_obj.attname, resolved.pk)
                    update_fields.append(field_obj.attname)
            else:
                text = str(new_value) if not isinstance(new_value, str) else new_value
                try:
                    converted = field_obj.to_python(text)
                    setattr(self._instance, field_name, converted)
                    update_fields.append(field_name)
                except Exception:
                    setattr(self._instance, field_name, text)
                    update_fields.append(field_name)

        try:
            if self._is_new:
                self._instance.save()
            elif update_fields:
                self._instance.save(update_fields=update_fields)
            self._is_new = False
            self._pending.clear()
            self._edit_mode = False
            self.notify("Saved successfully.", severity="information")
        except Exception as e:
            self.notify(f"Save failed: {e}", severity="error")

    def _resolve_fk(self, related_model, text: str):
        if not text:
            return None
        try:
            pk_val = int(text)
            return related_model.objects.get(pk=pk_val)
        except (ValueError, related_model.DoesNotExist):
            pass
        for obj in related_model.objects.all()[:500]:
            if str(obj) == text:
                return obj
        return None
