import logging
import sys
from pathlib import Path

from PyQt6 import QtCore, uic
from PyQt6.QtCore import QPoint, Qt
from PyQt6.QtCore import pyqtSignal as Signal
from PyQt6.QtGui import QFontMetrics, QKeySequence, QShortcut
from PyQt6.QtWidgets import (
    QApplication,
    QCheckBox,
    QFileDialog,
    QListWidgetItem,
    QMessageBox,
    QToolTip,
)

from vorta.profile_export import ImportFailedException, ProfileExport
from vorta.store.models import BackupProfileModel, SettingsModel
from vorta.utils import (
    borg_compat,
    get_asset,
    get_network_status_monitor,
    is_system_tray_available,
)
from vorta.views.dialogs.profile.export_window import ExportWindow
from vorta.views.dialogs.profile.import_window import ImportWindow
from vorta.views.dialogs.profile.profile_add_edit import AddProfileWindow, EditProfileWindow
from vorta.views.partials.loading_button import LoadingButton
from vorta.views.utils import get_colored_icon

from .about_tab import AboutTab
from .archive_tab import ArchiveTab
from .misc_tab import MiscTab
from .repo_tab import RepoTab
from .schedule_tab import ScheduleTab
from .source_tab import SourceTab

uifile = get_asset('UI/main_window.ui')
MainWindowUI, MainWindowBase = uic.loadUiType(uifile)

logger = logging.getLogger(__name__)


class MainWindow(MainWindowBase, MainWindowUI):
    loaded = Signal()

    def __init__(self, parent=None):
        super().__init__()
        self.setupUi(self)
        self.setWindowTitle('Vorta for Borg Backup')
        self.app = parent
        self.setWindowIcon(get_colored_icon("icon"))
        if sys.platform.startswith('linux'):
            self.app.setDesktopFileName('com.borgbase.Vorta')
        self.setWindowFlags(QtCore.Qt.WindowType.WindowCloseButtonHint | QtCore.Qt.WindowType.WindowMinimizeButtonHint)
        self.createStartBtn = LoadingButton(self.tr("Start Backup"))
        self.gridLayout.addWidget(self.createStartBtn, 0, 0, 1, 1)
        self.createStartBtn.setGif(get_asset("icons/loading"))

        # set log label height to two lines
        fontmetrics: QFontMetrics = self.logText.fontMetrics()
        self.logText.setMinimumHeight(fontmetrics.lineSpacing() * 2 + fontmetrics.leading())

        # Use previous window state
        previous_window_width = SettingsModel.get(key='previous_window_width')
        previous_window_height = SettingsModel.get(key='previous_window_height')
        self.resize(int(previous_window_width.str_value), int(previous_window_height.str_value))

        # Select previously used profile, if available
        prev_profile_id = SettingsModel.get(key='previous_profile_id')
        self.current_profile = BackupProfileModel.get_or_none(id=prev_profile_id.str_value)
        if self.current_profile is None:
            profiles = BackupProfileModel.select()
            self.current_profile = min(profiles, key=lambda p: (p.name.casefold(), p.name))

        # Load tab models
        self.repoTab = RepoTab(self.repoTabSlot, profile_provider=self.get_current_profile)
        self.sourceTab = SourceTab(self.sourceTabSlot, profile_provider=self.get_current_profile)
        self.archiveTab = ArchiveTab(self.archiveTabSlot, app=self.app, profile_provider=self.get_current_profile)
        self.scheduleTab = ScheduleTab(self.scheduleTabSlot, profile_provider=self.get_current_profile)
        self.miscTab = MiscTab(self.SettingsTabSlot, profile_provider=self.get_current_profile)
        self.aboutTab = AboutTab(self.AboutTabSlot, profile_provider=self.get_current_profile)
        self.aboutTab.set_borg_details(borg_compat.version, borg_compat.path)
        self.miscWidget.hide()
        self.tabWidget.setCurrentIndex(0)

        self.repoTab.repo_changed.connect(self.archiveTab.populate_from_profile)
        self.repoTab.repo_changed.connect(self.scheduleTab.schedulePage.populate_from_profile)
        self.repoTab.repo_added.connect(self.archiveTab.refresh_archive_list)
        self.miscTab.refresh_archive.connect(self.archiveTab.populate_from_profile)

        self.miscButton.clicked.connect(self.toggle_misc_visibility)
        self.createStartBtn.clicked.connect(self.app.create_backup_action)
        self.cancelButton.clicked.connect(self.app.backup_cancelled_event.emit)

        QShortcut(QKeySequence("Ctrl+W"), self).activated.connect(self.on_close_window)
        QShortcut(QKeySequence("Ctrl+Q"), self).activated.connect(self.on_close_window)

        self.app.backup_started_event.connect(self.backup_started_event)
        self.app.backup_finished_event.connect(self.backup_finished_event)
        self.app.backup_log_event.connect(self.set_log)
        self.app.backup_progress_event.connect(self.set_progress)
        self.app.backup_cancelled_event.connect(self.backup_cancelled_event)

        # Init profile list
        self.populate_profile_selector()
        self.profileSelector.itemClicked.connect(self.profile_clicked_action)
        self.profileSelector.currentItemChanged.connect(self.profile_selection_changed_action)
        self.profileRenameButton.clicked.connect(self.profile_rename_action)
        self.profileExportButton.clicked.connect(self.profile_export_action)
        self.profileDeleteButton.clicked.connect(self.profile_delete_action)
        self.profileAddButton.addAction(self.tr("Create new profile"), self.profile_add_action)
        self.profileAddButton.addAction(self.tr("Import from file…"), self.profile_import_action)

        # OS-specific startup options:
        if not get_network_status_monitor().is_network_status_available():
            # Hide Wifi-rule section in schedule tab.
            self.scheduleTab.networksPage.wifiListLabel.hide()
            self.scheduleTab.networksPage.wifiListWidget.hide()
            self.scheduleTab.page_2.hide()
            self.scheduleTab.toolBox.removeItem(1)

        # Connect to existing thread.
        if self.app.jobs_manager.is_worker_running():
            self.createStartBtn.setEnabled(False)
            self.createStartBtn.start()
            self.cancelButton.setEnabled(True)

        # Connect to palette change
        self._palette_connection = QApplication.instance().paletteChanged.connect(lambda p: self.set_icons())
        self.destroyed.connect(self._on_destroyed)

        self.set_icons()
        self.loaded.emit()

    def on_close_window(self):
        self.close()

    def get_current_profile(self):
        return BackupProfileModel.get(id=self.current_profile.id)

    def set_icons(self):
        self.profileAddButton.setIcon(get_colored_icon('plus'))
        self.profileRenameButton.setIcon(get_colored_icon('edit'))
        self.profileExportButton.setIcon(get_colored_icon('file-import-solid'))
        self.profileDeleteButton.setIcon(get_colored_icon('minus'))
        self.miscButton.setIcon(get_colored_icon('settings_wheel'))

    def _on_destroyed(self):
        try:
            QApplication.instance().paletteChanged.disconnect(self._palette_connection)
        except (TypeError, RuntimeError):
            pass

    def set_progress(self, text=''):
        self.progressText.setText(text)
        self.progressText.repaint()

    def set_log(self, text='', context=None):
        # Truncate very long messages to keep the status area readable
        if text and len(text) > 300:
            text = text[:300] + '...'
        self.logText.setText(text)
        self.logText.repaint()

    def _toggle_buttons(self, create_enabled=True):
        if create_enabled:
            self.createStartBtn.stop()
        else:
            self.createStartBtn.start()
        self.createStartBtn.setEnabled(create_enabled)
        self.createStartBtn.repaint()
        self.cancelButton.setEnabled(not create_enabled)
        self.cancelButton.repaint()

    def populate_profile_selector(self):
        # Clear the previous entries
        self.profileSelector.clear()

        # Keep track of the current item to be selected (if any)
        current_item = None

        # Add items to the QListWidget
        profiles = BackupProfileModel.select()
        for profile in sorted(profiles, key=lambda p: (p.name.casefold(), p.name)):
            item = QListWidgetItem(profile.name)
            item.setData(Qt.ItemDataRole.UserRole, profile.id)

            self.profileSelector.addItem(item)

            if profile.id == self.current_profile.id:
                current_item = item

        # Set the current profile as selected
        if current_item:
            self.profileSelector.setCurrentItem(current_item)

    def profile_selection_changed_action(self, index):
        profile = self.profileSelector.currentItem()
        backup_profile_id = profile.data(Qt.ItemDataRole.UserRole) if profile else None
        if not backup_profile_id:
            return

        self.current_profile = BackupProfileModel.get(id=backup_profile_id)
        SettingsModel.update({SettingsModel.str_value: self.current_profile.id}).where(
            SettingsModel.key == 'previous_profile_id'
        ).execute()

        self.app.profile_changed_event.emit()

    def profile_clicked_action(self):
        if self.miscWidget.isVisible():
            self.toggle_misc_visibility()

    def profile_rename_action(self):
        backup_profile_id = self.profileSelector.currentItem().data(Qt.ItemDataRole.UserRole)
        window = EditProfileWindow(rename_existing_id=backup_profile_id)
        self.window = window  # For tests
        window.setParent(self, QtCore.Qt.WindowType.Sheet)
        window.open()
        window.profile_changed.connect(self.profile_add_edit_result)
        window.rejected.connect(lambda: self.profileSelector.setCurrentIndex(self.profileSelector.currentIndex()))

    def profile_delete_action(self):
        if self.profileSelector.count() > 1:
            to_delete_id = self.profileSelector.currentItem().data(Qt.ItemDataRole.UserRole)
            to_delete = BackupProfileModel.get(id=to_delete_id)

            msg = self.tr("Are you sure you want to delete profile '{}'?").format(to_delete.name)
            reply = QMessageBox.question(
                self,
                self.tr("Confirm deletion"),
                msg,
                QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
                QMessageBox.StandardButton.No,
            )

            if reply == QMessageBox.StandardButton.Yes:
                to_delete.delete_instance(recursive=True)
                self.app.scheduler.remove_job(to_delete_id)  # Remove pending jobs
                self.profileSelector.takeItem(self.profileSelector.currentRow())
                self.profile_selection_changed_action(0)

        else:
            warn = self.tr("Cannot delete the last profile.")
            point = QPoint(0, int(self.profileDeleteButton.size().height() / 2))
            QToolTip.showText(self.profileDeleteButton.mapToGlobal(point), warn)

    def profile_add_action(self):
        window = AddProfileWindow()
        self.window = window  # For tests
        window.setParent(self, QtCore.Qt.WindowType.Sheet)
        window.open()
        window.profile_changed.connect(self.profile_add_edit_result)
        window.rejected.connect(lambda: self.profileSelector.setCurrentIndex(self.profileSelector.currentIndex()))

    def profile_export_action(self):
        """
        React to pressing "Export Profile" button and save current
        profile as .json file.
        """
        window = ExportWindow(profile=self.current_profile.refresh())
        self.window = window
        window.setParent(self, QtCore.Qt.WindowType.Sheet)
        window.show()

    def profile_import_action(self):
        """
        React to "Import Profile". Ask to select a .json file and import it as
        new profile.
        """

        def profile_imported_event(profile):
            QMessageBox.information(
                None,
                self.tr('Profile import successful!'),
                self.tr('Profile {} imported.').format(profile.name),
            )
            self.populate_profile_selector()
            self.app.profile_changed_event.emit()

        filename = QFileDialog.getOpenFileName(
            self,
            self.tr("Load profile"),
            str(Path.home()),
            self.tr("JSON (*.json);;All files (*)"),
        )[0]
        if filename:
            try:
                profile_export = ProfileExport.from_json(filename)
            except ImportFailedException as exception:
                QMessageBox.critical(None, self.tr('Failed to import profile'), str(exception))
                return
            window = ImportWindow(profile_export=profile_export)
            self.window = window
            window.setParent(self, QtCore.Qt.WindowType.Sheet)
            window.profile_imported.connect(profile_imported_event)
            window.show()

    def profile_add_edit_result(self, profile_name, profile_id):
        # Profile is renamed
        if self.profileSelector.currentItem().data(Qt.ItemDataRole.UserRole) == profile_id:
            profile = self.profileSelector.takeItem(self.profileSelector.currentRow())
            profile.setText(profile_name)
        # Profile is added
        else:
            profile = QListWidgetItem(profile_name)
            profile.setData(Qt.ItemDataRole.UserRole, profile_id)
        # Insert the profile at the correct position
        # Both the casefolded and the original name are used as the key to keep the order stable
        profile_key = (profile.text().casefold(), profile.text())
        row = 0
        for i in range(self.profileSelector.count()):
            item_name = self.profileSelector.item(i).text()
            if (item_name.casefold(), item_name) < profile_key:
                row += 1
        self.profileSelector.insertItem(row, profile)
        self.profileSelector.setCurrentItem(profile)

    def toggle_misc_visibility(self):
        if self.miscWidget.isVisible():
            self.miscWidget.hide()
            self.tabWidget.setCurrentIndex(0)
            self.miscButton.setStyleSheet("font-weight: normal;")
            self.tabWidget.show()
        else:
            self.tabWidget.hide()
            self.miscWidget.setCurrentIndex(0)
            self.miscButton.setStyleSheet("font-weight: bold;")
            self.miscWidget.show()

    def backup_started_event(self):
        self._toggle_buttons(create_enabled=False)
        self.archiveTab._toggle_all_buttons(enabled=False)
        self.set_log('')

    def backup_finished_event(self):
        if not self.app.jobs_manager.is_worker_running() and (
            self.archiveTab.remaining_refresh_archives == 0 or self.archiveTab.remaining_refresh_archives == 1
        ):  # Either the refresh is done or this is the last archive to refresh.
            self._toggle_buttons(create_enabled=True)
            self.archiveTab._toggle_all_buttons(enabled=True)

    def backup_cancelled_event(self):
        self._toggle_buttons(create_enabled=True)
        self.set_log(self.tr('Task cancelled'))

    def closeEvent(self, event):
        # Save window state in SettingsModel
        SettingsModel.update({SettingsModel.str_value: str(self.width())}).where(
            SettingsModel.key == 'previous_window_width'
        ).execute()
        SettingsModel.update({SettingsModel.str_value: str(self.height())}).where(
            SettingsModel.key == 'previous_window_height'
        ).execute()

        if not is_system_tray_available():
            if SettingsModel.get(key="enable_background_question").value:
                msg = QMessageBox()
                msg.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No)
                msg.setParent(self, QtCore.Qt.WindowType.Sheet)
                msg.setText(self.tr("Should Vorta continue to run in the background?"))
                msg.button(QMessageBox.StandardButton.Yes).clicked.connect(
                    lambda: self.miscTab.save_setting("disable_background_state", True)
                )
                msg.button(QMessageBox.StandardButton.No).clicked.connect(
                    lambda: (
                        self.miscTab.save_setting("disable_background_state", False),
                        self.app.quit(),
                    )
                )
                msg.setWindowTitle(self.tr("Quit"))
                dont_show_box = QCheckBox(self.tr("Don't show this again"))
                dont_show_box.clicked.connect(lambda x: self.miscTab.save_setting("enable_background_question", not x))
                dont_show_box.setTristate(False)
                msg.setCheckBox(dont_show_box)
                msg.exec()
            elif not SettingsModel.get(key="disable_background_state").value:
                self.app.quit()
        event.accept()
