import os
from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from .attributes import BaseAttribute, GenericFieldAttribute, \
    TagFieldAttribute, TimestampFieldAttribute
from .db.query import Query, BulkInsertQuery
from .response import InfluxDBResponse
from .serializers import MeasurementPointSerializer
from .exceptions import InfluxDBAttributeValueError

EXTENDED_ATTRIBUTE_PREFIX_NAME = '__attribute__'

TEMPLATE_FILE_NAME = 'simple_measurement.py.jinja'


class MeasurementMeta(type):
    def __init__(cls, name, *args, **kwargs):
        super(MeasurementMeta, cls).__init__(name, *args, **kwargs)
        attribute_names = cls._get_attribute_names()
        cls._extend_attributes(attribute_names)

        get_query = cls._factory_get_query()
        setattr(cls, 'get_query', get_query)

    def __call__(cls, *args, **kwargs):
        setattr(cls, '_get_attributes', cls._get_attributes)
        instance = type.__call__(cls, *args, **kwargs)
        return instance

    def _factory_get_query(cls):
        def get_query():
            class MeasurementQuery(Query):
                def __init__(self):
                    super(MeasurementQuery, self).__init__()

                def format(self, result, parser_class=cls.parser_class):
                    return parser_class(result, cls).convert()

                def evaluate(self, parser_class=cls.parser_class):
                    result = InfluxDBResponse(self.execute())
                    result.raise_if_error()
                    formatted_result = self.format(result, parser_class)
                    return formatted_result
            return MeasurementQuery().from_measurements(cls.measurement_name)
        return get_query

    def _get_attribute_names(cls):
        def filter_func(x):
            return isinstance(variables[x], BaseAttribute)
        variables = cls.__dict__
        attribute_names = list(filter(filter_func, variables))
        return attribute_names

    def _get_attributes(cls):
        def filter_func(x):
            return isinstance(x, BaseAttribute)
        variables = cls.__dict__.values()
        attributes = list(filter(filter_func, variables))
        return attributes

    def _get_timestamp_attributes(cls):
        def filter_func(x):
            return isinstance(x, TimestampFieldAttribute)
        attributes = cls._get_attributes()
        timestamp_attributes = list(filter(filter_func, attributes))
        return timestamp_attributes

    def _extend_attributes(cls, attribute_names):
        def generate_getter_and_setter(attr_name):
            def getx(self):
                attribute_field = getattr(self, attr_name)
                return attribute_field.get_internal_value()

            def setx(self, value):
                attribute_field = getattr(self, attr_name)
                attribute_field.set_internal_value(value)
            return getx, setx

        for attribute_name in attribute_names:
            ext_attribute_name = EXTENDED_ATTRIBUTE_PREFIX_NAME + attribute_name
            attribute_field = getattr(cls, attribute_name)
            attribute_field.attribute_name = attribute_name
            attribute_field.ext_attribute_name = ext_attribute_name

            getx, setx = generate_getter_and_setter(ext_attribute_name)
            prop = property(getx, setx)
            setattr(cls, ext_attribute_name, attribute_field)
            setattr(cls, attribute_name, prop)


class Measurement(object, metaclass=MeasurementMeta):
    parser_class = MeasurementPointSerializer
    measurement_name = 'default'

    def __init__(self, **kwargs):
        self.check_attribute_values(**kwargs)
        self.clone_attributes()
        self.fill_values(**kwargs)

    def check_attribute_values(self, **kwargs):
        def filter_required_attributes(x):
            return not x.default and not x.is_nullable

        attributes = self._get_attributes()
        required_attributes = list(filter(
            filter_required_attributes,
            attributes,
        ))
        required_attributes_names = [
            r.attribute_name
            for r in required_attributes
        ]
        for key in required_attributes_names:
            if key not in kwargs:
                raise InfluxDBAttributeValueError(
                    'The attribute \'{}\' cannot be nullable'.format(key)
                )

    def clone_attributes(self):
        attributes = self._get_attributes()
        for attr in attributes:
            cloned_attribute = attr.clone()
            cloned_attribute.attribute_name = attr.attribute_name
            cloned_attribute.ext_attribute_name = attr.ext_attribute_name
            setattr(self, attr.ext_attribute_name, cloned_attribute)

    def dict(self):
        dict_values = {}
        attributes = self.get_attributes()
        for attr in attributes:
            dict_values[attr.attribute_name] = getattr(self, attr.attribute_name)
        return dict_values

    def get_attributes(self):
        def filter_func(x):
            return isinstance(x, BaseAttribute)
        variables = self.__dict__.values()
        attributes = list(filter(filter_func, variables))
        return attributes

    def get_attribute_names(self):
        attributes = self.get_attributes()
        attribute_names = [attr.attribute_name for attr in attributes]
        return attribute_names

    def get_ext_attribute_names(self):
        attributes = self.get_attributes()
        attribute_names = [attr.ext_attribute_name for attr in attributes]
        return attribute_names

    def get_timestamp_attributes(self):
        def filter_func(x):
            return isinstance(x, TimestampFieldAttribute)
        attributes = self.get_attributes()
        timestamp_attributes = list(filter(filter_func, attributes))
        return timestamp_attributes

    def get_prep_value(self):
        def factory_filter_func(cls):
            def filter_func(attr):
                return isinstance(attr, cls)
            return filter_func

        attributes = self.get_attributes()
        field_attributes = list(filter(
            factory_filter_func(GenericFieldAttribute),
            attributes,
        ))
        tag_attributes = list(filter(
            factory_filter_func(TagFieldAttribute),
            attributes,
        ))
        timestamp_attributes = list(filter(
            factory_filter_func(TimestampFieldAttribute),
            attributes,
        ))

        prep_value_groups = []
        attributes_groups = [tag_attributes, field_attributes, timestamp_attributes]
        for attr_group in attributes_groups:
            prep_value_group = []
            for attr in attr_group:
                if attr.raw_value is not None:
                    attr_prep_value = attr.get_prep_value()
                    attr_name = attr.name or attr.attribute_name
                    if not isinstance(attr, TimestampFieldAttribute):
                        prep_value = '{}={}'.format(attr_name, attr_prep_value)
                    else:
                        prep_value = '{}'.format(attr_prep_value)
                    prep_value_group.append(prep_value)
            str_prep_value_group = ','.join(prep_value_group)
            prep_value_groups.append(str_prep_value_group)

        if prep_value_groups[0]:
            prep_value_groups[0] = ','.join(
                [self.measurement_name] + [prep_value_groups[0]]
            )
        else:
            prep_value_groups[0] = self.measurement_name
        final_prep_value = ' '.join(prep_value_groups)
        return final_prep_value

    def fill_values(self, **kwargs):
        try:
            for key, value in kwargs.items():
                setattr(self, key, value)
        except Exception as err:
            print('key', key)
            msg = '<\'{key}\'> : {msg}'.format(key=key, msg=err)
            raise InfluxDBAttributeValueError(msg)

    def items(self):
        return self.dict().items()

    @staticmethod
    def bulk_save(points):
        if not isinstance(points, list):
            raise InfluxDBAttributeValueError('points must be a list')
        str_points = ''
        for point in points:
            if not isinstance(point, Measurement):
                raise InfluxDBAttributeValueError(
                    'type of point must be Measurement'
                )
            prep_value = point.get_prep_value()
            str_points += prep_value
            str_points += '\n'
        return BulkInsertQuery(str_points).execute()


def SimpleMeasurement(measurement_name, field_names, tag_names=[]):
    current_dir_path = os.path.dirname(os.path.realpath(__file__))
    template_folder_path = Path(current_dir_path) / './templates/'
    template_folder_path = template_folder_path.resolve().as_posix()

    j2_env = Environment(
        loader=FileSystemLoader(template_folder_path),
        trim_blocks=True,
    )
    rendered_template = j2_env\
        .get_template(TEMPLATE_FILE_NAME)\
        .render(
            measurement_name=measurement_name,
            field_names=field_names,
            tag_names=tag_names,
        )

    exec(rendered_template)
    anonymous_measurement = locals().get('AnonymousMeasurement')
    return anonymous_measurement
