from itertools import product, combinations

from django.db import transaction
from django.db.models import Prefetch
from rest_framework import serializers, status
from api.models import TtUsagePreference, TtStartPreference, TtSetting, TtPos, TtPosModuleGroupModule, TtPathway, \
    TtPathwayPosModuleGroupModule, TtPosModuleGroup, TtStudent, TtStudentPosModule, TtStudentPathway
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
from rest_framework.exceptions import ValidationError
from api.translation import __
from backend.redis_client import redis_client


class PathwayGenerateByStudentChoice(AdminApiBase):
    def validate_request(self,request):
        rules = {
            "pos_ids": "required|array|exists:api.TtPos,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:
            self.validate_request(request)
            pos_ids = request.data.get("pos_ids")
            pos = TtPos.objects.filter(id__in=pos_ids)
            pos_pathway = TtPathway.objects.filter(pos_id__in=pos_ids)
            filter_for_pos_modules = TtStudentPosModule.objects.filter(
                pos_module_group_module__pos_module_group__pos_id__in=pos_ids
            ).select_related("pos_module_group_module__module")

            students = TtStudent.objects.filter(pos__id__in=pos_ids).prefetch_related(
                "pos",
                Prefetch(
                    "ttstudentposmodule_set",
                    queryset=filter_for_pos_modules
                )
            )
            action="pathway_generate_by_student_choice" # this is for audit log only

            existing_pathway_string_set = {}
            generated_pathway_string = set()
            # loop pos pathway to make pathway_string_1 become list
            for val in pos_pathway:
                if val.pathway_string:
                    existing_pathway_string_set[val.pathway_string] = val.id

            pathway_idata = []
            student_pathway_idata = []
            # if pathway is new generate, will depends on this value to assign student_pathway_idata
            student_pathway_string = {}

            for student in students:
                pos_module_group_module_ids = []
                module_ids = []
                credit = 0
                # since 1 academic term only have 1 pos, when multiple pos_ids pass in from FE, also in same academic term
                for student_pos in student.pos.all():
                    # since is using pos_ids to get, so must have pos_id and academic_term_id
                    if student_pos.id in pos_ids:
                        pos_id = student_pos.id
                        academic_term_id = student_pos.academic_term_id
                        break

                # calculate pathway table needed data
                for student_pos_module in student.ttstudentposmodule_set.all():
                    pos_module_group_module_ids.append(student_pos_module.pos_module_group_module_id)
                    module_ids.append(student_pos_module.pos_module_group_module.module.id)
                    credit+=student_pos_module.pos_module_group_module.module.credit_provided

                pathway_string = ";".join(map(str, sorted(pos_module_group_module_ids)))
                # if pathway not exists in db only will create the pathway
                if not existing_pathway_string_set.get(pathway_string):
                    student_pathway_string.setdefault(pathway_string, []).append(student.id)
                    if pathway_string not in generated_pathway_string:
                        generated_pathway_string.add(pathway_string)
                        pathway_string_2 = ";".join(map(str, sorted(module_ids)))
                        new_data = {
                            "pos_id": pos_id,
                            "academic_term_id": academic_term_id,
                            "pathway_string": pathway_string,
                            "pathway_string_2": pathway_string_2,
                            "credit": credit,
                            "planned_size": 0,
                            "status": TtPathway.STATUS_TO_CODE['active']
                        }
                        pathway_idata.append(new_data)
                else:
                    # if pathway already exists, just assign student into pathway
                    pathway_id = existing_pathway_string_set.get(pathway_string)
                    student_pathway_idata.append({
                        "pathway_id": pathway_id,
                        "student_id": student.id
                    })

            batch_size = 500
            inserted_pathways = []
            if pathway_idata:
                # this transaction.atomic() is when got error will rollback
                with transaction.atomic():
                    # convert dicts -> model instances, if not object cant used bulk_create
                    pathway_objects = [TtPathway(**data) for data in pathway_idata]
                    for i in range(0, len(pathway_objects), batch_size):
                        batch = TtPathway.objects.bulk_create(
                            pathway_objects[i:i + batch_size],
                            batch_size=batch_size
                        )
                        inserted_pathways.extend(batch)
            pathway_module_relation_idata = []
            # loop the pathway, get pathway_string, having relation_idata to insert relation for pathway_pos_module_group_module
            if inserted_pathways:
                for inserted_pathway in inserted_pathways:
                    # convert back pathway_string to array
                    pathway_string = inserted_pathway.pathway_string
                    pathway_arr = [int(x) for x in inserted_pathway.pathway_string.split(";")]
                    assigned_student_ids = student_pathway_string.get(pathway_string, [])
                    student_count = len(assigned_student_ids)
                    inserted_pathway.planned_size = student_count
                    # assign student pathway relation
                    for student_id in assigned_student_ids:
                        student_pathway_idata.append({
                            "pathway_id": inserted_pathway.id,
                            "student_id": student_id
                        })
                    # assign pathway pos module group module relation
                    for pos_module_group_module_id in pathway_arr:
                        relation_data = {
                            "pathway_id": inserted_pathway.id,
                            "pos_module_group_module_id": pos_module_group_module_id,
                        }
                        pathway_module_relation_idata.append(relation_data)
                TtPathway.objects.bulk_update(inserted_pathways, fields=['planned_size'])

                if pathway_module_relation_idata:
                    # convert dicts -> model instances, if not object cant used bulk_create
                    pathway_module_relation_objects = [TtPathwayPosModuleGroupModule(**data) for data in pathway_module_relation_idata]
                    # this transaction.atomic() is when got error will rollback
                    with transaction.atomic():
                        for i in range(0, len(pathway_module_relation_objects), batch_size):
                            TtPathwayPosModuleGroupModule.objects.bulk_create(
                                pathway_module_relation_objects[i:i + batch_size],
                                batch_size=batch_size,
                                ignore_conflicts=True
                            )

            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
                        )

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

            #get ids of inserted pathway to show in audit trail
            pathway_ids = []
            for inserted_pathway in inserted_pathways:
                pathway_ids.append(inserted_pathway.id)

            name = ",".join(pos.values_list("name", flat=True))
            remark_param = {
                "name":name,
                "mode":"auto",
            }

            # keep track of which pos generated the pathway ids
            pathway_audit_data = {
                "pos_ids": request.data.get('pos_ids'),
                "pathway_ids": pathway_ids,
            }

            # 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=pathway_audit_data,
            )
            response = {
                "pathway_generated": len(pathway_idata)
            }

            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 calculate_credit(modules,target_credit):
    """
    :param modules: all module from 1 pos module group
    :param credit_requirement: the number of credit needed
    :return:
    """
    result = []
    for r in range(1, len(modules) + 1):
        for combo in combinations(modules, r):
            total_credit = sum(m["credit"] for m in combo)
            if total_credit == target_credit:
                result.append([m["id"] for m in combo])
    return result

def calculate_credit_pathway(modules_info, credit_requirement):
    """
    :param modules_info: data key by pos_module_group_id and value with array of id and credit
    :param credit_requirement: data key by pos_module_group_id and value with credit requirement for this group
    :return: all unique pathway in array, but if got 1 module group no pathway can select, will return empty so wont have next action
    """
    fulfil_group = {}
    for pos_module_group_id, required_credit in credit_requirement.items():
        modules = modules_info.get(pos_module_group_id, [])
        if modules:
            combos = calculate_credit(modules, required_credit)
            if combos:
                fulfil_group[pos_module_group_id] = combos
            else:
                # got 1 group not fulfil, direct return [], so no more next step
                return []

    # merge from all group to make it become a pathway
    all_pathways = []
    for combo in product(*fulfil_group.values()):
        merged = sorted([m for group in combo for m in group])
        all_pathways.append(merged)
    # remove duplicate group
    unique_pathways = [list(t) for t in sorted({tuple(p) for p in all_pathways})]

    return unique_pathways
