import logging
from pathlib import PurePath

from peewee import fn
from PyQt6 import QtCore, QtGui, uic
from PyQt6.QtCore import QFileInfo, QMimeData, QPoint, Qt, QUrl, pyqtSlot
from PyQt6.QtGui import QShortcut
from PyQt6.QtWidgets import (
    QApplication,
    QHeaderView,
    QMenu,
    QTableWidgetItem,
)

from vorta.filedialog import VortaFileSelector
from vorta.store.models import SettingsModel, SourceFileModel
from vorta.utils import (
    FilePathInfoAsync,
    get_asset,
    pretty_bytes,
    sort_sizes,
)
from vorta.views.base_tab import BaseTab
from vorta.views.dialogs.archive.exclude import ExcludeDialog
from vorta.views.utils import get_colored_icon

uifile = get_asset('UI/source_tab.ui')
SourceUI, SourceBase = uic.loadUiType(uifile)

logger = logging.getLogger(__name__)


class SourceColumn:
    Path = 0
    Size = 1
    FilesCount = 2


class SizeItem(QTableWidgetItem):
    def __init__(self, s):
        super().__init__(s)
        self.setTextAlignment(Qt.AlignmentFlag.AlignVCenter + Qt.AlignmentFlag.AlignRight)

    def __lt__(self, other):
        if other.text() == '':
            return False
        elif self.text() == '':
            return True
        else:
            return sort_sizes([self.text(), other.text()]) == [
                self.text(),
                other.text(),
            ]


class FilesCount(QTableWidgetItem):
    def __lt__(self, other):
        # Verify that conversion is only performed on valid integers
        # If one of the 2 elements is no number, put these elements at the end
        # This is important if the text is "Calculating..." or ""
        if self.text().isdigit() and other.text().isdigit():
            return int(self.text()) < int(other.text())  # Compare & return result
        else:
            if not self.text().isdigit():
                return 1  # Move one down if current item has no valid count
            if not other.text().isdigit():
                return 0


class SourceTab(BaseTab, SourceBase, SourceUI):
    updateThreads = []

    def __init__(self, parent=None, profile_provider=None):
        super().__init__(parent=parent, profile_provider=profile_provider)
        self.setupUi(parent)

        # Prepare source files view
        header = self.sourceFilesWidget.horizontalHeader()
        header.setVisible(True)
        header.setSortIndicatorShown(1)

        header.setSectionResizeMode(SourceColumn.Path, QHeaderView.ResizeMode.Stretch)
        header.setSectionResizeMode(SourceColumn.Size, QHeaderView.ResizeMode.ResizeToContents)
        header.setSectionResizeMode(SourceColumn.FilesCount, QHeaderView.ResizeMode.ResizeToContents)

        self.sourceFilesWidget.setSortingEnabled(True)
        self.sourceFilesWidget.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
        self.sourceFilesWidget.customContextMenuRequested.connect(self.sourceitem_contextmenu)
        self.sourceFilesWidget.setAlternatingRowColors(True)

        # Shortcuts
        shortcut_copy = QShortcut(QtGui.QKeySequence.StandardKey.Copy, self.sourceFilesWidget)
        shortcut_copy.activated.connect(self.source_copy)

        # Connect signals
        self.addButton.clicked.connect(self.source_add)
        self.removeButton.clicked.connect(self.source_remove)
        self.updateButton.clicked.connect(self.sources_update)
        self.bExclude.clicked.connect(self.show_exclude_dialog)
        header.sortIndicatorChanged.connect(self.update_sort_order)

        # Populate
        self.track_profile_change(call_now=True)
        self.set_icons()

        # Listen for events
        self.track_palette_change()

    def set_icons(self):
        "Used when changing between light- and dark mode"
        self.addButton.setIcon(get_colored_icon('plus'))
        self.removeButton.setIcon(get_colored_icon('minus'))
        self.updateButton.setIcon(get_colored_icon('refresh'))

        for row in range(self.sourceFilesWidget.rowCount()):
            path_item = self.sourceFilesWidget.item(row, SourceColumn.Path)
            db_item = SourceFileModel.get(dir=path_item.text(), profile=self.profile())

            if db_item.path_isdir:
                path_item.setIcon(get_colored_icon('folder'))
            else:
                path_item.setIcon(get_colored_icon('file'))

    @pyqtSlot(QPoint)
    def sourceitem_contextmenu(self, pos: QPoint):
        """Show a context menu for the source item at `pos`."""
        # index under cursor
        index = self.sourceFilesWidget.indexAt(pos)
        if not index.isValid():
            return  # popup only for items

        menu = QMenu(self.sourceFilesWidget)

        menu.addAction(
            get_colored_icon('copy'),
            self.tr("Copy"),
            lambda: self.source_copy(index=index),
        )
        menu.addAction(get_colored_icon('minus'), self.tr("Remove"), self.source_remove)

        menu.popup(self.sourceFilesWidget.viewport().mapToGlobal(pos))

    def set_path_info(self, path, data_size, files_count):
        # disable sorting temporarily
        sorting = self.sourceFilesWidget.isSortingEnabled()
        self.sourceFilesWidget.setSortingEnabled(False)

        items = self.sourceFilesWidget.findItems(path, QtCore.Qt.MatchFlag.MatchExactly)
        # Conversion int->str->int needed because QT limits int to 32-bit
        data_size = int(data_size)
        files_count = int(files_count)

        for item in items:
            try:
                db_item = SourceFileModel.get(dir=path, profile=self.profile())
            except SourceFileModel.DoesNotExist:
                continue

            if QFileInfo(path).isDir():
                self.sourceFilesWidget.item(item.row(), SourceColumn.FilesCount).setText(format(files_count))
                db_item.path_isdir = True
                self.sourceFilesWidget.item(item.row(), SourceColumn.Path).setIcon(get_colored_icon('folder'))
            else:
                # No files count, if entry itself is a file
                self.sourceFilesWidget.item(item.row(), SourceColumn.FilesCount).setText("")
                db_item.path_isdir = False
                self.sourceFilesWidget.item(item.row(), SourceColumn.Path).setIcon(get_colored_icon('file'))

            self.sourceFilesWidget.item(item.row(), SourceColumn.Size).setText(pretty_bytes(data_size))

            db_item.dir_size = data_size
            db_item.dir_files_count = files_count
            db_item.save()
        # Remove thread from list when it's done
        for thrd in self.updateThreads:
            if thrd.objectName() == path:
                self.updateThreads.remove(thrd)

        # enable sorting again
        self.sourceFilesWidget.setSortingEnabled(sorting)
        self.update_total_size()

    def update_path_info(self, index_row: int):
        """
        Update the information for the source in the given table row.

        This displays `Calculating...` in the updated rows and creates a
        `FilePathInfoAsync` instance to get the new information.
        The method `set_path_info` will update the row with the information
        provided by this instance.

        Parameters
        ----------
        index_row : int
            The index of the row to update.
        """
        logger.debug(f"Updating source in row {index_row}.")  # Debug #1080

        path = self.sourceFilesWidget.item(index_row, SourceColumn.Path).text()
        self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText(self.tr("Calculating…"))
        self.sourceFilesWidget.item(index_row, SourceColumn.FilesCount).setText(self.tr("Calculating…"))
        getDir = FilePathInfoAsync(path, self.profile().get_combined_exclusion_string())
        getDir.signal.connect(self.set_path_info)
        getDir.setObjectName(path)
        self.updateThreads.append(getDir)  # this is ugly, is there a better way to keep the thread object?
        getDir.start()

    def add_source_to_table(self, source, update_data=None):
        # disable sorting temporarily
        sorting = self.sourceFilesWidget.isSortingEnabled()
        self.sourceFilesWidget.setSortingEnabled(False)

        if update_data is None:
            update_data = SettingsModel.get(key="get_srcpath_datasize").value

        index_row = self.sourceFilesWidget.rowCount()
        self.sourceFilesWidget.setRowCount(self.sourceFilesWidget.rowCount() + 1)
        # Insert all items on current row, add tooltip containing the path name
        new_item = QTableWidgetItem(source.dir)
        new_item.setToolTip(source.dir)
        self.sourceFilesWidget.setItem(index_row, SourceColumn.Path, new_item)
        self.sourceFilesWidget.setItem(index_row, SourceColumn.Size, SizeItem(""))
        self.sourceFilesWidget.setItem(index_row, SourceColumn.FilesCount, FilesCount(""))

        logger.debug(f"Added item number {index_row}" + f" from {self.sourceFilesWidget.rowCount()}")

        if update_data:
            self.update_path_info(index_row)

            # Debug #1080
            logger.debug("Updated info for previously added item.")

        else:  # Use cached data from DB
            if source.dir_size > -1:
                self.sourceFilesWidget.item(index_row, SourceColumn.Size).setText(pretty_bytes(source.dir_size))

                if source.path_isdir:
                    self.sourceFilesWidget.item(index_row, SourceColumn.FilesCount).setText(
                        format(source.dir_files_count)
                    )
                    self.sourceFilesWidget.item(index_row, SourceColumn.Path).setIcon(get_colored_icon('folder'))
                else:
                    self.sourceFilesWidget.item(index_row, SourceColumn.Path).setIcon(get_colored_icon('file'))

        # enable sorting again
        self.sourceFilesWidget.setSortingEnabled(sorting)

    def populate_from_profile(self):
        profile = self.profile()
        self.sourceFilesWidget.setRowCount(0)  # Clear rows

        for source in SourceFileModel.select().where(SourceFileModel.profile == profile):
            self.add_source_to_table(source, False)
        self.update_total_size()
        # Fetch the Sort by Column and order
        sourcetab_sort_column = int(SettingsModel.get(key='sourcetab_sort_column').str_value)
        sourcetab_sort_order = int(SettingsModel.get(key='sourcetab_sort_order').str_value)

        # Sort items as per settings
        self.sourceFilesWidget.sortItems(sourcetab_sort_column, Qt.SortOrder(sourcetab_sort_order))

    def update_sort_order(self, column: int, order: int):
        """Save selected sort by column and order to settings"""
        SettingsModel.update({SettingsModel.str_value: str(column)}).where(
            SettingsModel.key == 'sourcetab_sort_column'
        ).execute()
        SettingsModel.update({SettingsModel.str_value: str(order.value)}).where(
            SettingsModel.key == 'sourcetab_sort_order'
        ).execute()

    def sources_update(self):
        """
        Update each row in the sources table.

        Calls `update_path_info` for each row. to do the job.
        """
        row_count = self.sourceFilesWidget.rowCount()

        logger.debug(f"Updating sources ({row_count})")  # Debug #1080

        for row in range(0, row_count):
            self.update_path_info(row)  # Update data for each entry

    def source_add(self):
        # Selected paths from file dialog
        file_dialog = VortaFileSelector(
            self, window_title='Add Files and Folders', title='Select files and folders to include as sources:'
        )
        paths = file_dialog.get_paths()
        if paths:
            for path in paths:
                # Add sources to the table
                new_source, created = SourceFileModel.get_or_create(dir=path, profile=self.profile())
                if created:
                    self.add_source_to_table(new_source)
                    new_source.save()
            self.update_total_size()

    def source_copy(self, index=None):
        """
        Copy a source path to the clipboard.

        Copies the first selected source if no index is specified.
        """
        if index is None:
            indexes = self.sourceFilesWidget.selectionModel().selectedRows()

            if not indexes:
                return

            index = indexes[0]

        path = PurePath(self.sourceFilesWidget.item(index.row(), SourceColumn.Path).text())

        data = QMimeData()
        data.setUrls([QUrl(path.as_uri())])
        data.setText(str(path))

        QApplication.clipboard().setMimeData(data)

    def source_remove(self):
        indexes = self.sourceFilesWidget.selectionModel().selectedRows()
        profile = self.profile()
        # sort indexes, starting with lowest
        indexes.sort()
        # remove each selected row, starting with the highest index (otherwise, higher indexes become invalid)
        for index in reversed(indexes):
            path = self.sourceFilesWidget.item(index.row(), SourceColumn.Path).text()
            db_item = SourceFileModel.get(
                dir=path,
                profile=profile,
            )
            db_item.delete_instance()
            self.sourceFilesWidget.removeRow(index.row())

            for thrd in self.updateThreads[:]:
                if thrd.objectName() == path:
                    try:
                        thrd.signal.disconnect(self.set_path_info)
                    except (RuntimeError, TypeError):
                        pass
                    self.updateThreads.remove(thrd)

            logger.debug(f"Removed source in row {index.row()}")
        self.update_total_size()

    def update_total_size(self):
        """
        Update the total size and files count for all sources.
        """
        total_size, total_files = (
            SourceFileModel.select(fn.SUM(SourceFileModel.dir_size), fn.SUM(SourceFileModel.dir_files_count))
            .where(SourceFileModel.profile == self.profile(), SourceFileModel.dir_size >= 0)
            .scalar(as_tuple=True)
        )

        if total_size is not None:
            total_files = total_files or 0
            self.totalSizeLabel.setText(
                self.tr("Total Size: {size}, {count} files").format(size=pretty_bytes(total_size), count=total_files)
            )
        else:
            self.totalSizeLabel.setText("")

    def show_exclude_dialog(self):
        window = ExcludeDialog(self.profile(), self)
        window.setParent(self, QtCore.Qt.WindowType.Sheet)
        self._window = window  # for testing
        window.show()

    # NOTE: This function is temporarily removed.
    # Reason: This paste option has been removed as part of the addition of new File Dialog.
    # This function is no longer used. Kept here for reference or possible future use.

    # def paste_text(self):
    #     sources = QApplication.clipboard().text().splitlines()
    #     invalidSources = ""
    #     for source in sources:
    #         if len(source) > 0:  # Ignore empty newlines
    #             if source.startswith('file://'):  # Allow pasting multiple files/folders copied from file manager
    #                 source = source[7:]
    #             if not os.path.exists(source):
    #                 invalidSources = invalidSources + "\n" + source
    #             else:
    #                 new_source, created = SourceFileModel.get_or_create(dir=source, profile=self.profile())
    #                 if created:
    #                     self.add_source_to_table(new_source)
    #                     new_source.save()

    #     if len(invalidSources) != 0:  # Check if any invalid paths
    #         msg = QMessageBox()
    #         msg.setText(self.tr("Some of your sources are invalid:") + invalidSources)
    #         self._msg = msg  # for testing
    #         msg.exec()
