# -*- coding: utf-8 -*-
"""
/***************************************************************************
 PluginDatasetDialog
 This plugin connects to the NDFF api
                              -------------------
        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 os
import time
from qgis.PyQt import uic
from qgis.PyQt.QtWidgets import (
    QAbstractItemView,
    QDialog,
    QHeaderView,
    QMessageBox,
    QApplication,
    QComboBox,
)
from qgis.PyQt.QtGui import (
    QStandardItemModel,
    QStandardItem,
    QDesktopServices,
    QGuiApplication,
)
from qgis.PyQt.QtCore import (
    QDateTime,
    Qt,
    QItemSelection,
    QUrl,
    QItemSelectionModel,
    QModelIndex,
)
from .ext.ndff.connector import NdffConnector
from .ext.ndff.api import NdffDataset
from .ext.ndff.utils import is_uri
from .ext.ndff.exceptions import NdffException
from ndff_connector_plugin import PLUGIN_DIALOGS_TITLE
from ndff_connector_plugin.ndffc_plugin_dataset_create_dialog import PluginDatasetCreateDialog
from collections import deque
import logging
from . import LOGGER_NAME
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):

    SELECT_PROTOCOL_MESSAGE = 'Selecteer een Protocol indien bekend of wenselijk'

    def __init__(self, parent=None):
        """Constructor."""
        super(PluginDatasetDialog, self).__init__(parent)
        self.setupUi(self)
        self.connector = None
        self.record = None
        self.field = None

        self.PLUGIN_DIALOGS_TITLE = PLUGIN_DIALOGS_TITLE

        self.combo_layer_fields.fieldChanged.connect(self.layer_field_changed)
        # explicit use of textChanged below because we want it to react both on manual as on code changed
        self.le_default_value.textChanged.connect(self.default_value_edited)
        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(self, layer, connector: NdffConnector, record: dict, field: str) -> None:
        """
        Show the dialog, using the arguments to fill widgets

        :param: layer, used to fill the dropdown with fields (of the layer)
        :param: connector, used to get the api, credentials etc.
        :param: record
        :param: field
        """
        log.debug(f'Calling show of PluginDatasetDialog for layer: {layer} field: {field}')
        if layer is None or connector is None or field is None or field == '':
            # should not happen!
            raise NdffException(f'Calling PluginDatasetDialog with some None value: \nlayer: {layer} \nfield: {field} \nconnector: {connector} \nrecord: {record}')
        self.connector = connector
        self.record = record
        self.field = field
        self.combo_layer_fields.setLayer(layer)
        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_default_value.setText('')
        self.le_search_value.setText('')
        self.datasets_model.removeRows(0, self.datasets_model.rowCount())
        self.tree_datasets.header().setStretchLastSection(True)
        # show default for field (dataset) IF available
        if connector.get_field_default(field):
            self.le_default_value.setText(f'{connector.get_field_default(field)}')
        else:
            self.le_default_value.setText('')
        # after cleanup, show the dialog, and process all events, else we will get a black dialog while
        # the datasets are being requested from API and the Treeview is being created
        self.showNormal()
        QApplication.processEvents()
        self.build_datasets_tree()

    def get_startmap_identity(self):
        dataset_types = self.connector.get_dataset_types()
        root_identity = None
        for dir_type in dataset_types:
            # description = dir_type['description']
            # log.debug(description)
            if dir_type['category'] == 'startmap':
                root_identity = dir_type['identity']
                break
        return root_identity

    def get_map_identity(self):
        dataset_types = self.connector.get_dataset_types()
        map_identity = None
        for dir_type in dataset_types:
            description = dir_type['description']
            # log.debug(description)
            if dir_type['category'] == 'gewone map':
                map_identity = dir_type['identity']
                break
        return map_identity

    def build_datasets_tree(self):
        # 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_startmap_identity()
        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:
        # 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_default_value.text()) > 0 and self.le_default_value.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_default_value.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):
        # log.debug(selection_idx)  # QItemSelection
        # log.debug(selection_idx.first())  # QItemSelection
        # log.debug(selection_idx.indexes()[0])  # QModelIndex
        # log.debug(f' row:{selection_idx.indexes()[0].row()} column: {selection_idx.indexes()[0].column()}')
        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_default_value.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.
        User will have to give a name to the Dataset,
        # TODO AND a DatasetType (for now hardcoded to:
        """
        api = self.connector.get_api()
        parent_uri = self.le_default_value.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_map_identity()
            index = dlg.cb_protocols.currentIndex()
            if index > 0:
                protocol = dlg.cb_protocols.itemData(index, Qt.UserRole)
                d.protocol = protocol
            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_default_value.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):
        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):
        current_identity = self.le_default_value.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.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 = ''
            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:
        log.debug('OK clicked; Dataset dialog')
        data_field = self.combo_layer_fields.currentField()
        default = self.le_default_value.text()
        log.debug(f'Mapping of observation dataset set to data "{data_field}" with default: "{default}" in connector')

        if len(data_field) == 0 and len(default) == 0:
            self.le_default_value.setText('Selecteer of voer een Dataset URI in, selecteer een veld die de dataset URI bevat of klik Cancel/Annuleren.')
            return

        if len(data_field) == 0:  # that is: an empty string: NO mapping
            self.connector.set_field_mapping(self.field, None)
        else:
            self.connector.set_field_mapping(self.field, data_field)
        if len(default) == 0:  # empty input: that is, user removed the uri or value from the input
            self.connector.set_field_default(self.field, None)
        else:
            # add the ndff-field->default to the defaults of the datasource
            self.connector.set_field_default(self.field, default)
        # log.debug(f'Connector now: {self.connector}')
        self.done(QDialog.Accepted)

    @staticmethod
    def open_in_browser(link: str):
        QDesktopServices.openUrl(QUrl(link))

    def layer_field_changed(self, data_field: str):
        """
        Setting the dropdown AND the example from data based on a field/attribute from a data record

        So observation_field here is a data attribute
        """
        # show if there is a mapping for field = SET dropdown value AND the corresponding data if a record is available
        log.debug(f'Calling layer_field_changed for data field {data_field}')
        example_label = ''
        if data_field != '' and self.record and self.record[data_field]:
            example_label = self.record[data_field]  # TODO value <=> key ???
            # and cleanup the default value field
            self.le_default_value.setText('')
        self.lbl_example_value.setText(f'{example_label}')

    def default_value_edited(self, default_text: str):
        if default_text != '':
            # if a default field is defined, let's make the field dropdown empty
            self.combo_layer_fields.setField('')
