import re
import copy
from itertools import product, combinations

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

from api.helper.activity_sequencing_helper import helper_build_update_sequencing_data, \
    helper_build_update_redis_sequencing_data
from api.models import TtUsagePreference, TtStartPreference, TtSetting, TtPos, TtPosModuleGroupModule, TtPathway, \
    TtPathwayPosModuleGroupModule, TtPosModuleGroup, TtActivity, TtActivityWeek, TtWeek, TtStudentSet, \
    TtActivitySequencing
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, get_next_available_code
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 JtaCreate(AdminApiBase):
    def validate_request(self,request):
        # no need give name option, this is FE control
        rules = {
            "primary_activity_id": "required|exists:api.TtActivity,id",
            "secondary_activity_ids": "required|array|exists:api.TtActivity,id",
            "name": "required",
            "desc": "nullable",
            "size_option": "required",
            "staff_option": "required",
            "location_option": "required",
            "tag_option": "required",
        }

        # 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)

        # custom error after basic validation
        primary_activity_id = request.data["primary_activity_id"]
        secondary_activity_ids = request.data["secondary_activity_ids"]

        activity_ids = list(secondary_activity_ids) + [primary_activity_id]

        # get all activity with all related data needed
        activities = (
            TtActivity.objects.filter(id__in=activity_ids)
            .select_related("week_pattern", "activity_template")
            .prefetch_related(
                "week",
                "week_pattern__week",
                "staff",
                "location",
                "student_set",
                "staff_preset",
                "staff_suitability",
                "location_preset",
                "location_suitability",
                "tag",
                "sequencing_from",
            )
        )

        invalid_activities_name = ""
        scheduled_primary_name = ""
        scheduled_secondary_name = ""
        # separate the primary and secondary activity
        for activity in activities:
            if activity.id == primary_activity_id:
                # this just a safety check, byright FE wont show jta parent record to user
                if activity.is_jta == 1 and activity.jta_parent_id is not None:
                    raise ValidationError({
                        "error": __('validation.jta_parent_cant_do_jta', activity_name=activity.name),
                        "errors": {
                            "primary_activity_id": [
                                __('validation.jta_parent_cant_do_jta', activity_name=activity.name)
                            ]
                        },
                    })
                if activity.scheduled == 1:
                    scheduled_primary_name = activity.name
            else:
                # this just a safety check, byright FE wont show jta parent record to user
                if activity.is_jta == 1 and activity.jta_parent_id is not None:
                    raise ValidationError({
                        "error": __('validation.jta_parent_cant_do_jta', activity_name=activity.name),
                        "errors": {"secondary_activity_ids": [__('validation.jta_parent_cant_do_jta', activity_name=activity.name)]},
                    })
                if activity.scheduled == 1:
                    if scheduled_secondary_name:
                        scheduled_secondary_name += ", "
                    scheduled_secondary_name += activity.name

                if activity.is_jta:
                    if invalid_activities_name:
                        invalid_activities_name += ", "
                    invalid_activities_name += activity.name

        if scheduled_primary_name or scheduled_secondary_name:
            error = ""
            errors = {}
            if scheduled_primary_name:
                error = __('validation.scheduled_activity_not_allow_create_jta', activities_name=scheduled_primary_name)
                errors["primary_activity_id"] = [error]
            if scheduled_secondary_name:
                error = __('validation.scheduled_activity_not_allow_create_jta', activities_name=scheduled_secondary_name)
                errors["secondary_activity_ids"] = [error]

            raise ValidationError({
                "error": error,
                "errors": errors,
            })

        if invalid_activities_name:
            raise ValidationError({
                "error": __('validation.secondary_activity_not_allow_is_jta', activities_name=invalid_activities_name),
                "errors": {"secondary_activity_ids": [__("validation.secondary_activity_not_allow_is_jta",activities_name=invalid_activities_name)]},
            })
        return activities

    def post(self, request):
        # Validate input
        try:
            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"])
            action = "jta_create"
            activities = self.validate_request(request)

            primary_activity_id = request.data["primary_activity_id"]
            secondary_activity_ids = request.data["secondary_activity_ids"]

            activity_ids = list(secondary_activity_ids) + [primary_activity_id]

            primary_activity = None
            secondary_activities = []
            # separate the primary and secondary activity
            for activity in activities:
                if activity.id == primary_activity_id:
                    primary_activity = activity
                else:
                    secondary_activities.append(activity)

            # using set first cause can avoid duplicate id problem
            staff_ids = {s.id for s in primary_activity.staff.all()}
            staff_suitability_ids = {s.id for s in primary_activity.staff_suitability.all()}
            staff_preset_ids = {s.id for s in primary_activity.staff_preset.all()}

            location_ids = {l.id for l in primary_activity.location.all()}
            location_suitability_ids = {l.id for l in primary_activity.location_suitability.all()}
            location_preset_ids = {l.id for l in primary_activity.location_preset.all()}

            student_set_ids = {ss.id for ss in primary_activity.student_set.all()}
            tag_ids = set()
            copy_sequencing_to_child = False
            activity_sequencing_idata = []
            # need to update redis after complete
            sequencing_related_ids = set()
            if request.data.get("tag_option") != "none":
                tag_ids = {t.tag_id for t in primary_activity.tag.all()}
            # when primary is_jta, some action can be skip
            primary_is_jta = False
            if primary_activity.is_jta:
                # merge to primary activity
                new_activity = primary_activity
                new_activity.name = request.data.get("name")
                new_activity.desc = request.data.get("desc")
                primary_is_jta = True
            else:
                # create new activity most of the things is follow primary activity
                new_activity = copy.deepcopy(primary_activity)
                new_activity.pk = None
                new_activity.code = get_next_available_code(TtActivity,"code","MV",10)
                new_activity.name = request.data.get("name")
                new_activity.desc = request.data.get("desc")
                new_activity.is_jta = 1
                # if first time create JTA and have sequencing only need copy it to child
                if primary_activity.sequencing:
                    copy_sequencing_to_child = True

            # will handle relation for new activity also, put in here because want use 1 for loop to complate all
            for secondary_activity in secondary_activities:
                # student_set is must group tgt
                student_set_ids.update(ss.id for ss in secondary_activity.student_set.all())

                if request.data.get("size_option") == "all":
                    new_activity.planned_size += secondary_activity.planned_size
                if request.data.get("staff_option") == "all":
                    staff_ids.update(s.id for s in secondary_activity.staff.all())
                    staff_suitability_ids.update(s.id for s in secondary_activity.staff_suitability.all())
                    staff_preset_ids.update(s.id for s in secondary_activity.staff_preset.all())
                if request.data.get("location_option") == "all":
                    location_ids.update(l.id for l in secondary_activity.location.all())
                    location_suitability_ids.update(l.id for l in secondary_activity.location_suitability.all())
                    location_preset_ids.update(l.id for l in secondary_activity.location_preset.all())
                if request.data.get("tag_option") == "all":
                    tag_ids.update(t.id for t in secondary_activity.tag.all())
            new_activity.save()

            # convert from set to list
            staff_ids = list(staff_ids)
            staff_suitability_ids = list(staff_suitability_ids)
            staff_preset_ids = list(staff_preset_ids)
            location_ids = list(location_ids)
            location_suitability_ids = list(location_suitability_ids)
            location_preset_ids = list(location_preset_ids)
            student_set_ids = list(student_set_ids)
            tag_ids = list(tag_ids)

            # since want return function update_m2m_field_bulk, so have this param just for pass in
            old_new_data = {
                "old_data": {},
                "new_data": {},
            }
            
            redis_week_pattern = []
            if new_activity.week_pattern_id is None:
                # when no preset week_pattern only need do this action
                week_pattern = [a.id for a in primary_activity.week.all()]
                redis_week_pattern = convert_to_redis_week([a.week for a in primary_activity.week.all()])
                # when primary is JTA, no need update m2m field, cause already have data
                if not primary_is_jta:
                    old_new_data = self.update_m2m_field(new_activity, 'week', None, week_pattern, old_new_data)
            if tag_ids:
                old_new_data = self.update_m2m_field(new_activity,'tag',None,tag_ids,old_new_data,False,True)

            if staff_preset_ids:
                old_new_data = self.update_m2m_field(new_activity, 'staff_preset', None, staff_preset_ids, old_new_data)

            if staff_suitability_ids:
                old_new_data = self.update_m2m_field(new_activity, 'staff_suitability', None, staff_suitability_ids,old_new_data)

            if location_preset_ids:
                old_new_data = self.update_m2m_field(new_activity, 'location_preset', None, location_preset_ids,old_new_data)

            if location_suitability_ids:
                old_new_data = self.update_m2m_field(new_activity, 'location_suitability',None,location_suitability_ids, old_new_data)

            if student_set_ids:
                old_new_data = self.update_m2m_field(new_activity, 'student_set', None, student_set_ids, old_new_data)

            if copy_sequencing_to_child:
                # need include new create activity also, so no need add extra handle for redis
                sequencing_related_ids.add(new_activity.id)
                for val in primary_activity.sequencing_from.all():
                    sequencing_related_ids.add(val.sequencing_activity_id)
                    if val.type == TtActivitySequencing.TYPE_TO_CODE["same_time"]:
                        activity_sequencing_idata.extend(
                            helper_build_update_sequencing_data(new_activity.id, val.sequencing_activity_id, "same_time")
                        )
                    elif val.type == TtActivitySequencing.TYPE_TO_CODE["preceding"]:
                        activity_sequencing_idata.extend(
                            helper_build_update_sequencing_data(new_activity.id, val.sequencing_activity_id, "preceding")
                        )
                    elif val.type == TtActivitySequencing.TYPE_TO_CODE["succeeding"]:
                        activity_sequencing_idata.extend(
                            helper_build_update_sequencing_data(new_activity.id, val.sequencing_activity_id, "succeeding")
                        )
                if activity_sequencing_idata:
                    TtActivitySequencing.objects.bulk_create(activity_sequencing_idata)

            # update old activity is_jta and jta_parent_id = new_activity.id
            update_activity = []
            ids = secondary_activity_ids if primary_is_jta else activity_ids

            track_activity_ids(ids)
            TtActivity.objects.filter(id__in=ids).update(
                is_jta=1,
                jta_parent_id=new_activity.id
            )

            update_activity = [
                {"id": i, "is_jta": 1, "jta_parent_id": new_activity.id} 
                for i in ids
            ]
            old_new_data["new_data"] = get_add_new_data(new_activity)
            old_new_data["new_data"]["activity_ids"] = activity_ids
            redis_activity_column = [
                "id", "code", "name", "desc", "planned_size", "real_size", "slot_required", "activity_type_id",
                "duration", "suggested_day", "suggested_time_slot", "suggested_time","scheduled_start_slot",
                "department_id", "module_id", "academic_term_id",
                "activity_template_id", "week_pattern_id", "week_pattern", "availability_id", "availability_pattern",
                "start_preference_id",
                "start_preference_pattern", "usage_preference_id", "usage_preference_pattern", "student_sets",
                "staff_requirement_type", "staff_required_no", "staff_preset", "staff_suitability_id",
                "location_requirement_type", "location_required_no", "location_preset", "location_suitability_id",
                "is_jta", "jta_parent_id", "is_variant", "variant_parent_id", "status", "scheduled"
            ]
            redis_data = {
                "insert": {},
                "update": {}
            }
            redis_activity_table = "activity"
            redis_activity_data = {}
            redis_student_set_table = "student_set"

            for secondary_activity_id in secondary_activity_ids:
                redis_data["update"].setdefault(redis_activity_table, []).append({
                    "id": secondary_activity_id,
                    "is_jta": 1,
                    "jta_parent_id": new_activity.id
                })
            if not primary_is_jta:
                redis_data["update"].setdefault(redis_activity_table, []).append({
                    "id": primary_activity.id,
                    "is_jta": 1,
                    "jta_parent_id": new_activity.id
                })

            for column in redis_activity_column:
                match column:
                    case "week_pattern":
                        redis_activity_data[column] = redis_week_pattern
                    case "staff_suitability_id":
                        # 2025-12-17 request for redis side store as int(only can have 1 suitability) first
                        # redis_activity_data[column] = list(current_activity_template.staff_suitability.values_list("id",flat=True))
                        redis_activity_data[column] = staff_suitability_ids[0] if staff_suitability_ids else None
                    case "location_suitability_id":
                        # 2025-12-17 request for redis side store as int(only can have 1 suitability) first
                        # redis_activity_data[column] = list(current_activity_template.location_suitability.values_list("id",flat=True))
                        redis_activity_data[column] = location_suitability_ids[0] if location_suitability_ids else None
                    case "staff_preset":
                        redis_activity_data[column] = staff_preset_ids
                    case "location_preset":
                        redis_activity_data[column] = location_preset_ids
                    case "student_sets":
                        # when create dun have student set, so can force to empty list
                        redis_activity_data[column] = student_set_ids
                    case "staff_required_no":
                        redis_activity_data[column] = new_activity.staff_requirement
                    case "location_required_no":
                        redis_activity_data[column] = new_activity.location_requirement
                    case "slot_required":
                        redis_activity_data[column] = int(new_activity.duration / minute_per_slot)
                    case "usage_preference_id" | "availability_id" | "start_preference_id" | "week_pattern_id":
                        # jose request if no id, dun store null, want to store "0"
                        value = getattr(new_activity, column)
                        redis_activity_data[column] = 0 if value in (None, "") else value
                    case "suggested_time":
                        value = getattr(new_activity, column)
                        # Convert time object to string "HH:MM" if it exists, otherwise None
                        redis_activity_data[column] = value.strftime("%H:%M") if value else None
                    case _:
                        redis_activity_data[column] = getattr(new_activity, column)
            redis_data["insert"].setdefault(redis_activity_table, []).append(redis_activity_data)

            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 copy_sequencing_to_child and sequencing_related_ids:
                sequencing_related_activities = TtActivity.objects.filter(id__in=sequencing_related_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)
                    )

            # update redis activity
            bulk_sync_to_redis(redis_client, redis_data)

            # kafka push
            create_activity = model_to_dict(new_activity)
            create_activity.pop("updated_by", None)
            create_activity = {k: v for k, v in create_activity.items() if v not in [None, [], ""]}
            fields = [
                "staff_preset",
                "location_preset",
                "staff_suitability",
                "location_suitability",
                "student_set",
                "week"
            ]

            for field in fields:
                create_activity[field] = [i.id for i in getattr(new_activity, field).all()]
            if create_activity.get('suggested_time'):
                create_activity['suggested_time'] = create_activity['suggested_time'].strftime('%H:%M:%S')
            if tag_ids:
                create_activity['tag'] = tag_ids

            # no need send sequencing to microservices
            create_activity.pop("sequencing", None)

            method = "jta_create"
            kafka_topic = self.kafka_config['MICROSERVICES_TT_TOPIC']
            if kafka_topic:
                kafka_request_data = {
                    "session_id": request.user.name,
                    "create_activity": create_activity,
                    "update_activity": update_activity,
                }
                send_request(kafka_topic, kafka_request_data, None, method)

            # insert audit trail and details
            # can insert the parent first, then only insert details
            name = new_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
            AuditTrailDetails.custom_insert(
                audit_trail=audit_trail,
                action=action,
                remark_param=remark_param,
                new_data=old_new_data['new_data'],
            )
            response = {
                "activity_id": new_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)
