import datetime
import math
import re

from django.db.models import Count, Q, Prefetch
from django.forms.models import model_to_dict
from rest_framework import serializers, status
from api.models import TtUsagePreference, TtSetting, TtActivityTemplate
from api.models.audit_trail import AuditTrail
from api.models.audit_trail_details import AuditTrailDetails
from api.models.tt_academic_term import TtAcademicTerm
from api.models.tt_activity import TtActivity
from api.models.tt_week import TtWeek
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, get_missing_code_from_db, get_auto_generate_name, \
    get_missing_name, convert_to_redis_week, get_clean_activity_name, get_auto_generate_code
from rest_framework.exceptions import ValidationError
from api.translation import __
from backend import settings
from backend.kafka import send_request
from backend.redis_client import redis_client


class ActivityGenerate(AdminApiBase):
    def validate_request(self,request):
        status_code_string = ",".join(str(v) for v in TtAcademicTerm.STATUS_TO_CODE.values())
        rules = {
            "activity_template_id": "required|array|not_exists:api.TtActivityTemplate,code",
        }

        # 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:
            setting_params = {
                "minute_per_slot",
            }
            settings = TtSetting.get_multiple_setting(setting_params)
            minute_per_slot = int(settings["minute_per_slot"])
            self.validate_request(request)
            existing_activity_exclude_condition = TtActivity.objects.exclude(
                Q(is_jta=1, jta_parent_id__isnull=True) |
                Q(is_variant=1, variant_parent_id__isnull=False)
            )
            # get activity template
            activity_templates = (
                TtActivityTemplate.objects
                .select_related("department", "academic_term", "module", "week_pattern","activity_type", "zone", "availability","start_preference", "usage_preference")
                .prefetch_related(
                    "week",
                    "staff_suitability",
                    "staff",
                    "location_suitability",
                    "location",
                    "tag__tag",
                    Prefetch("ttactivity_set", queryset=existing_activity_exclude_condition)
                    # tags means the relation add in models, __tag means the relation table tag_id get the tag object #2
                )
                # .annotate(activity_count=Count("ttactivity"))
                .filter(id__in=request.data.get("activity_template_id"))
            )
            # make it key by id
            activity_templates_map = {x.id: x for x in activity_templates}

            action="activity_generate" # this is for audit log only

            missing_code = get_missing_code_from_db(self.default_code_prefix,"tt_activity",10)
            last_activity = TtActivity.objects.filter(code__startswith=self.default_code_prefix).order_by('-code').first()
            last_code = last_activity.code if last_activity else None
            kafka_activities = []
            for activity_template in activity_templates:
                old_new_data = {
                    "old_data": {},
                    "new_data": {},
                }

                if activity_template:
                    module_size = activity_template.module.planned_size if activity_template.module else 0
                    required_activity = math.ceil(module_size / activity_template.planned_size) if module_size > 0 and activity_template.planned_size > 0 else 0
                    existing_activity = activity_template.ttactivity_set.count()
                    need_to_generate = max(required_activity - existing_activity,0)

                    activity_name_prefix = activity_template.name+"/"
                    # get the name with match with prefix
                    existing_activity_name = list(
                        activity_template.ttactivity_set.filter(name__startswith=activity_name_prefix)
                        .values_list("name", flat=True)
                    )
                    # get all missing activity name with same prefix as activity template name
                    missing_activity_name = get_missing_name(existing_activity_name,activity_name_prefix,2)
                    last_activity_name = None
                    # get highest activity name from existing activity name
                    if existing_activity_name:
                        # remove the name not end with number
                        cleaned_existing_activity_name = [get_clean_activity_name(val) for val in existing_activity_name]
                        # last_activity_name = max((n for n in cleaned_existing_activity_name if n.startswith(activity_name_prefix)), key=lambda x: int(x.split('/')[-1]))
                        def extract_number(name):
                            last_part = name.split('/')[-1]
                            match = re.search(r'\d+', last_part)
                            return int(match.group()) if match else 0
                        last_activity_name = max(
                            (n for n in cleaned_existing_activity_name if n.startswith(activity_name_prefix)),
                            key=extract_number,
                            default=None
                        )
                    if need_to_generate > 0:
                        idata = []
                        for count in range(need_to_generate):
                            # handle missing or latest activity name
                            if missing_activity_name:
                                activity_name = missing_activity_name[0]
                                missing_activity_name.pop(0)
                            else:
                                last_activity_name = get_auto_generate_name(last_activity_name,activity_name_prefix,2)
                                activity_name = last_activity_name

                            # handle missing or latest activity code
                            if missing_code:
                                new_code = missing_code[0]
                                missing_code.pop(0)
                            else:
                                last_code = get_auto_generate_code(last_code,self.default_code_prefix,10)
                                new_code = last_code

                            if activity_template.duration != 0:
                                slot_required = activity_template.duration // minute_per_slot
                            else:
                                slot_required = 0
                            new_data = {
                                "code": new_code,
                                "name": activity_name,
                                "desc": activity_template.desc,
                                "academic_term_id": activity_template.academic_term_id,
                                "activity_type_id": activity_template.activity_type_id,
                                "activity_template_id": activity_template.id,
                                "department_id": activity_template.department_id,
                                "sequence_number": activity_template.sequence_number,
                                "zone_id": activity_template.zone_id,
                                "module_id": activity_template.module_id,
                                "duration": activity_template.duration,
                                "slot_required": slot_required,
                                "planned_size": activity_template.planned_size,
                                "real_size": 0,
                                "staff_requirement": activity_template.staff_requirement,
                                "staff_requirement_type": activity_template.staff_requirement_type,
                                "location_requirement": activity_template.location_requirement,
                                "location_requirement_type": activity_template.location_requirement_type,
                                "scheduled_start_time": None,
                                "scheduled_day": None,
                                "suggested_day": None,
                                "suggested_time": None,
                                "week_pattern_id": activity_template.week_pattern_id,
                                "availability_id": activity_template.availability_id,
                                "availability_pattern": activity_template.availability_pattern,
                                "start_preference_id": activity_template.start_preference_id,
                                "start_preference_pattern": activity_template.start_preference_pattern,
                                "usage_preference_id": activity_template.usage_preference_id,
                                "usage_preference_pattern": activity_template.usage_preference_pattern,
                                "status": TtActivity.STATUS_TO_CODE['active'],
                            }
                            for i in range(1, 11):
                                extra_data_field = f"extra_data_{i}"
                                new_data[extra_data_field] = getattr(activity_template, extra_data_field, None)

                            idata.append(new_data)

                        new_activity = TtActivity.objects.bulk_create(
                            [TtActivity(**data) for data in idata],
                        )

                        week_pattern = list(activity_template.week.values_list("id", flat=True))
                        week_pattern_for_redis = convert_to_redis_week(list(activity_template.week.values_list("week", flat=True)))

                        if week_pattern:
                            old_new_data = self.update_m2m_field_bulk(new_activity, 'week', week_pattern, old_new_data)

                        staff_preset_ids = [s.id for s in activity_template.staff.all()]
                        if staff_preset_ids:
                            old_new_data = self.update_m2m_field_bulk(new_activity, 'staff_preset', staff_preset_ids, old_new_data)

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

                        location_preset_ids = [s.id for s in activity_template.location.all()]
                        if location_preset_ids:
                            old_new_data = self.update_m2m_field_bulk(new_activity, 'location_preset', location_preset_ids,old_new_data)

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

                        new_ids = list(activity_template.tag.values_list("tag_id", flat=True)) if activity_template else []
                        if new_ids:
                            old_new_data = self.update_m2m_field_bulk(new_activity, 'tag', new_ids, old_new_data,False, True)

                        redis_activity_table = "activity"
                        # from here make it same name as db first, got different, only do extra handle
                        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","sequence_number","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": {},
                        }
                        new_activity_ids = [a.id for a in new_activity]
                        # get 1 more time latest activity, if not every activity.staff.all() will call 1 time db to get data
                        latest_activities = (
                            TtActivity.objects.filter(id__in=new_activity_ids)
                            .prefetch_related(
                                "location_preset",
                                "location_suitability",
                                "staff_preset",
                                "staff_suitability",
                            )
                        )
                        # need loop new_activity to insert every created record
                        for activity in latest_activities:
                            redis_activity_data = {}
                            for column in redis_activity_column:
                                match column:
                                    case "week_pattern":
                                        redis_activity_data[column] = 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] = activity.staff_suitability.values_list("id",flat=True).first() or None
                                        suitability_list = list(activity.staff_suitability.all())
                                        redis_activity_data[column] = suitability_list[0].id if suitability_list 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] = activity.location_suitability.values_list("id",flat=True).first() or None
                                        suitability_list = list(activity.location_suitability.all())
                                        redis_activity_data[column] = suitability_list[0].id if suitability_list else None
                                    case "staff_preset":
                                        redis_activity_data[column] = [s.id for s in activity.staff_preset.all()]
                                    case "location_preset":
                                        redis_activity_data[column] = [l.id for l in activity.location_preset.all()]
                                    case "student_sets":
                                        # when create dun have student set, so can force to empty list
                                        redis_activity_data[column] = []
                                    case "staff_required_no":
                                        redis_activity_data[column] = activity.staff_requirement
                                    case "location_required_no":
                                        redis_activity_data[column] = activity.location_requirement
                                    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(activity, column)
                                        redis_activity_data[column] = 0 if value in (None, "") else value
                                    case _:
                                        redis_activity_data[column] = getattr(activity, column)
                            redis_data["insert"].setdefault(redis_activity_table, []).append(redis_activity_data)

                            # grouping for kafka
                            kafka_data = model_to_dict(activity)
                            kafka_data['week'] = week_pattern
                            
                            fields = [
                                ("staff_preset", activity.staff_preset),
                                ("location_preset", activity.location_preset),
                                ("staff_suitability", activity.staff_suitability),
                                ("location_suitability", activity.location_suitability),
                            ]

                            for key, queryset in fields:
                                objs = queryset.all()
                                if objs:
                                    kafka_data[key] = [o.id for o in objs]

                            if new_ids:
                                kafka_data['tag'] = new_ids
                            # pop None
                            kafka_data = {k: v for k, v in kafka_data.items() if v not in [None, [], ""]}
                            kafka_activities.append(kafka_data)

                        bulk_sync_to_redis(redis_client, redis_data)

                        # insert audit trail and details
                        # can insert the parent first, then only insert details
                        audit_trail = AuditTrail.objects.create(
                            user_id=request.user.id,
                            type=self.audit_type,
                            ip_address=get_ip(request)
                        )
                        remark_param = {
                            "name":activity_template.name,
                            "activity_count": len(new_activity)
                        }

                        # 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'],
                        )

            # kafka push
            method = "activity_generate"
            kafka_topic = self.kafka_config['MICROSERVICES_TT_TOPIC']
            if kafka_topic:
                kafka_request_data = {
                    "session_id": request.user.name,
                    "activity": kafka_activities,
                }
                send_request(kafka_topic, kafka_request_data, None, method)
            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)