import json
import logging

from urllib.parse import urlparse
from datetime import datetime
from ..exceptions import NdffException
from ..utils import (
    is_uri,
    is_numeric,
)
from typing import Optional

log = logging.getLogger(__name__)


class NdffObject:
    """
    A NdffObject is the base class for NDFF classes handled by the NDFF api.
    """

    NUMERIC_NON_ZERO_OR_URI_FIELDS = ()
    URI_FIELDS = ()
    OTHER_FIELDS = ()
    OPTIONAL_FIELDS = ()

    # location is here to make code checker happy, AND because we want to have is_valid at this level
    # a location should always be a valid location dict, never something else!
    # NOTE: the creation of a valid location dict is the responsibility of the NdffConnector object!
    location: Optional[dict] = None

    def fields(self):
        return self.__dict__

    def get(self, field_name):
        if field_name in self.__dict__:
            if field_name == 'location':
                if self.location:
                    # TODO: create better string representation (eg for Polygon)
                    return str(self.location)
                else:
                    return 'Location ??'
            else:
                return self.__dict__[field_name]
        else:
            return None

    def set(self, field_name: str, field_value):
        # TODO more checks?
        # ONLY set available properties (NOT adding new ones)
        if field_name in self.__dict__:
            if field_name == 'location' and not isinstance(field_value, dict):
                raise ValueError('Not possible to set location for an NdffObject: value is NOT a (location) dict!')
            self.__dict__[field_name] = field_value
        else:
            raise ValueError(f'Not possible to set "{field_name}" for this NdffObject: NOT an available property!')

    def is_valid(self) -> (bool, list):
        errors = []
        none_message = 'Verplicht veld "{}" is nog leeg of onbekend'
        not_uri_message = 'Veld "{}" zou een geldige URI moeten zijn: "{}"'
        not_dict_message = 'Veld "{}" zou een "dict" moeten zijn, maar is "{}"'
        missing_location_field = '"Lokatie" zou een veld "{}" (waarde) moeten hebben, of de waarde is niet geldig'
        wrong_location_buffer_type = 'De buffer/nauwkeurigheid moet een numerieke waarde zijn (meters), maar heeft de waarde "{}"'
        wrong_geometry = 'Er is iets misgegaan bij het maken van de NDFF geometry, check data'
        wrong_point_buffer = 'De bufferwaarde/nauwkeurigheid moet een getal zijn, en groter zijn dan 0 voor (multi)points, maar heeft waarde "{}"'
        wrong_polygon_buffer = 'De bufferwaarde/nauwkeurigheid moet een getal zijn, en voor (multi)polygonen/lijnen altijd 0 zijn, maar heeft waarde "{}"'
        period_wrong_order = 'De start van de periode: "{}", ligt niet VOOR of OP het einde van de periode: "{}"'
        period_in_future = 'De periode "{}" - "{}" ligt in de toekomst'
        value_not_zero = 'De waarde voor {} moet numeriek en groter dan 0 zijn, OF een URI (bij codes), maar heeft waarde "{}"'

        # URI fields:
        for name in self.URI_FIELDS:
            if self.__dict__[name] in (None, '', ' ', '-', '?'):
                if name not in self.OPTIONAL_FIELDS:  # 20220421 dwelling is now an optional URI value
                    errors.append(none_message.format(name))
            elif not is_uri(self.__dict__[name]):
                # Mmm... except when it is an abundance_value, which can be both uri and literal...
                if name != 'abundance_value':
                    errors.append(not_uri_message.format(name, self.__dict__[name]))

        # NON uri fields, we can only check for None/Null
        for name in self.OTHER_FIELDS:
            if self.__dict__[name] in (None, '', ' ', '-', '?'):
                errors.append(none_message.format(name))

        # NON zero fields:
        for name in self.NUMERIC_NON_ZERO_OR_URI_FIELDS:
            # EITHER numeric, THEN not zero
            # OR a URI
            if is_uri(self.__dict__[name]):
                pass  # OK
            elif is_numeric(self.__dict__[name]):
                # could be OK
                if self.__dict__[name] in (None, '', '0', 0):
                    # ERR: numeric fields should not be zero
                    errors.append(value_not_zero.format(name, self.__dict__[name]))
            else:
                # not a URI and not numeric
                errors.append(value_not_zero.format(name, self.__dict__[name]))

        # ONLY if this ndff object has a 'location' field
        if 'location' in self.__dict__:
            # Location related fields:
            # "location": {
            #     "buffer": 5,
            #     "geometry": {
            #         "type": "Point",
            #         "coordinates": [
            #             408241,
            #             78648
            #             ]
            #         }
            #     },
            if self.location in (None, '', ' ', '-', '?'):
                errors.append(none_message.format('location'))
            elif not isinstance(self.location, dict):
                errors.append(not_dict_message.format('location', type(self.location)))
            else:
                # ok, we have a dict, check keys
                # check for buffer and geometry
                if 'buffer' not in self.location:
                    errors.append(missing_location_field.format('buffer/Lokatie-nauwkeurigheid'))
                if 'geometry' not in self.location:
                    errors.append(missing_location_field.format('geometry'))
                # check for valid buffer value
                if 'buffer' in self.location and self.location['buffer'] in ('', None, '-', '?'):
                    if none_message.format('location_buffer') not in errors:
                        errors.append(none_message.format('location_buffer'))
                elif 'buffer' in self.location and not is_numeric(self.location['buffer']):
                    errors.append(wrong_location_buffer_type.format(self.location['buffer']))
                # further geometry checking...
                if 'geometry' in self.location and 'coordinates' in self.location['geometry'] \
                        and 'type' in self.location['geometry'] and \
                        self.location['geometry']['type'].upper() in ('POINT', 'POLYGON', 'MULTIPOLYGON', 'LINESTRING', 'MULTILINESTRING'):
                    # going to check the value of the buffer:
                    if 'buffer' in self.location and self.location['geometry']['type'].upper() in ('POINT',):
                        # a (MULTI)POINT  and (MULTI)LINESTRINGS should ALWAYS have a NON-ZERO buffer
                        if not is_numeric(self.location['buffer']) or float(self.location['buffer']) <= 0:
                            errors.append(wrong_point_buffer.format(self.location['buffer']))
                    elif 'buffer' in self.location and self.location['geometry']['type'].upper() in \
                            ('POLYGON', 'MULTIPOLYGON', 'LINESTRING', 'MULTILINESTRING'):
                        # while a (MULTI)POLYGON should always have a ZERO buffer
                        if not is_numeric(self.location['buffer']) or float(self.location['buffer']) > 0:
                            errors.append(wrong_polygon_buffer.format(self.location['buffer']))
                    else:
                        pass
                else:
                    errors.append(wrong_geometry)

        # only checking for period_start... if it is set, there should also be a period_end
        if 'period_start' in self.__dict__:
            # period (period_start and period_stop)
            # NDFF 400 message: "Datetime heeft een ongeldig formaat, gebruik 1 van de volgende formaten:
            # YYYY-MM-DDThh:mm:ss, YYYY-MM-DD hh:mm:ss, YYYY-MM-DDThh:mm, YYYY-MM-DD hh:mm, YYYY-MM-DDThh, YYYY-MM-DD hh, YYYY-MM-DD, YYYY-MM, YYYY

            # append errors (IF any) after validation of the periods
            start, period_start_errors = self.is_valid_datetime_string('period_start')
            if start is None or period_start_errors:
                errors += period_start_errors
            stop, period_stop_errors = self.is_valid_datetime_string('period_stop')
            if stop is None or period_stop_errors:
                errors += period_stop_errors

            if self.period_start is None:
                errors.append(none_message.format('period_start'))
            if self.period_stop is None:
                errors.append(none_message.format('period_stop'))

            # NOT going to check if stop > start, as NDFF also accepts start == stop :-(
            if start and stop:
                if stop >= datetime.now():
                    errors.append(period_in_future.format(self.period_start, self.period_stop))
                if start > stop:
                    errors.append(period_wrong_order.format(self.period_start, self.period_stop))

        # other fields
        if len(errors) == 0:
            return True, []
        else:
            # insert the 'identity' of this observation
            #errors.insert(0, f'* Fouten voor NdffObject {self.identity} *')
            return False, errors

    def set_extra_info(self, key_uri: str, value: str):
        # the key of an extra info field should ALWAYS be an uri! SHOULD BE CHECKED BEFORE SETTING !!!
        # the value can be both a data value or a value mapped to a NDFF uri
        # BOTH should be valid strings, NOT none
        if key_uri and value:
            self.extra_info.append({'key': key_uri, 'value': value})
        else:
            raise ValueError(f'Setting an Extra Info Key/Value, one of them is None: "{key_uri}" - "{value}"')

    def is_valid_datetime_string(self, datetime_field) -> (datetime, list):
        valid_datetime = None
        errors = []
        wrong_datetime_format = 'Veld "{}" met waarde "{}" zou een NDFF-geldig formaat moeten hebben: ' \
                                'YYYY-MM-DDThh:mm:ss, YYYY-MM-DD hh:mm:ss, YYYY-MM-DDThh:mm, YYYY-MM-DD hh:mm, ' \
                                'YYYY-MM-DDThh, YYYY-MM-DD hh, YYYY-MM-DD, YYYY-MM, YYYY'
        wrong_datetime_format2 = 'LET OP: in de "{}"-datums mag ook YYYY-MM-DDThh:mm:ss.sss of YYYY-MM-DDThh:mm:ss.ssssss ' \
                                 'worden gebruikt, maar de seconden zullen worden afgerond bij het ' \
                                 'verzenden naar de NDFF api.'

        if self.get(datetime_field) is None:
            return None, wrong_datetime_format

        datetime_string = str(self.get(datetime_field)).strip()  # strip to be sure to remove leading/trailing spaces
        try:
            # just try to create a datetime from iso format
            valid_datetime = datetime.fromisoformat(datetime_string)
            # ok, that worked, done:
            return valid_datetime, []
        except ValueError:
            pass

        # apparently one of those other possible formats
        datetime_string = datetime_string.replace('T', ' ')
        date_and_time = datetime_string.split(' ')
        try:
            if len(date_and_time) == 1:
                # apparently ONLY a Date
                # possible values: YYYY-MM-DD, YYYY-MM, YYYY
                # go from shortest to longest
                if len(date_and_time[0]) == 4:
                    valid_datetime = datetime.strptime(date_and_time[0], '%Y')
                elif len(date_and_time[0]) == 7:
                    valid_datetime = datetime.strptime(date_and_time[0], '%Y-%m')
                elif len(date_and_time[0]) == 10:
                    valid_datetime = datetime.strptime(date_and_time[0], '%Y-%m-%d')
                else:
                    raise NdffException('Wrong DATE format')
            elif len(date_and_time) == 2:
                # apparently a Date AND a Time
                # we already removed the T so possible values: YYYY-MM-DD hh:mm:ss, YYYY-MM-DD hh:mm, YYYY-MM-DD hh
                date_and_time = datetime_string
                if len(date_and_time) == 13:
                    valid_datetime = datetime.strptime(date_and_time, '%Y-%m-%d %H')
                elif len(date_and_time) == 16:
                    valid_datetime = datetime.strptime(date_and_time, '%Y-%m-%d %H:%M')
                elif len(date_and_time) == 19:
                    valid_datetime = datetime.strptime(date_and_time, '%Y-%m-%d %H:%M:%S')
                else:
                    raise NdffException('Wrong DATETIME format')
            else:
                raise NdffException('Wrong DATETIME format')
        except NdffException:
            errors.append(wrong_datetime_format.format(datetime_field, datetime_string))
            errors.append(wrong_datetime_format2.format(datetime_field))

        return valid_datetime, errors


class NdffDatasetType(NdffObject):
    """
    A NdffDatasetType is ...
    """

    def __init__(self, identity: str = None, category: str = None, description: str = None, ndff_uri: str = None):
        self.identity = identity  # URI
        self.category = category  # any
        self.description = description  # any
        self.ndff_uri = ndff_uri  # URI

    def to_ndff_datasettype_json(self, datasettype_identity=None) -> str:
        # https://accapi.ndff.nl/api/v2/domains/708/datasettypes/21534/
        # {
        #     "_links": {
        #         "self": {
        #             "href": "https://accapi.ndff.nl/api/v2/domains/708/datasettypes/21534/"
        #         }
        #     },
        #     "category": "gewone map",
        #     "description": "map Test NGB",
        #     "identity": "http://ndff.nl/foldertypes/test_ngb/map"
        # }
        data = {}

        if datasettype_identity:
            data['identity'] = datasettype_identity
        else:
            data['identity'] = self.identity
        data['category'] = self.category
        data['description'] = self.description

        return json.dumps(data, indent=2)


class NdffProtocol(NdffObject):
    """
    A NdffProtocol is ...
    """

    # all URI_FIELDS that should have a URI as value
    URI_FIELDS = (
        'identity',
    )

    OTHER_FIELDS = (
        'description',
    )

    def __init__(self, identity: str = None, category: str = None, description: str = None, ndff_uri: str = None):
        self.identity = identity  # URI
        self.category = category
        self.description = description  # any
        self.ndff_uri = ndff_uri  # URI

    def to_ndff_protocol_json(self, protocol_identity=None) -> str:
        # https://accapi.ndff.nl/api/v2/domains/708/datasettypes/21534/
        # {
        #     "_links": {
        #         "self": {
        #             "href": "https://accapi.ndff.nl/api/v2/domains/708/datasettypes/21534/"
        #         }
        #     },
        #     "category": "gewone map",
        #     "description": "map Test NGB",
        #     "identity": "http://ndff.nl/foldertypes/test_ngb/map"
        # }
        data = {}

        if protocol_identity:
            data['identity'] = protocol_identity
        else:
            data['identity'] = self.identity
        data['description'] = self.description

        return json.dumps(data, indent=2)


class NdffDataset(NdffObject):
    """
    A NdffDataset is ...
    """

    # all URI_FIELDS that should have a URI as value
    URI_FIELDS = (
        'dataset_type',
        'parent',
        'identity',
        'protocol',
    )

    OTHER_FIELDS = (
        'period_start',
        'period_stop',
        'description',
    )

    OPTIONAL_FIELDS = (
        'duration',
        'location_coverage',
        'protocol',
    )

    # TODO should we have a constructor without arguments?
    def __init__(self, description: str = None, dataset_type_uri: str = None, parent_uri: str = None):
        self.ndff_uri = None  # URI optional
        self.identity = None  # URI mandatory
        self.parent = parent_uri  # URI mandatory
        self.dataset_type = dataset_type_uri  # URI mandatory
        self.description = description  # any mandatory
        self.duration = 0  # any
        # location: mandatory for now always fixed starting coordinates ??
        self.location = {'buffer': 10, 'geometry': {'type': 'Point', 'coordinates': [5.38759722, 52.1556583]}}
        self.location_coverage = 0  # ?? don't know what this is for?

        self.protocol = None  # URI optional

        self.period_start = None  # mandatory NDFF Date(Time) str: YYYY-MM-DDThh:mm:ss, YYYY-MM-DD hh:mm:ss, YYYY-MM-DDThh:mm, YYYY-MM-DD hh:mm, YYYY-MM-DDThh, YYYY-MM-DD hh, YYYY-MM-DD, YYYY-MM, YYYY
        self.period_stop = None  # mandatory NDFF Date(Time) str: YYYY-MM-DDThh:mm:ss, YYYY-MM-DD hh:mm:ss, YYYY-MM-DDThh:mm, YYYY-MM-DD hh:mm, YYYY-MM-DDThh, YYYY-MM-DD hh, YYYY-MM-DD, YYYY-MM, YYYY

        self.extra_info = []
        self.involved = []

    @staticmethod
    def from_dataset_json_to_dataset(dataset_json_dict: dict):
        """
        {
             '_links': {
                 'self': {
                     'href': 'https://accapi.ndff.nl/api/v2/domains/708/datasets/2922579/'}
             },
             'datasetType': 'http://ndff.nl/foldertypes/test_ngb/map',
             'description': 'test_richard',
             'duration': None,
             'extrainfo': [],
             'identity': 'http://ndff.nl/testngb/folders/2922579',
             'involved': [],
             'location': None,
             'locationCoverage': None,
             'parent': 'http://ndff.nl/folders/2920720',
             'periodStart': None,
             'periodStop': None,
             'protocol': None
         }
        """
        d = NdffDataset()
        d.dataset_type = dataset_json_dict['datasetType']
        d.description = dataset_json_dict['description']
        d.duration = dataset_json_dict['duration']
        d.extra_info = dataset_json_dict['extrainfo']
        d.identity = dataset_json_dict['identity']
        d.involved = dataset_json_dict['involved']
        d.location = dataset_json_dict['location']
        d.location_coverage = dataset_json_dict['locationCoverage']
        d.parent = dataset_json_dict['parent']
        d.period_start = dataset_json_dict['periodStart']
        d.period_stop = dataset_json_dict['periodStop']
        d.protocol = dataset_json_dict['protocol']
        if dataset_json_dict['_links'] and dataset_json_dict['_links']['self'] and dataset_json_dict['_links']['self']['href']:
            d.ndff_uri = dataset_json_dict['_links']['self']['href']
        return d

    def to_ndff_dataset_json(self, dataset_identity=None) -> str:
        # https://accapi.ndff.nl/api/v2/domains/708/datasets/2929738/

        # {
        #     "_links": {
        #         "self": {
        #             "href": "https://accapi.ndff.nl/api/v2/domains/708/datasets/2929738/"
        #         }
        #     },
        #     "datasetType": "http://ndff.nl/foldertypes/test_ngb/map",
        #     "description": "test_dataset",
        #     "duration": 0,
        #     "extrainfo": [],
        #     "identity": "http://ndff.nl/testngb/folders/2929738",
        #     "involved": [],
        #     "location": {
        #         "buffer": 10,
        #         "geometry": {
        #             "type": "Point",
        #             "coordinates": [
        #                 5.38759722,
        #                 52.1556583
        #             ]
        #         }
        #     },
        #     "locationCoverage": 0,
        #     "parent": "http://ndff.nl/folders/2920720",
        #     "periodStart": null,
        #     "periodStop": null,
        #     "protocol": null
        # }
        data = {}

        if dataset_identity:
            data['identity'] = dataset_identity
        else:
            data['identity'] = self.identity
        data['parent'] = self.parent
        data['datasetType'] = self.dataset_type
        data['description'] = self.description
        data['duration'] = self.duration
        data['protocol'] = self.protocol
        # periods could be datetime objects (in case of Postgis Datasource)
        data['periodStart'] = str(self.period_start)
        data['periodStop'] = str(self.period_stop)
        data['location'] = self.location
        data['locationCoverage'] = self.location_coverage
        # note: this is the data holder for the lists in the json (NOT fields from record or observation)
        data['extrainfo'] = self.extra_info
        data['involved'] = self.involved

        return json.dumps(data, indent=2)


class NdffObservation(NdffObject):
    """
    A NdffObservation object is more of a simple data holder, able to validate itself and to serialize itself to json

    A NdffObservation object is created using the NdffConnector's 'map_data_to_ndff_observation'.

    It is the responsibility of the NdffConnector to create a more or less valid NdffObservation.
    """

    # all URI_FIELDS that should have a URI as value
    URI_FIELDS = (
        'taxon',
        'abundance_schema',
        'determination_method',
        'dataset',
        'biotope',
        'identity',
        'lifestage',
        'subject_type',
        'survey_method',
        'sex',
        'activity',
        'dwelling',
        'abundance_value',  # NOTE: this can be both a URI and a literal (mostly integer....)
    )

    OTHER_FIELDS = (
        #'abundance_value',
        'period_start',
        'period_stop',
    )

    # abundance value is a special case. In case of exact count it should be a number >0, but in case of (Tansley,
    # Londo, etc. codes, it MUST be a URI)
    NUMERIC_NON_ZERO_OR_URI_FIELDS = (
        'abundance_value',
    )

    OPTIONAL_FIELDS = (
        'dwelling',
    )

    def __init__(self):
        self.taxon = None  # URI
        self.abundance_schema = None  # URI
        self.abundance_value = None  # any
        self.activity = None  # URI
        self.determination_method = None  # URI
        self.dataset = None  # URI
        self.biotope = None  # URI
        self.identity = None  # URI, actually the INTERNAL identity (NOT the NDFF identity link)
        self.lifestage = None  # URI
        self.period_start = None  # NDFF Date(Time) str: YYYY-MM-DDThh:mm:ss, YYYY-MM-DD hh:mm:ss, YYYY-MM-DDThh:mm, YYYY-MM-DD hh:mm, YYYY-MM-DDThh, YYYY-MM-DD hh, YYYY-MM-DD, YYYY-MM, YYYY
        self.period_stop = None  # NDFF Date(Time) str: YYYY-MM-DDThh:mm:ss, YYYY-MM-DD hh:mm:ss, YYYY-MM-DDThh:mm, YYYY-MM-DD hh:mm, YYYY-MM-DDThh, YYYY-MM-DD hh, YYYY-MM-DD, YYYY-MM, YYYY
        self.sex = None  # URI
        self.subject_type = None  # URI
        self.survey_method = None  # URI
        self.dwelling = None  # URI OPTIONAL

        self.extra_info = []
        self.involved = []

        # a location should always be a valid location dict, never something else!
        # NOTE: the creation of a valid location dict is the responsibility of the NdffConnector object!
        self.location: dict = Optional[dict]

    def to_ndff_observation_json(self, observation_identity=None, observation_dataset=None) -> str:
        """
        The observation_identity and observation_dataset params make it possible
        to override the real identity/dataset content of the observation, thereby
        making it easier to create unique observation json for testing...

        Create valid JSON for NDFF based on current values of all fields.
        If the param 'observation_identity' is None, the field 'identity'
        should be a valid identity(-uri).
        If the param 'observation_dataset' is None, the field 'dataset' should
        be a valid dataset(-uri). AND should be available/defined at NDFF

        :param: observation_identity (optional)
        :param: observation_dataset (optional)
        :return: NDFF json as string
        """
        # json = '''{"abundanceSchema": "http://ndff-ecogrid.nl/codes/scales/exact_count",
        #     "abundanceValue": 1,
        #     "activity": "http://ndff-ecogrid.nl/codes/domainvalues/observation/activities/calling",
        #     "determinationMethod": "http://ndff-ecogrid.nl/codes/domainvalues/observation/determinationmethods/550",
        #     "extrainfo": [],
        #     "dataset": "http://notatio.nl/dataset/2",
        #     "biotope": "http://ndff-ecogrid.nl/codes/domainvalues/location/biotopes/unknown",
        #     "identity": "http://notatio.nl/waarneming/7500",
        #     "involved": [
        #         {
        #         "involvementType": "http://ndff-ecogrid.nl/codes/involvementtypes/data_owner",
        #         "person": "http://telmee.nl/contacts/persons/1261085"
        #         }
        #     ],
        #     "lifestage": "http://ndff-ecogrid.nl/codes/domainvalues/observation/lifestages/509",
        #     "location": {
        #         "buffer": 5,
        #         "geometry": {
        #             "type": "Point",
        #             "coordinates": [
        #                 408241,
        #                 78648
        #                 ]
        #             }
        #         },
        #     "periodStart": "2014-08-29 01:22:00",
        #     "periodStop": "2014-08-29 01:22:00",
        #     "sex": "http://ndff-ecogrid.nl/codes/domainvalues/observation/sexes/undefined",
        #     "subjectType": "http://ndff-ecogrid.nl/codes/subjecttypes/live/individual",
        #     "surveyMethod": "http://ndff-ecogrid.nl/codes/domainvalues/survey/surveymethods/na",
        #     "taxon": "http://ndff-ecogrid.nl/taxonomy/taxa/pipistrellusnathusii",
        #     "dwelling": "http://ndff-ecogrid.nl/codes/domainvalues/observation/dwellings/unknown",
        #     "extrainfo": [
        #         {
        #           "key": "http://ndff-ecogrid.nl/codes/keys/external/location_id",
        #           "value": "NL09_G6140002"
        #         },
        #         {
        #           "key": "http://ndff-ecogrid.nl/codes/keys/external/original_visit_id",
        #           "value": "NL09_G6140002_2014_7_21_VB1_1"
        #         }
        #     ]
        #
        # }

        if not self.is_valid()[0]:
            raise ValueError(self.is_valid()[1])

        data = {}

        if observation_identity:
            data['identity'] = observation_identity
        else:
            data['identity'] = self.identity
        if observation_dataset:
            data['dataset'] = observation_dataset
        else:
            data['dataset'] = self.dataset
        data['abundanceSchema'] = self.abundance_schema
        data['abundanceValue'] = self.abundance_value
        data['activity'] = self.activity
        data['determinationMethod'] = self.determination_method
        # dataset ^
        data['biotope'] = self.biotope
        # identity ^
        data['lifestage'] = self.lifestage
        data['sex'] = self.sex
        data['subjectType'] = self.subject_type
        data['surveyMethod'] = self.survey_method
        data['taxon'] = self.taxon
        # periods could be datetime objects (in case of Postgis Datasource), that is the reason we cast to str
        #data['periodStart']             = str(self.period_start)
        #data['periodStop']              = str(self.period_stop)
        # some time strings contain decimal seconds, API does not want that, that is the reason we format as below
        # https://ndff.zendesk.com/hc/nl/requests/41288
        data['periodStart'] = (self.is_valid_datetime_string('period_start')[0]).strftime('%Y-%m-%dT%H:%M:%S')
        data['periodStop'] = (self.is_valid_datetime_string('period_stop')[0]).strftime('%Y-%m-%dT%H:%M:%S')
        data['location'] = self.location
        # dwelling is an OPTIONAL key, not to be sent if unknown or empty
        # BUT also the value like: http://ndff-ecogrid.nl/codes/domainvalues/observation/dwellings/unknown
        # or
        #  http://ndff-ecogrid.nl/codes/domainvalues/observation/dwellings/na
        # is NOT going to be sent
        if self.dwelling and str(self.dwelling).lower() not in ('none', '', '-',
                                                                'http://ndff-ecogrid.nl/codes/domainvalues/observation/dwellings/unknown',
                                                                'http://ndff-ecogrid.nl/codes/domainvalues/observation/dwellings/na'):
            # dwelling is not a separate field anymore: # data['dwelling']= self.dwelling
            self.extra_info.append({'key': 'http://ndff-ecogrid.nl/codes/keys/observation/dwellings', 'value': self.dwelling})

        # note: this is the data holder for the lists in the json (NOT fields from record or observation)
        data['extrainfo'] = self.extra_info
        data['involved'] = self.involved

        return json.dumps(data, indent=2)


class NdffResult(dict):
    """
    A NdffResult is ...
    """

    # CREATE TABLE public.ndff_log
    # (
    #   id serial,
    #   object_type character varying,
    #   object_id integer,  -- identity uri
    #   ndff_uri character varying,
    #   http_method character varying,
    #   http_status character varying,
    #   http_response character varying,
    #   tstamp timestamp with time zone
    # )

    def __init__(self,
                 waarneming_id,
                 object_type,
                 object_id,
                 ndff_uri,
                 http_method,
                 http_status,
                 http_response,
                 id=None,
                 tstamp=None,
                 related_uri=None):
        super().__init__()
        #log.debug('NdffResult init')
        # temporary?
        # remove query part from nddf_uri
        # after searching the uri's contained ?format=javascript
        o = urlparse(ndff_uri)
        ndff_uri = o.scheme + "://" + o.netloc + o.path
        self['waarneming_id'] = waarneming_id
        self['id'] = id
        self['object_type'] = object_type
        self['object_id'] = object_id
        self['ndff_uri'] = ndff_uri
        self['http_method'] = http_method
        self['http_status'] = http_status
        #self['http_response'] = json.dumps(http_response)
        # 20220422 NOT returning a string representation of the http response, but the response as we got it after 'handling'
        # it IS very much possible that this is now a dict or a json object...
        self['http_response'] = http_response
        self['tstamp'] = tstamp
        # 20221211 RD: ALSO add the 'related_uri' uri to the output IF it is there
        self['related_uri'] = related_uri

    @staticmethod
    def quote_if_needed(value):
        ret = value
        if ',' in value:
            # create quoted value
            ret = f'"{value}"'
        return ret

    def as_tuple(self):
        if self['tstamp'] is None:
            self['tstamp'] = datetime.now().isoformat(timespec='seconds')
        return (self['tstamp'],
                self['object_type'],
                self['http_method'],
                self['http_status'],
                self['object_id'],
                self['ndff_uri'],
                self['related_uri'],
                self.quote_if_needed(self['http_response'])
                )
