#  This work is based on original code developed and copyrighted by TNO 2023.
#  Subsequent contributions are licensed to you by the developers of such code and are
#  made available to the Project under one or several contributor license agreements.
#
#  This work is licensed to you under the Apache License, Version 2.0.
#  You may obtain a copy of the license at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
#  Contributors:
#      TNO         - Initial implementation
#  Manager:
#      TNO
from dataclasses import dataclass
from datetime import datetime
from uuid import uuid4

import pytz
from pyecore.ecore import EDate

import esdl
from esdl.profiles.profilemanager import (
    ProfileManager,
    UnknownProfileNameException,
    ProfileType,
)
from influxdb import InfluxDBClient

from esdl.utils.datetime_utils import parse_date


@dataclass
class ConnectionSettings:
    """
    The ConnectionSettings class represents all required settings that are required to connect to an
    InfluxDB server
    """

    host: str
    """hostname of the InfluxDB server to connect to"""
    port: int
    """port to connect to"""
    username: str
    """username of the user that has permission to access the database"""
    password: str
    """password of the user"""
    database: str
    """database to connect to"""
    ssl: bool
    """indicates whether or not to use SSL when connecting to the InfluxDB server"""
    verify_ssl: bool
    """indicates wether or not to verify SSL certificates"""


class InfluxDBProfileManager(ProfileManager):
    """
    InfluxDBProfileManager: manages profile data that can be loaded from and saved to InfluxDB (v1 only).
    InfluxDBProfileManager is a subclass of ProfileManager, so it also provides functionality to convert from/to
    different ESDL profiles and to load/save to CSV
    """

    def __init__(self, settings: ConnectionSettings, source_profile=None):
        """
        Constructor of the InfluxDBProfileManager

        :param settings: connection settings to connect to the InfluxDB server
        :param source_profile: the source profile of which the data is copied into this profiles manager instance
        """
        super().__init__()

        # Temporary fix, for ESDL profiles that have HTTP(S) in the hostname
        settings.host = settings.host.replace("http://", "").replace("https://", "")

        self.database_settings = settings
        self.influxdb_client = InfluxDBClient(
            host=settings.host,
            port=settings.port,
            username=settings.username,
            password=settings.password,
            database=settings.database,
            ssl=settings.ssl,
            verify_ssl=settings.verify_ssl,
        )

        if settings.database not in [
            db["name"] for db in self.influxdb_client.get_list_database()
        ]:
            self.influxdb_client.create_database(settings.database)

        if source_profile:
            self.convert(source_profile)
            if isinstance(source_profile, InfluxDBProfileManager):
                self.database_settings = source_profile.database_settings

    def load_influxdb(
        self,
        measurement: str,
        fields: list,
        from_datetime: datetime = None,
        to_datetime: datetime = None,
        filters: list = None,
    ):
        """
        Loads profile information from InfluxDB

        :param measurement: the name of the measurement to use in InfluxDB
        :param fields: a list of field names that need to be loaded from InfluxDB
        :param from_datetime: the start datetime (included in the data)
        :param to_datetime: the end datetime (included in the data)
        :param filters: a list of dictionaries with 'tag' and 'value' keys, that can be used to filter on the data
        :return: None
        """
        self.clear_profile()

        where_clause_list = list()
        if from_datetime and to_datetime:
            influxdb_startdate = from_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")
            influxdb_enddate = to_datetime.strftime("%Y-%m-%dT%H:%M:%SZ")

            where_clause_list.append("time >= '" + influxdb_startdate + "'")
            where_clause_list.append("time <= '" + influxdb_enddate + "'")

        if filters:
            for filter in filters:
                where_clause_list.append(f"{filter['tag']}='{filter['value']}'")

        fields_string = ",".join(['"' + field + '"' for field in fields])
        query_string = f'SELECT {fields_string} FROM "{measurement}"'
        if where_clause_list:
            query_string += " WHERE (" + " AND ".join(where_clause_list) + ")"

        res = self.influxdb_client.query(query_string)

        if res:
            res_list = res.get_points()
            for elem in res_list:
                dt = parse_date(elem["time"])

                try:
                    aware_dt = pytz.utc.localize(
                        dt
                    )  # Assume timezone is UTC if no TZ was given
                except (
                    ValueError
                ):  # ValueError: No naive datetime (tzinfo is already set)
                    aware_dt = dt

                row = [aware_dt]
                if self.start_datetime is None:
                    self.start_datetime = aware_dt

                for f in fields:
                    row.append(elem[f])

                self.profile_data_list.append(row)
                self.num_profile_items += 1

            self.end_datetime = self.profile_data_list[-1][0]
            header = ["datetime"]
            header.extend(fields)
            self.profile_header = header
            self.profile_type = ProfileType.DATETIME_LIST

            tags = None
            if filters:
                tags = dict()
                for filter in filters:
                    tags[filter["tag"]] = filter["value"]
            return self.get_esdl_influxdb_profile(measurement, fields, tags)
        else:
            return None

    @staticmethod
    def _parse_esdl_profile_filters(esdl_filters):
        if esdl_filters is None or esdl_filters == "":
            return []

        filter_str_list = esdl_filters.split(" AND ")
        filter_list = list()
        for f in filter_str_list:
            try:
                [key, value] = f.split("=")
                if key is not None and key != "" and value is not None and value != "":
                    filter_list.append({"key": key, "value": value})
                else:
                    raise WrongFilterFormatException(
                        "Filter specification in ESDL profile cannot be parsed"
                    )
            except:
                raise WrongFilterFormatException(
                    "Filter specification in ESDL profile cannot be parsed"
                )

        return filter_list

    @staticmethod
    def create_esdl_influxdb_profile_manager(
        esdl_profile: esdl.InfluxDBProfile,
        username: str = None,
        password: str = None,
        use_ssl: bool = False,
        verify_ssl: bool = False,
    ):
        """
        method to create an instance of InfluxDBProfileManager using an esdl.InfluxDBProfile. The data referenced to in
        the profile is loaded from InfluxDB

        :param esdl_profile: esdl.InfluxDBProfile that is used as an input profile
        :param username: username to be used to connect to the InfluxDB server mentioned in the esdl.InfluxDBProfile,
                         defaults to None
        :param password: password to be used to connect to the InfluxDB server mentioned in the esdl.InfluxDBProfile,
                         defaults to None
        :param use_ssl: indicates if HTTPS should be used instead of HTTP to connect to the InfluxDB server, defaults
                        to False
        :param verify_ssl: verify SSL certificates for HTTPS requests, defaults to False
        """
        conn_settings = ConnectionSettings(
            host=esdl_profile.host,
            port=esdl_profile.port,
            database=esdl_profile.database,
            username=username,
            password=password,
            ssl=use_ssl,
            verify_ssl=verify_ssl,
        )

        prof = InfluxDBProfileManager(conn_settings)
        prof.load_influxdb(
            measurement=esdl_profile.measurement,
            fields=[esdl_profile.field],
            from_datetime=esdl_profile.startDate,
            to_datetime=esdl_profile.endDate,
            filters=InfluxDBProfileManager._parse_esdl_profile_filters(
                esdl_profile.filters
            ),
        )

        return prof

    @staticmethod
    def _create_esdl_filters_from_tags_dict(tags: dict = None):
        if not tags:
            return ""
        else:
            filter_list = list()

            for k, v in tags.items():
                filter_list.append(f"{k}='{v}'")

            return " AND ".join(filter_list)

    def get_esdl_influxdb_profile(
        self, measurement: str, field_names: list, tags: dict = None
    ):
        """
        Creates an esdl.InfluxDBProfile instance (or a list of instances) that refers to the data in the database.
        Is called by load_influxdb and save_influxdb

        :param measurement: name of the InfluxDB measurement where the data must be written to
        :param field_names: list of the fields that should be written to InfluxDB
        :param tags: dictionary with tags and tag values, that should be used when writing this data to InfluxDB
        :return: an esdl.InfluxDBProfile instance or a list of esdl.InfluxDBProfile instances in case multiple fields
                 were specified, with proper references to the data in the database
        """
        profile_list = list()
        for field in field_names:
            esdl_profile = esdl.InfluxDBProfile(
                id=str(uuid4()),
                name=field,
                host=self.database_settings.host,
                port=self.database_settings.port,
                database=self.database_settings.database,
                measurement=measurement,
                field=field,
                filters=InfluxDBProfileManager._create_esdl_filters_from_tags_dict(
                    tags
                ),
                startDate=EDate.from_string(
                    self.start_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
                ),
                endDate=EDate.from_string(
                    self.end_datetime.strftime("%Y-%m-%dT%H:%M:%S.%f%z")
                ),
            )
            profile_list.append(esdl_profile)

        if len(profile_list) == 1:
            return profile_list[0]
        else:
            return profile_list

    def save_influxdb(self, measurement: str, field_names: list, tags: dict = None):
        """
        Saves profile information to InfluxDB

        :param measurement: name of the InfluxDB measurement where the data must be written to
        :param field_names: list of the fields that should be written to InfluxDB
        :param tags: dictionary with tags and tag values, that should be used when writing this data to InfluxDB
        :return: an esdl.InfluxDBProfile instance or a list of esdl.InfluxDBProfile instances in case multiple fields
                 were specified, with proper references to the data in the database
        """
        json_body = []
        if not measurement or measurement == "":
            raise NoDataException(
                "Measurement name is not specified, so no data can be written to InfluxDB"
            )
        if not field_names:
            raise NoDataException(
                "No field names are specified, so no data can be written to InfluxDB"
            )
        if tags and not isinstance(tags, dict):
            raise WrongTagsFormatException(
                "The tags parameter should be a dictionary with tag names as keys"
            )
        for profile_row in self.profile_data_list:
            dt_string = profile_row[0].strftime("%Y-%m-%dT%H:%M:%S%z")
            fields = {}
            for fld_name in field_names:
                if fld_name in self.profile_header:
                    idx = self.profile_header.index(fld_name)
                    fields[self.profile_header[idx]] = profile_row[idx]
                else:
                    raise UnknownProfileNameException(
                        f"Given field name ({fld_name}) is not recognized as a profile name"
                    )

            if fields:
                data = {"measurement": measurement, "time": dt_string, "fields": fields}
                if tags:
                    data["tags"] = tags

                json_body.append(data)

        if json_body:
            self.influxdb_client.write_points(
                points=json_body,
                database=self.database_settings.database,
                batch_size=100,
            )
            return self.get_esdl_influxdb_profile(measurement, field_names, tags)

        else:
            return None


class NoDataException(Exception):
    """Thrown when no profile data can be written to InfluxDB"""

    pass


class WrongFilterFormatException(Exception):
    """Thrown when filter specification in ESDL profile cannot be parsed"""

    pass


class WrongTagsFormatException(Exception):
    """Thrown when the tags parameter has the wrong structure"""

    pass
