import logging
import pathlib
from typing import List

from textual.widgets import DataTable

import shovl.schemas as schemas
import shovl.services as services
from shovl.providers.bucket import BucketProvider

from .path import PathScreen
from .service import ServiceScreen

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


class BucketScreen(ServiceScreen):
    """
    Screen to browse the contents of a S3 bucket.
    """

    BINDINGS = [
        ("backspace", "back", "back"),
        ("f", "filter", "filter"),
        ("d", "download", "download"),
        ("u", "upload", "upload"),
        ("x", "delete", "delete"),
    ]

    connection: schemas.BucketConfig
    provider: BucketProvider

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

    current_entity: schemas.BucketEntity | None

    root: bool

    ### Lifecycle methods ###

    def __init__(
        self,
        config: schemas.ConfigSchema,
        connection: schemas.BucketConfig,
        provider: BucketProvider,
    ) -> None:
        """
        Initialize the bucket 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_download(self) -> None:
        """
        Download the selected bucket entity.
        """
        entity = self.get_selected_row()
        if entity and entity.type in (
            schemas.BucketEntityType.file,
            schemas.BucketEntityType.folder,
        ):

            def callback(path: pathlib.Path | None) -> None:
                if path:
                    self.run_in_thread(
                        target=self.download_worker,
                        name="DownloadWorker",
                        entity=entity,
                        target_path=path,
                    )

            self.app.push_screen(
                PathScreen(
                    label="Enter local target path:",
                    force_directory=True,
                ),
                callback=callback,
            )
        else:
            self.notify_info("Only files and folders can be downloaded.")

    def action_upload(self) -> None:
        entity = self.current_entity
        if entity and entity.type in (
            schemas.BucketEntityType.bucket,
            schemas.BucketEntityType.folder,
        ):

            def callback(path: pathlib.Path | None) -> None:
                if path:
                    self.run_in_thread(
                        target=self.upload_worker,
                        name="UploadWorker",
                        entity=entity,
                        source_path=path,
                    )

            self.app.push_screen(
                PathScreen(label="Enter local file or directory:"),
                callback=callback,
            )
        else:
            self.notify_info(
                "Please select an S3 bucket or folder as a target."
            )

    def action_delete(self) -> None:
        """
        Delete the selected bucket entity, if allowed by configuration.
        """
        entity = self.get_selected_row()
        if entity and entity.type in (
            schemas.BucketEntityType.file,
            schemas.BucketEntityType.folder,
        ):
            self.run_in_thread(
                target=self.delete_worker,
                name="DeleteWorker",
                entity=entity,
            )
        else:
            self.notify_info("Only files and folders can be deleted.")

    ### Event handlers ###

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

        self.table.add_columns("Name", "Size", "Last Modified", "Type")

        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.BucketEntityType.bucket,
            schemas.BucketEntityType.folder,
        ):
            self.run_in_thread(
                target=self.inspection_worker,
                name="InspectionWorker",
                entity=entity,
            )

    ### Class methods ###

    def get_selected_row(self) -> schemas.BucketEntity | None:
        """
        Get the data of the currently selected row in the table and return it
        as a BucketEntity 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.BucketEntity(
                name=services.resolve_credentials(self.connection.bucket_name),
                type=schemas.BucketEntityType.bucket,
            )
        ]
        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.BucketEntity) -> None:
        """
        Add an item to the history stack.
        """
        self.history.append(entity)

    def go_back_in_history(self) -> schemas.BucketEntity | 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.BucketEntity) -> None:
        """
        Inspect a specific row in the bucket table.
        """
        if entity.type == schemas.BucketEntityType.bucket:
            entities = self.provider.inspect(prefix=None)
        elif entity.type == schemas.BucketEntityType.folder:
            entities = self.provider.inspect(prefix=entity.name)
        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 download(
        self, entity: schemas.BucketEntity, target_path: pathlib.Path
    ) -> None:
        """
        Download a specific bucket entity.
        """
        if entity.type in (
            schemas.BucketEntityType.file,
            schemas.BucketEntityType.folder,
        ):
            self.provider.download(
                key=entity.name,
                target=target_path,
                progress_callback=self.download_progress_callback,
            )

    def upload(
        self, entity: schemas.BucketEntity, source_path: pathlib.Path
    ) -> None:
        """
        Upload local files to bucket.
        """
        if entity.type in (
            schemas.BucketEntityType.bucket,
            schemas.BucketEntityType.folder,
        ):
            self.provider.upload(
                key=entity.name,
                type=entity.type,
                source=source_path,
                progress_callback=self.upload_progress_callback,
            )

    def delete(self, entity: schemas.BucketEntity) -> None:
        """
        Upload local files to bucket.
        """
        if entity.type in (
            schemas.BucketEntityType.folder,
            schemas.BucketEntityType.file,
        ):
            self.provider.delete(
                key=entity.name,
                progress_callback=self.delete_progress_callback,
            )

    def filter_main(self) -> None:
        """
        Filter bucket 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 = [
                entity
                for entity in self.all_entities
                if (query in entity.name.lower())
            ]

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

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

            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)

                size = (
                    services.format_file_size(entity.size)
                    if entity.size
                    else ""
                )
                last_modified = (
                    services.to_time_string(entity.last_modified)
                    if entity.last_modified
                    else ""
                )

                self.table.add_row(
                    name,
                    size,
                    last_modified,
                    entity.type.value,
                )

        super().refresh_main(show_truncated_hint=show_truncated_hint)

    def get_entity_display_name(self, entity: schemas.BucketEntity) -> str:
        """
        Get the display name for a bucket entity, including an icon if
        configured.
        """
        shortened_name = pathlib.Path(entity.name).name

        if entity.type == schemas.BucketEntityType.bucket:
            icon = self.config.gui.icons.bucket
        elif entity.type == schemas.BucketEntityType.folder:
            icon = self.config.gui.icons.folder
        elif entity.type == schemas.BucketEntityType.file:
            icon = self.config.gui.icons.file
        else:
            icon = ""

        if icon:
            return f"{icon} {shortened_name}"
        else:
            return shortened_name

    ### Callbacks ###

    def download_progress_callback(self, current: int, total: int) -> None:
        """
        Callback function to update the status screen with download progress.
        """
        self.app.call_from_thread(
            lambda: (
                self.status_screen.update_label(
                    f"Downloading file {current}/{total}..."
                )
                if self.status_screen
                else None
            )
        )

    def upload_progress_callback(self, current: int, total: int) -> None:
        """
        Callback function to update the status screen with upload progress.
        """
        self.app.call_from_thread(
            lambda: (
                self.status_screen.update_label(
                    f"Uploading file {current}/{total}..."
                )
                if self.status_screen
                else None
            )
        )

    def delete_progress_callback(self, current: int, total: int) -> None:
        """
        Callback function to update the status screen with delete progress.
        """
        self.app.call_from_thread(
            lambda: (
                self.status_screen.update_label(
                    f"Deleting file {current}/{total}..."
                )
                if self.status_screen
                else None
            )
        )

    ### Workers ###

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

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

            self.inspect(entity=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 bucket entity: {message}"
                )
            )

    def download_worker(
        self, entity: schemas.BucketEntity, target_path: pathlib.Path
    ) -> None:
        """
        Download an entity.
        """
        try:
            self.app.call_from_thread(self.push_status_screen)

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

            self.download(entity=entity, target_path=target_path)

            logger.debug("Download successful.")

            self.app.call_from_thread(self.dismiss_status_screen)

            self.app.call_from_thread(
                lambda: self.notify_info(
                    f"Download of '{entity.name}' completed."
                )
            )
        except Exception as exception:
            message = str(exception)
            logger.error(f"Download 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"Download failed: {message}")
            )

    def upload_worker(
        self, entity: schemas.BucketEntity, source_path: pathlib.Path
    ) -> None:
        """
        Upload a path.
        """
        try:
            self.app.call_from_thread(self.push_status_screen)

            logger.debug(f"Uploading '{source_path}' to '{entity.name}'...")

            self.upload(entity=entity, source_path=source_path)

            logger.debug("Upload successful.")

            if self.current_entity:
                self.inspect(entity=self.current_entity)
                self.app.call_from_thread(
                    self.refresh_main, show_truncated_hint=False
                )

            self.app.call_from_thread(self.dismiss_status_screen)

            self.app.call_from_thread(
                lambda: self.notify_info(
                    f"Upload of '{source_path}' completed."
                )
            )
        except Exception as exception:
            message = str(exception)
            logger.error(f"Upload 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"Upload failed: {message}")
            )

    def delete_worker(self, entity: schemas.BucketEntity) -> None:
        """
        Delete a path.
        """
        try:
            self.app.call_from_thread(self.push_status_screen)

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

            self.delete(entity=entity)

            logger.debug("Deletion successful.")

            if self.current_entity:
                self.inspect(entity=self.current_entity)
                self.app.call_from_thread(self.refresh_main)

            self.app.call_from_thread(self.dismiss_status_screen)

            self.app.call_from_thread(
                lambda: self.notify_info(
                    f"Deletion of '{entity.name}' completed."
                )
            )
        except Exception as exception:
            message = str(exception)
            logger.error(f"Deletion 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"Deletion failed: {message}")
            )
