"""Serializers for the employment app."""

from datetime import date, timedelta

from django.contrib.auth import get_user_model
from django.db.models import Max, Value
from django.db.models.functions import Coalesce
from django.utils.duration import duration_string
from django.utils.translation import ugettext_lazy as _
from rest_framework_json_api import relations
from rest_framework_json_api.serializers import (
    ModelSerializer,
    Serializer,
    SerializerMethodField,
    ValidationError,
)

from timed.employment import models
from timed.tracking.models import Absence, Report


class UserSerializer(ModelSerializer):

    included_serializers = {
        "supervisors": "timed.employment.serializers.UserSerializer",
        "supervisees": "timed.employment.serializers.UserSerializer",
    }

    class Meta:
        """Meta information for the user serializer."""

        model = get_user_model()
        fields = [
            "email",
            "first_name",
            "is_active",
            "is_staff",
            "is_superuser",
            "last_name",
            "supervisees",
            "supervisors",
            "tour_done",
            "username",
            "is_reviewer",
        ]
        read_only_fields = [
            "first_name",
            "is_active",
            "is_staff",
            "is_superuser",
            "last_name",
            "supervisees",
            "supervisors",
            "username",
            "is_reviewer",
        ]


class WorktimeBalanceSerializer(Serializer):
    date = SerializerMethodField()
    balance = SerializerMethodField()
    user = relations.ResourceRelatedField(
        model=get_user_model(), read_only=True, source="id"
    )

    def get_date(self, instance):
        user = instance.id
        today = date.today()

        if instance.date is not None:
            return instance.date

        # calculate last reported day if no specific date is set
        max_absence_date = Absence.objects.filter(user=user, date__lt=today).aggregate(
            date=Max("date")
        )
        max_report_date = Report.objects.filter(user=user, date__lt=today).aggregate(
            date=Max("date")
        )

        last_reported_date = max(
            max_absence_date["date"] or date.min, max_report_date["date"] or date.min
        )

        instance.date = last_reported_date
        return instance.date

    def get_balance(self, instance):
        balance_date = self.get_date(instance)
        start = date(balance_date.year, 1, 1)

        # id is mapped to user instance
        _, _, balance = instance.id.calculate_worktime(start, balance_date)
        return duration_string(balance)

    included_serializers = {"user": "timed.employment.serializers.UserSerializer"}

    class Meta:
        resource_name = "worktime-balances"


class AbsenceBalanceSerializer(Serializer):
    credit = SerializerMethodField()
    used_days = SerializerMethodField()
    used_duration = SerializerMethodField()
    balance = SerializerMethodField()

    user = relations.ResourceRelatedField(model=get_user_model(), read_only=True)

    absence_type = relations.ResourceRelatedField(
        model=models.AbsenceType, read_only=True, source="id"
    )

    absence_credits = relations.SerializerMethodResourceRelatedField(
        source="get_absence_credits",
        model=models.AbsenceCredit,
        many=True,
        read_only=True,
    )

    def _get_start(self, instance):
        return date(instance.date.year, 1, 1)

    def get_credit(self, instance):
        """
        Calculate how many days are approved for given absence type.

        For absence types which fill worktime this will be None.
        """
        if "credit" in instance:
            return instance["credit"]

        # id is mapped to absence type
        absence_type = instance.id

        start = self._get_start(instance)

        # avoid multiple calculations as get_balance needs it as well
        instance["credit"] = absence_type.calculate_credit(
            instance.user, start, instance.date
        )
        return instance["credit"]

    def get_used_days(self, instance):
        """
        Calculate how many days are used of given absence type.

        For absence types which fill worktime this will be None.
        """
        if "used_days" in instance:
            return instance["used_days"]

        # id is mapped to absence type
        absence_type = instance.id

        start = self._get_start(instance)

        # avoid multiple calculations as get_balance needs it as well
        instance["used_days"] = absence_type.calculate_used_days(
            instance.user, start, instance.date
        )
        return instance["used_days"]

    def get_used_duration(self, instance):
        """
        Calculate duration of absence type.

        For absence types which fill worktime this will be None.
        """
        # id is mapped to absence type
        absence_type = instance.id
        if not absence_type.fill_worktime:
            return None

        start = self._get_start(instance)
        absences = sum(
            [
                absence.calculate_duration(
                    models.Employment.objects.get_at(instance.user, absence.date)
                )
                for absence in Absence.objects.filter(
                    user=instance.user,
                    date__range=[start, instance.date],
                    type_id=instance.id,
                ).select_related("type")
            ],
            timedelta(),
        )
        return duration_string(absences)

    def get_absence_credits(self, instance):
        """Get the absence credits for the user and type."""
        if "absence_credits" in instance:
            return instance["absence_credits"]

        # id is mapped to absence type
        absence_type = instance.id

        start = self._get_start(instance)
        absence_credits = models.AbsenceCredit.objects.filter(
            absence_type=absence_type,
            user=instance.user,
            date__range=[start, instance.date],
        ).select_related("user")

        # avoid multiple calculations when absence credits need to be included
        instance["absence_credits"] = absence_credits

        return absence_credits

    def get_balance(self, instance):
        # id is mapped to absence type
        absence_type = instance.id
        if absence_type.fill_worktime:
            return None

        return self.get_credit(instance) - self.get_used_days(instance)

    included_serializers = {
        "absence_type": "timed.employment.serializers.AbsenceTypeSerializer",
        "absence_credits": "timed.employment.serializers.AbsenceCreditSerializer",
    }

    class Meta:
        resource_name = "absence-balances"


class EmploymentSerializer(ModelSerializer):
    included_serializers = {
        "user": "timed.employment.serializers.UserSerializer",
        "location": "timed.employment.serializers.LocationSerializer",
    }

    def validate(self, data):
        """Validate the employment as a whole.

        Ensure the end date is after the start date and there is only one
        active employment per user and there are no overlapping employments.

        :throws: django.core.exceptions.ValidationError
        :return: validated data
        :rtype:  dict
        """
        instance = self.instance
        start_date = data.get("start_date", instance and instance.start_date)
        end_date = data.get("end_date", instance and instance.end_date)
        if end_date and start_date >= end_date:
            raise ValidationError(_("The end date must be after the start date"))

        user = data.get("user", instance and instance.user)
        employments = models.Employment.objects.filter(user=user)
        # end date not set means employment is ending today
        end_date = end_date or date.today()
        employments = employments.annotate(
            end=Coalesce("end_date", Value(date.today()))
        )
        if instance:
            employments = employments.exclude(id=instance.id)

        if any([e.start_date <= end_date and start_date <= e.end for e in employments]):
            raise ValidationError(
                _("A user can't have multiple employments at the same time")
            )

        return data

    class Meta:
        model = models.Employment
        fields = [
            "user",
            "location",
            "percentage",
            "worktime_per_day",
            "start_date",
            "end_date",
        ]


class LocationSerializer(ModelSerializer):
    """Location serializer."""

    class Meta:
        """Meta information for the location serializer."""

        model = models.Location
        fields = ["name", "workdays"]


class PublicHolidaySerializer(ModelSerializer):
    """Public holiday serializer."""

    location = relations.ResourceRelatedField(read_only=True)

    included_serializers = {
        "location": "timed.employment.serializers.LocationSerializer"
    }

    class Meta:
        """Meta information for the public holiday serializer."""

        model = models.PublicHoliday
        fields = ["name", "date", "location"]


class AbsenceTypeSerializer(ModelSerializer):
    """Absence type serializer."""

    class Meta:
        """Meta information for the absence type serializer."""

        model = models.AbsenceType
        fields = ["name", "fill_worktime"]


class AbsenceCreditSerializer(ModelSerializer):
    """Absence credit serializer."""

    included_serializers = {
        "absence_type": "timed.employment.serializers.AbsenceTypeSerializer"
    }

    class Meta:
        """Meta information for the absence credit serializer."""

        model = models.AbsenceCredit
        fields = ["user", "absence_type", "date", "days", "comment", "transfer"]


class OvertimeCreditSerializer(ModelSerializer):
    class Meta:
        model = models.OvertimeCredit
        fields = ["user", "date", "duration", "comment", "transfer"]