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 log_critical_error, get_exception_detail, get_ip, bulk_sync_to_redis, check_resource_map_available, check_availability_available, calculate_slot_by_day
from rest_framework.exceptions import ValidationError
from api.translation import __
from backend.kafka import send_request
from backend.redis_client import redis_client


class BookingSchedule(AdminApiBase):
    def validate_request(self,request):
        # wont update module id, just want based on module id get the activity template
        rules = {
            "activity_id": "required|exists:api.TtActivity,id",
            "type": "required", # staff or location
            "resource_ids": "required|array",
            "slot": "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)

    def post(self, request):
        # Validate input
        try:
            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")
            resource_ids = request.data.get("resource_ids")
            type = request.data.get("type")
            slot = request.data.get("slot")

            activity = TtActivity.objects.filter(id=activity_id).select_related("department","activity_type").prefetch_related("week","staff","location").first()

            if not activity.is_booking:
                raise ValidationError({
                    "error": __("validation.invalid", field=__("attr.activity")),
                    "errors": {"id": __("validation.invalid", field=__("attr.activity"))}
                })

            action = "booking_schedule"  # 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 = {
                "new_data": {},
            }
            audit_key = str(activity_id)
            old_new_data["new_data"].setdefault(audit_key, {})

            # 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_week,slot_per_day)
            activity_week_ids = list(aw.id for aw in activity.week.all())

            # 0 is Monday 6 is Sunday
            day = slot // slot_per_day
            # * 60 means need convert to second
            time_in_second = (slot % slot_per_day) * minute_per_slot * 60
            hours = time_in_second // 3600
            minutes = (time_in_second % 3600) // 60
            time = datetime.time(hour=hours, minute=minutes)

            activity.scheduled = 1
            activity.scheduled_start_slot = slot
            activity.scheduled_day = day
            activity.scheduled_start_time = time
            activity.save()

            if type == "staff":
                activity.staff.set(resource_ids)
                for resource_id in resource_ids:
                    staff_with_affected_week.setdefault(resource_id, set()).update(activity_week_ids)
                    staff_ids.add(resource_id)

            if type == "location":
                activity.location.set(resource_ids)
                for resource_id in resource_ids:
                    location_with_affected_week.setdefault(resource_id, set()).update(activity_week_ids)
                    location_ids.add(resource_id)

            old_new_data["new_data"][audit_key]["scheduled"] = activity.scheduled
            old_new_data["new_data"][audit_key]["scheduled_start_slot"] = activity.scheduled_start_slot
            old_new_data["new_data"][audit_key]["scheduled_day"] = activity.scheduled_day
            old_new_data["new_data"][audit_key]["scheduled_start_time"] = activity.scheduled_start_time.isoformat() if activity.scheduled_start_time else None
            old_new_data["new_data"][audit_key]["staff_ids"] = sorted([s.id for s in activity.staff.all()])
            old_new_data["new_data"][audit_key]["location_ids"] = sorted([l.id for l in activity.location.all()])

            redis_data = {
                "update": {}
            }

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

            redis_data["update"].setdefault(redis_activity_table, []).append({
                "id": activity.id,
                "scheduled": 1,
                "scheduled_start_slot": slot,
            })

            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 = {}
            final_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()],
            }
            return_data = copy.deepcopy(final_data)

            final_data['id'] = activity.id
            final_data['staff_ids'] = [s.id for s in activity.staff.all()]
            final_data['location_ids'] = [loc.id for loc in activity.location.all()]
            final_data['scheduled_day'] = activity.scheduled_day if activity.scheduled_day is not None else None
            final_data['week_ids'] = activity_week_ids

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

            if kafka_topic:
                kafka_request_data = {
                    "session_id": request.user.name,
                    "activity": final_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,
                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_week,slot_per_day):
        """
        prevent have 2 user doing same action in same time, so having this validation to double confirm
        """
        start_slot = request.data.get("slot")
        activity_week_ids = list(aw.id for aw in activity.week.all())
        slot_required = activity.slot_required
        suggested_day = activity.suggested_day

        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())
        day_pattern = None
        if suggested_day:
            day_pattern = calculate_slot_by_day([suggested_day], slot_per_week, slot_per_day)

        # 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 request.data.get("type") == "staff":
            staff_ids = request.data.get("resource_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
                    # if got suggested day, combine with day pattern only do checking
                    if day_pattern:
                        availability_pattern = "".join([max(a, b) for a, b in zip(availability_pattern, day_pattern)])
                    availability_available = check_availability_available(availability_pattern, start_slot, slot_required)
                    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 request.data.get("type") == "location":
            location_ids = request.data.get("resource_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
                    # if got suggested day, combine with day pattern only do checking
                    if day_pattern:
                        availability_pattern = "".join([max(a, b) for a, b in zip(availability_pattern, day_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)