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.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, \
    TtAcademicTermWeek, 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
from rest_framework.exceptions import ValidationError
from api.translation import __
from backend.kafka import send_request
from backend.redis_client import redis_client


class VariantCreate(AdminApiBase):
    def validate_request(self,request):
        rules = {
            "activity_id": "required|exists:api.TtActivity,id",
            "week_ids": "required|array|exists:api.TtWeek,id",
        }

        # 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

    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["activity_id"]
            week_ids= request.data['week_ids']
            action = "variant_create"

            activity = (
                TtActivity.objects.filter(id=activity_id)
                .select_related("week_pattern","activity_template")
                .prefetch_related(
                    "week",
                    "week_pattern__week",
                    "staff",
                    "location",
                    "student_set",
                    "staff_preset",
                    "staff_suitability",
                    "location_preset",
                    "location_suitability",
                    "sequencing_from",
                    "tag"
                ).first()
            )
            staff_preset_ids = [s.id for s in activity.staff_preset.all()]

            affected_week_pattern = list(TtWeek.objects.filter(id__in=week_ids).values("id","week"))

            academic_term_week_pattern = list(
                TtAcademicTermWeek.objects
                .filter(academic_term_id=activity.academic_term_id)
                .select_related("week")
                .order_by("week__week")
                .values("week_id", "week__week")
            )

            academic_term_week_pattern = [
                {
                    "id": w["week_id"],
                    "week": w["week__week"],
                }
                for w in academic_term_week_pattern
            ]

            new_activity = copy.deepcopy(activity)

            if activity.week_pattern:
                current_week_pattern = list(activity.week_pattern.week.all().values("id","week"))
                change_week_pattern_by = "insert"
            else:
                current_week_pattern = list(activity.week.all().values("id","week"))
                change_week_pattern_by = "delete"
            # remove selected week from current_week_pattern
            week_ids_set = set(week_ids)
            main_week_pattern = [
                w for w in current_week_pattern
                if w["id"] not in week_ids_set
            ]
            # must save new activity first only can change activity, if not after change activity will affect new activity
            # default to 002 first, if checked this activity already is variant, need add a handle make it become 003 or other based on db
            new_code = activity.code + " 002"
            new_activity.id = None
            variant_parent_id = activity.id
            if activity.is_variant == 1:
                # new_activity there the name range of week also need remove when already is variant
                new_activity.name = remove_variant_activity_name_range(new_activity.name)
                parent_code = activity.code
                # when varaint_parent_id is not null, will direct used this value
                if activity.variant_parent_id:
                    variant_parent_id = activity.variant_parent_id
                    parent_activity = TtActivity.objects.filter(id=variant_parent_id).first()
                    parent_code = parent_activity.code
                # when main activity already is variant, need go db get latest code prefix
                base_code = re.sub(r"\s\d{3}$", "", activity.code)
                new_code = base_code + " " + get_next_code_count(base_code)

            new_activity.week_pattern_id = None
            new_activity.code = new_code
            new_activity.name = new_activity.name + " " + format_variant_activity_name_week_pattern_to_ranges(affected_week_pattern, academic_term_week_pattern)
            new_activity.scheduled_start_time = None
            new_activity.scheduled_day = None
            new_activity.scheduled_start_slot = None
            new_activity.scheduled = 0
            new_activity.is_variant = 1
            new_activity.is_jta = 0 # since is direct copy, need force this column be 0
            new_activity.variant_parent_id = variant_parent_id
            new_activity.save()

            if activity.is_variant == 1:
                # when already is variant, no need update code for main record
                new_name = remove_variant_activity_name_range(activity.name)
                activity.name = new_name + " " + format_variant_activity_name_week_pattern_to_ranges(main_week_pattern, academic_term_week_pattern)
            else:
                activity.name = activity.name + " " + format_variant_activity_name_week_pattern_to_ranges(main_week_pattern, academic_term_week_pattern)
                activity.code = activity.code + " 001"
            activity.week_pattern_id = None
            activity.is_variant = 1
            activity.save()
            variant_parent = {
                "id": activity.id,
                "name": activity.name,
                "code": activity.code,
                "week_pattern_id": None,
                "is_variant": 1,
            }

            match change_week_pattern_by:
                case "insert":
                    activity_week_objs = [
                        TtActivityWeek(
                            week_id=val['id'],
                            activity_id=activity.id
                        )
                        for val in main_week_pattern
                    ]
                    TtActivityWeek.objects.bulk_create(activity_week_objs)
                    variant_parent['week_ids'] = [week['id'] for week in main_week_pattern]
                case "delete":
                    TtActivityWeek.objects.filter(activity_id=activity_id,week_id__in=week_ids).delete()
                    variant_parent['delete_week_ids'] = week_ids

            # add relation activity_week and student_set_activity for new activity
            activity_week_objs = [
                TtActivityWeek(
                    week_id=val['id'],  # use existing dict
                    activity_id=new_activity.id
                )
                for val in affected_week_pattern
            ]
            TtActivityWeek.objects.bulk_create(activity_week_objs)

            # since want return function update_m2m_field_bulk, so have this param just for pass in
            old_new_data = {
                "old_data": {},
                "new_data": {},
            }
            tag_ids = list(activity.tag.values_list("tag_id", flat=True))
            if tag_ids:
                self.update_m2m_field(new_activity,'tag',None,tag_ids,old_new_data,False,True)

            staff_preset_ids = [s.id for s in activity.staff_preset.all()]
            if staff_preset_ids:
                self.update_m2m_field(new_activity, 'staff_preset', None, staff_preset_ids, old_new_data)

            staff_suitability_ids = [s.id for s in activity.staff_suitability.all()]
            if staff_suitability_ids:
                self.update_m2m_field(new_activity, 'staff_suitability', None, staff_suitability_ids,old_new_data)

            location_preset_ids = [s.id for s in activity.location_preset.all()]
            if location_preset_ids:
                self.update_m2m_field(new_activity, 'location_preset', None, location_preset_ids,old_new_data)

            location_suitability_ids = [s.id for s in activity.location_suitability.all()]
            if location_suitability_ids:
                self.update_m2m_field(new_activity, 'location_suitability',None,location_suitability_ids, old_new_data)

            activity_sequencing_idata = []
            sequencing_related_ids = set()
            # need include new create activity also, so no need add extra handle for redis
            sequencing_related_ids.add(new_activity.id)
            for val in 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)

            if activity.student_set.exists():
                student_set_activity_objs = [
                    TtStudentSetActivity(
                        student_set_id=val.id,
                        activity_id=new_activity.id
                    )
                    for val in activity.student_set.all()
                ]
                TtStudentSetActivity.objects.bulk_create(student_set_activity_objs)

            staff_with_affected_week = {}
            location_with_affected_week = {}
            student_set_with_affected_week = {}
            staff_ids = set()
            location_ids = set()
            student_set_ids = set()

            # recalculate resource map
            for staff in 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 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 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)

            staff_ids = list(staff_ids)
            location_ids = list(location_ids)
            student_set_ids = list(student_set_ids)
            redis_data = {
                "insert": {},
                "update": {}
            }
            redis_activity_table = "activity"
            redis_staff_table = "staff"
            redis_location_table = "location"
            redis_student_set_table = "student_set"

            main_week = sorted({w["week"] for w in main_week_pattern})
            redis_data["update"].setdefault(redis_activity_table, []).append({
                "id": activity_id,
                "name": activity.name,
                "code": activity.code,
                "week_pattern": convert_to_redis_week(main_week),
                "week_pattern_id": 0,
                "is_variant": activity.is_variant,
            })

            sub_week = sorted({w["week"] for w in affected_week_pattern})
            sub_week_pattern_for_redis = convert_to_redis_week(sub_week)

            # 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}
            # 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,
                staff_with_affected_week=staff_with_affected_week,
                location_with_affected_week=location_with_affected_week,
                student_set_with_affected_week=student_set_with_affected_week,
                slot_per_week=slot_per_week,
            )

            # insert new_activity into redis
            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_activity_data = {}
            for column in redis_activity_column:
                match column:
                    case "week_pattern":
                        redis_activity_data[column] = sub_week_pattern_for_redis
                    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] = new_activity.staff_suitability.values_list("id",flat=True).first() or 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] = new_activity.location_suitability.values_list("id",flat=True).first() or None
                    case "staff_preset":
                        # since already schedule, can direct used scheduled staff as preset
                        redis_activity_data[column] = list(new_activity.staff.values_list("id", flat=True))
                    case "location_preset":
                        # since already schedule, can direct used scheduled location as preset
                        redis_activity_data[column] = list(new_activity.location.values_list("id", flat=True))
                    case "student_sets":
                        redis_activity_data[column] = student_set_ids
                    case "staff_required_no":
                        staff_required_no = new_activity.staff_requirement
                        if staff_required_no == 0:
                            staff_required_no = new_activity.staff.count()
                        redis_activity_data[column] = staff_required_no
                    case "location_required_no":
                        location_required_no = new_activity.location_requirement
                        if location_required_no == 0:
                            location_required_no = new_activity.location.count()
                        redis_activity_data[column] = location_required_no
                    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 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)

            variant_child = model_to_dict(new_activity)
            variant_child = {k: v for k, v in variant_child.items() if v not in [None, [], ""]}

            # unused field in new_activity
            remove_fields = [
                "location",
                "staff"
            ]
            for remove_field in remove_fields:
                variant_child.pop(remove_field, None)
            # kafka push
            fields = [
                "staff_preset",
                "location_preset",
                "staff_suitability",
                "location_suitability",
                "student_set",
                "tag"
            ]

            for field in fields:
                variant_child[field] = [i.id for i in getattr(new_activity, field).all()]
            # need convert it to string, because Time format can be json
            if variant_child.get('suggested_time'):
                variant_child['suggested_time'] = variant_child['suggested_time'].strftime('%H:%M:%S')
            if sub_week:
                variant_child['week'] = sub_week
            if tag_ids:
                variant_child['tag'] = tag_ids

            # no need send sequencing to microservices
            variant_child.pop("sequencing", None)
            method = "variant_create"
            kafka_topic = self.kafka_config['MICROSERVICES_TT_TOPIC']
            if kafka_topic:
                kafka_request_data = {
                    "session_id": request.user.name,
                    "variant_parent": variant_parent,
                    "variant_child": variant_child
                }
                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,
            }

            relation_new_data = old_new_data["new_data"]
            base_new_data = get_add_new_data(new_activity)
            base_new_data["variant_parent"] = variant_parent
            base_new_data["source_activity_id"] = activity_id
            base_new_data["affected_week_ids"] = week_ids
            for k, v in relation_new_data.items():
                if k not in base_new_data:
                    base_new_data[k] = v
            old_new_data["new_data"] = base_new_data

            # 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 = {

            }

            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 get_next_code_count(base_code):
    """
    if current db code is 001,003,004, this function will return 002
    """
    qs = TtActivity.objects.filter(code__startswith=base_code)

    used = set()
    for a in qs.only("code"):
        m = re.search(r"\s(\d+)$", a.code)
        if m:
            used.add(int(m.group(1)))

    i = 1
    while i in used:
        i += 1

    return f"{i:03d}"

