"""
Serializers for Units module
"""

import logging
from datetime import date, datetime

from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers

from djangophysics.core.serializers import UserSerializer
from .models import Quantity, UnitConversionPayload, Dimension, \
    CustomUnit, Unit


class QuantitySerializer(serializers.Serializer):
    """
    Serialize a Quantity
    """
    system = serializers.CharField(label="Unit system of the Quantity")
    unit = serializers.CharField(label="Units of the quantity")
    value = serializers.FloatField(label="Magnitude of the quantity")
    date_obj = serializers.DateField(label="date of conversion")

    @staticmethod
    def validate_system(system):
        """
        Validate unit system by name
        :param system: system name
        """
        from .models import UnitSystem
        if not UnitSystem.is_valid(system):
            raise serializers.ValidationError('Invalid system')
        return system

    @staticmethod
    def validate_unit(unit):
        """
        Validating unit requires knowledge of the system
        that might embark user information for
        custom units
        Let‘s say it is valid for now
        """
        return unit

    @staticmethod
    def validate_date_obj(value):
        """
        Validate date
        :param value:
        """
        if isinstance(value, date):
            return value
        try:
            datetime.strptime(value, 'YYYY-MM-DD')
        except ValueError:
            raise serializers.ValidationError(
                'Invalid date format, use YYYY-MM-DD')
        return value

    def create(self, validated_data):
        """
        Create a Quantity object
        :param validated_data: cleaned data
        """
        return Quantity(**validated_data)

    def update(self, instance, validated_data):
        """
        Update a Quantity object
        :param instance: Quantity object
        :param validated_data: cleaned data
        """
        instance.system = validated_data.get('system', instance.system)
        instance.unit = validated_data.get('unit', instance.unit)
        instance.value = validated_data.get('value', instance.value)
        instance.date_obj = validated_data.get('date_obj', instance.date_obj)
        return instance


class UnitSerializer(serializers.Serializer):
    """
    Unit class serializer
    """
    code = serializers.CharField(label="Technical name of the unit")
    name = serializers.CharField(label="Human readable name of the unit")
    dimensions = serializers.SerializerMethodField(
        label="Dimensions the unit belongs to")

    @swagger_serializer_method(serializer_or_field=serializers.ListField)
    def get_dimensions(self, obj):
        """
        Get dimension of unit
        :param obj: Unit instance
        """
        return str(obj.dimensions)


class DimensionSerializer(serializers.Serializer):
    """
    Dimension class serializer
    """
    code = serializers.CharField(
        label="technical name of the unit (enclosed in brackets)")
    name = serializers.CharField(
        label="Human readable name of the unit")
    dimension = serializers.CharField(
        label="Mathematical expression of the dimension")
    base_unit = serializers.SerializerMethodField(
        label="Name of the base unit")

    def create(self, validated_data):
        """
        Create a Dimension object from cleaned data
        :param validated_data: cleaned data
        """
        return Dimension(**validated_data)

    def update(self, instance, validated_data):
        """
        Update a Dimension object
        :param instance: Dimension object
        :param validated_data: cleaned data
        """
        instance.unit_system = validated_data.get(
            'system',
            instance.unit_system)
        instance.code = validated_data.get('code', instance.code)
        instance.value = validated_data.get('name', instance.name)
        instance.dimension = validated_data.get('date_obj', instance.dimension)
        return instance

    @swagger_serializer_method(serializer_or_field=serializers.CharField)
    def get_base_unit(self, obj: Dimension) -> Unit:
        """
        Get base unit for dimension object
        :param obj: Dimension object
        """
        return obj.base_unit


class DimensionWithUnitsSerializer(DimensionSerializer):
    """
    Serialize a Dimension including associated units
    """
    units = UnitSerializer(label="Units of this dimension", many=True)

    @swagger_serializer_method(serializer_or_field=UnitSerializer)
    def get_units(self, obj: Dimension) -> [Unit]:
        """
        Get units for this dimension
        """
        try:
            return obj.units()
        except KeyError as e:
            logging.error(str(e))
            return []


class UnitSystemSerializer(serializers.Serializer):
    """
    Serialize a UnitSystem
    """
    system_name = serializers.CharField()


class UnitConversionPayloadSerializer(serializers.Serializer):
    """
    Serialize a UnitConversionPayload
    """
    data = QuantitySerializer(
        label="List of unit conversions",
        many=True,
        required=False)
    base_system = serializers.CharField(
        label="Unit system used for conversion",
        required=True)
    base_unit = serializers.CharField(
        label="Unit to express the result in",
        required=True)
    batch_id = serializers.CharField(
        label="User defined batch ID",
        required=False)
    key = serializers.CharField(
        label="User defined categorization key",
        required=False)
    eob = serializers.BooleanField(
        label="End of batch ? triggers the conversion",
        default=False)
    _errors = {}

    def is_valid(self, raise_exception=False) -> bool:
        """
        Check validity of a converson payload
        """
        super().is_valid(raise_exception=raise_exception)
        if not self.initial_data.get('data') and \
                (not self.initial_data.get('batch_id')
                 or (self.initial_data.get('batch_id')
                     and not self.initial_data.get('eob'))):
            self._errors = {
                'data': 'data has to be provided if batch_id is not provided'
                        'or batch_id is provided and eob is False'
            }

        if self._errors and raise_exception:
            raise serializers.ValidationError(self.errors)
        return not bool(self._errors)

    @staticmethod
    def validate_base_system(value: str):
        """
        Validate unit system
        :param value: unit system name
        """
        from djangophysics.units.models import UnitSystem
        if not UnitSystem.is_valid(value):
            raise serializers.ValidationError('Invalid unit system')
        return value

    @staticmethod
    def validate_base_unit(value: str) -> str:
        """
        Validate base unit
        :param value: name of unit
        """
        from djangophysics.units.models import Unit
        if not Unit.is_valid(value):
            raise serializers.ValidationError('Invalid unit')
        return value

    def create(self, validated_data: {}) -> UnitConversionPayload:
        """
        Create a UnitConversionPayload
        :param validated_data: cleaned data
        """
        return UnitConversionPayload(**validated_data)

    def update(self, instance, validated_data: {}) -> UnitConversionPayload:
        """
        Update a UnitConversionPayload
        :param instance: UnitConversionPayload
        :param validated_data: cleaned data
        """
        self.data = validated_data.get('data', instance.data)
        self.base_system = validated_data.get(
            'base_system',
            instance.base_system)
        self.base_unit = validated_data.get('base_system', instance.base_unit)
        self.batch_id = validated_data.get('batch_id', instance.batch_id)
        self.key = validated_data.get('key', instance.key)
        self.eob = validated_data.get('eob', instance.eob)
        return instance


class CustomUnitSerializer(serializers.ModelSerializer):
    """
    Serialize a custom unit
    """
    id = serializers.CharField(
        label="ID of the CustomUnit",
        read_only=True)
    user = UserSerializer(
        label="Owner of the unit",
        read_only=True)
    unit_system = serializers.CharField(
        label="Unit system of the unit",
        read_only=True)

    class Meta:
        """
        Meta
        """
        model = CustomUnit
        fields = [
            'id',
            'user',
            'key',
            'unit_system',
            'code',
            'name',
            'relation',
            'symbol',
            'alias']
