import os
import platform
import shutil
import sys
import tempfile
from functools import partial

import machineid
import requests
from qgis.core import Qgis, QgsApplication, QgsSettings, QgsZipUtils
from qgis.PyQt.Qt import QUrl
from qgis.PyQt.QtCore import (
    PYQT_VERSION_STR,
    QT_VERSION_STR,
    QObject,
    QThread,
    pyqtSignal,
    pyqtSlot,
)
from qgis.PyQt.QtGui import QDesktopServices, QIcon
from qgis.PyQt.QtWidgets import (
    QAbstractItemView,
    QCheckBox,
    QHeaderView,
    QLabel,
    QLineEdit,
    QPushButton,
    QStackedLayout,
    QTableWidget,
    QTableWidgetItem,
)
from qgis.utils import plugins

from openlog.__about__ import (
    DIR_PLUGIN_ROOT,
    __integrated_extensions__,
    __title__,
    __version__,
)

PREMIUM_API = "https://account.apeiron.technology"
EXTENSION_PATH = os.path.join(DIR_PLUGIN_ROOT, "extensions")
APEIRON_URL = "https://apeiron.technology/openlog/"


class RequestsWorker(QObject):
    """
    Class for requesting API in another thread.
    """

    finished = pyqtSignal(bool)

    def __init__(self):
        super().__init__()
        self.method = None
        self.url = None
        self.json = None
        self.what: str = ""
        self.response = None

    def set_parameters(self, method: str, url: str, json: dict, what: str) -> None:

        self.response = None
        self.method = method
        self.url = url
        self.json = json
        self.what = what

    def run(self):

        try:
            self.response = requests.request(
                method=self.method, url=self.url, json=self.json, timeout=10
            )
        except Exception:
            self.response = None

        self.finished.emit(True)


class ExtensionsHandler:
    def __init__(self, settings_widget) -> None:
        """
        Class for handling premium logging and extensions installations.
        Args:
            - import settings_widget : Widget containing premium UI (ConfigOptionsPage)
        """
        self.settings_widget = settings_widget
        self.extension_grp = self.settings_widget.extension_grp
        self.extension_tbl: QTableWidget = self.settings_widget.extension_tbl
        self.extension_tbl.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.extension_tbl.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.extension_tbl.setSelectionMode(QAbstractItemView.ContiguousSelection)
        self.extension_tbl.setSelectionMode(QAbstractItemView.ExtendedSelection)
        self.description_label: QLabel = self.settings_widget.description_label
        self.login_btn: QPushButton = self.settings_widget.login_btn
        self.signup_btn: QPushButton = self.settings_widget.signup_btn
        self.account_btn: QPushButton = self.settings_widget.account_btn
        self.login_info: QLabel = self.settings_widget.login_info
        self.connection_status: QLabel = self.settings_widget.connection_status
        self.install_btn: QPushButton = self.settings_widget.install_btn
        self.uninstall_btn: QPushButton = self.settings_widget.uninstall_btn
        self.description_label: QLabel = self.settings_widget.description_label
        self.user_edit: QLineEdit = self.settings_widget.user_edit
        self.password_edit: QLineEdit = self.settings_widget.password_edit
        self.rmb_me: QCheckBox = self.settings_widget.rmb_me
        self.autologin: QCheckBox = self.settings_widget.autologin
        self.uncheck_all_btn: QPushButton = self.settings_widget.uncheck_all_btn
        self.check_all_btn: QPushButton = self.settings_widget.check_all_btn

        # buttons icons
        self.uncheck_all_btn.setIcon(
            QIcon(
                str(DIR_PLUGIN_ROOT / "resources" / "images" / "icon_uncheck_all.svg")
            )
        )
        self.check_all_btn.setIcon(
            QIcon(str(DIR_PLUGIN_ROOT / "resources" / "images" / "icon_check_all.svg"))
        )
        self.uncheck_all_btn.pressed.connect(self._unselect_all)
        self.check_all_btn.pressed.connect(self._select_all)

        # table header
        self.extension_tbl.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
        self.description_label.setVisible(False)

        # saved credentials
        self._get_credentials()

        # signals
        self.login_btn.pressed.connect(self._login_premium)
        self.signup_btn.pressed.connect(
            partial(QDesktopServices.openUrl, QUrl(APEIRON_URL))
        )
        self.account_btn.pressed.connect(
            partial(QDesktopServices.openUrl, QUrl(PREMIUM_API))
        )
        self.uninstall_btn.pressed.connect(self._uninstall_extension)
        self.install_btn.pressed.connect(self._install_extension)
        self.extension_tbl.itemSelectionChanged.connect(self._on_selection_changed)

        # installed extensions
        self.installed_extensions = []
        self._find_installed_extensions()

        # disable buttons
        self.install_btn.setEnabled(False)
        self.uninstall_btn.setEnabled(False)

        # available extensions
        self.available_extensions = []

        # credentials
        self.login: str = None
        self.pwd: str = None

        # connection status
        self.authenticated = False
        self.api_errors = ["invalid credentials", "unknown hardware", "expired account"]
        self.connection_status.setText("Status : Disconnected")

        # set multiplotlayout to QTablewidget to overlay spinner
        self.stack_layout = QStackedLayout()
        self.extension_tbl.setLayout(self.stack_layout)

        self.worker = None
        self.thread = None

        self._requests_at_opening()

    def _select_all(self) -> None:
        """
        Select all rows.
        """
        self.extension_tbl.selectAll()

    def _unselect_all(self) -> None:
        """
        Unselect all rows.
        """
        self.extension_tbl.clearSelection()

    def _update_worker(self):
        """
        Instantiate a new Qthread and RequestsWorker
        """

        # set new Qthread for next request
        self.worker = RequestsWorker()
        self.thread = QThread()
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)
        self.thread.finished.connect(self._on_request_finished)

    def _on_request_finished(self) -> None:
        """
        Unique slot connected to RequestsWorker.finished signal.
        """

        what = self.worker.what
        response = self.worker.response
        if what == "extensions":

            if response:
                plugins = response.json()
            else:
                plugins = []
            # remove already integrated extensions in Openlog
            plugins = [
                plugin
                for plugin in plugins
                if plugin["name"] not in __integrated_extensions__
            ]
            self.available_extensions = plugins
            self._fill_table()

            # post qgis infos
            if self.authenticated:
                self._update_user_infos()

        elif what == "auth":

            if response:
                if response.status_code == 200:
                    if response.text in self.api_errors:
                        self.authenticated = False
                        self.login_info.setStyleSheet("color: red;")
                        self.login_info.setText(response.text)
                        self.connection_status.setText("Status : Disconnected")
                    else:
                        self.authenticated = True
                        self.connection_status.setText("Status : Connected")
                        self.login_info.setText("")
            else:
                self.login_info.setStyleSheet("color: red;")
                self.login_info.setText(
                    "Unable to connect to server. Please check your connection and disable any VPN."
                )
                self.connection_status.setText("Status : Disconnected")
                self.authenticated = False

            self._get_available_extensions()

        self._on_selection_changed()

    def _get_system_info(self) -> dict:
        """
        Extract informations displayed in QGIS/Help/About.
        """
        gdal_version = ""
        proj_version = ""
        sqlite_version = ""
        try:
            import sqlite3

            import osgeo

            gdal_version = osgeo.gdal.__version__
            proj_version = f"{osgeo.osr.GetPROJVersionMajor()}.{osgeo.osr.GetPROJVersionMinor()}.{osgeo.osr.GetPROJVersionMicro()}"
            sqlite_version = sqlite3.sqlite_version

        except ImportError:
            pass

        result = {
            "QGIS version": Qgis.version(),
            "Openlog_version": __version__,
            "Python": sys.version,
            "Python exe": sys.executable,
            "Qt": QT_VERSION_STR,
            "PyQt": PYQT_VERSION_STR,
            "GDAL": gdal_version,
            "PROJ": proj_version,
            "SQLITE": sqlite_version,
            "OS": platform.platform(),
            "plugins": ", ".join(plugins.keys()),
        }
        return result

    def _requests_at_opening(self):
        """
        Method to be executed inside QThread.
        First extensions metadata are pulled from API, then login.
        """
        self._auto_login()

    def _auto_login(self):
        """
        Method to log automatically.
        """
        if (
            self.autologin.isChecked()
            and self.user_edit is not None
            and self.password_edit is not None
        ):
            self._login_premium()

    def _login_premium(self):
        """
        Connect to openlog premium API and get available extensions.
        """
        login = self.user_edit.text()
        mdp = self.password_edit.text()
        self.login = login
        self.pwd = mdp
        mac = self._get_machine_id()
        self._save_credentials()
        data = {"login": login, "mdp": mdp, "mac": mac}

        self._update_worker()
        self.worker.set_parameters(
            method="GET", url=PREMIUM_API + "/user_auth/", json=data, what="auth"
        )
        self.thread.start()

    def _get_available_extensions(self):
        """
        Pull from API available extensions.
        """
        user_metadata = {"openlog_version": __version__, "email": self.login}
        self._update_worker()
        self.worker.set_parameters(
            method="GET",
            url=PREMIUM_API + "/extensions/",
            json=user_metadata,
            what="extensions",
        )
        self.thread.start()

    def _update_user_infos(self):
        """
        Extract setup informations and send it to API.
        """
        login = self.user_edit.text()
        mdp = self.password_edit.text()
        self.login = login
        self.pwd = mdp
        mac = self._get_machine_id()
        self._save_credentials()
        data = {"login": login, "mdp": mdp, "mac": mac}
        data["qgis_infos"] = self._get_system_info()

        self._update_worker()
        self.worker.set_parameters(
            method="POST", url=PREMIUM_API + "/qgis_infos/", json=data, what=""
        )
        self.thread.start()

    def _get_credentials(self) -> None:
        """
        Set previously saved credentials.
        """
        if self.rmb_me.isChecked():
            settings = QgsSettings()
            settings.beginGroup(__title__)
            username = settings.value("openlog_username")
            password = settings.value("openlog_password")
            self.user_edit.setText(username)
            self.password_edit.setText(password)

    def _save_credentials(self) -> None:
        """
        Save current credentials.
        """
        if self.rmb_me.isChecked():
            settings = QgsSettings()
            settings.beginGroup(__title__)
            settings.setValue("openlog_username", self.user_edit.text())
            settings.setValue("openlog_password", self.password_edit.text())

    def _find_installed_extensions(self):
        """
        We scan extensions folder.
        """

        self.installed_extensions = []
        files = os.listdir(EXTENSION_PATH)
        files = [file for file in files if file.__contains__(".dist-info")]
        # find packages names
        package_names = [
            file.replace("openlog_qgis_plugin_", "").split("-")[0] for file in files
        ]
        # find versions
        versions = []
        for file_info in files:
            path_info = os.path.join(EXTENSION_PATH, file_info)
            with open(os.path.join(path_info, "METADATA"), "r") as f:
                text = f.readlines()
            text = [elt for elt in text if elt.split(":")[0] == "Version"][0]
            versions.append(text.replace("Version: ", "").replace("\n", ""))

        for name, version in zip(package_names, versions):
            self.installed_extensions.append({"name": name, "version": version})

    def _get_installed_extension(self, package_name: str) -> dict:
        """
        Return installed extension metadata.
        If not found, return None.
        """
        for ext in self.installed_extensions:
            if ext.get("name") == package_name:
                return ext

        return None

    def _get_package_description(self, package_name: str) -> str:

        for ext in self.available_extensions:
            if ext.get("name") == package_name:
                return ext.get("description")

    def _fill_table(self):
        """
        Fill table widget with available extensions.
        """

        # clear table
        self.extension_tbl.clearSelection()
        self.extension_tbl.clearContents()
        while self.extension_tbl.rowCount() != 0:
            self.extension_tbl.removeRow(self.extension_tbl.rowCount() - 1)

        # offline
        if len(self.available_extensions) == 0:
            self._fill_table_offline()

        for plugin in self.available_extensions:
            plugin_name = plugin.get("name")
            plugin_version = plugin.get("version")
            dosp_date = plugin.get("dosp")
            dosp_date = "" if dosp_date == "None" else dosp_date

            row = self.extension_tbl.rowCount()
            self.extension_tbl.insertRow(row)
            self.extension_tbl.setItem(row, 0, QTableWidgetItem(plugin_name))
            self.extension_tbl.setItem(row, 1, QTableWidgetItem(plugin_version))
            self.extension_tbl.setItem(row, 3, QTableWidgetItem(dosp_date))
            # add installed version if exist
            self._find_installed_extensions()
            installed_version = self._get_installed_extension(plugin_name)
            if installed_version is not None:
                self.extension_tbl.setItem(
                    row, 2, QTableWidgetItem(installed_version["version"])
                )
            else:
                self.extension_tbl.setItem(row, 2, QTableWidgetItem("Not installed"))

    def _fill_table_offline(self) -> None:
        """
        Fill table with only installed extensions.
        """
        self._find_installed_extensions()
        for plugin in self.installed_extensions:
            plugin_name = plugin.get("name")
            plugin_version = plugin.get("version")

            row = self.extension_tbl.rowCount()
            self.extension_tbl.insertRow(row)
            self.extension_tbl.setItem(row, 0, QTableWidgetItem(plugin_name))
            self.extension_tbl.setItem(row, 1, QTableWidgetItem("?"))
            self.extension_tbl.setItem(row, 2, QTableWidgetItem(plugin_version))
            self.extension_tbl.setItem(row, 3, QTableWidgetItem("?"))

    def _on_selection_changed(self):
        """
        Slot for handling button states depending selected extension.
        """

        self.install_btn.setEnabled(False)
        self.uninstall_btn.setEnabled(False)
        selected_indexes = self.extension_tbl.selectedIndexes()
        if len(selected_indexes) > 0:
            self.install_btn.setEnabled(False)
            self.uninstall_btn.setEnabled(False)
            for index in selected_indexes:
                selected_row = index.row()
                name = self.extension_tbl.item(selected_row, 0).text()
                latest_version = self.extension_tbl.item(selected_row, 1).text()
                current_version = self.extension_tbl.item(selected_row, 2).text()
                if current_version != "Not installed":
                    self.uninstall_btn.setEnabled(True)
                if current_version != latest_version:
                    self.install_btn.setEnabled(True)

            # display description
            if len(selected_indexes) == 4:
                description = self._get_package_description(name)
                self.description_label.setVisible(True)
                self.description_label.setText(description)

        # buttons enable/disable
        if not self.authenticated:
            self.install_btn.setEnabled(False)

    def _remove_extension(self, name: str) -> None:
        """
        Remove extensions from folder.
        """
        folder_pattern = "openlog_qgis_plugin_" + name
        for fld in os.listdir(EXTENSION_PATH):
            if folder_pattern in fld:
                shutil.rmtree(os.path.join(EXTENSION_PATH, fld))

    def _uninstall_extension(self):
        """
        Uninstall selected extensions.
        """
        selected_indexes = self.extension_tbl.selectedIndexes()
        for index in selected_indexes:
            selected_row = index.row()
            extension = self.extension_tbl.item(selected_row, 0).text()
            self._remove_extension(extension)

        # refresh table
        self._fill_table()

    def _get_machine_id(self) -> str:
        """
        Call machineid.id() in a try/except block.
        """
        id = None
        try:
            id = machineid.id()
        except Exception as e:
            pass
        return id

    def _install_extension(self):
        """
        Install selected extension.
        """
        selected_indexes = self.extension_tbl.selectedIndexes()
        for index in selected_indexes:
            selected_row = index.row()
            extension = self.extension_tbl.item(selected_row, 0).text()
            latest_version = self.extension_tbl.item(selected_row, 1).text()
            current_version = self.extension_tbl.item(selected_row, 2).text()
            if latest_version == current_version:
                continue
            metadata = [
                ext
                for ext in self.available_extensions
                if ext["package_name"] == extension
            ][0]
            json = {
                "login": self.login,
                "mdp": self.pwd,
                "mac": self._get_machine_id(),
                "id_repo": metadata["project_id"],
                "package_name": extension,
                "version": metadata["version"],
                "openlog_version": metadata["openlog_minimal_version"],
            }
            try:
                response = requests.get(
                    PREMIUM_API + "/get_package/", json=json, stream=True
                )
            except Exception:
                self.login_info.setStyleSheet("color: red;")
                self.login_info.setText(
                    "Unable to connect to server. Please try later."
                )
                return

            if response.status_code == 200:
                self._remove_extension(extension)
                # create temp folder
                folder = tempfile.mkdtemp()
                output = os.path.join(folder, "openlog_ex.zip")
                with open(output, "wb") as f:
                    for chunk in response.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk)
                QgsZipUtils.unzip(zip=output, dir=EXTENSION_PATH)
            # refresh table
            self._fill_table()
