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.redis_client import redis_client


class ResourcesUpdateCurrent(AdminApiBase):
    def validate_request(self,request):
        # wont update module id, just want based on module id get the activity template
        rules = {
            "activity_id": "required",
        }
        # 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")
            .prefetch_related(
                "staff_preset",
                "staff_suitability",
                "location_preset",
                "location_suitability",
                "student_set",
                "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"),scheduled=1)
        )
        if not edit_activity:
            raise ValidationError({
                "error": __('validation.activity_must_scheduled'),
                "errors": {"module": [__('validation.activity_must_scheduled')]}
            })

        rules = {
            # "day": "nullable",
            # "time": "nullable",
            "staff_ids": "nullable|array",
            "location_ids": "nullable|array",
        }

        # 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 edit_activity


    def post(self, request):
        # Validate input
        try:
            edit_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"])
            first_activity = edit_activity.first()
            activity_ids = [request.data.get("activity_id")]
            action = "resource_update_current"  # this is for audit log only

            staff_with_affected_week = {}
            location_with_affected_week = {}
            staff_ids = set()
            location_ids = set()

            academic_term_id = first_activity.academic_term_id
            # academic_term = TtAcademicTerm.objects.filter(id=academic_term_id).prefetch_related(Prefetch("week", queryset=TtWeek.objects.order_by("week"))).first()
            # if not allow schedule based on new requirement, will direct return error
            self.validate_slot_available(request,first_activity,minute_per_slot,slot_per_day)
            if first_activity.week_pattern:
                activity_week_ids = list(aw.id for aw in first_activity.week_pattern.week.all())
            else:
                activity_week_ids = list(aw.id for aw in first_activity.week.all())

            # if staff or location changed, remove old staff/location and add new staff/location
            if "staff_ids" in request.data:
                activity_staff_to_create = []
                current_staff_ids = list(activity_staff.id for activity_staff in first_activity.staff.all())
                input_staff_ids = request.data.get("staff_ids")
                # if new staff not same as old staff, remove old and add new
                if set(current_staff_ids) != set(input_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
                    first_activity.staff.set(list(input_staff_ids))

                    # if this activity is using preset type, need change the preset value also(follow scientia)
                    if first_activity.staff_requirement_type == TtActivity.STAFF_REQUIREMENT_TYPE["preset"]:
                        first_activity.staff_preset.set(list(input_staff_ids))

            if "location_ids" in request.data:
                activity_location_to_create = []
                current_location_ids = list(activity_location.id for activity_location in first_activity.location.all())
                input_location_ids = request.data.get("location_ids")
                # if new location not same as old location, remove old and add new
                if set(current_location_ids) != set(input_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)
                    first_activity.location.set(list(input_location_ids))
                    # if this activity is using preset type, need change the preset value also(follow scientia)
                    if first_activity.location_requirement_type == TtActivity.LOCATION_REQUIREMENT_TYPE["preset"]:
                        first_activity.location_preset.set(list(input_location_ids))

            redis_data = {
                "update": {}
            }

            redis_activity_table = "activity"
            redis_activity_data = {}
            redis_staff_table = "staff"
            redis_location_table = "location"

            if "staff_ids" in request.data and first_activity.staff_requirement_type == TtActivity.STAFF_REQUIREMENT_TYPE["preset"]:
                redis_activity_data["staff_preset"] = request.data.get("staff_ids")
            if "location_ids" in request.data and first_activity.location_requirement_type == TtActivity.LOCATION_REQUIREMENT_TYPE["preset"]:
                redis_activity_data["location_preset"] = request.data.get("location_ids")
            if redis_activity_data:
                redis_activity_data["id"] = first_activity.id
                redis_data["update"].setdefault(redis_activity_table, []).append(redis_activity_data)

            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)

            # 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 = ",".join(edit_activity.values_list("name", flat=True))
            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,
            )

            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)

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

        if first_activity.week_pattern:
            activity_week_ids = list(aw.id for aw in first_activity.week_pattern.week.all())
        else:
            activity_week_ids = list(aw.id for aw in first_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 = first_activity.slot_required

        not_allow_staffs_names = []
        not_allow_locations_names = []
        scheduled_staff_ids = list(activity_staff.id for activity_staff in first_activity.staff.all())
        scheduled_location_ids = list(activity_location.id for activity_location in first_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": {}
        }
        day_name = __("attr.days_name."+str(first_activity.scheduled_day))
        time = first_activity.scheduled_start_time
        if not_allow_staffs_names:
            staff_names_str = ', '.join(not_allow_staffs_names)
            msg = __("validation.not_available_schedule_to", name=staff_names_str, type=__("attr.staff"),day=day_name,time=time)
            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.not_available_schedule_to", name=location_names_str, type=__("attr.location"),day=day_name,time=time)
            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)