import copy
import datetime
from collections import defaultdict

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

from api.helper.resource_map_helper import helper_recalculate_and_get_redis_resource_map
from api.models import TtSetting, TtWeekPattern, TtAvailability, TtStartPreference, \
    TtUsagePreference, TtActivityTemplate, TtActivity, TtAcademicTerm, TtWeek, TtStaff, TtLocation, TtActivityStaff, \
    TtStudentSet, TtActivityLocation
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 decrypt_aes_128_cbc, encrypt_aes_128_cbc, log_critical_error, get_exception_detail, \
    get_add_new_data, get_ip, get_old_new_data, bulk_sync_to_redis, get_multiple_old_new_data, search_old_new_data, \
    time_to_slot, convert_to_redis_week, check_resource_map_available, \
    get_resource_map_redis_data, check_availability_available
from rest_framework.exceptions import ValidationError
from api.translation import __
from backend.kafka import send_request
from backend.redis_client import redis_client


class BookingSwap(AdminApiBase):
    def validate_request(self,request):
        # wont update module id, just want based on module id get the activity template
        rules = {
            "activity_id": "required",
            "constraint_breaking_mode": "nullable",
        }
        # 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)

        activity = (
            TtActivity.objects
            .select_related("department", "activity_type", "zone")
            .prefetch_related(
                "week",
                "staff",
                "location",
                # tags means the relation add in models, __tag means the relation table tag_id get the tag object #2
            )
            .filter(id=request.data.get("activity_id")).first()
        )
        if not activity.is_booking:
            raise ValidationError({
                "error": __("validation.invalid", field=__("attr.activity")),
                "errors": {"activity_od": __("validation.invalid", field=__("attr.activity"))}
            })

        if not activity.scheduled:
            raise ValidationError({
                "error": __("validation.not_available_swap_for_non_scheduled_activity"),
                "errors": {"activity_od": __("validation.not_available_swap_for_non_scheduled_activity")}
            })

        rules = {
            # "day": "nullable",
            # "time": "nullable",
            "staff_ids": "nullable|array|exists:api.TtStaff,id",
            "location_ids": "nullable|array|exists:api.TtLocation,id",
        }

        # if need rename field, can put here
        attribute = {
            # "email": __("attr.email"),
        }

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

        # custom error after basic validation

        if error:
            raise ValidationError(error)

        return activity


    def post(self, request):
        # Validate input
        try:
            activity = self.validate_request(request)
            setting_params = {
                "minute_per_slot",
                "slot_per_day",
                "slot_per_week"
            }
            settings = TtSetting.get_multiple_setting(setting_params)
            minute_per_slot = int(settings["minute_per_slot"])
            slot_per_day = int(settings["slot_per_day"])
            slot_per_week = int(settings["slot_per_week"])
            activity_id = request.data.get("activity_id")
            action = "booking_swap"  # this is for audit log only
            constraint_breaking_mode = request.data.get("constraint_breaking_mode")
            staff_with_affected_week = {}
            location_with_affected_week = {}
            staff_ids = set()
            location_ids = set()
            old_new_data = {
                "old_data": {},
                "new_data": {},
            }
            old_new_data["old_data"].setdefault(activity_id, {})
            old_new_data["new_data"].setdefault(activity_id, {})

            # if not allow schedule based on new requirement, will direct return error
            if not constraint_breaking_mode:
                self.validate_slot_available(request,activity,slot_per_day)

            activity_week_ids = list(aw.id for aw in activity.week.all())

            # if staff or location changed, remove old staff/location and add new staff/location
            if "staff_ids" in request.data:
                current_staff_ids = list(activity_staff.id for activity_staff in activity.staff.all())
                input_staff_ids = request.data.get("staff_ids")
                input_staff_ids_safe = input_staff_ids if input_staff_ids else []
                # if new staff not same as old staff, remove old and add new
                if set(current_staff_ids) != set(input_staff_ids_safe):
                    old_new_data["old_data"][activity_id]["staff"] = sorted(current_staff_ids)
                    
                    if current_staff_ids:
                        staff_ids.update(current_staff_ids)
                        for current_staff_id in current_staff_ids:
                            staff_with_affected_week.setdefault(current_staff_id, set()).update(activity_week_ids)

                    if input_staff_ids:
                        for s_id in input_staff_ids:
                            staff_with_affected_week.setdefault(s_id, set()).update(activity_week_ids)
                            staff_ids.add(s_id)
                    # this action will do delete old staff from this activity and add new staff in this activity relation
                    activity.staff.set(list(input_staff_ids))
                    old_new_data["new_data"][activity_id]["staff"] = sorted(list(activity_staff.id for activity_staff in activity.staff.all()))

            if "location_ids" in request.data:
                current_location_ids = list(activity_location.id for activity_location in activity.location.all())
                input_location_ids = request.data.get("location_ids")
                input_location_ids_safe = input_location_ids if input_location_ids else []
                # if new location not same as old location, remove old and add new
                if set(current_location_ids) != set(input_location_ids_safe):
                    old_new_data["old_data"][activity_id]["location"] = sorted(current_location_ids)

                    if current_location_ids:
                        location_ids.update(current_location_ids)
                        for current_location_id in current_location_ids:
                            location_with_affected_week.setdefault(current_location_id, set()).update(activity_week_ids)

                    if input_location_ids:
                        for s_id in input_location_ids:
                            location_with_affected_week.setdefault(s_id, set()).update(activity_week_ids)
                            location_ids.add(s_id)
                        location_ids.update(input_location_ids)
                    activity.location.set(list(input_location_ids))
                    old_new_data["new_data"][activity_id]["location"] = sorted(list(activity_location.id for activity_location in activity.location.all()))

            redis_data = {
                "update": {}
            }

            staff_with_affected_week = {k: v for k, v in staff_with_affected_week.items() if v}
            location_with_affected_week = {k: v for k, v in location_with_affected_week.items() if v}
            redis_data = helper_recalculate_and_get_redis_resource_map(
                redis_data=redis_data,
                staff_with_affected_week=staff_with_affected_week,
                location_with_affected_week=location_with_affected_week,
                slot_per_week=slot_per_week,
            )
            bulk_sync_to_redis(redis_client, redis_data)

            return_data = {
                "code": activity.code,
                "name": activity.name,
                "desc": activity.desc,
                "department": activity.department.name if activity.department else None,
                "department_id": activity.department_id,
                "planned_size": activity.planned_size,
                "real_size": 0,
                "activity_type_id": activity.activity_type_id,
                "activity_type": activity.activity_type.name if activity.activity_type else None,
                "activity_type_color": activity.activity_type.color if activity.activity_type else None,
                "scheduled_start_time": str(activity.scheduled_start_time),
                "scheduled_start_slot": activity.scheduled_start_slot,
                "scheduled_day": __(
                    "attr.days_name." + str(activity.scheduled_day)) if activity.scheduled_day is not None else None,
                "duration": activity.duration,
                "slot_required": activity.slot_required,
                "location": [l.name for l in activity.location.all()],
                "staff": [s.name for s in activity.staff.all()],
            }
            microservices_data = {}

            microservices_data['id'] = activity.id
            microservices_data['staff_ids'] = [s.id for s in activity.staff.all()]
            microservices_data['location_ids'] = [loc.id for loc in activity.location.all()]

            method = "booking_swap"
            kafka_topic = self.kafka_config['MICROSERVICES_TT_TOPIC']

            if kafka_topic:
                kafka_request_data = {
                    "session_id": request.user.name,
                    "activity": microservices_data,
                }
                send_request(kafka_topic, kafka_request_data, None, method)
            # 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,
                old_data=old_new_data["old_data"],
                new_data=old_new_data["new_data"],
            )

            response = {
                "data": return_data
            }

            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)

    def validate_slot_available(self,request,activity,slot_per_day):
        start_slot = activity.scheduled_start_slot


        activity_week_ids = list(aw.id for aw in activity.week.all())
        # if FE got pass in day and time, then can calculate the new start_slot, when check resource map will used new start slot

        slot_required = activity.slot_required

        not_allow_staffs_names = []
        not_allow_locations_names = []
        scheduled_staff_ids = list(activity_staff.id for activity_staff in activity.staff.all())
        scheduled_location_ids = list(activity_location.id for activity_location in activity.location.all())

        # no need check activity availability, cause if this activity not available in current time, not even can do this action
        # when FE pass in empty staff_ids, then means need remove staff from this activity
        if "staff_ids" in request.data:
            staff_ids = request.data.get("staff_ids")
            staffs = TtStaff.objects.filter(id__in=staff_ids).select_related("availability").prefetch_related("ttstaffresourcemap_set")
            for staff in staffs:
                # no need validate for current scheduled resources
                if staff.id not in scheduled_staff_ids:
                    staff_resource_map_obj = {
                        rm.week_id: rm.pattern
                        for rm in staff.ttstaffresourcemap_set.all()
                        if rm.week_id in activity_week_ids
                    }
                    availability_pattern = staff.availability.pattern if staff.availability else staff.availability_pattern
                    availability_available = check_availability_available(availability_pattern, start_slot, slot_per_day)
                    available = check_resource_map_available(staff_resource_map_obj, start_slot, slot_required)
                    if not available or not availability_available:
                        not_allow_staffs_names.append(staff.name)

        if "location_ids" in request.data:
            location_ids = request.data.get("location_ids")
            locations = TtLocation.objects.filter(id__in=location_ids).select_related("availability").prefetch_related("ttlocationresourcemap_set")
            for location in locations:
                # no need validate for current scheduled resources
                if location.id not in scheduled_location_ids:
                    location_resource_map_obj = {
                        rm.week_id: rm.pattern
                        for rm in location.ttlocationresourcemap_set.all()
                        if rm.week_id in activity_week_ids
                    }
                    availability_pattern = location.availability.pattern if location.availability else location.availability_pattern
                    availability_available = check_availability_available(availability_pattern, start_slot, slot_required)
                    available = check_resource_map_available(location_resource_map_obj, start_slot, slot_required)
                    if not available or not availability_available:
                        not_allow_locations_names.append(location.name)

        error_details = {
            "error": "",
            "errors": {}
        }

        if not_allow_staffs_names:
            staff_names_str = ', '.join(not_allow_staffs_names)
            msg = __("validation.resources_scheduled_by_other", name=staff_names_str)
            error_details["error"] = msg
            error_details["errors"]["staff_ids"] = [msg]

        if not_allow_locations_names:
            location_names_str = ', '.join(not_allow_locations_names)
            msg = __("validation.resources_scheduled_by_other", name=location_names_str)
            error_details["error"] = msg
            error_details["errors"]["location_ids"] = [msg]

        # if got location or staff not available, return error
        if error_details["error"]:
            raise ValidationError(error_details)