import math
from collections import defaultdict

from django.contrib.postgres.aggregates import ArrayAgg
from django.db.models import Count, F, Q, Prefetch
from rest_framework import status
from rest_framework.exceptions import ValidationError

from api.models import TtModule, TtActivityTemplate, TtStudentSet, TtActivity, TtStudentSetActivity, \
    TtStudentSetResourceMap
from api.translation import __
from api.utils import log_critical_error, get_exception_detail, remove_variant_activity_name_range
from api.validator import BaseValidator
from api.views.admin.base import AdminApiBase

class ActivityTemplateAllocatorRules(AdminApiBase):
    def validate_request(self,request):
        rules = {
            "id": "required|array|exists:api.TtActivityTemplate,id",
        }
        # if need rename field, can put here
        attribute = {
            # "email": __("attr.email"),
        }
        # validate id first, cause need get pos 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):
        try:
            self.api_log_skip_outgoing_data=True
            self.validate_request(request)
            """
            {
                student_set_id: 1,
                student_set_name:ss1,
                activity_id: 1 or None,
                real_size: None,(for now dun have, so return None
                total_size: 5,(planned_size)
            }
            
            Activity set
            {
                activity_id: 1,
                activity_name: A1,
                planned_size: 100,
            }
            """
            activity_template_ids = request.data['id']
            # extra filter to exclude variant child record
            # filtered_activities = TtActivity.objects.exclude(
            #     is_variant=1,variant_parent_id__gt=0
            # )
            filtered_activities = TtActivity.objects.exclude(
                # Exclude variant child record
                Q(is_variant=1, variant_parent_id__gt=0) |
                # exclude jta child record
                Q(is_jta=1) & (Q(jta_parent_id=0) | Q(jta_parent_id__isnull=True))
            ).select_related("week_pattern").prefetch_related(
                "week",
                "week_pattern__week"
            )
            activity_template_data = (
                TtActivityTemplate.objects.filter(id__in=activity_template_ids)
                .prefetch_related(
                    Prefetch(
                        "ttactivity_set",
                        queryset=filtered_activities,
                        to_attr="parent_activities"  # custom the name, and to_attr will convert parent_activities become python list, so no need .all() to access it
                    )
                )
            )

            # if not manual change the variant parent name, the remove_variant_activity_name_range will change the variant name become normal name
            related_activity = [
                {
                    "id": val.id,
                    "activity_template_name": val.name,
                    "activity": [
                        {
                            "id": a.id,
                            "name": remove_variant_activity_name_range(a.name) if a.is_variant else a.name,
                            "planned_size": a.planned_size}
                        for a in val.parent_activities
                    ]
                }
                for val in activity_template_data
            ]

            # get all student set in this activity template by module_id of the template
            # student_set_data = (
            #     TtStudentSet.objects
            #     .filter(module__id__in=module_ids)
            #     .values("id", "code", "name", "planned_size")
            # )

            # from activity template, find related student set
            activity_template_student_set = (
                TtActivityTemplate.objects
                .filter(id__in=activity_template_ids)
                .values("module__ttstudentset__name","module__ttstudentset__id","module__ttstudentset__planned_size","id","module__ttstudentset__availability_pattern","module__ttstudentset__availability__pattern")
            )
            student_set_data = []
            student_set_ids = set()
            for val in activity_template_student_set:
                # if student set id is null, means no student set have this module template, so no need display to FE
                if val['module__ttstudentset__id']:
                    student_set_ids.add(val['module__ttstudentset__id'])
                    student_set_data.append({
                        "id": val['module__ttstudentset__id'],
                        "name": val['module__ttstudentset__name'],
                        "planned_size": val['module__ttstudentset__planned_size'],
                        "availability_pattern": val['module__ttstudentset__availability_pattern'],
                        "availability_id_pattern": val['module__ttstudentset__availability__pattern'],
                        "activity_template_id": val['id'],
                        "activity_id": None,
                    })

            student_set_ids = {s['id'] for s in student_set_data}
            # get relation for this template activity with student set
            student_set_activity = (
                TtStudentSetActivity.objects
                .filter(
                    student_set_id__in=student_set_ids,
                    activity__activity_template_id__in=activity_template_ids
                ).select_related("student_set","activity")
            )

            # key as the student_set_id so can used for compare
            # activity_map = {r["student_set_id"]: r['activity_id'] for r in student_set_activity}

            # check the student set got join any activity or not, if no just return None to FE
            activity_map = defaultdict(dict)
            for val in student_set_activity:
                activity = val.activity

                # if no this line, when None will have error
                variant_parent_id = activity.variant_parent_id or 0
                jta_parent_id = activity.jta_parent_id or 0
                is_jta_child = (activity.is_jta == 1 and jta_parent_id == 0)
                is_variant_child = (activity.is_variant == 1 and variant_parent_id > 0)

                # exclude jta child and variant parent activity
                if is_jta_child or is_variant_child:
                    continue
                student_set_id = val.student_set_id
                activity_template_id = activity.activity_template_id

                activity_map[student_set_id][activity_template_id] = activity.id

            student_set_resource_map = defaultdict(list)
            queryset = TtStudentSetResourceMap.objects.filter(student_set_id__in=student_set_ids)
            for obj in queryset:
                student_set_resource_map[obj.student_set_id].append(obj)

            # put the activity_id into the listing, if that student set no relation with the activity, will return None
            for s in student_set_data:
                not_available_activity = set()
                current_activity_id = activity_map.get(s["id"], {}).get(s["activity_template_id"])
                s["activity_id"] = current_activity_id
                resource_map = student_set_resource_map.get(s['id'])
                if resource_map:
                    for activity_t in activity_template_data:
                        for activity in activity_t.parent_activities:
                            if activity.id != current_activity_id:
                                # JTA SCENARIO
                                if activity.is_jta:
                                    child_activity = TtActivity.objects.filter(id=activity.jta_parent_id).first()
                                    # handle for jta child activity
                                    if child_activity:
                                        if child_activity.is_variant:
                                            # if jta is variant, need get all his variant activity and check for crashing
                                            child_variant_activities = TtActivity.objects.filter(variant_parent_id=child_activity.id)
                                            for child_variant_activity in child_variant_activities:
                                                crashed = crash_checking(child_variant_activity, s, resource_map)
                                                if crashed:
                                                    not_available_activity.add(activity.id)
                                                    break

                                        crashed = crash_checking(child_activity, s, resource_map)
                                        if crashed:
                                            not_available_activity.add(activity.id)
                                else:
                                    # NON JTA SCENARIO
                                    if activity.is_variant:
                                        # if the activity is variant, check all variant child activity also
                                        child_variant_activities = TtActivity.objects.filter(variant_parent_id=activity.id)
                                        for child_variant_activity in child_variant_activities:
                                            crashed = crash_checking(child_variant_activity, s, resource_map)
                                            if crashed:
                                                not_available_activity.add(activity.id)
                                                break
                                    crashed = crash_checking(activity, s, resource_map)
                                    if crashed:
                                        not_available_activity.add(activity.id)

                # FE no need this data, so can unset it
                s.pop("availability_id_pattern")
                s.pop("availability_pattern")

                s["not_available_activity"] = list(not_available_activity)

            response = {
                "student_set_list": student_set_data,
                "activity_list": related_activity
            }
            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 crash_checking(activity, student_set, resource_map_data):
    if activity.scheduled == 1:
        if activity.week_pattern:
            weeks = {w.id for w in activity.week_pattern.week.all()}
        else:
            weeks = {w.id for w in activity.week.all()}

        # scheduled_start_slot is start from 0, so no need to - 1
        start_slot = activity.scheduled_start_slot
        slot_required = activity.slot_required
        availability = student_set.get("availability_id_pattern") or student_set.get("availability_pattern")

        for val in resource_map_data:
            if val.week_id in weeks:
                resource_pattern = val.pattern or ("0" * 672)
                # Check each slot in the required range [start_slot : start_slot + slot_required]
                for i in range(slot_required):
                    slot_idx = start_slot + i

                    avail_char = availability[slot_idx]
                    res_char = resource_pattern[slot_idx]

                    # Combined bit logic: if BOTH are '0', result is '0', else '1'
                    combined_slot = '0' if (avail_char == '0' and res_char == '0') else '1'
                    if combined_slot != '0':
                        return True
    return False
