import os
from pathlib import Path
from typing import Tuple

from PyQt5.QtCore import pyqtSignal, QObject
from PyQt5.QtWidgets import QWizard

from qgis._core import QgsProject, QgsApplication
from ....tasks.copy_folders_from_device import CopyFoldersFromDevice
from ....tasks.load_files import LoadFiles


class NameMapperController(QObject):

    """
        Controller of the page name_mapper.ui of the CartoDruid Wizard.
        :ivar request_validation: pyqtSignal used to relaunch the validatePage from the name_mapper_ui.
        :vartype request_validation: pyqtSignal
        :ivar show_progress_bar: pyqtSignal used to relaunch the show or hide the progress bar.
        :vartype show_progress_bar: pyqtSignal
        :ivar download_successful: Boolean indicating whether the download was successful or not.
        :vartype download_successful: bool
        :ivar s_task: stores the download Cartodruid configuration task.
        :vartype s_task: QTask
        :ivar d_task: stores the download files from device task.
        :vartype s_task: QTask
        :ivar validating: whether the controller is validating the current state or not.
        :vartype validating: bool
    """

    request_validation = pyqtSignal()

    show_progress_bar = pyqtSignal(bool)

    wizard = None

    download_successful = False

    def __init__(self, wizard:QWizard):
        """
            Constructor.
            :param wizard: current QWizard instance.
        """
        super().__init__()
        self.wizard = wizard
        self.c_task = None
        self.d_task = None
        self.validating = False

    def test_valid_name_mappers(self, state: dict) -> Tuple[bool,str]:
        """
            Check if the pc files or device files have repeated values within them.
            :param state: current state of the page.
            :return: false and the repeated value in case that a repeated value was found, true and empty string otherwise.
        """
        device_names_archive = []
        host_names_archive = []
        for conn in state["connections"]:
            if conn[0] in device_names_archive:
                return False, conn[0]
            else:
                device_names_archive.append(conn[0])
            if conn[1] in host_names_archive:
                return False, conn[1]
            else:
                host_names_archive.append(conn[1])
        return True, ""

    def create_name_mapper_config(self, state:dict):
        """
            Create the name_mapper_config from the current state of the page.
            :param state: current state of the page.
            :return: a name_mapper dictionary with the connections found in the current state.
        """
        name_mapper_config = {}
        for conn in state["connections"]:
            if conn[0] == conn[1]:
                continue
            else:
                name_mapper_config[conn[1]] = conn[0]
        return name_mapper_config

    def create_device_list(self, files:list):
        """
            Create the device list, getting only the file_names from the files.
            :param files: list of file names.
            :return: list of device names.
        """
        names_list = []
        for file in files:
            if "/" in file or "\\" in file:
                names_list.append(Path(file).name)
            else:
                names_list.append(file)
        return self.create_tuple_list(names_list)

    def create_tuple_list(self, files:list, delete:bool = False):
        """
            Create a tuple_list with the files found in files and the delete bool as in (file, delete).
            :param files: list of file names. Every row will be a new Tuple for the return list.
            :param delete: bool value that will be added as second value for the tuple.
            :return: tuple_list with the files found in files and the delete bool value.
        """
        tuple_list = []
        for file in files:
            tuple_list.append((file, delete))
        return tuple_list

    def create_pc_list(self):
        """
        Create the pc list, with the files found in the current ./cartodruid/data path.
        :return: if the data path exists, return the list of pc files, else return an empty list.
        """
        data_folder = os.path.join(
            Path(QgsProject.instance().fileName()).parent.as_posix(),
            "cartodruid",
            "data"
        )
        if os.path.exists(data_folder):
            data_list = os.listdir(data_folder)
            return self.create_device_list(data_list)
        else:
            return []

    def duplicate_tuple_list_device_to_pc(self, device_files:list, pc_files:list):
        """
            Duplicates all the files found in device_files that does not exist in pc_files.
            :param device_files: list of tuples with device file names and deletable bool.
            :param pc_files: list of tuples with pc file names and deletable bool.
            :return: a new pc_files list with the files that weren't found in device_files.
        """
        new_files = []
        for file, delete in device_files:
            if (file, False) not in pc_files and (file, True) not in pc_files:
                new_files.append((file, True))
        pc_files = new_files + pc_files
        return pc_files

    def create_start_connections(self, device_files:list):
        """
            Create start connections between files with the same name in pc and device.
            :param device_files: list of tuples with device file names and deletable bool.
            :return: list of connections.
        """
        connections = []
        for file, delete in device_files:
            connections.append((file, file))
        return connections


    def delete_duplicates(self, tuple_list:list[tuple]):
        """
            Delete duplicate tuples.
            :param tuple_list: list of tuples with (file_name str, deletable bool).
            :return: tuple_list without duplicate file_name values.
        """
        list_copy = tuple_list.copy()
        already_found = []
        for tuple_item in list_copy:
            if tuple_item in already_found:
                if tuple_item in tuple_list:
                    tuple_list.remove(tuple_item)
            else:
                already_found.append(tuple_item)
        return tuple_list

    def initialize_data(self) -> Tuple[list,list,list]:
        """
            Initialize the data for the QGraphicView of the current page.
            If an options were found then it loads what it reads from them.
            Otherwise, it creates a list from the selected device files in the previous page and another with the
            files found on the ./cartodruid/data path and duplicates from the device files, if they aren't already in the list,
            and connects the duplicated values between pc and device files.
            :return: tuple_list with pc_files, device_files and connections.
        """
        if self.wizard.current_options.name_mapper_device_files:
            device_files = self.wizard.current_options.name_mapper_device_files if (
                self.wizard.current_options.name_mapper_device_files) else []
            pc_files = self.wizard.current_options.name_mapper_host_files if (
                self.wizard.current_options.name_mapper_device_files) else []
            connections = self.wizard.current_options.name_mapper_connections if (
                self.wizard.current_options.name_mapper_connections) else []
            current_pc_files = self.create_pc_list()
            current_device_files = self.create_device_list(self.wizard.current_options.data_list_selected_files)
            for file in pc_files:
                if any(file[0] == pc_file for pc_file, _ in current_pc_files):
                    file[1] = False
                else:
                    file[1] = True
            for file in current_device_files:
                if not any(file[0] == device_file for device_file, _ in device_files):
                    device_files.append((file[0], False))
                    pc_files.append((file[0], True))
                    connections.append((file[0], file[0]))
            device_files_copy = device_files.copy()
            remove_connections = []
            for file in device_files_copy:
                if not any(file[0] == device_file for device_file, _ in current_device_files):
                    remove_connections.append(file[0])
                    device_files.remove(file)
            connections = [t for t in connections if t[1] not in remove_connections]
            pc_files = self.delete_duplicates(pc_files)
            device_files = self.delete_duplicates(device_files)

        else:
            pc_files = self.create_pc_list()
            device_files = self.create_device_list(self.wizard.current_options.data_list_selected_files)
            pc_files = self.duplicate_tuple_list_device_to_pc(device_files, pc_files)
            connections = self.create_start_connections(device_files)

        return pc_files, device_files, connections


    def validate_page(self, state:dict):
        """
            Validates the current state to see if it is valid. If it is then start the download process and returns False.
            If the download process already happened, and it was successful, then return True.
            :param state: dict with current state of the page.
            :return: True if the validation and download process was successful, False otherwise.
        """
        if self.download_successful:
            self.download_successful = False
            return True
        if not self.validating:
            self.validating = True
            validate, culprit = self.test_valid_name_mappers(state)
            if validate:
                name_config = self.create_name_mapper_config(state)
                self.wizard.c_manager.add_name_mappers_to_config(name_config)
                self.wizard.current_options.name_mapper_device_files = state["right"]
                self.wizard.current_options.name_mapper_host_files = state["left"]
                self.wizard.current_options.name_mapper_connections = state["connections"]
                self.wizard.save_carto_project_selections()
                copy_folders = [True]
                for _ in range(len(self.wizard.c_manager.config)-1):
                    copy_folders.append(False)
                self.launch_first_copy_process(copy_folders)
            else:
                self.wizard.logger.info("You can't have a duplicated values in the same column. Repeated name: {culprit}".format(culprit=culprit))
        return False



    def launch_first_copy_process(self, copy_folders: list):
        """
            Launch the first process that will copy the folder structure from the CartoDruid project.
            :param copy_folders: list of folder paths to copy from.
        """
        if not self.c_task:
            transformed_config = self.wizard.c_manager.convert_config_relative_path_to_absolute(self.wizard.c_manager.config)
            self.c_task = CopyFoldersFromDevice(self.wizard.s_eng, transformed_config, self.wizard.device, self.wizard.s_listener, copy_folders)
            self.c_task.taskCompleted.connect(self._copy_directories_completed)
            self.c_task.taskTerminated.connect(self._copy_directories_finished)
            self.wizard.logger.info("Starting project copy.")
            self.show_progress_bar.emit(True)
            QgsApplication.taskManager().addTask(self.c_task)


    def _copy_directories_finished(self):
        """
            launch_first_copy_process task terminated method. This method is called when CopyFoldersFromDevice is finished abruptly.
            Clean the current task, hides the progress bar and set the interactables enable again.
        """
        self.wizard.logger.error("There was an error copying the directories or it was terminated by the user.")
        self.c_task = None
        self.show_progress_bar.emit(False)

    def _copy_directories_completed(self):
        """
            launch_first_copy_process task completed method. This method is called when CopyFoldersFromDevice is finished successfully.
            Clean the current task, hides the progress bar, set the interactables enable again, prepares the state of the page
            to pass to the next page and launches the download process.
        """
        self.wizard.logger.info("Project copy ended.")
        self.c_task = None
        self.show_progress_bar.emit(False)
        self._download_process_finished = True
        self.launch_first_download_process()

    def launch_first_download_process(self):
        """
            Launch the first download process that will download the files from the CartoDruid project.
        """
        if not self.d_task:
            transformed_config = self.wizard.c_manager.convert_config_relative_path_to_absolute(self.wizard.c_manager.config)
            self.d_task = LoadFiles(self.wizard.s_eng, transformed_config, self.wizard.device, True, self.wizard.s_listener)
            #self.set_enable_interactuables(False)
            self.d_task.taskCompleted.connect(self._download_files_completed)
            self.d_task.taskTerminated.connect(self._download_files_finished)
            self.wizard.logger.info("Starting project download.")
            self.show_progress_bar.emit(True)
            QgsApplication.taskManager().addTask(self.d_task)

    def _download_files_finished(self):
        """
            launch_first_download_process task terminated method. This method is called when LoadFiles is finished abruptly.
            Clean the current task, hides the progress bar and set the interactables enable again.
        """
        self.wizard.logger.error("There was an error downloading the files or it was terminated by the user.")
        self.d_task = None
        self.validating = False
        self.show_progress_bar.emit(False)
        #self.set_enable_interactuables(True)
        self.wizard.next()

    def _download_files_completed(self):
        """
            launch_first_download_process task completed method. This method is called when LoadFiles is finished successfully.
            Clean the current task, hides the progress bar, set the interactables enable again, prepares the state of the page
            to pass to the next page and launches the download process.
        """
        self.wizard.logger.info("Project download ended.")
        self.d_task = None
        self.validating = False
        self.show_progress_bar.emit(False)
        self._download_process_finished = True
        self.download_successful = True
        #self.set_enable_interactuables(True)
        self.wizard.next()