import re
import copy
from itertools import product, combinations

from django.db import transaction
from django.db.models import Q
from django.forms import model_to_dict
from rest_framework import serializers, status

from api.helper.activity_sequencing_helper import helper_build_update_redis_sequencing_data
from api.helper.resource_map_helper import helper_recalculate_and_get_redis_resource_map
from api.models import TtUsagePreference, TtStartPreference, TtSetting, TtPos, TtPosModuleGroupModule, TtPathway, \
    TtPathwayPosModuleGroupModule, TtPosModuleGroup, TtActivity, TtActivityWeek, TtWeek, TtStudentSetActivity, \
    TtStudentSet
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, bulk_sync_to_redis, convert_to_redis_week, \
    get_resource_map_redis_data, format_variant_activity_name_week_pattern_to_ranges, remove_variant_activity_name_range
from rest_framework.exceptions import ValidationError
from api.translation import __
from api.services.integration.tracking import track_activity_ids
from backend.kafka import send_request
from backend.redis_client import redis_client


class JtaSplit(AdminApiBase):
    def validate_request(self,request):
        # no need give name option, this is FE control
        rules = {
            "activity_id": "required|exists:api.TtActivity,id",
            "name": "required",
            "desc": "nullable",
            "split_activity_ids": "required|array",
        }

        # if need rename field, can put here
        attribute = {
            # "email": __("attr.email"),
        }

        validator = BaseValidator(request.data,rules,attribute)
        error = validator.validate()
        if error:
            raise ValidationError(error)

        activity = TtActivity.objects.filter(id=request.data.get("activity_id")).prefetch_related("jta_children").first()
        # custom error after basic validation
        # add validation if is_variant = 1, not allow to split
        if not activity.is_jta:
            raise ValidationError({
                "error": __('validation.cant_split_non_jta_activity'),
                "errors": {"activity_id": [__('validation.cant_split_non_jta_activity')]},
            })
        if activity.jta_parent_id:
            raise ValidationError({
                "error": __('validation.cant_split_jta_parent_activity'),
                "errors": {"activity_id": [__('validation.cant_split_jta_parent_activity')]},
            })
        if activity.is_variant:
            raise ValidationError({
                "error": __('validation.cant_split_variant_activity'),
                "errors": {"activity_id": [__('validation.cant_split_variant_activity')]},
            })
        split_activity_ids = request.data.get("split_activity_ids")
        invalid_parent_ids = []
        # double confirm split_activity_ids is parent of activity_id
        current_parent = {
            activity_parent.id: activity_parent
            for activity_parent in activity.jta_children.all()
        }
        for split_activity_id in split_activity_ids:
            split_parent = current_parent.get(split_activity_id)
            if not split_parent:
                invalid_parent_ids.append(split_activity_id)
        if invalid_parent_ids:
            invalid_parent_names = TtActivity.objects.filter(id__in=invalid_parent_ids).values_list("name", flat=True)
            invalid_parent_name = ", ".join(invalid_parent_names)
            raise ValidationError({
                "error": __('validation.is_not_jta_parent_for_activity', parent_name=invalid_parent_name, child_name=activity.name),
                "errors": {"split_activity_ids": [__('validation.is_not_jta_parent_for_activity', parent_name=invalid_parent_name, child_name=activity.name)]},
            })
    def post(self, request):
        # Validate input
        try:
            self.validate_request(request)
            setting_params = {
                "slot_per_week",
                "minute_per_slot",
            }
            settings = TtSetting.get_multiple_setting(setting_params)
            slot_per_week = int(settings["slot_per_week"])
            minute_per_slot = int(settings["minute_per_slot"])
            activity_id = request.data.get("activity_id")
            split_activity_ids = request.data.get("split_activity_ids")

            action = "jta_split"

            activity_name = None
            parent_activity_ids = []

            staff_with_affected_week = {}
            location_with_affected_week = {}
            student_set_with_affected_week = {}

            staff_ids = set()
            location_ids = set()
            student_set_ids = set()
            sequencing_activity_ids = None
            reschedule_resource_map = False
            # get all activity with all related data needed
            parent_activities = (
                TtActivity.objects.filter(
                    id__in=split_activity_ids,
                )
                .select_related("week_pattern", "activity_template")
                .prefetch_related(
                    "student_set",
                )
            )

            # later will remove student set based on it
            parent_student_set_ids = set()
            # remove parent from child first
            for parent_activity in parent_activities:
                # check parent got what student set
                parent_student_set_ids.update({parent_activity_ss.id for parent_activity_ss in parent_activity.student_set.all()})
                parent_activity_ids.append(parent_activity.id)

            # delete first based on whatever FE send, later will delete one more time is remain 1 jta as parent
            track_activity_ids(parent_activity_ids)
            TtActivity.objects.filter(id__in=parent_activity_ids).update(
                is_jta=0,
                jta_parent_id=None
            )

            # get child activity to see still have parent or not, remain 1 also count as no parent
            child_activity = (
                TtActivity.objects.filter(
                    id=activity_id,
                )
                .select_related("week_pattern", "activity_template")
                .prefetch_related(
                    "week",
                    "week_pattern__week",
                    "student_set",
                    "staff",
                    "location",
                    "sequencing",
                    "jta_children", # actually is getting this activity jta_parent
                ).first()
            )
            if child_activity.week_pattern:
                week_ids = [aw.id for aw in child_activity.week_pattern.week.all()]
            else:
                week_ids = [aw.id for aw in child_activity.week.all()]
            activity_name = child_activity.name
            delete_child = False
            # if no parent, perform normal delete
            remaining_parent_ids = [ca_p.id for ca_p in child_activity.jta_children.all()]
            # if got parent, delete student set from child
            if len(remaining_parent_ids) > 1:
                update_module_and_template = True

                current_parent_student_set_ids = set()
                # compare current parent student set and parent_student_set_ids, if parent_student_set_ids got value in current parent student set, no need remove
                current_parent_activities = (
                    TtActivity.objects.filter(
                        id__in=remaining_parent_ids,
                    )
                    .select_related("week_pattern", "activity_template")
                    .prefetch_related(
                        "student_set",
                    )
                )
                for current_parent_activity in current_parent_activities:
                    current_parent_student_set_ids.update(
                        ss.id for ss in current_parent_activity.student_set.all()
                    )
                    # double check current parent having the same module and template id or not, if no, take either 1 parent to replace it
                    if (
                            child_activity.module_id == current_parent_activity.module_id
                            and child_activity.activity_template_id == current_parent_activity.activity_template_id
                    ):
                        update_module_and_template = False

                # compare remove parent and current parent student set, if remove parent student set got inside current parent student set, no need remove that student set
                # student set id remain in "parent_student_set_ids" means want to remove from child activity
                parent_student_set_ids.difference_update(current_parent_student_set_ids)
                child_activity.name = request.data.get("name")
                child_activity.desc = request.data.get('desc')

                if update_module_and_template:
                    # just get the last parent also can
                    last_parent_activity = current_parent_activities.last()
                    # update this 2 column just incase want to remain the structure that always have module and template in db
                    child_activity.module_id = last_parent_activity.module_id
                    child_activity.activity_template_id = last_parent_activity.activity_template_id

                child_activity.save()

                parent_student_set_ids = list(parent_student_set_ids)

                if parent_student_set_ids:
                    for parent_student_set_id in parent_student_set_ids:
                        student_set_ids.add(parent_student_set_id)
                        if child_activity.scheduled == 1:
                            reschedule_resource_map = True
                            if parent_student_set_id not in student_set_with_affected_week:
                                student_set_with_affected_week[parent_student_set_id] = set()
                            student_set_with_affected_week[parent_student_set_id].update(week_ids)
                    # delete student set relation if got student set is only under the deleted parent
                    TtStudentSetActivity.objects.filter(activity_id=activity_id,student_set_id__in=parent_student_set_ids).delete()
            elif len(remaining_parent_ids) == 1:
                # if only 1 parent, count as delete child, so need remove the parent jta relation also
                last_parent_id = remaining_parent_ids[0]
                track_activity_ids([last_parent_id])
                TtActivity.objects.filter(id=last_parent_id).update(
                    is_jta=0,
                    jta_parent_id=None,
                )
                # append to this parent_activity_ids also, so no need extra handle to redis part
                parent_activity_ids.append(last_parent_id)
                delete_child = True
            else:
                delete_child = True

            if delete_child:
                # before delete activity, get staff/location/student_set with week
                sequencing_activity_ids = [activity_sequencing.id for activity_sequencing in child_activity.sequencing.all()]
                # no matter scheduled or not, also need update redis student set allocated_activity_ids column
                for student_set in child_activity.student_set.all():
                    student_set_ids.add(student_set.id)

                # this part of action when the jta is scheduled only need do reschedule, if no scheduled, can ignore
                if child_activity.scheduled == 1:
                    reschedule_resource_map = True
                    for staff in child_activity.staff.all():
                        if staff.id not in staff_with_affected_week:
                            staff_with_affected_week[staff.id] = set()
                        staff_with_affected_week[staff.id].update(week_ids)
                        staff_ids.add(staff.id)

                    for location in child_activity.location.all():
                        if location.id not in location_with_affected_week:
                            location_with_affected_week[location.id] = set()
                        location_with_affected_week[location.id].update(week_ids)
                        location_ids.add(location.id)

                    for student_set in child_activity.student_set.all():
                        if student_set.id not in student_set_with_affected_week:
                            student_set_with_affected_week[student_set.id] = set()
                        student_set_with_affected_week[student_set.id].update(week_ids)
                        # student_set_ids.add(student_set.id)
                child_activity.delete()


            staff_ids = list(staff_ids)
            location_ids = list(location_ids)
            student_set_ids = list(student_set_ids)

            redis_data = {
                "delete": {},
                "update": {}
            }
            redis_staff_table = "staff"
            redis_location_table = "location"
            redis_student_set_table = "student_set"
            redis_activity_table = "activity"

            # Filter out entries with empty sets to prevent passing {id: []} to the function
            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}
            student_set_with_affected_week = {k: v for k, v in student_set_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,
            )

            if student_set_with_affected_week and reschedule_resource_map:
                # since got update student set, this helper already cater if got update student set, no need pass in student details also will update student resource map
                redis_data = helper_recalculate_and_get_redis_resource_map(
                    redis_data=redis_data,
                    student_set_with_affected_week=student_set_with_affected_week,
                    slot_per_week=slot_per_week,
                )
            else:
                # get_resource_map_redis_data will handle redis allocated_activity_ids, but if not scheduled, wont update this column, so having this handle
                if student_set_ids:
                    student_sets = TtStudentSet.objects.filter(id__in=student_set_ids).prefetch_related("activity")
                    for student_set in student_sets:
                        ss_activity_ids = {ss_activity.id for ss_activity in student_set.activity.all()}
                        redis_data["update"].setdefault(redis_student_set_table, []).append({
                            "id": student_set.id,
                            "allocated_activity_ids": list(ss_activity_ids)
                        })

            if sequencing_activity_ids:
                sequencing_related_activities = TtActivity.objects.filter(id__in=sequencing_activity_ids).prefetch_related("sequencing_from")
                for related_sequencing_activity in sequencing_related_activities:
                    redis_data["update"].setdefault(redis_activity_table, []).append(
                        helper_build_update_redis_sequencing_data(related_sequencing_activity)
                    )

            kafka_update = []
            redis_activity_table = "activity"
            for parent_activity_id in parent_activity_ids:
                data = {
                    "id": parent_activity_id,
                    "is_jta": 0,
                    "jta_parent_id": None
                }

                redis_data["update"].setdefault(redis_activity_table, []).append(data)
                kafka_update.append(data)
            if delete_child:
                redis_data["delete"].setdefault(redis_activity_table, []).append(activity_id)

            # update redis activity
            bulk_sync_to_redis(redis_client, redis_data)

            #kafka
            method = "jta_split"
            kafka_topic = self.kafka_config['MICROSERVICES_TT_TOPIC']
            if kafka_topic and kafka_update and activity_id:
                kafka_request_data = {
                    "session_id": request.user.name,
                    "activity_update": kafka_update,
                }
                if delete_child:
                    kafka_request_data['activity_delete'] = activity_id
                else:
                    # send latest jta child details and student set to microservices tt
                    latest_child_activity = TtActivity.objects.filter(id=activity_id,).prefetch_related("student_set",).first()
                    kafka_request_data['child_activity_update'] = {
                        "id": latest_child_activity.id,
                        "name": latest_child_activity.name,
                        "desc": latest_child_activity.desc,
                        "module_id": latest_child_activity.module_id,
                        "activity_template_id": latest_child_activity.activity_template_id,
                        "student_set": [latest_child_activity_ss.id for latest_child_activity_ss in latest_child_activity.student_set.all()],
                    }
                send_request(kafka_topic, kafka_request_data, None, method)


            # insert audit trail and details
            # can insert the parent first, then only insert details
            name = activity_name

            audit_trail = AuditTrail.objects.create(
                user_id=request.user.id,
                type=self.audit_type,
                ip_address=get_ip(request)
            )
            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
            old_new_data = {
                "old_data": {
                    "jta_id": {
                        "id": activity_id,
                    }
                },
                "new_data": {
                    "activity_ids": parent_activity_ids,
                },
            }
            AuditTrailDetails.custom_insert(
                audit_trail=audit_trail,
                action=action,
                remark_param=remark_param,
                old_data=old_new_data['old_data'],
                new_data=old_new_data['new_data'],
            )
            response = {
                "activity_id": "" if delete_child else activity_id,
            }

            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)
