import datetime
from collections import defaultdict

from django.db.models import Case, When, F, CharField, Count, Q
from django.db.models.functions import Substr
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
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
from rest_framework.exceptions import ValidationError
from api.translation import __


class BookingSwapGetResources(AdminApiBase):
    def validate_request(self,request):
        rules = {
            "activity_id": "nullable|exists:api.TtActivity,id",
            "type": "required", # staff or location
            "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)

    def post(self, request):
        # Validate input
        try:
            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")
            activity = TtActivity.objects.filter(id=activity_id).prefetch_related("location_suitability","location","staff").first()
            if not activity.is_booking:
                raise ValidationError({
                    "error": __("validation.invalid", field=__("attr.activity")),
                    "errors": {"id": __("validation.invalid", field=__("attr.activity"))}
                })

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

            type = request.data.get("type")
            planned_size = activity.planned_size
            resources = None
            start_slot = activity.scheduled_start_slot
            slot_required = activity.slot_required
            target_slice = "0" * slot_required
            activity_week_ids = list(aw.id for aw in activity.week.all())
            return_data = []

            if type == "location":
                location_ids = list(
                    activity.location_suitability.all()
                    .values_list('location__id', flat=True)
                    .distinct()
                )
                # 0 means all
                resources = TtLocation.objects.filter(Q(capacity__gte=planned_size) | Q(capacity=0))
                if location_ids:
                    resources = resources.filter(id__in=location_ids)
                resources_model = TtLocation
                resource_map_set_name = "ttlocationresourcemap"
                # scheduled resources need include in return data also
                for l in activity.location.all():
                    return_data.append({
                        "id": l.id,
                        "code": l.code,
                        "name": l.name
                    })
            elif type == "staff":
                resources = TtStaff.objects.all()
                resources_model = TtStaff
                resource_map_set_name = "ttstaffresourcemap"
                # scheduled resources need include in return data also
                for s in activity.staff.all():
                    return_data.append({
                        "id": s.id,
                        "code": s.code,
                        "name": s.name
                    })
            if not constraint_breaking_mode:
                # this query is only check for availability is available to schedule this slot, and get those resources ids
                availability_resources_ids = list(
                    resources.annotate(
                        selected_pattern=Case(
                            When(availability__isnull=False, then=F('availability__pattern')),
                            default=F('availability_pattern'),
                            output_field=CharField(),
                        ) # using case to check if got availability_id, used preset availability, else will used custom availability
                    )
                    .annotate(
                        target_slice=Substr('selected_pattern', start_slot, slot_required)
                    )
                    .filter(
                        target_slice=target_slice  # Only keep locations where these slots are '0'
                    )
                    .values_list('id', flat=True)  # Grab just a clean list of IDs, e.g., [1, 4, 12]
                )

                # this query is check resources resource map is available for this booking or not, will used result in first query to filter out some data

                custom_filter = {
                    "id__in": availability_resources_ids,
                    f"{resource_map_set_name}__week_id__in": activity_week_ids,
                }
                match_resources = (
                    resources_model.objects.filter(**custom_filter)
                    .annotate(
                        target_slice=Substr(f"{resource_map_set_name}__pattern", start_slot, slot_required)
                    )
                    .filter(target_slice=target_slice)
                    .annotate(
                        match_count=Count(f"{resource_map_set_name}__week_id") # check got how many resource map is match
                    )
                    .filter(match_count=len(activity_week_ids)) # since need all row also match, so will compare match_count with length of week_ids, if not same means got week not available, will skip this record
                )
            else:
                match_resources = resources_model.objects.filter(status=TtLocation.STATUS_TO_CODE["active"])

            # just a safety check to prevent repeat resources
            seen_ids = {item["id"] for item in return_data}
            for resource in match_resources:
                if resource.id not in seen_ids:
                    return_data.append({
                        "id": resource.id,
                        "code": resource.code,
                        "name": resource.name,
                    })
                    seen_ids.add(resource.id)
            return_data.sort(key=lambda x: x["name"].lower())
            response = {
                "resources": 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)