# -*- coding: utf-8 -*-
"""
/***************************************************************************
 PluginDatasetDialog
                              -------------------
        begin                : 2021-12-20
        git sha              : $Format:%H$
        copyright            : (C) 2021 by Zuidt
        email                : richard@zuidt.nl
 ***************************************************************************/

/***************************************************************************
 *                                                                         *
 *   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 os
import time
from collections import deque

from ndff_connector_plugin import PLUGIN_DIALOGS_TITLE
from ndff_connector_plugin.ndffc_plugin_dataset_create_dialog import (
    PluginDatasetCreateDialog,
)
from qgis.PyQt import uic
from qgis.PyQt.QtCore import (
    QDateTime,
    QItemSelection,
    QItemSelectionModel,
    QModelIndex,
    Qt,
    QUrl,
)
from qgis.PyQt.QtGui import (
    QDesktopServices,
    QGuiApplication,
    QStandardItem,
    QStandardItemModel,
)
from qgis.PyQt.QtWidgets import (
    QAbstractItemView,
    QApplication,
    QComboBox,
    QDialog,
    QHeaderView,
    QMessageBox,
)

from . import LOGGER_NAME
from .ext.ndff.api.object import (
    NdffDataset,
)
from .ext.ndff.connector.connector import (
    NdffConnector,
)
from .ext.ndff.utils import (
    is_uri,
)

log = logging.getLogger(LOGGER_NAME)


# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer
FORM_CLASS, _ = uic.loadUiType(os.path.join(
    os.path.dirname(__file__), 'ndffc_plugin_dataset_dialog_base.ui'))


class PluginDatasetDialog(QDialog, FORM_CLASS):
    """
    The Dataset Dialog

    With this dialog the user will first retrieve all dataset for the user's current domain, so the user can select
    one Dataset (identity) to be used as 'Map/Dataset/Directory' to put the NdffObservations in.

    Another use of this dialog is to actually CREATE a new Dataset (via a PluginDatasetCreateDialog instance) as
    child from another dataset.
    """

    SELECT_PROTOCOL_MESSAGE = 'Selecteer een Protocol indien bekend of wenselijk'
    MODE_DEFAULT_SELECTION = 0
    MODE_DATASET_MAPPING = 1

    def __init__(self, parent=None):
        """
        Constructor of the ui of this dialog
        """
        super(PluginDatasetDialog, self).__init__(parent)
        self.setupUi(self)
        self.connector = None
        self.record = None
        self.field = None
        self.mode = self.MODE_DEFAULT_SELECTION

        self.PLUGIN_DIALOGS_TITLE = PLUGIN_DIALOGS_TITLE

        self.btn_get_datasets.clicked.connect(self.build_datasets_tree)
        self.btn_create_dataset.clicked.connect(self.create_dataset)
        self.btn_edit_dataset.clicked.connect(self.edit_dataset)

        # Treeview
        self.datasets_model = QStandardItemModel()
        self.datasets_model.setHorizontalHeaderLabels(['Naam', 'Link', 'Identity'])
        self.tree_datasets.setModel(self.datasets_model)
        # make treeview cells non-editable:
        self.tree_datasets.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.tree_datasets.selectionModel().selectionChanged.connect(self.dataset_row_clicked)
        self.lbl_selected.linkActivated.connect(self.open_in_browser)

    def show_for_dataset_mapping(self, connector: NdffConnector, record: dict, field: str):
        """
        Show this dialog for the mapping af dataset value to ndff uri's
        """
        # do a normal show with layer = None
        self.show(connector, record, field, self.MODE_DATASET_MAPPING)
        # find out if we have a valid mapping for current dataset VALUE
        dataset_field = 'dataset'
        if self.connector.get_field_mapping('dataset'):
            dataset_field = self.connector.get_field_mapping('dataset')
        dataset_value = self.record[dataset_field]
        mapped_value = self.connector.get_data_mapping(self.field, dataset_value)
        if mapped_value:
            self.le_selected_uri.setText(mapped_value)
        self.build_datasets_tree()

    def show_for_dataset_default(self, connector: NdffConnector, record: dict, field: str):
        """
        Show this dialog to select a default
        """
        self.show(connector, record, field, self.MODE_DEFAULT_SELECTION)
        self.build_datasets_tree()

    def show(self, connector: NdffConnector, record: dict, field: str, mode: int) -> None:
        """
        Show the dialog, using the arguments to fill widgets

        :param: connector, used to get the api, credentials etc.
        :param: record
        :param: field
        """
        log.debug(f'Calling show of PluginDatasetDialog')
        self.connector = connector
        self.record = record
        self.field = field
        self.mode = mode

        api = self.connector.get_api()
        self.btn_get_datasets.setText(f'Klik hier om alle datasets op te halen van "{api.user}" - domain "{api.domain}"')
        # cleanup
        self.le_selected_uri.setText('')
        self.le_search_value.setText('')
        self.datasets_model.removeRows(0, self.datasets_model.rowCount())
        self.tree_datasets.header().setStretchLastSection(True)
        # if we have a default value for dataset, then this dialog is used to set/update the default value
        # try to make the current default value selected:
        if connector.get_field_default(field):
            self.le_selected_uri.setText(f'{connector.get_field_default(field)}')
        else:
            self.le_selected_uri.setText('')
        # after cleanup, show the dialog
        # the datasets are being requested from API and the Treeview is being created later
        self.showNormal()

    def get_domain_datasettype_identity_for_category(self, category: str) -> str:
        """
        As dataset types are domain specific, the dataset types for the domain will be retrieved
        and the identity of the requested category (e.g. 'startmap', 'gewone map') will be returned.

        The purpose of this method is to search for the identity of it for current domain.

        With this method you will retrieve all dataset types of current domain, and return the identity (uri-string)
        of the (one/first) with the proper category.
        """
        dataset_types = self.connector.get_dataset_types()
        dataset_type_identity = None
        for dir_type in dataset_types:
            if dir_type['category'] == category:
                dataset_type_identity = dir_type['identity']
                break
        return dataset_type_identity

    def build_datasets_tree(self):
        """
        This method will first retrieve all datasets for current domain (via the NdffConnector/API calls).
        And then create a Qt TreeModel model from it and then show this tree model in a table/tree in the dialog
        so the user can select one dataset (as selection or as parent for a newly created dataset)
        """
        # just get all datasets, and try to select current dataset in it
        QGuiApplication.setOverrideCursor(Qt.WaitCursor)
        all_datasets = self.connector.get_datasets()
        # back to normal cursor...
        QGuiApplication.restoreOverrideCursor()
        self.le_search_value.setText(f'{len(all_datasets)} datasets opgehaald...')
        root_dir_identity = self.get_domain_datasettype_identity_for_category('startmap')
        log.debug(f'ROOT = {root_dir_identity}')
        if root_dir_identity:
            selected = self.build_tree_model(all_datasets, root_dir_identity)
        else:
            log.debug('ERROR finding the root dir for this domain/user')
            return
        self.tree_datasets.expandAll()
        # make the width's of the columns fit as good as possible:
        self.tree_datasets.header().setStretchLastSection(False)
        self.tree_datasets.header().setSectionResizeMode(QHeaderView.ResizeToContents)
        if selected.isValid():
            self.tree_datasets.scrollTo(selected, hint=QAbstractItemView.PositionAtCenter)
        else:
            log.debug(f'Selected row index for treeview is NOT valid !!! {selected.row()}')

    # ingelogd als ngbtest, de datasets zien van vakantieganger waarop permissie:
    # https://accapi.ndff.nl/api/v2/domains/249/datasets/?domain_key=3a02863211375bfad08940c9e4733f41
    def find_tree_parents(self, all_datasets: deque, root_dir_identity: str):
        """
        Having all datasets (with ndff identities), with a link parent ndff identity, either:
        - try to find the root_dir_identity given as parameter (normal use-case where the full dataset tree is retrieved)
        - try to find fake root's: when there is no root_dir_identity, we try to find all nodes of which we do not
        have a parent our self. This use case is when we actually retrieve several dataset-tree's which are part(s) of a
        bigger tree for which we do not have full permission (when working in other's domains)
        """
        root_parents = {}
        for dataset in all_datasets:
            dataset_identity = dataset['identity']
            dataset_parent = dataset['parent']
            root_parents[dataset_parent] = dataset
            if dataset_identity in root_parents.keys():
                del root_parents[dataset_identity]
        # Ok with first run we have a set which potentially are parents, but it's possible we flag them as parent
        # because of the order the dataset is in... So going again over this list to remove the false ones...
        # Possibly not the most efficient way, but I do not see a way to find the right stop condition
        for dataset in all_datasets:
            dataset_identity = dataset['identity']
            if dataset_identity in root_parents.keys():
                del root_parents[dataset_identity]
        # Ok we collected all parents; clear the model and create the parent row(s)
        self.datasets_model.setRowCount(0)
        root = self.datasets_model.invisibleRootItem()
        for identity, dataset in root_parents.items():
            title = f'DOMEIN ({self.connector.get_api().domain})'
            if identity == root_dir_identity:
                title = (dataset['description']).upper()
            parent_item = QStandardItem('')
            parent_item.setFlags(Qt.NoItemFlags)
            identity_item = QStandardItem('')
            identity_item.setFlags(Qt.NoItemFlags)
            root.appendRow([
                QStandardItem(title),
                parent_item,
                identity_item
            ])
            item = root.child(root.rowCount() - 1)  # last appended row
            root_parents[identity] = item
            # ALSO: do make the parents not-selectable (no observations in the root folders)
            item.setFlags(Qt.NoItemFlags)
            item.setForeground(Qt.black)
        return root_parents

    def build_tree_model(self, all_datasets: deque, root_dir_identity: str) -> QModelIndex:
        """
        Create a QtModel from a deque of all datasets AND select a row in it given by current le_selected_uri

        Every dataset in the deque has references to other datasets.
        So first try to find a parent, then it's children etc.

        After creation of the tree model, it will return the QModelIndex of current default value identity IF found.
        """
        # http://pharma-sas.com/common-manipulation-of-qtreeview-using-pyqt5/
        # Map of parent identities (key) with a QStandardItem (value)
        # find_tree_parents clears the model AND creates one or more parents
        seen_parents = self.find_tree_parents(all_datasets, root_dir_identity)

        # now we are sure we have all parent nodes, we keep iterating over all dataset until all nodes are 'placed'
        while all_datasets:
            dataset = all_datasets.popleft()
            dataset_identity = dataset['identity']
            dataset_parent = dataset['parent']
            if dataset_parent not in seen_parents:
                # not yet seen: put back in the basket, try later.... (when we collected more 'parents')
                all_datasets.append(dataset)
                continue
            parent = seen_parents[dataset_parent]
            parent.appendRow([
                QStandardItem(dataset['description']),
                QStandardItem(dataset['_links']['self']['href']),
                QStandardItem(dataset_identity)
            ])
            seen_parents[dataset_identity] = parent.child(parent.rowCount() - 1)

        # Find selected row
        if len(self.le_selected_uri.text()) > 0 and self.le_selected_uri.text() in seen_parents:
            # find current dataset, get the QStandardItem of its row and use the index() to select it
            selection = seen_parents[self.le_selected_uri.text()].index()
            self.tree_datasets.selectionModel().select(selection, QItemSelectionModel.ClearAndSelect | QItemSelectionModel.Rows)
        else:
            self.lbl_selected.setText('Niets geselecteerd, of deze dataset bestaat niet binnen uw domain')
            selection = QModelIndex()
        return selection

    def dataset_row_clicked(self, selection_idx: QItemSelection):
        """
        Handle a click in the table/tree with Datasets.

        The result will be to set the le_selected_uri input with the identity of the selected dataset/folder
        """
        self.lbl_selected.setText('')
        if selection_idx.isEmpty():
            return
        if selection_idx.indexes()[0].isValid() and selection_idx.indexes()[0].siblingAtColumn(2).isValid():
            name = self.datasets_model.data(selection_idx.indexes()[0].siblingAtColumn(0))
            link = self.datasets_model.data(selection_idx.indexes()[0].siblingAtColumn(1))
            identity = self.datasets_model.data(selection_idx.indexes()[0].siblingAtColumn(2))
            self.le_selected_uri.setText(identity)
            result_txt = f'{name} - <a href="{link}">{link}</a>'
            self.lbl_selected.setText(result_txt)
        else:
            log.debug('Row clicked but non valid index...')  # should never happen

    def create_dataset(self):
        """
        Create a new Dataset with current selected dataset as parent.
        The dataset type identity of a 'gewone map' is used for the dataset.

        User will have to give a name to the Dataset, and optionally a begin/end time.
        IF this domain holds 'protocols' the user can select one and it will be given to the dataset too.
        """
        api = self.connector.get_api()
        parent_uri = self.le_selected_uri.text()
        identity_uri = f'http://ndff.nl/{api.user}/folders/{time.time_ns()}'
        dlg = PluginDatasetCreateDialog(self)
        if is_uri(parent_uri):
            dlg.le_identity_uri.setReadOnly(False)  # ?? we do want users to be able to edit the identity
            dlg.le_parent_uri.setText(parent_uri)
            dlg.le_identity_uri.setText(identity_uri)
            now = QDateTime.currentDateTime()
            dlg.dt_begin.setDateTime(now.addDays(-1))
            dlg.dt_end.setDateTime(now)
            dlg.le_description.setPlaceholderText("NIET LEEG LATEN")
            self.fill_protocols_combo(dlg.cb_protocols)
            dlg.show()

        if dlg.exec_():
            d = NdffDataset()
            # minimal fields needed:
            d.parent = parent_uri
            d.identity = dlg.le_identity_uri.text()
            d.description = dlg.le_description.text()
            d.period_start = dlg.dt_begin.dateTime().toString(Qt.ISODate)
            d.period_stop = dlg.dt_end.dateTime().toString(Qt.ISODate)
            d.dataset_type = self.get_domain_datasettype_identity_for_category('gewone map')
            index = dlg.cb_protocols.currentIndex()
            if index > 0:
                protocol = dlg.cb_protocols.itemData(index, Qt.UserRole)
                d.protocol = protocol
            # IF the user selected a location:
            if dlg.location is not None:
                d.location = dlg.location

            valid, reason = d.is_valid()
            if not valid:
                QMessageBox.information(self, self.PLUGIN_DIALOGS_TITLE, f"{reason}")
            else:
                result = self.connector.create_dataset(d)
                if str(result['http_status']) == '201':
                    # success
                    # make just created dataset current selection, BEFORE building the tree, then it gets selected
                    self.le_selected_uri.setText(d.identity)
                    self.build_datasets_tree()
                else:
                    QMessageBox.information(self, self.PLUGIN_DIALOGS_TITLE, f"Er ging iets mis: {result['http_status']}\n{result}")

    def fill_protocols_combo(self, cb_protocols: QComboBox, show_message=True):
        """
        Certain domains have protocols attached, this method will check for protocols (for this domain) and fill
        the dropdown with the identities/labels for the protocols, so a user can set a protocol for a Dataset.
        """
        cb_protocols.clear()
        protocols = self.connector.get_protocols()
        i = 0
        if show_message:
            cb_protocols.addItem(self.SELECT_PROTOCOL_MESSAGE)
            log.debug(f'Retrieved {len(protocols)}')
            i += 1
        for protocol in protocols:
            log.debug(protocol)
            cb_protocols.addItem(f'{protocol["description"]}', userData=protocol['identity'])  # going to search back for a protocol using the identity of it
            cb_protocols.setItemData(i, protocol["identity"], Qt.ToolTipRole)
            i += 1

    def edit_dataset(self):
        """
        Method to EDIT the selected Dataset.

        After the selection in the table, the identity is used to retrieve the full Dataset information from the API.
        It will be shown in a PluginDatasetCreateDialog so the user can edit the values.
        """
        current_identity = self.le_selected_uri.text()
        # MMM, should we also create a get_dataset() into the connector? Ideas was the connector was responsible
        # for paging, which isn't needed here....
        dataset = self.connector.search_dataset(current_identity)
        api = self.connector.get_api()
        log.debug(dataset)
        if dataset is None:
            log.error(f'Searched for dataset "{current_identity}" in domain {api.domain} for user {api.user}, but Nothing found...')
            return
        dlg = PluginDatasetCreateDialog(self)
        dlg.le_identity_uri.setText(dataset.identity)
        dlg.le_parent_uri.setText(dataset.parent)
        dlg.le_description.setText(dataset.description)
        if dataset.period_start:
            dlg.dt_begin.setDateTime(QDateTime.fromString(dataset.period_start, Qt.ISODate))
        if dataset.period_stop:
            dlg.dt_end.setDateTime(QDateTime.fromString(dataset.period_stop, Qt.ISODate))
        dlg.le_identity_uri.setReadOnly(True)  # ?? we do not want users to edit the identity??? we are UPDATING
        if dataset.protocol:
            self.fill_protocols_combo(dlg.cb_protocols, show_message=False)
            identity = dataset.protocol
            index = dlg.cb_protocols.findData(identity, Qt.UserRole)
            dlg.cb_protocols.setCurrentIndex(index)
        else:
            self.fill_protocols_combo(dlg.cb_protocols, show_message=True)

        dlg.lbl_current_location.setText(f"Locatie  -->  {dataset.get('location')}")

        dlg.show()
        if dlg.exec_():
            # minimal fields needed:
            dataset.identity = dlg.le_identity_uri.text()
            dataset.description = dlg.le_description.text()
            dataset.period_start = dlg.dt_begin.dateTime().toString(Qt.ISODate)
            dataset.period_stop = dlg.dt_end.dateTime().toString(Qt.ISODate)
            index = dlg.cb_protocols.currentIndex()
            protocol = dlg.cb_protocols.itemData(index, Qt.UserRole)
            dataset.protocol = protocol
            # if index > 0:
            #     protocol = dlg.cb_protocols.itemData(index, Qt.UserRole)
            #     dataset.protocol = protocol
            # else:
            #     dataset.protocol = ''
            if dlg.location is not None:
                dataset.location = dlg.location

            valid, reason = dataset.is_valid()
            if not valid:
                QMessageBox.information(self, self.PLUGIN_DIALOGS_TITLE, f"{reason}")
            else:
                # put_dataset(self, ndff_dataset_json, ndff_uri, dataset_id, epsg='EPSG:4326')
                result = self.connector.get_api().put_dataset(dataset.to_ndff_dataset_json(), dataset.ndff_uri, dataset.identity)
                if str(result['http_status']) == '200':
                    # success
                    self.build_datasets_tree()
                else:
                    QMessageBox.information(self, self.PLUGIN_DIALOGS_TITLE, f"Er ging iets mis: {result['http_status']}\n{result}")

    def accept(self) -> None:
        """
        Qt api call to accept current state of the Datasets
        """
        log.debug('OK clicked; Dataset dialog')
        selected_uri = self.le_selected_uri.text()
        if self.mode == self.MODE_DEFAULT_SELECTION:
            # we are using the dialog to set up a default value to be used as dataset
            if len(selected_uri) == 0:  # empty input: that is, user removed the uri or value from the input
                self.connector.set_field_default(self.field, None)
            #else:
                # NOT doing this here, as in this case the dialog is a sub dialog of the 'define a default dialog', so
                # it is still possible that the user cancels that action.
                # Aqadd the ndff-field->default to the defaults of the datasource
                # self.connector.set_field_default(self.field, selected_uri)
        else:
            # self.MODE_DATASET_MAPPING
            # we are using the dialog to map a user (dataset)value to a ndff dataset uri
            # warning text in case user did not select or typed input
            user_warning = 'Selecteer een URI uit de lijst, of gebruik "Annuleer/Cancel"'
            if self.le_selected_uri.text() in ('', user_warning):
                self.le_selected_uri.setText(user_warning)
                return
            dataset_field = 'dataset'
            if self.connector.get_field_mapping('dataset'):
                dataset_field = self.connector.get_field_mapping('dataset')
            dataset_value = self.record[dataset_field]
            self.connector.set_data_mapping(self.field, dataset_value, selected_uri)
        self.done(QDialog.Accepted)

    @staticmethod
    def open_in_browser(link: str):
        """
        Helper method to open an url into user's default browser
        """
        QDesktopServices.openUrl(QUrl(link))

