from itertools import product, combinations

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

from api.helper.pathway_helper import helper_pathway_generate_assign_student_to_pathway
from api.models import TtUsagePreference, TtStartPreference, TtSetting, TtPos, TtPosModuleGroupModule, TtPathway, \
    TtPathwayPosModuleGroupModule, TtPosModuleGroup
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 PathwayManualGenerate(AdminApiBase):
    def validate_request(self,request):
        pathway_generate_string = ",".join(str(v) for v in TtPathway.PATHWAY_GENERATE_TYPE)
        rules = {
            "pos_id": "required|exists:api.TtPos,id",
            "pos_module_group_module_ids": "required|array",
            "planned_size": "nullable",
        }

        # 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
        # array must in self pos module group module id
        try:
            # get pos_module_group_ids from db
            pos_module_group_module_ids = list(TtPosModuleGroupModule.objects.filter(pos_module_group_id__pos_id=request.data['pos_id']).values_list("id",flat=True))
            for pos_module_group_module_id in request.data['pos_module_group_module_ids']:
                if pos_module_group_module_id not in pos_module_group_module_ids:
                    raise ValidationError()

        except Exception as e:
            # any error get from here throw invalid format error
            raise ValidationError({
                "error": __('validation.invalid_format'),
                "errors": {"pathway_requirement": __("validation.invalid_format")}
            })

    def post(self, request):
        # Validate input
        try:
            self.validate_request(request)
            pos = TtPos.objects.get(id=request.data['pos_id'])

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

            # FE might give data like requirement []
            pos_modules = TtPosModuleGroupModule.objects.filter(pos_module_group__pos_id=pos.id).select_related("pos_module_group__module_group","module").order_by('id')
            modules_info = {}
            pos_module_group_module_ids = {}
            pos_module_group_module_with_credit = {}
            # form the data used for make pathway
            for pos_module in pos_modules:
                module_data = {
                    "id": pos_module.id,
                    "pos_module_group_id": pos_module.pos_module_group_id,
                    "module_group_id": pos_module.pos_module_group.module_group.id,
                    "module_group_name": pos_module.pos_module_group.module_group.name,
                    "module_id": pos_module.module.id,
                    "module_name": pos_module.module.name,
                    "credit_provided": pos_module.module.credit_provided
                }
                modules_info[pos_module.id] = module_data
                pos_module_group_module_ids.setdefault(pos_module.pos_module_group_id, []).append(pos_module.id)
                # when type is credit will used this param
                pos_module_group_module_with_credit.setdefault(pos_module.pos_module_group_id, []).append(
                    {
                        "id":pos_module.id,
                        "credit":pos_module.module.credit_provided
                    }
                )

            # almost same function from pathway_generate, just the generated_pathway is direct based on FE array to decide
            generated_pathway = request.data['pos_module_group_module_ids']

            # need get current pos all pathway
            existing_pathway_string = list(TtPathway.objects.filter(pos_id=pos.id).values_list("pathway_string", flat=True))

            pathway_idata = []
            credit = 0
            # just safety purpose to sorted again
            generated_pathway = sorted(generated_pathway)
            pathway_string = ";".join(map(str, generated_pathway))
            # wont insert repeated pathway
            if pathway_string not in existing_pathway_string:
                # used to generate pathway_string_2
                used_module_ids = set()
                for val in generated_pathway:
                    # need to get module id from modules_info
                    module_info = modules_info[val]
                    credit += module_info['credit_provided']
                    used_module_ids.add(module_info['module_id'])

                used_module_ids = sorted(used_module_ids)
                planned_size = request.data.get("planned_size") or 0
                # this length check is check for if "Module1" is "core" and "optional" in 1 pos, when both "core" and "optional" of "Module1" is getting in same pathway, this count as not valid pathway, no need insert
                if len(generated_pathway) == len(used_module_ids):
                    pathway_string_2 = ";".join(map(str, used_module_ids))
                    new_data = {
                        "pos_id": pos.id,
                        "academic_term_id": pos.academic_term_id,
                        "pathway_string": pathway_string,
                        "pathway_string_2": pathway_string_2,
                        "credit": credit,
                        "planned_size": planned_size,
                        "status": TtPathway.STATUS_TO_CODE['active']
                    }
                    pathway_idata.append(new_data)
            # insert pathway, 500 record as 1 query to avoid problem of too large of data insert in 1 query
            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):
                        inserted_pathways = TtPathway.objects.bulk_create(
                            pathway_objects[i:i + batch_size],
                            batch_size=batch_size
                        )

            # loop the pathway, get pathway_string, having relation_idata to insert relation for pathway_pos_module_group_module
            pathway_module_relation_idata = []
            inserted_pathway_ids = []
            if inserted_pathways:
                for inserted_pathway in inserted_pathways:
                    inserted_pathway_ids.append(inserted_pathway.id)
                    # convert back pathway_string to array
                    pathway_arr = [int(x) for x in inserted_pathway.pathway_string.split(";")]
                    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)

                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
                            )
            helper_pathway_generate_assign_student_to_pathway(pos.id,inserted_pathway_ids,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)
            )
            remark_param = {
                "name":pos.name,
                "mode":"manual",
            }

            # keep track of which pos generated the pathway ids
            pathway_audit_data = {
                "pos_id": request.data["pos_id"],
                "pathway_ids": inserted_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 = {
                "idata": pathway_idata,
                "relation": pathway_module_relation_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
