import logging
import tempfile
from typing import Union

from qgis._core import QgsFeature
from qgis.core import (
    QgsGeometry,
)
from qgis.PyQt.QtCore import (
    QDate,
    QDateTime,
    QTime,
)

from .ext.ndff.api.object import (
    NdffObservation,
)
from .ext.ndff.connector.connector import (
    NdffConnector,
)
from .ext.ndff.utils import (
    ellipsize2string,
    is_numeric,
)

log = logging.getLogger(__name__)


class QgisNdffConnector(NdffConnector):
    """
    NdffConnector instance specific for use in QGIS plugins.
    Because QGIS internally works with QgsFeatures instead of records, we need a specific connector.

    Note that instances of NdffConnectors are normally created using the static creation methods in NdffConnector!

    The NdffConnector.create_from_directories is overridden to return an actual QgisNdffConnector
    """

    def __init__(
        self,
        ndff_api_settings: dict = {},
        datasource_settings: dict = {},
        field_mappings: dict = {},
        data_mappings: dict = {},
        client_settings: dict = {},
        ndff_log_dir: str = tempfile.gettempdir(),
    ):
        """
        Constructor for a QGIS NdffConnector.

        Note that instances of NdffConnectors are normally created using the static creation methods in NdffConnector!

        The NdffConnector.create_from_directories is overridden to return an actual QgisNdffConnector
        """
        super().__init__(
            ndff_api_settings,
            datasource_settings,
            field_mappings,
            data_mappings,
            client_settings,
            ndff_log_dir,
        )
        # TODO remove datasource_settings here?

    @staticmethod
    def create_from_directories(
        global_config_dir: str,
        user_config_dir: str,
        from_within_qgis: bool = False,
        class_to_use: object = object,
    ):
        """
        Static creation function to create a QgisNdffConnector (instead of a normal NdffConnector,
        see ndff.connector.connector.py).
        Override for ndff.connector.connector.py.

        :param str global_config_dir:
        :param str user_config_dir:
        :param bool from_within_qgis: optional flag if this connector is used from within QGIS
        :param bool class_to_use: because we want to be able to also create other connectors
            (like QgsNdffConnector), it is possible to add a class name here.
        :return: QgisNdffConnector
        """
        nc = NdffConnector.create_from_directories(
            global_config_dir,
            user_config_dir,
            from_within_qgis=True,
            class_to_use=QgisNdffConnector,
        )
        return nc

    def next_ndff_observation(self) -> Union[NdffObservation, bool]:
        """
        Method to fetch the next record (which is a QgsFeature in QgisNdffConnector) as NdffObservation.

        Handles an StopIteration exception if there is no next record.

        Mostly used for CLI or QGIS/client use.

        :return: NdffObservation OR false in case the iterator returned a StopIteration exception.
        """
        try:
            feature = self.next_record()  # QgsFeature !!
        except StopIteration:
            return False
        record = self.map_qgsfeature_to_dict(feature)
        observation = self.map_data_to_ndff_observation(record)
        self.check_fix_qgis_location(
            observation, record
        )  # feedback is discarded here...
        return observation

    @staticmethod
    def map_qgsfeature_to_dict(feature: QgsFeature) -> dict:
        """
        Map a QgsFeature to a dictionary.

        Because other ndff connectors work record base with pure dicts, it is necessary in certain code paths to
        morph a QgsFeature to a dict. Note that this also needs some work on geometry and date(time) fields.

        :param feature: QgsFeature
        :return: dict
        """
        # whould it be safer to do a deepcopy here?  record = deepcopy(feature.attributeMap())
        record = feature.attributeMap()
        # hack to be able to move around a simple dict instead of a QgsFeature:
        # we move/keep the geometry IF available in the field 'geometry'
        if feature.hasGeometry():
            record["geometry"] = feature.geometry()
        # FIX special treatment of QDate en QDatetime and QTime!!
        for field in record:
            if type(record[field]) is QDateTime:
                record[field] = record[field].toString("yyyy-MM-ddThh:mm:ss")
            elif type(record[field]) is QDate:
                record[field] = record[field].toString("yyyy-MM-dd")
            elif type(record[field]) is QTime:
                record[field] = record[field].toString("hh:mm:ss")
        return record

    def check_fix_qgis_location(
        self, observation: NdffObservation, record: dict = None
    ) -> str:
        """
        Method called either to 'just show' a location representation in the dialog (on the 'location' button),
        or called after the user changed the mapping/properties of the location of current layer.

        The logic is that either the user created a valid field mapping to an x/y or (e)wkt column OR choose to use
        the data's QgsGeometry column.
        IF the location comes from the QgsGeometry, build a json construct from it which can be used by the
        NdffConnector to create a valid 'location'-construct.

        The location of the observation should now either be set via field mapping,
        OR all location fields are UNSET, and we can try to create a valid geometry here based on the
        QgsGeometry of current QgsFeature (IF available)

        :param observation: NdffObservation
        :param record: dict
        :return: (Dutch) feedback string, to be used for user interaction
        """
        if observation.location and isinstance(observation.location, dict):
            # mappings created a valid location: show it
            if self.get_field_mapping("location"):
                feedback = f"{self.get_field_mapping('location')}  -->  {observation.get('location')}"
            else:
                feedback = f"{self.get_field_mapping('location_x')}, {self.get_field_mapping('location_y')}  -->  {observation.get('location')}"
        elif "geometry" in record and isinstance(record["geometry"], QgsGeometry):
            # We moved the geometry into the connector via WKT... BUT shapely wkt creation does not handle 3D geoms...
            # TODO: find out if it is better to always use WKB !
            if record["geometry"].get().is3D():
                # after asWkb the geometry is a QByteArray, let's cast that to a more pythonic bytes/bytearray for
                # later use in connector/ndff lib.
                # bytearray is mutable, bytes are not
                log.info("Geometry contains Z-value: currently DROPPING it!")
                record["geometry"].get().dropZValue()
                temp_loc_object = {"location": bytes(record["geometry"].asWkb())}
            else:
                temp_loc_object = {"location": record["geometry"].asWkt()}
            # valid QgsGeometry in the record, let's make the connector build a location here:
            buffer = self.get_field_default("location_buffer")
            # check if it was mapped (probably not, users do not put that in data...)
            if self.get_field_mapping("location_buffer"):
                field = self.get_field_mapping("location_buffer")
                if field in record:
                    buffer = record[self.get_field_mapping("location_buffer")]
            if is_numeric(buffer):
                temp_loc_object["location_buffer"] = buffer
            # SHOW FEEDBACK IN DIALOG
            if self.create_geom(observation, temp_loc_object):
                feedback = f"QGIS  -->  {ellipsize2string(observation.get('location'), max_len=120)} ..."
            else:
                feedback = f"QGIS  -->  FOUT: {observation.get('location')}"
        else:
            # no location
            feedback = " ?? "

        return feedback
