from collections import defaultdict

from django.db import transaction
from rest_framework import serializers, status
from api.models import TtSetting, TtStudentSet, TtPosModuleGroupModule, TtPathway, TtStudentPathway, TtWeek,TtStudentResourceMap
from api.models.audit_trail import AuditTrail
from api.models.audit_trail_details import AuditTrailDetails
from api.models.tt_student import TtStudent
from api.views.admin.base import AdminApiBase
from api.validator import BaseValidator
from api.utils import decrypt_aes_128_cbc, encrypt_aes_128_cbc, generate_kafka_data, log_critical_error, get_exception_detail, \
    get_add_new_data, get_ip, bulk_sync_to_redis, get_slot_default_pattern, get_next_available_code
from rest_framework.exceptions import ValidationError
from api.translation import __
from backend.kafka import send_request
from backend.redis_client import redis_client


class StudentCreate(AdminApiBase):
    def validate_request(self,request,settings):
        slot_per_week = settings["slot_per_week"]
        rules = {
            "code": "nullable|not_exists:api.TtStudent,code",
            "name": "required",
            "desc": "nullable",
            "email": "nullable",
            "department_id": "nullable|exists:api.TtDepartment,id",
            "academic_term_ids": "nullable|array|exists:api.TtAcademicTerm,id",
            "pos_ids": "nullable|array|exists:api.TtPos,id",
            "pos_module_ids": "nullable|array|exists:api.TtPosModuleGroupModule,id",
            "tag": "nullable|array|exists:api.TtTag,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)

    def post(self, request):
        # Validate input
        try:
            setting_params = {
                "slot_per_week",
            }
            settings = TtSetting.get_multiple_setting(setting_params)
            slot_per_week = settings["slot_per_week"]
            action = "student_create"
            self.validate_request(request,settings)

            old_new_data = {
                "old_data": None,
                "new_data": None,
            }

            idata = {}
            # not insert to student table
            ignore_field = ['pos_ids','pos_module_ids','tag','timestamp','signature']
            valid_fields = {f.column for f in TtStudent._meta.fields}

            for key, val in request.data.items():
                if key not in ignore_field:
                    match key:
                        case _:
                            if key in valid_fields:#for insert, key can check in default part, cause if got special handle, i can check the way to use key also
                                # default the key will same as db column name
                                idata[key] = val

            # if FE no pass in code or is null or empty, will used default code
            code = request.data.get("code")
            if not code:
                idata["code"] = get_next_available_code(TtStudent, "code", "MV", 10)
            idata['status'] = TtStudent.STATUS_TO_CODE['active']
            idata['created_by'] = request.user.id

            student = TtStudent.objects.create(**idata)

            old_new_data['new_data']=get_add_new_data(student)

            academic_term_ids = request.data.get("academic_term_ids", [])
            old_new_data = self.update_m2m_field(student, 'academic_term', None, academic_term_ids, old_new_data)

            pos_ids = request.data.get("pos_ids", [])
            old_new_data = self.update_m2m_field(student, 'pos', None, pos_ids, old_new_data)

            pos_module_ids = request.data.get("pos_module_ids", [])
            old_new_data = self.update_m2m_field(student, 'pos_module', None, pos_module_ids, old_new_data)

            new_ids = request.data.get("tag", [])
            old_new_data = self.update_m2m_field(student,'tag',None,new_ids,old_new_data,False,True)

            # check need to update student pathway or not
            if pos_ids and pos_module_ids:
                student_pathway_idata = []
                batch_size = 500
                pos_module_group_module = TtPosModuleGroupModule.objects.filter(id__in=pos_module_ids).select_related("pos_module_group")
                # group pos with pos_module_id by pos_id as key
                pos_module_group_module_map = {}
                for val in pos_module_group_module:
                    pos_module_group_module_map.setdefault(val.pos_module_group.pos_id,[]).append(val.id)

                pathway_string_list = []
                for key,val in pos_module_group_module_map.items():
                    pathway_string = ";".join(map(str, sorted(val)))
                    pathway_string_list.append(pathway_string)

                pathways = TtPathway.objects.filter(pathway_string__in=pathway_string_list)
                for pathway in pathways:
                    student_pathway_idata.append({
                        "student_id": student.id,
                        "pathway_id": pathway.id,
                    })

                if student_pathway_idata:
                    student_pathway_objects = [TtStudentPathway(**data) for data in student_pathway_idata]
                    with transaction.atomic():
                        for i in range(0, len(student_pathway_objects), batch_size):
                            TtStudentPathway.objects.bulk_create(
                                student_pathway_objects[i:i + batch_size],
                                batch_size=batch_size,
                                ignore_conflicts=True
                            )

            # handle resource map
            weeks_data = TtWeek.objects.filter(ttacademicterm__id__in=academic_term_ids).order_by("week").distinct()
            resource_map_data = []
            resource_map_redis_data = {}
            default_resource_map_pattern = "0" * int(slot_per_week)
            for val in weeks_data:
                resource_map_data.append({
                    "student_id": student.id,
                    "week_id": val.id,
                    "pattern": default_resource_map_pattern
                })
                # redis week is start from 0, 0 means week 1
                redis_week = int(val.week) - 1
                resource_map_redis_data[redis_week] = default_resource_map_pattern

            if resource_map_data:
                resource_map_object = [TtStudentResourceMap(**data) for data in resource_map_data]
                TtStudentResourceMap.objects.bulk_create(resource_map_object)

            redis_student_table = "student"
            redis_student_column = [
                "id", "code", "name", "email", "department_id",
                "academic_term_ids", "pos_ids", "pos_module",
                "student_set_ids", "resource_map"
            ]
            redis_data = {
                "insert": {},
            }
            redis_student_data = {}
            pos_module_redis_data = []
            if pos_module_ids:
                pos_module_group_module = TtPosModuleGroupModule.objects.filter(
                    id__in=pos_module_ids
                ).prefetch_related(
                    "pos_module_group",
                ).select_related("module")
                for pos_module in pos_module_group_module:
                    pos_module_redis_data.append({
                        "module_id": pos_module.module_id,
                        "pos_id": pos_module.pos_module_group.pos_id,
                    })
            for column in redis_student_column:
                match column:
                    case "academic_term_ids":
                        redis_student_data[column] = academic_term_ids or []
                    case "pos_ids":
                        redis_student_data[column] = pos_ids or []
                    case "pos_module":
                        redis_student_data[column] = pos_module_redis_data
                    case "student_set_ids":
                        # allocation only have student set ids
                        redis_student_data[column] = []
                    case "resource_map":
                        redis_student_data[column] = resource_map_redis_data
                    case _:
                        redis_student_data[column] = getattr(student, column)
            redis_data["insert"].setdefault(redis_student_table, []).append(redis_student_data)
            bulk_sync_to_redis(redis_client, redis_data)

            # kafka push
            method = "student_create"
            kafka_topic = self.kafka_config['MICROSERVICES_TT_TOPIC']
            new_data = old_new_data.get("new_data")
            kafka_student = []
            if new_data and kafka_topic:
                relation_fields = [
                    "academic_term",
                    "pos",
                    "tag",
                ]
                kafka_student = generate_kafka_data(new_data, relation_fields)
                # custom handle for pos_module data in microservices
                kafka_student["pos_module"] = []
                if pos_module_ids:
                    pos_module_group_module = TtPosModuleGroupModule.objects.filter(
                        id__in=pos_module_ids
                    ).prefetch_related(
                        "pos_module_group",
                    ).select_related("module")
                    for pos_module in pos_module_group_module:
                        kafka_student["pos_module"].append({
                            "pos_module_group_module_id": pos_module.id,
                            "pos_id": pos_module.pos_module_group.pos_id,
                            "module_group_id": pos_module.pos_module_group.module_group_id,
                            "module_id": pos_module.module.id
                        })
                kafka_request_data = {
                    "session_id": request.user.name,
                    "student": kafka_student,
                }
                send_request(kafka_topic, kafka_request_data, None ,method)

            # 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":student.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 = {

            }

            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)