import datetime
from collections import defaultdict, Counter

from django.db import transaction
from django.db.models import Prefetch, Q
from rest_framework import serializers, status

from api.helper.activity_sequencing_helper import helper_build_update_sequencing_data, \
    helper_build_update_redis_sequencing_data
from api.models import TtSetting,TtActivity,TtActivitySequencing
from api.models.audit_trail import AuditTrail
from api.models.audit_trail_details import AuditTrailDetails
from api.views.admin.base import AdminApiBase
from api.validator import BaseValidator
from api.utils import log_critical_error, get_exception_detail, get_ip, bulk_sync_to_redis
from rest_framework.exceptions import ValidationError
from api.translation import __
from backend.redis_client import redis_client


class ActivitySequencing(AdminApiBase):
    def validate_request(self,request,settings):
        slot_per_day = int(settings["slot_per_day"])
        minute_per_slot = int(settings["minute_per_slot"])
        slot_per_hour = 60 // minute_per_slot
        slot_per_hour = int(slot_per_hour)
        # wont update module id, just want based on module id get the activity template
        rules = {
            "id": "required|exists:api.TtActivity,id",
        }
        # if need rename field, can put here
        attribute = {
            # "email": __("attr.email"),
        }
        # validate id first, cause need get activity by id, if wrong id direct return error
        validator = BaseValidator(request.data,rules,attribute)
        error = validator.validate()
        if error:
            raise ValidationError(error)

        edit_activity = (
            TtActivity.objects
            .select_related("activity_template", "department", "academic_term", "week_pattern","module", "activity_type", "zone", "availability","start_preference", "usage_preference")
            .filter(id=request.data.get("id"))
            .first()
        )
        rules = {
            "same_time": "nullable|array|exists:api.TtActivity,id",
            "preceding": "nullable|array|exists:api.TtActivity,id",
            "succeeding": "nullable|array|exists:api.TtActivity,id",
            "enable_min": "nullable",
            "min_day": "nullable",
            "min_hour": "nullable",
            "min_minute": "nullable",
            "enable_max": "nullable",
            "max_day": "nullable",
            "max_hour": "nullable",
            "max_minute": "nullable",
        }
        # if need rename field, can put here
        attribute = {
            # "email": __("attr.email"),
        }

        validator = BaseValidator(request.data,rules,attribute)
        error = validator.validate()

        if error:
            raise ValidationError(error)

        same_time = request.data.get("same_time") or []
        preceding = request.data.get("preceding") or []
        succeeding = request.data.get("succeeding") or []
        enable_min = request.data.get("enable_min") or 0
        enable_max = request.data.get("enable_max") or 0

        all_ids = same_time + preceding + succeeding
        counter = Counter(all_ids)

        duplicate_ids = [i for i, c in counter.items() if c > 1]

        if duplicate_ids:
            duplicate_activities = TtActivity.objects.filter(id__in=duplicate_ids)
            activity_name = ", ".join(
                duplicate_activities.values_list("name", flat=True)
            )
            raise ValidationError({
                "error": __('validation.duplicate_sequencing_activity',activity_name=activity_name),
                "errors": {"same_time": __("validation.duplicate_sequencing_activity",activity_name=activity_name)}
            })

        if enable_min and enable_max:
            min_day = request.data.get("min_day") or 0
            min_hour = request.data.get("min_hour") or 0
            min_minute = request.data.get("min_minute") or 0
            min_slot_required = (min_day * slot_per_day) + (min_hour * slot_per_hour) + (min_minute // minute_per_slot)

            max_day = request.data.get("max_day") or 0
            max_hour = request.data.get("max_hour") or 0
            max_minute = request.data.get("max_minute") or 0
            max_slot_required = (max_day * slot_per_day) + (max_hour * slot_per_hour) + (max_minute // minute_per_slot)

            if max_slot_required < min_slot_required:
                raise ValidationError({
                    "error": __('validation.maximum_time_spacing_must_greater_than_minimum'),
                    "errors": {"max_day": __("validation.maximum_time_spacing_must_greater_than_minimum")}
                })

        return edit_activity

    def post(self, request):
        # Validate input
        try:
            setting_params = {
                "minute_per_slot",
                "slot_per_day",
            }
            settings = TtSetting.get_multiple_setting(setting_params)
            minute_per_slot = int(settings["minute_per_slot"])
            slot_per_day = int(settings["slot_per_day"])
            activity = self.validate_request(request,settings)

            old_new_data = {
                "old_data": {"data": {}},
                "new_data": {"data": {}},
            }
            action="activity_sequencing" # this is for audit log only
            activity_id = activity.id
            related_ids = set()
            old_same_time_ids = set()
            old_preceding_ids = set()
            old_succeeding_ids = set()
            new_same_time_ids = set(request.data.get("same_time") or [])
            new_preceding_ids = set(request.data.get("preceding") or [])
            new_succeeding_ids = set(request.data.get("succeeding") or [])
            enable_min = request.data.get("enable_min") or 0
            enable_max = request.data.get("enable_max") or 0

            # only need get activity_id = activity.id, cause this relation always is 2 tow, when add D and A, will add the A and D also
            current_activity_sequencing = TtActivitySequencing.objects.filter(activity_id=activity_id)
            for val in current_activity_sequencing:
                # here is get for old related first, for used to update redis data
                related_ids.add(val.sequencing_activity_id)
                if val.type == TtActivitySequencing.TYPE_TO_CODE["same_time"]:
                    old_same_time_ids.add(val.sequencing_activity_id)
                elif val.type == TtActivitySequencing.TYPE_TO_CODE["preceding"]:
                    old_preceding_ids.add(val.sequencing_activity_id)
                elif val.type == TtActivitySequencing.TYPE_TO_CODE["succeeding"]:
                    old_succeeding_ids.add(val.sequencing_activity_id)

            old_new_data['old_data']['data'].update({
                "activity_id": activity.id,
                "same_time": list(old_same_time_ids),
                "preceding": list(old_preceding_ids),
                "succeeding": list(old_succeeding_ids),
                "min_day": activity.min_day,
                "min_hour": activity.min_hour,
                "min_minute": activity.min_minute,
                "min_slot_required": activity.min_slot_required,
                "max_day": activity.max_day,
                "max_hour": activity.max_hour,
                "max_minute": activity.max_minute,
                "max_slot_required": activity.max_slot_required,
            })
            with transaction.atomic():
                # delete all sequencing relation that include current edit activity
                TtActivitySequencing.objects.filter(
                    Q(activity_id=activity_id) | Q(sequencing_activity_id=activity_id)
                ).delete()

                activity_sequencing_idata = []
                for val in new_same_time_ids:
                    activity_sequencing_idata.extend(helper_build_update_sequencing_data(activity_id,val,"same_time"))
                    related_ids.add(val)

                for val in new_preceding_ids:
                    activity_sequencing_idata.extend(helper_build_update_sequencing_data(activity_id,val,"preceding"))
                    related_ids.add(val)

                for val in new_succeeding_ids:
                    activity_sequencing_idata.extend(helper_build_update_sequencing_data(activity_id,val,"succeeding"))
                    related_ids.add(val)

                slot_per_hour = 60 // minute_per_slot
                slot_per_hour = int(slot_per_hour)
                if enable_min:
                    activity.min_day = request.data.get("min_day") or 0
                    activity.min_hour = request.data.get("min_hour") or 0
                    activity.min_minute = request.data.get("min_minute") or 0
                    activity.min_slot_required = (activity.min_day * slot_per_day) + (activity.min_hour * slot_per_hour) + (activity.min_minute // minute_per_slot)
                else:
                    activity.min_day = None
                    activity.min_hour = None
                    activity.min_minute = None
                    activity.min_slot_required = None
                if enable_max:
                    activity.max_day = request.data.get("max_day") or 0
                    activity.max_hour = request.data.get("max_hour") or 0
                    activity.max_minute = request.data.get("max_minute") or 0
                    activity.max_slot_required = (activity.max_day * slot_per_day) + (activity.max_hour * slot_per_hour) + (activity.max_minute // minute_per_slot)
                else:
                    activity.max_day = None
                    activity.max_hour = None
                    activity.max_minute = None
                    activity.max_slot_required = None
                activity.save()

                if activity_sequencing_idata:
                    TtActivitySequencing.objects.bulk_create(activity_sequencing_idata)

            old_new_data['new_data']['data'].update({
                "activity_id": activity.id,
                "same_time": list(new_same_time_ids),
                "preceding": list(new_preceding_ids),
                "succeeding": list(new_succeeding_ids),
                "min_day": activity.min_day,
                "min_hour": activity.min_hour,
                "min_minute": activity.min_minute,
                "min_slot_required": activity.min_slot_required,
                "max_day": activity.max_day,
                "max_hour": activity.max_hour,
                "max_minute": activity.max_minute,
                "max_slot_required": activity.max_slot_required,
            })

            redis_data = {
                "update": {}
            }
            redis_activity_table = "activity"

            redis_data["update"].setdefault(redis_activity_table, []).append({
                "id": activity.id,
                "sequencing": {
                    "same_time": list(new_same_time_ids),
                    "preceding": list(new_preceding_ids),
                    "succeeding": list(new_succeeding_ids),
                    "min_slot_required": activity.min_slot_required if activity.min_slot_required is not None else -1,
                    "max_slot_required": activity.max_slot_required if activity.max_slot_required is not None else -1,
                }
            })
            # remove current activity from related_ids, prevent double insert in redis
            related_ids.discard(activity_id)
            if related_ids:
                related_activities = TtActivity.objects.filter(id__in=related_ids).prefetch_related("sequencing_from")
                for related_activity in related_activities:
                    redis_data["update"].setdefault(redis_activity_table, []).append(
                        helper_build_update_redis_sequencing_data(related_activity)
                    )

            bulk_sync_to_redis(redis_client, redis_data)

            # insert audit trail and details
            # can insert the parent first, then only insert details
            audit_trail = AuditTrail.objects.create(
                user_id=request.user.id,
                type=self.audit_type,
                ip_address=get_ip(request)
            )

            name = activity.name
            remark_param = {
                "name":name
            }

            # call to a function for insert details, so in future if want change to used worker, can modify in function only
            AuditTrailDetails.custom_insert(
                audit_trail=audit_trail,
                action=action,
                remark_param=remark_param,
                new_data=old_new_data['new_data'],
                old_data=old_new_data['old_data'],
            )

            response = {

            }

            return self.api_response(data=response)
        except ValidationError as e:
            first_message = e.detail['error']
            errors = e.detail['errors']
            return self.api_response(error=first_message,errors=errors,code=status.HTTP_400_BAD_REQUEST)
        except Exception as e:
            e_details = get_exception_detail(e)
            log_critical_error(user_id=None,descr=e_details['descr'],url=e_details['url'],trace=e_details['trace'])
            return self.api_response(error=__("message.internal_server_error"),code=status.HTTP_500_INTERNAL_SERVER_ERROR)