# -*- coding: utf-8 -*-
"""
/***************************************************************************
 qgis_plugin
                                 A QGIS plugin
 A plugin to sync between cartodroid and QGIS.
 Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/
                              -------------------
        begin                : 2025-12-10
        git sha              : $Format:%H$
        copyright            : (C) 2025 by ITACYL
        email                : palferfr@itacyl.es
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
"""
import json
import logging
import sys
import traceback
from pathlib import Path
from typing import Callable

from PyQt5.QtCore import QStringListModel, pyqtSlot, pyqtSignal, QObject, QTimer, QSettings, QTranslator, QVariant
from PyQt5.QtWidgets import QListView, QMessageBox, QPushButton, QWidget, QLabel

from .configuration.configuration_manager import ConfigurationManager
from PyQt5.QtCore import QT_TRANSLATE_NOOP

from .configuration.photo_layers_configuration import PhotoLayersConfiguration
from .dialog.project_wizard.project_wizard import ProjectWizard
from .events.project_configuration_changed import ProjectConfigurationChanged
from .i18n import tr
from .localsync.config import config

from qgis.PyQt.QtCore import QCoreApplication
from qgis._core import Qgis, QgsField, QgsExifTools, QgsFeature, QgsGeometry, QgsPointXY
from qgis._gui import QgisInterface
from qgis.core import QgsApplication, QgsProject, QgsVectorLayer
from qgis.PyQt.QtGui import QIcon
from qgis.PyQt.QtWidgets import QAction, QComboBox, QWidgetAction
from .configuration.json_reader import JsonReader
from .dialog.messages_dialog import MessagesDialog

from .dialog.localsync_save_layers import LocalsyncSaveLayers
from . import constants
from .constants import QGIS_PLUGIN_NAME, QGIS_LABEL_SELECTED_PROJECT_TEXT

from . import resources

from .localsync.core.sync_engine import SyncEngine
from .localsync.project.project_manager import ProjectManager
from .logger.qgis_logger_handler import QgisLoggerHandler

# Initialize Qt resources from file resources.py

from .dialog.localsync_conf_panel import LocalsyncConfPanel
from .signals.global_signals import lcpc2p, cac_check
from .tasks.load_files import LoadFiles

import os

from .ui_controllers.devices_combo import DevicesCombo

def excepthook(type_, value, tb):
    traceback.print_exception(type_, value, tb)

sys.excepthook = excepthook

JSON_MALFORMED = "Json malformed"

class SyncListener(QObject):

    """
        This class is in charge of maintaining in the main thread all the actions of the message bar management as well
         as the message dialogue associated to it.
         :ivar start_signal: Signal to create a new message bar.
         :type start_signal: pyqtSignal
         :ivar message_bar: reference to the current message bar displayed.
         :type message_bar: QMessageBar
    """

    start_signal = pyqtSignal(str, int, int, bool)

    def __init__(self, iface: QgisInterface, dlg: MessagesDialog):
        """
        Constructor.
        :param iface: QgisInterface of the QGIS application.
        :param dlg: message dialogue reference associated with the message bar.
        """
        super().__init__()
        self.iface = iface
        self.dlg = dlg
        self.message_bar = None
        self.start_signal.connect(self._on_start_ui)

    def create_or_update_message_bar(self, msg:str, level:Qgis.MessageLevel = Qgis.Info, time: int = 0,
                                     clear_messages: bool = True):
        """
            Sends the signal to create or update the message bar.
            :param msg: message to be displayed.
            :param level: message level. It changes the color of the message bar.
            :param time: time in milliseconds to clean the new created message bar.
            :param clear_messages: clear the messages in the dialog before creating the new message bar.
        """
        self.start_signal.emit(msg, level, time, clear_messages)

    @pyqtSlot(str, int, int, bool)
    def _on_start_ui(self, msg: str, level: Qgis.MessageLevel, time: int, clear_messages: bool):
        """
            Create or update the message bar, and clears the messages in the dialog.
            :param msg: message to be displayed.
            :param level: message level. It changes the color of the message bar.
            :param time: time in milliseconds to clean the new created message bar.
            :param clear_messages: clear the messages in the dialog before creating the new message bar.
        """
        if clear_messages:
            self.dlg.clear_messages()
        self.iface.messageBar().clearWidgets()
        self.message_bar = None

        self._create_new_bar_and_push(msg, level, time)

    def _create_new_bar_and_push(self, msg: str, level: Qgis.MessageLevel, time: int):
        """
            Creates a new message bar and pushes it into the QGIS interface.
            :param msg: message to be displayed in the message bar.
            :param level: message level. It changes the color of the message bar.
            :param time: time in milliseconds to clean the new created message bar.
        """
        message_bar = self.iface.messageBar().createMessage(msg)
        self.message_bar = message_bar
        btn_messages = QPushButton(self.message_bar)
        btn_messages.setText(tr("Messages"))
        btn_messages.clicked.connect(self._show_messages_dialog)
        self.message_bar.layout().addWidget(btn_messages)
        self.message_bar.setLevel(level)
        self.iface.messageBar().pushWidget(self.message_bar, level)
        if time > 0:
            QTimer.singleShot(time, lambda: self._remove_message(message_bar))

    def _remove_message(self, item):
        """
            Remove the message bar if it is still been displayed.
            :param item: message bar reference that we want to remove.
        """
        if item == self.message_bar:
            self.iface.messageBar().clearWidgets()
            self.message_bar = None


    def _show_messages_dialog(self):
        """
            Message bar button functionality. It shows the message dialog.
        """
        self.dlg.show()



class LocalSyncPlugin:
    """
        QGIS Plugin Implementation. This class manages all the plugin functionalities.
        :ivar logger: logger of the application. Used to keep logs.
        :vartype logger: logging.Logger
        :ivar info_dialog: reference to the message dialog.
        :vartype info_dialog: MessagesDialog
        :ivar sync_listener: reference to the SyncListener object.
        :vartype sync_listener: SyncListener
        :ivar block_buttons: a boolean that block the upload/download functionality when needed.
        :vartype block_buttons: bool
        :ivar project_name: name of the current project that is going to be synchronized.
        :vartype project_name: str
        :ivar searching_projects: bool that indicates if there is a task in progress about searching projects in the
        connected device.
        :vartype searching_projects: bool
        :ivar s_eng: reference to the SyncEngine object. Main functionality of the plugin.
        :vartype s_eng: SyncEngine
        :ivar p_manager: reference to the ProjectManager object. Search projects in the current selected device.
        :vartype p_manager: ProjectManager
        :ivar search_projects_task: Reference to the project search request task.
        :vartype search_projects_task: QgsTask
        :ivar load_files_task: Reference to the project search request task.
        :vartype load_files_task: QgsTask
        :ivar plugin_toolbar: Reference to the plugin toolbar object.
        :vartype plugin_toolbar: QToolBar
        :ivar json_reader: reference to the JsonReader object. Used to read the search paths in the configuration dialog.
        :vartype json_reader: JsonReader
        :ivar actions: Reference to all the interactuales created on the plugin toolbar.
        :vartype actions: list[QAction]
        :ivar menu: name of the plugin menu.
        :vartype menu: str
    """



    def __init__(self, iface: QgisInterface):
        """
        Constructor.

        :param iface: An interface instance that will be passed to this class
            which provides the hook by which you can manipulate the QGIS
            application at run time.
        """

        locale = QSettings().value('locale/userLocale')[0:2]

        locale_path = os.path.join(
            os.path.dirname(__file__),
            'i18n',
            f'local_sync_plugin_{locale}.qm'
        )
        self.translator = None
        if os.path.exists(locale_path):
            if hasattr(self, 'translator'):
                QCoreApplication.removeTranslator(self.translator)
            self.translator = QTranslator()
            self.translator.load(locale_path)
            QCoreApplication.installTranslator(self.translator)

        self.logger = QgisLoggerHandler(
            QGIS_PLUGIN_NAME,
            level=logging.INFO
        ).get_logger()

        self.info_dialog = MessagesDialog()
        for handler in self.logger.handlers:
            if handler._handler_id == "qgis":
                handler.set_dialog(self.info_dialog)

        config.temporal_dir = os.path.join(
            QgsApplication.qgisSettingsDirPath(),
            QGIS_PLUGIN_NAME,
            "tmp"
        )

        self.sync_listener = SyncListener(iface, self.info_dialog)
        self.block_buttons = False
        self.project_name = ""
        self.searching_projects = False

        self._selected_device = None
        self.devices_combo = None
        self.projects_button = None
        self.selected_project = None
        self.s_eng = SyncEngine()
        self.p_manager = ProjectManager(self.s_eng)
        self.check_project = ProjectConfigurationChanged(self.s_eng, self.sync_listener)
        self.search_projects_task = None
        self.load_files_task = None
        # Save reference to the QGIS interface
        self.iface = iface
        self.plugin_toolbar = self.iface.addToolBar('localsync_plugin')
        self.plugin_toolbar.setObjectName('localsync_plugin')
        self.json_reader = JsonReader()
        # Declare instance attributes
        self.actions = {}
        self.menu = tr(u'&qgis_plugin')
        self.c_manager = ConfigurationManager(self.s_eng)
        self.photo_configuration = PhotoLayersConfiguration()

        project = QgsProject.instance()
        project.readProject.connect(self.project_loading)
        project.projectSaved.connect(self.project_saved)
        project.cleared.connect(self.new_project)

        lcpc2p.launchCartoConfig2ndPage.connect(self.open_select_project_window)
        cac_check.confirmationReady.connect(self.continue_download)

    def project_loading(self):
        """
            Called when a project is loaded. Read the custom configuration saved in it if there are.
        """
        adb_activated = self.c_manager.adb_activated
        self.c_manager.read_config()
        self.photo_configuration.read_config()
        if adb_activated != self.c_manager.adb_activated:
            self.clear_devices_combo()

    def new_project(self):
        """
            Called when a new project is created. Empties the current configuration.
        """
        if self.c_manager:
            self.c_manager.config = []
        self.photo_configuration.restart_config()
        self.clear_devices_combo()
        self.c_manager.change_protocol_used(True)
        self.change_label_value()
        if hasattr(self, "project_wizard"):
            self.project_wizard.restart_wizard()



    def project_saved(self):
        """
            Called when a project is saved.
        """
        pass
        #self.save_config()



    def clear_devices_combo(self):
        """
            Clear the device combo.
        """
        if self.devices_combo:
            self.devices_combo.clear()


    def add_action(
        self,
        text: str,
        name: str,
        icon_path: str="",
        callback:Callable=None,
        enabled_flag:bool=True,
        add_to_menu:bool=True,
        add_to_toolbar:bool=True,
        status_tip:str=None,
        whats_this:str=None,
        parent:QWidget=None) -> QAction:

        """Add a toolbar icon to the toolbar.

        :param icon_path: Path to the icon for this action. Can be a resource
            path (e.g. ':/plugins/foo/bar.png') or a normal file system path.
        :param text: Text that should be shown in menu items for this action.
        :param callback: Function to be called when the action is triggered.
        :param name: type of object been initialised.
        :param enabled_flag: A flag indicating if the action should be enabled
            by default. Defaults to True.
        :param add_to_menu: Flag indicating whether the action should also
            be added to the menu. Defaults to True.
        :param add_to_toolbar: Flag indicating whether the action should also
            be added to the toolbar. Defaults to True.
        :param status_tip: Optional text to show in a popup when mouse pointer
            hovers over the action.
        :param parent: Parent widget for the new action. Defaults None.
        :param whats_this: Optional text to show in the status bar when the
            mouse pointer hovers over the action.
        :returns: The action that was created. Note that the action is also
            added to self.actions list.
        """

        if name != "device" and name != "projects" and name != "selected_project":
            icon = QIcon(icon_path)
            action = QAction(icon, text, parent)
            action.triggered.connect(callback)
            action.setEnabled(enabled_flag)
        else:
            if name == "device":
                self.devices_combo = DevicesCombo(self.s_eng, self.sync_listener)
                self.devices_combo.placeholderText = tr("Select device...")
                self.devices_combo.setToolTip(text)
                self.devices_combo.setEnabled(enabled_flag)
                self.devices_combo.setMinimumWidth(200)
                self.devices_combo.currentIndexChanged.connect(self.device_combo_index_changed)

                action = QWidgetAction(parent)
                action.setDefaultWidget(self.devices_combo)

            elif name == "projects":
                icon = QIcon(icon_path)
                action = QAction(icon, text, parent)
                action.triggered.connect(callback)
                action.setEnabled(enabled_flag)
                self.projects_button = action
            else:
                self.selected_project = QLabel()
                self.selected_project.setText("")
                self.c_manager.configuration_changed.connect(self.change_label_value)

                action = QWidgetAction(parent)
                action.setDefaultWidget(self.selected_project)

        if status_tip is not None:
            action.setStatusTip(status_tip)

        if whats_this is not None:
            action.setWhatsThis(whats_this)

        if add_to_toolbar:
            self.plugin_toolbar.addAction(action)

        if add_to_menu:
            self.iface.addPluginToMenu(
                self.menu,
                action)

        if name == "upload" or name == "download" or name == "projects":
            action.setEnabled(False)

        self.actions[name] = action

        return action



    def change_label_value(self):
        if self.selected_project:
            projects = self.c_manager.search_for_projects_in_config()
            if projects:
                if len(projects) == 1:
                    project_list = projects[0]
                else:
                    project_list = ", ".join(projects)

                MESSAGE = QT_TRANSLATE_NOOP(
                    "CartoDruid_Device_Sync",
                    "Current project: <b>{projects}</b>"
                )
                text = tr(MESSAGE).format(projects=project_list)
                self.selected_project.setText(text)
                self.projects_button.setIcon(QIcon(':/resources/carto-folder-update.png'))
                self.projects_button.setText(tr("Change project"))

            else:
                self.selected_project.setText("")
                self.projects_button.setIcon(QIcon(':/resources/carto-folder.png'))
                self.projects_button.setText(tr("Connect to project"))



    def initGui(self):
        """Create the menu entries and toolbar icons inside the QGIS GUI."""

        icon_path = ':/resources/new-down.png'
        self.add_action(
            icon_path=icon_path,
            text=tr(u'Download from device'),
            callback=self.download,
            parent=self.iface.mainWindow(),
            name='download')
        icon_path = ':/resources/new-up.png'
        self.add_action(
            icon_path=icon_path,
            text=tr(u'Upload to device'),
            callback=self.upload,
            parent=self.iface.mainWindow(),
            name='upload')
        self.add_action(
            text=tr(u'Devices'),
            parent=self.iface.mainWindow(),
            name='device')
        icon_path = ':/resources/gears.png'
        self.add_action(
            icon_path=icon_path,
            text=tr(u'Configuration'),
            callback=self.open_config_window,
            parent=self.iface.mainWindow(),
            name='configuration')
        self.add_action(
            text=tr(u'Selected project'),
            parent=self.iface.mainWindow(),
            name='selected_project')

        icon_path = ':/resources/carto-folder.png'
        self.add_action(
            icon_path=icon_path,
            text=tr(u'Select project'),
            parent=self.iface.mainWindow(),
            callback=self.open_select_project_window,
            name='projects')



    def get_layers_in_edit_mode(self):
        """
            Get the layers that are opened in edit mode.
            :return: List of layers opened and in edit mode currently.
        """
        editable_layers = []
        for layer in QgsProject.instance().mapLayers().values():
            if layer.isEditable():
                editable_layers.append(layer)
        return editable_layers


    def upload(self):
        """
            Start the process of uploading files if there are no layers currently in edit mode. If there are any layers,
            then start the save layers modal dialog.
        """
        editable_layers = self.get_layers_in_edit_mode()
        if not editable_layers:
            if self.devices_combo.currentData():
                self.load_files(False)
        else:
            self.launch_save_modal(editable_layers, False)


    def continue_download(self, stop_continue:bool):
        if not stop_continue:
            self.load_files(True)

    def download(self):
        """
            Start the process of download files if there are no layers currently in edit mode. If there are any layers,
            then start the save layers modal dialog.
        """
        editable_layers = self.get_layers_in_edit_mode()
        if not editable_layers:
            if self.devices_combo.currentData():

                if not self.check_project.check_project_configuration_changed(self.devices_combo.currentData()):
                    self.continue_download(False)
                #self.load_files(True)
        else:
            self.launch_save_modal(editable_layers, True)


    def open_select_project_window(self, second_page:bool = False):

        """
            Open the project wizard, and initialise it.
        """
        if  QgsProject.instance().fileName():

            if not hasattr(self, "project_wizard"):
                self.project_wizard = ProjectWizard(self, self.c_manager,self.p_manager,self.s_eng, self.sync_listener, self.photo_configuration)

            if self.devices_combo.currentData():
                self.project_wizard.initialise(self.devices_combo.currentData(),second_page)
                # show the dialog
                self.project_wizard.exec()
        else:
            QMessageBox.critical(None, tr("Can't open cartodruid project wizard."),
                                 tr("An active project must exist to be able to open this wizard. Open a project or create a new one to complete to be able to use the wizard."))


    def open_config_window(self):
        """
            Open the configuration window.
            Instantiate the configuration dialog if it is the first time this method has been called.
        """
        if not hasattr(self, "conf_dlg"):
            self.conf_dlg = LocalsyncConfPanel(self, self.c_manager, self.photo_configuration)
            self.c_manager.conf_dlg = self.conf_dlg

        # show the dialog
        self.conf_dlg.show()
        # Run the dialog event loop
        self.conf_dlg.open()

        formated_conf = json.dumps(self.c_manager.config, indent=4, ensure_ascii=False)
        formated_photo_conf = json.dumps(self.photo_configuration.config, indent=4, ensure_ascii=False)

        self.conf_dlg.conf_text_edit.setText(formated_conf)
        self.conf_dlg.photo_layers_edit.setText(formated_photo_conf)


    def launch_save_modal(self, layers:list, download:bool):
        """
            Launch the dialog for saving the layers with edit mode activated. After the dialog is closed, depending on
            the result, the download or upload action is restarted.
            :param layers: List of layers to save.
            :param download: indicates whether to download (True) or upload (False).
        """
        if not hasattr(self, "save_dlg"):
            self.save_dlg = LocalsyncSaveLayers()

        if layers:
            model = QStringListModel()
            model.setStringList([layer.name() for layer in layers])

            self.save_dlg.list_view.setModel(model)
            self.save_dlg.list_view.setEditTriggers(QListView.NoEditTriggers)

        # show the dialog
        self.save_dlg.show()

        # Run the dialog event loop
        result = self.save_dlg.exec_()

        if result:
            for layer in layers:
                layer.commitChanges()
            if download:
                self.download()
            else:
                self.upload()

    def load_ended(self):
        """
            Method called when the download/upload of files task end. Reload all the opened layers.
            Reactivate the control of buttons when finished.
        """
        self.block_plugin_actions(False)
        for layer in QgsProject.instance().mapLayers().values():
            layer.reload()
        self.photo_configuration.create_or_update_photo_layers()

            
    def load_terminated(self):
        self.block_plugin_actions(False)

    def load_files(self, pull:bool):
        """
            Start the process of downloading or uploading files between the device and the pc.
            To start the process is necessary that the buttons are not blocked, we have data about the current
            device and the configuration paths exists.
            :param pull: whether to download (True) or upload (False).
        """
        current_device = self.devices_combo.currentData()
        if not self.searching_projects and not self.block_buttons and not current_device.no_data:
            transformed_config = self.c_manager.convert_config_relative_path_to_absolute(self.c_manager.config)
            if self.c_manager.check_if_config_path_exists(transformed_config, True, current_device):
                if QgsProject.instance().fileName():
                    self.block_plugin_actions(True)
                    self.load_files_task = LoadFiles(self.s_eng, transformed_config,
                                                     current_device, pull, self.sync_listener)
                    self.load_files_task.taskCompleted.connect(self.load_ended)
                    self.load_files_task.taskTerminated.connect(self.load_terminated)
                    QgsApplication.taskManager().addTask(self.load_files_task)
                else:
                    self.logger.warning("You need to have an open project in QGIS to use this plugin.")
        else:
            self.logger.warning("A load command was issued, but there is no device selected.")


    def block_plugin_actions(self, block: bool):
        """
            Block plugin actions.
            :param block: whether to block plugin actions.
        """
        #self.projects_button.blockSignals(block)
        self.devices_combo.blockSignals(block)
        self.enable_buttons(not block)
        self.block_buttons = block


    def device_combo_index_changed(self, index):
        self.enable_buttons(self.devices_combo.currentData() != None)


    def enable_buttons(self, enable: bool):
        for button in self.actions:
            if button == "download" or button == "upload" or button == "projects":
                self.actions[button].setEnabled(enable)


    def unload(self):
        """Removes the plugin menu item and icon from QGIS GUI."""
        lcpc2p.launchCartoConfig2ndPage.disconnect()
        cac_check.confirmationReady.disconnect(self.continue_download)
        for action_key in self.actions:
            self.iface.removePluginMenu(
                tr(u'&qgis_plugin'),
                self.actions[action_key])
            self.iface.removeToolBarIcon(self.actions[action_key])
        if self.translator:
            QCoreApplication.removeTranslator(self.translator)