import datetime
from collections import defaultdict

from rest_framework import serializers, status

from api.models import TtDepartment, TtZone, TtAvailability, TtStartPreference, TtUsagePreference, TtTag, TtSetting, \
    TtModule, TtAcademicTerm, TtActivityType, TtStaff, TtLocation, TtSuitability, TtActivityTemplate, TtWeekPattern, \
    TtWeek, TtPosModuleGroupModule, TtActivity, TtLocationResourceMap, TtStaffResourceMap
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, \
    calculate_preset_pattern_time, time_to_slot, calculate_slot_by_day, get_slot_default_pattern, \
    calculate_slot_by_start_time_slot
from rest_framework.exceptions import ValidationError
from api.translation import __


class BookingPreschedule(AdminApiBase):
    def validate_request(self,request):
        rules = {
            "activity_id": "required|exists:api.TtActivity,id",
            "type": "required", # staff or location
            "resource_ids": "required|array",
            "constraint_breaking_mode": "nullable",
        }
        # if need rename field, can put here
        attribute = {
            # "email": __("attr.email"),
        }
        # validate id first, cause need get activity_template by id, if wrong id direct return error
        validator = BaseValidator(request.data,rules,attribute)
        error = validator.validate()
        if error:
            raise ValidationError(error)

        # based on type to check staff or location table

    def post(self, request):
        # Validate input
        try:
            setting_params = {
                "slot_per_day",
                "slot_per_week",
            }
            settings = TtSetting.get_multiple_setting(setting_params)
            slot_per_day = int(settings["slot_per_day"])
            slot_per_week = int(settings["slot_per_week"])
            # self.api_log_skip_outgoing_data=True
            self.validate_request(request)
            constraint_breaking_mode = request.data.get("constraint_breaking_mode")

            activity_id = request.data.get("activity_id")
            resource_ids = request.data.get("resource_ids")
            # booking wont have week_pattern_id, so can direct get week
            activity = TtActivity.objects.filter(id=activity_id).select_related("activity_type").prefetch_related("week").first()
            slot_required = activity.slot_required
            default_pattern = get_slot_default_pattern(slot_per_week)
            # all is available first
            pattern = default_pattern["availability"]

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

            type = request.data.get("type")
            resource = None
            week_ids = [activity_week.id for activity_week in activity.week.all()]

            if type == "location":
                resources = (
                    TtLocation.objects.filter(id__in=resource_ids)
                    .select_related("availability")
                    .prefetch_related(
                        'ttactivity_set',
                    )
                )
                resource_maps = TtLocationResourceMap.objects.filter(location_id__in=resource_ids,week_id__in=week_ids)
            elif type == "staff":
                resources = (
                    TtStaff.objects.filter(id__in=resource_ids)
                    .select_related('availability')
                    .prefetch_related(
                        'ttactivity_set',
                    )
                )
                resource_maps = TtStaffResourceMap.objects.filter(staff_id__in=resource_ids,week_id__in=week_ids)

            scheduled_activity_ids = set()
            # group all availability first
            for resource in resources:
                activity_ids = [a.id for a in resource.ttactivity_set.all()]
                scheduled_activity_ids.update(activity_ids)
                availability_pattern = resource.availability.pattern if resource.availability else resource.availability_pattern
                if availability_pattern and not constraint_breaking_mode:
                    pattern = "".join([max(a, b) for a, b in zip(pattern, availability_pattern)])

            # constraint break wont ignore suggested_day and suggested time
            if activity.suggested_time_slot is not None:
                time_pattern = calculate_slot_by_start_time_slot(activity.suggested_time_slot,activity.slot_required, slot_per_week,slot_per_day)
                # 1. Use zip() to pair up characters: ('0','0'), ('0','0') ... ('1','0')
                # 2. max() picks "1" if it exists in the pair
                # 3. "".join() turns it back into final string
                pattern = "".join([max(a, b) for a, b in zip(pattern, time_pattern)])

            if activity.suggested_day is not None:
                day_pattern = calculate_slot_by_day([activity.suggested_day], slot_per_week,slot_per_day)
                # 1. Use zip() to pair up characters: ('0','0'), ('0','0') ... ('1','0')
                # 2. max() picks "1" if it exists in the pair
                # 3. "".join() turns it back into final string
                pattern = "".join([max(a, b) for a, b in zip(pattern, day_pattern)])
            if not constraint_breaking_mode:
                for resource_map in resource_maps:
                    pattern = "".join([max(a, b) for a, b in zip(pattern, resource_map.pattern)])

            # convert pattern to list for update scheduled activity
            pattern = list(pattern)
            scheduled_activity = []
            scheduled_activity_sets = (
                TtActivity.objects.filter(id__in=scheduled_activity_ids)
                .select_related("week_pattern","activity_type")
                .prefetch_related(
                    "week",
                    "week_pattern__week",
                    "staff",
                    "location"
                )
            )

            for scheduled_activity_set in scheduled_activity_sets:
                if scheduled_activity_set.week_pattern_id:
                    scheduled_activity_week_ids = [wp.id for wp in scheduled_activity_set.week_pattern.week.all()]
                else:
                    scheduled_activity_week_ids = [w.id for w in scheduled_activity_set.week.all()]
                # if got overlap, need return to FE display in timetable
                has_overlap = bool(set(week_ids).intersection(scheduled_activity_week_ids))
                # data for let FE know got what scheduled activity, so can display it
                if has_overlap:
                    scheduled_activity.append({
                        "id": scheduled_activity_set.id,
                        "name": scheduled_activity_set.name,
                        "code": scheduled_activity_set.code,
                        "activity_type": scheduled_activity_set.activity_type.name if scheduled_activity_set.activity_type else None,
                        "activity_type_color": scheduled_activity_set.activity_type.color if scheduled_activity_set.activity_type else None,
                        "activity_type_id": scheduled_activity_set.activity_type_id,
                        "scheduled_day": __("attr.days_name." + str(scheduled_activity_set.scheduled_day)) if scheduled_activity_set.scheduled_day is not None else None,
                        "scheduled_start_time": scheduled_activity_set.scheduled_start_time,
                        "scheduled_start_slot": scheduled_activity_set.scheduled_start_slot,
                        "slot_required": scheduled_activity_set.slot_required,
                        "duration": scheduled_activity_set.duration,
                        "staff": [{"id": s.id, "name": s.name, "code": s.code} for s in getattr(scheduled_activity_set, "staff").all()],
                        "location": [{"id": l.id, "name": l.name, "code": l.code} for l in getattr(scheduled_activity_set, "location").all()]
                    })
            total_slot = len(pattern)
            # having an all 1 pattern list, at the end will return this to FE
            result_pattern_list = [1] * total_slot
            # loop 0 till total slot + 1, if not + 1, will miss last slot checking
            for i in range(total_slot - slot_required + 1):
                # based on previous calculated pattern value, if that position is 0, will having this checking
                if pattern[i] == "0":
                    # if i is 5 and slot_required is 4, affected slot will be pattern[5:9], so the result will get position 5,6,7,8 data, wont get till position 9
                    affected_slot = pattern[i: i + slot_required]
                    if "1" not in affected_slot:
                        # if affected slot all is 0, means this position "i" is available to schedule, so will update result pattern list to 0
                        result_pattern_list[i] = 0

            response = {
                "scheduled_activity": scheduled_activity,
                "availability": result_pattern_list,
            }

            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)