import logging
from typing import List

from textual.widgets import DataTable

import shovl.schemas as schemas
from shovl.providers.database import DatabaseProvider

from .service import ServiceScreen

logger = logging.getLogger("shovl.screens")


class DatabaseScreen(ServiceScreen):
    """
    Screen to browse the contents of a database.
    """

    BINDINGS = [
        ("backspace", "back", "back"),
        ("f", "filter", "filter"),
        ("p", "preview", "preview"),
    ]

    connection: schemas.DatabaseConfig
    provider: DatabaseProvider

    history: List[schemas.DatabaseEntity | None]
    all_entities: List[schemas.DatabaseEntity]
    filtered_entites: List[schemas.DatabaseEntity]

    current_entity: schemas.DatabaseEntity | None

    root: bool

    ### Lifecycle methods ###

    def __init__(
        self,
        config: schemas.ConfigSchema,
        connection: schemas.DatabaseConfig,
        provider: DatabaseProvider,
    ) -> None:
        """
        Initialize the database screen.
        """
        super().__init__(config=config)
        self.connection = connection
        self.provider = provider

        self.history = []
        self.all_entities = []
        self.filtered_entities = []

        self.current_entity = None

        self.root = False

        self.set_title(f"shovl - {self.connection.name}")

    ### Actions ###

    def action_back(self) -> None:
        """
        Go back one step in history, if available.
        """
        previous_entity = self.go_back_in_history()
        if previous_entity:
            self.run_in_thread(
                target=self.inspection_worker,
                name="InspectionWorker",
                entity=previous_entity,
                add_to_history=False,
            )
        elif self.root:
            self.provider.disconnect()
            self.app.pop_screen()
        else:
            self.push_status_screen()
            self.initialize_table()
            self.dismiss_status_screen()

        self.current_filter = ""

    def action_preview(self) -> None:
        """
        Preview the selected database entity.
        """
        entity = self.get_selected_row()

        if entity and entity.type in (
            schemas.DatabaseEntityType.table,
            schemas.DatabaseEntityType.view,
        ):
            self.run_in_thread(
                target=self.preview_worker,
                name="PreviewWorker",
                entity=entity,
            )

            self.current_filter = ""
        else:
            self.notify_info("Please select a table or view to preview.")

    def action_sql(self) -> None:
        """
        Open the SQL editor screen.
        """
        pass  # todo
        # from shovl.screens.sql import SqlScreen

        # self.app.push_screen(SqlScreen(config=self.config))

    ### Event handlers ###

    def on_mount(self) -> None:
        """
        andle the screen mount event by initializing the table.
        """
        super().on_mount()

        self.initialize_table()

    def on_data_table_cell_selected(
        self, event: DataTable.CellSelected
    ) -> None:
        """
        Handle Enter key on list items.
        """
        entity = self.get_selected_row()

        if entity and entity.type in (
            schemas.DatabaseEntityType.schemas,
            schemas.DatabaseEntityType.schema,
            schemas.DatabaseEntityType.tables,
            schemas.DatabaseEntityType.views,
            schemas.DatabaseEntityType.table,
            schemas.DatabaseEntityType.view,
        ):
            self.run_in_thread(
                target=self.inspection_worker,
                name="InspectionWorker",
                entity=entity,
            )

    ### Class methods ###

    def get_selected_row(self) -> schemas.DatabaseEntity | None:
        """
        Get the data of the currently selected row in the table and return it
        as a DatabaseEntity object.
        """
        if self.table.cursor_row is None:
            return None

        if self.table.cursor_row >= len(self.filtered_entities):
            return None

        return self.filtered_entities[self.table.cursor_row]

    def initialize_table(self) -> None:
        """
        Initialize the table with the root bucket entity.
        """
        self.history = []
        self.all_entities = [
            schemas.DatabaseEntity(
                name="Schemas",
                type=schemas.DatabaseEntityType.schemas,
            )
        ]
        self.filtered_entities = self.all_entities.copy()
        self.refresh_main(show_truncated_hint=True)
        self.focus_main()
        self.root = True
        self.current_entity = None

    def add_to_history(self, entity: schemas.DatabaseEntity) -> None:
        """
        Add an item to the history stack.
        """
        self.history.append(entity)

    def go_back_in_history(self) -> schemas.DatabaseEntity | None:
        """
        Go back in history and return the previous entity, or None if at root.
        """
        if len(self.history) > 1:
            self.history.pop()
            return self.history[-1]
        elif len(self.history) == 1:  # root
            self.history.pop()
            return None
        elif len(self.history) == 0:
            return None

    def inspect(self, entity: schemas.DatabaseEntity) -> None:
        """
        Inspect a specific row in the database table.
        """
        if entity.type == schemas.DatabaseEntityType.schemas:
            entities = self.provider.inspect_schemas()
        elif entity.type == schemas.DatabaseEntityType.schema:
            entities = [
                schemas.DatabaseEntity(
                    name="tables",
                    type=schemas.DatabaseEntityType.tables,
                    dbschema=entity.name,
                ),
                schemas.DatabaseEntity(
                    name="views",
                    type=schemas.DatabaseEntityType.views,
                    dbschema=entity.name,
                ),
            ]
        elif entity.type == schemas.DatabaseEntityType.tables:
            entities = self.provider.inspect_tables(
                schema=entity.dbschema or ""
            )
        elif entity.type == schemas.DatabaseEntityType.views:
            entities = self.provider.inspect_views(schema=entity.dbschema or "")
        elif entity.type == schemas.DatabaseEntityType.table:
            entities = self.provider.inspect_table(
                table_name=entity.name, schema=entity.dbschema or ""
            )
        elif entity.type == schemas.DatabaseEntityType.view:
            entities = self.provider.inspect_view(
                view_name=entity.name, schema=entity.dbschema or ""
            )
        else:
            raise Exception(f"Unsupported entity type: {entity.type}")

        self.all_entities = entities
        self.filtered_entities = self.all_entities.copy()

        self.root = False

        self.current_filter = ""

        self.current_entity = entity

    def fetch_preview(self, entity: schemas.DatabaseEntity) -> None:
        """
        Preview a table or view.
        """
        self.all_entities = self.provider.fetch_preview(
            schema=entity.dbschema or "",
            name=entity.name or "",
        )
        self.filtered_entities = self.all_entities.copy()

    def filter_main(self) -> None:
        """
        Filter database entities based on filter query. Meant to
        be called from a separate thread to avoid blocking the UI.
        """
        query = self.current_filter.lower().strip()

        if not query:
            self.filtered_entities = self.all_entities.copy()
        else:
            self.filtered_entities = []
            for entity in self.all_entities:
                if not entity.data and query in entity.name.lower():
                    self.filtered_entities.append(entity)
                    continue
                if not entity.data:
                    continue
                if any(
                    query in str(value).lower()
                    for value in entity.data.values()
                ):
                    self.filtered_entities.append(entity)

    def refresh_main(self, show_truncated_hint: bool) -> None:
        """
        Add rows to the table based on a list of DatabaseEntity objects.
        """
        with self.app.batch_update():
            self.table.clear(columns=True)

            if not self.filtered_entities:
                super().refresh_main(show_truncated_hint=show_truncated_hint)
                return

            if self.filtered_entities[0].type in (
                schemas.DatabaseEntityType.schemas,
                schemas.DatabaseEntityType.schema,
                schemas.DatabaseEntityType.tables,
                schemas.DatabaseEntityType.views,
                schemas.DatabaseEntityType.table,
                schemas.DatabaseEntityType.view,
            ):
                self.table.add_columns("Name", "Type")
            elif (
                self.filtered_entities[0].type
                == schemas.DatabaseEntityType.column
            ):
                self.table.add_columns("Name", "Data Type", "Nullable", "Type")
            elif (
                self.filtered_entities[0].type == schemas.DatabaseEntityType.row
            ):
                assert self.filtered_entities[0].data is not None
                self.table.add_columns(*self.filtered_entities[0].data.keys())

            for index, entity in enumerate(self.filtered_entities):
                if (
                    index >= self.config.gui.max_list_items
                    and self.config.gui.max_list_items > 0
                ):
                    if show_truncated_hint:
                        self.notify_info(
                            f"Showing first {self.config.gui.max_list_items} "
                            f"items. Refine your filter to see more."
                        )
                    break

                name = self.get_entity_display_name(entity=entity)

                if entity.type in (
                    schemas.DatabaseEntityType.schemas,
                    schemas.DatabaseEntityType.schema,
                    schemas.DatabaseEntityType.tables,
                    schemas.DatabaseEntityType.views,
                    schemas.DatabaseEntityType.table,
                    schemas.DatabaseEntityType.view,
                ):
                    self.table.add_row(name, entity.type.value)
                elif entity.type == schemas.DatabaseEntityType.column:
                    self.table.add_row(
                        name,
                        entity.datatype or "",
                        "True" if entity.nullable else "False",
                        entity.type.value,
                    )
                elif entity.type == schemas.DatabaseEntityType.row:
                    assert entity.data is not None
                    self.table.add_row(
                        *[str(value) for value in entity.data.values()]
                    )

        super().refresh_main(show_truncated_hint=show_truncated_hint)

    def get_entity_display_name(self, entity: schemas.DatabaseEntity) -> str:
        """
        Get the display name for a database entity, including an icon if
        configured.
        """
        if entity.type == schemas.DatabaseEntityType.schemas:
            icon = self.config.gui.icons.schemas
        elif entity.type == schemas.DatabaseEntityType.schema:
            icon = self.config.gui.icons.dbschema
        elif entity.type == schemas.DatabaseEntityType.schema:
            icon = self.config.gui.icons.dbschema
        elif entity.type == schemas.DatabaseEntityType.tables:
            icon = self.config.gui.icons.tables
        elif entity.type == schemas.DatabaseEntityType.views:
            icon = self.config.gui.icons.views
        elif entity.type == schemas.DatabaseEntityType.table:
            icon = self.config.gui.icons.table
        elif entity.type == schemas.DatabaseEntityType.view:
            icon = self.config.gui.icons.view
        else:
            icon = ""

        if icon:
            return f"{icon} {entity.name}"
        else:
            return entity.name

    ### Workers ###

    def inspection_worker(
        self, entity: schemas.DatabaseEntity, add_to_history: bool = True
    ) -> None:
        """
        Inspect a database entity.
        """
        try:
            self.app.call_from_thread(self.push_status_screen)

            logger.debug(f"Inspecting '{entity.name}'...")

            self.inspect(entity)

            logger.debug("Inspection successfully.")

            self.app.call_from_thread(
                self.refresh_main, show_truncated_hint=True
            )

            if add_to_history:
                self.add_to_history(entity)

            self.app.call_from_thread(self.dismiss_status_screen)
        except Exception as exception:
            message = str(exception)
            logger.error(f"Inspection failed: {message}")
            self.app.call_from_thread(self.dismiss_status_screen)
            self.app.call_from_thread(self.focus_main)
            self.app.call_from_thread(
                lambda: self.notify_error(
                    f"Unable to inspect database entity: {message}"
                )
            )

    def preview_worker(
        self,
        entity: schemas.DatabaseEntity,
        add_to_history: bool = True,
    ) -> None:
        """
        Execute a database query.
        """
        try:
            self.app.call_from_thread(self.push_status_screen)

            logger.debug(f"Previewing '{entity.name}'...")

            self.fetch_preview(entity=entity)

            logger.debug("Preview successfully.")

            self.app.call_from_thread(
                self.refresh_main, show_truncated_hint=True
            )

            if add_to_history:
                self.add_to_history(entity)

            self.app.call_from_thread(self.dismiss_status_screen)
        except Exception as exception:
            message = str(exception)
            logger.error(f"Query failed: {message}")
            self.app.call_from_thread(self.dismiss_status_screen)
            self.app.call_from_thread(self.focus_main)
            self.app.call_from_thread(
                lambda: self.notify_error(f"Preview failed: {message}")
            )
