from collections import defaultdict
import json
from datetime import datetime, date, time

from django.core.management.base import BaseCommand
from django.conf import settings
from django.forms import model_to_dict

from api.models import TtStaff, TtLocation, TtStudentSet, TtActivityTemplate, TtActivity, TtDepartment, TtZone, \
    TtAcademicTerm, TtWeek, TtTag, TtUsagePreference, TtStartPreference, TtAvailability, TtSuitability, TtWeekPattern, \
    TtModuleGroup, TtPos, TtModule, TtPosModuleGroup, TtActivityType, TtTimeBlock, TtFreeBlock, TtResourceBreak, \
    TtMaximumHour, TtMaximumWorkspan, TtJourney, TtTravelTable, TtConstraintProfile, TtSetting, \
    TtStudent, TtStudentSetStudent, TtStudentPosModule
from api.utils import convert_to_redis_week, bulk_sync_to_redis, generate_kafka_data, get_resource_map_redis_data, \
    import_get_resource_map_redis_data, get_exception_detail, log_critical_error
from backend.kafka import send_request
from backend.redis_client import redis_client

class Command(BaseCommand):
    def redis_based_setting(self):
        setting_params = [
            "initial_date",
            "initial_day",
            "minute_per_slot",
            "slot_per_day",
            "slot_per_week",
            "violation_key"
        ]
        settings = TtSetting.get_multiple_setting(setting_params)
        redis_data = {
            "insert": {},
            "update": {},
        }
        redis_table = "based_setting"
        raw_violation = settings.get("violation_key")
        violation_key = json.loads(raw_violation) if raw_violation else []
        redis_setting = {
            "id": 1,
            "initial_date": settings.get("initial_date", ""),
            "initial_day": int(settings.get("initial_day", 1)),
            "minute_per_slot": int(settings.get("minute_per_slot", 0)),
            "slot_per_day": int(settings.get("slot_per_day", 0)),
            "slot_per_week": int(settings.get("slot_per_week", 0)),
            "violation_key": violation_key
        }

        redis_data["insert"][redis_table] = [redis_setting]
        bulk_sync_to_redis(redis_client, redis_data)

    def redis_academic_term(self):
        all_data = TtAcademicTerm.objects.prefetch_related("week").all()
        redis_table = "academic_term"

        redis_column = [
            "id", "code", "name", "start_date", "end_date", "start_time", "end_time", "week", "default_availability",
            "default_usage", "default_start"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }

        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "week":
                        week_name = [w.week for w in data.week.all()]
                        week_for_redis = convert_to_redis_week(week_name)
                        redis_table_data[column] = week_for_redis
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_availability(self):
        all_data = TtAvailability.objects.all()
        redis_table = "availability"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "pattern"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_start_preference(self):
        all_data = TtStartPreference.objects.all()
        redis_table = "start_preference"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "pattern"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_usage_preference(self):
        all_data = TtUsagePreference.objects.all()
        redis_table = "usage_preference"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "pattern"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_time_block(self):
        all_data = TtTimeBlock.objects.all()
        redis_table = "time_block"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "start_slot","end_slot"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_free_block(self):
        all_data = TtFreeBlock.objects.prefetch_related("time_block").all()
        redis_table = "free_block"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "free_block", "time_block_ids", "enabled"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "enabled":
                        status_code = getattr(data,"status")
                        is_enabled = (status_code == TtFreeBlock.STATUS_TO_CODE['active'])
                        redis_table_data[column] = is_enabled
                    case "time_block_ids":
                        time_block = [val.id for val in data.time_block.all()]
                        redis_table_data[column] = time_block
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_resource_break(self):
        all_data = TtResourceBreak.objects.prefetch_related("time_block").all()
        redis_table = "resource_break"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "min_consecutive_free_period","time_block_ids", "enabled"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "enabled":
                        status_code = getattr(data,"status")
                        is_enabled = (status_code == TtResourceBreak.STATUS_TO_CODE['active'])
                        redis_table_data[column] = is_enabled
                    case "time_block_ids":
                        time_block = [val.id for val in data.time_block.all()]
                        redis_table_data[column] = time_block
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_maximum_hour(self):
        all_data = TtMaximumHour.objects.prefetch_related("time_block").all()
        redis_table = "maximum_hour"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "maximum_period","time_block_ids", "enabled"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "enabled":
                        status_code = getattr(data,"status")
                        is_enabled = (status_code == TtMaximumHour.STATUS_TO_CODE['active'])
                        redis_table_data[column] = is_enabled
                    case "time_block_ids":
                        time_block = [val.id for val in data.time_block.all()]
                        redis_table_data[column] = time_block
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_maximum_workspan(self):
        all_data = TtMaximumWorkspan.objects.prefetch_related("time_block").all()
        redis_table = "maximum_workspan"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "maximum_period","time_block_ids", "enabled"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "enabled":
                        status_code = getattr(data,"status")
                        is_enabled = (status_code == TtMaximumWorkspan.STATUS_TO_CODE['active'])
                        redis_table_data[column] = is_enabled
                    case "time_block_ids":
                        time_block = [val.id for val in data.time_block.all()]
                        redis_table_data[column] = time_block
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_journey(self):
        all_data = TtJourney.objects.all()
        redis_table = "journey"

        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_data["insert"].setdefault(redis_table, []).append({
                "id": data.id,
                "from_zone_id": data.from_zone_id,
                "to_zone_id": data.to_zone_id,
                "slot_required": data.slot_required
            })

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_travel_table(self):
        all_data = TtTravelTable.objects.prefetch_related("journey").all()
        redis_table = "travel_table"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "journey_ids"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "journey_ids":
                        journey_ids = [val.id for val in data.journey.all()]
                        redis_table_data[column] = journey_ids
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_constraint_profile(self):
        all_data = TtConstraintProfile.objects.prefetch_related("free_block","resource_break","maximum_workspan","maximum_hour","travel_table").all()
        redis_table = "constraint_profile"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "free_block_ids", "resource_break_ids", "maximum_workspan_ids",
            "maximum_hour_ids", "travel_table_ids",
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "free_block_ids"|"resource_break_ids"|"maximum_workspan_ids"|"maximum_hour_ids"|"travel_table_ids":
                        key = column.removesuffix("_ids")
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, key).all()])
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_department(self):
        all_data = TtDepartment.objects.prefetch_related("sub_department").all()
        redis_table = "department"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "sub_department_id"

        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "sub_department_id":
                        sub_department_ids = [val.id for val in data.sub_department.all()]
                        redis_table_data[column] = sub_department_ids
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_zone(self):
        all_data = TtZone.objects.prefetch_related("sub_zone").all()
        redis_table = "zone"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "sub_zone_id"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "sub_zone_id":
                        sub_zone_ids = [val.id for val in data.sub_zone.all()]
                        redis_table_data[column] = sub_zone_ids
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_staff(self):
        all_data = TtStaff.objects.prefetch_related("shared_with_department","avoid_concurrency","constraint_profile").all()
        redis_table = "staff"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "department_id", "zone_id",
            "maximum_period", "contract_period", "constraint_profile", "avoid_concurrency",
            "availability_id", "availability_pattern", "start_preference_id",
            "start_preference_pattern", "usage_preference_id", "usage_preference_pattern",
            "shared_with_all_department",
            "shared_with_department", "is_part_time", "resource_map", "allocated_activity_ids"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }
        ids = set()
        for data in all_data:
            ids.add(data.id)
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "shared_with_department" | "avoid_concurrency" | "constraint_profile":
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, column).all()])
                    case "allocated_activity_ids": # will handle in update part
                        redis_table_data[column] = []
                    case "resource_map": # will handle in update part
                        redis_table_data[column] = {}
                    case "usage_preference_id" | "availability_id" | "start_preference_id":
                        # jose request if no id, dun store null, want to store "0"
                        value = getattr(data, column)
                        redis_table_data[column] = 0 if value in (None, "") else value
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        # for update resource map and allocation_activity_ids
        redis_data["update"][redis_table] = get_resource_map_redis_data("staff", ids)
        bulk_sync_to_redis(redis_client, redis_data)

    def redis_location(self):
        all_data = TtLocation.objects.prefetch_related("shared_with_department","avoid_concurrency","constraint_profile").all()
        redis_table = "location"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "department_id", "zone_id", "capacity", "maximum_period", "contract_period",
            "constraint_profile", "avoid_concurrency",
            "availability_id", "availability_pattern", "start_preference_id",
            "start_preference_pattern", "usage_preference_id", "usage_preference_pattern", "shared_with_all_department", "is_online",
            "shared_with_department", "resource_map", "allocated_activity_ids"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }
        ids = set()
        for data in all_data:
            ids.add(data.id)
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "shared_with_department" | "avoid_concurrency" | "constraint_profile":
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, column).all()])
                    case "allocated_activity_ids": # will handle in update part
                        redis_table_data[column] = []
                    case "resource_map": # will handle in update part
                        redis_table_data[column] = {}
                    case "usage_preference_id" | "availability_id" | "start_preference_id":
                        # jose request if no id, dun store null, want to store "0"
                        value = getattr(data, column)
                        redis_table_data[column] = 0 if value in (None, "") else value
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        # for update resource map and allocation_activity_ids
        redis_data["update"][redis_table] = get_resource_map_redis_data("location", ids)
        bulk_sync_to_redis(redis_client, redis_data)

    def redis_suitability(self):
        all_data = TtSuitability.objects.prefetch_related("staff","location").all()
        redis_table = "suitability"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "department_id", "staff_id", "location_id"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "staff_id"|"location_id":
                        key = column.removesuffix("_id")
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, key).all()])
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_week_pattern(self):
        all_data = TtWeekPattern.objects.prefetch_related("week")
        redis_table = "week_pattern"

        redis_column = [
            "id", "code", "name", "week",
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }

        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "week":
                        week_name = [w.week for w in data.week.all()]
                        week_for_redis = convert_to_redis_week(week_name)
                        redis_table_data[column] = week_for_redis
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_module_group(self):
        all_data = TtModuleGroup.objects.all()
        redis_table = "module_group"

        redis_column = [
            "id", "code", "name"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }

        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_pos(self):
        all_data = TtPos.objects.prefetch_related("module_group").all()
        redis_table = "pos"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "desc", "department_id", "academic_term_id", "planned_size", "max_credit","module_group_id"
        ]
        redis_data = {
            "insert": {},
        }
        for data in all_data:
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "module_group_id":
                        key = column.removesuffix("_id")
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, key).all()])
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_module(self):
        all_data = TtModule.objects.prefetch_related("week").all()
        redis_table = "module"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "department_id", "academic_term_id", "planned_size", "real_size", "credit_provided",
            "week_pattern_id", "week_pattern", "availability_id", "availability_pattern", "start_preference_id",
            "start_preference_pattern", "usage_preference_id", "usage_preference_pattern"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }
        ids = set()
        for data in all_data:
            ids.add(data.id)
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "week_pattern":
                        week_name = [w.week for w in data.week.all()]
                        week_for_redis = convert_to_redis_week(week_name)
                        redis_table_data[column] = week_for_redis or []
                    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(data, column)
                        redis_table_data[column] = 0 if value in (None,"") else value
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_activity_template(self):
        all_data = (
            TtActivityTemplate.objects
            .prefetch_related(
                "week","staff_suitability","staff","location_suitability","location"
            )
            .all()
        )
        redis_table = "activity_template"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "desc", "planned_size", "slot_required", "activity_type_id", "duration",
            "department_id", "module_id", "academic_term_id",
            "week_pattern_id", "week_pattern", "availability_id", "availability_pattern",
            "start_preference_id", "start_preference_pattern", "usage_preference_id", "usage_preference_pattern",
            "staff_requirement_type", "staff_required_no", "staff_preset", "staff_suitability_id",
            "location_requirement_type", "location_required_no", "location_preset", "location_suitability_id",
            "status"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }
        ids = set()
        for data in all_data:
            ids.add(data.id)
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "staff_suitability_id":
                        suitability_list = list(data.staff_suitability.all())
                        redis_table_data[column] = suitability_list[0].id if suitability_list else None
                    case "location_suitability_id":
                        suitability_list = list(data.location_suitability.all())
                        redis_table_data[column] = suitability_list[0].id if suitability_list else None
                    case "staff_preset":
                        redis_table_data[column] = [s.id for s in data.staff.all()]
                    case "location_preset":
                        redis_table_data[column] = [l.id for l in data.location.all()]
                    case "week_pattern":
                        week_name = [w.week for w in data.week.all()]
                        week_for_redis = convert_to_redis_week(week_name)
                        redis_table_data[column] = week_for_redis or []
                    case "staff_required_no":
                        redis_table_data[column] = data.staff_requirement
                    case "location_required_no":
                        redis_table_data[column] = data.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(data, column)
                        redis_table_data[column] = 0 if value in (None, "") else value
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_activity(self):
        all_data = (
            TtActivity.objects
            .prefetch_related(
                "week","staff_suitability","staff_preset","staff",
                "location_suitability","location_preset","location","ttstudentset_set"
            )
            .all()
        )
        redis_table = "activity"
        # from here make it same name as db first, got different, only do extra handle
        redis_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_data = {
            "insert": {},
            "update": {},
        }
        ids = set()
        for data in all_data:
            ids.add(data.id)
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "staff_suitability_id":
                        suitability_list = list(data.staff_suitability.all())
                        redis_table_data[column] = suitability_list[0].id if suitability_list else None
                    case "location_suitability_id":
                        suitability_list = list(data.location_suitability.all())
                        redis_table_data[column] = suitability_list[0].id if suitability_list else None
                    case "staff_preset":
                        redis_table_data[column] = [s.id for s in data.staff_preset.all()]
                    case "location_preset":
                        redis_table_data[column] = [l.id for l in data.location_preset.all()]
                    case "week_pattern":
                        week_name = [w.week for w in data.week.all()]
                        week_for_redis = convert_to_redis_week(week_name)
                        redis_table_data[column] = week_for_redis or []
                    case "student_sets":
                        student_set_ids = [ss.id for ss in data.ttstudentset_set.all()]
                        redis_table_data[column] = student_set_ids
                    case "staff_required_no":
                        redis_table_data[column] = data.staff_requirement
                    case "location_required_no":
                        redis_table_data[column] = data.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(data, column)
                        redis_table_data[column] = 0 if value in (None, "") else value
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        bulk_sync_to_redis(redis_client, redis_data)

    def redis_student_set(self):
        all_data = TtStudentSet.objects.prefetch_related("activity","module","constraint_profile").all()
        redis_table = "student_set"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "department_id", "academic_term_id", "constraint_profile", "pos_id", "module_id",
            "planned_size", "availability_id", "availability_pattern", "start_preference_id",
            "start_preference_pattern", "usage_preference_id", "usage_preference_pattern", "resource_map",
            "allocated_activity_ids"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }
        ids = set()
        for data in all_data:
            ids.add(data.id)
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "constraint_profile":
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, column).all()])
                    case "allocated_activity_ids": # this part even not schedule activity also have activity, so can add in this part
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, "activity").all()])
                    case "module_id":
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, "module").all()])
                    case "resource_map": # will handle in update part
                        redis_table_data[column] = {}
                    case "usage_preference_id" | "availability_id" | "start_preference_id":
                        # jose request if no id, dun store null, want to store "0"
                        value = getattr(data, column)
                        redis_table_data[column] = 0 if value in (None, "") else value
                    case _:
                        value = getattr(data, column)
                        if isinstance(value, (datetime, date, time)):
                            # Safely converts date, datetime, AND time objects to clean ISO strings
                            redis_table_data[column] = value.isoformat()
                        else:
                            redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        # for update resource map only, activity can direct get at prefetch(activity)
        redis_data["update"][redis_table] = import_get_resource_map_redis_data("student_set", ids)
        bulk_sync_to_redis(redis_client, redis_data)

    def redis_student(self):
        all_data = TtStudent.objects.prefetch_related("academic_term","pos","pos_module").all()
        redis_table = "student"
        # from here make it same name as db first, got different, only do extra handle
        redis_column = [
            "id", "code", "name", "email", "department_id", 
            "academic_term_ids", "pos_ids", "pos_module",
            "student_set_ids", "resource_map"
        ]
        redis_data = {
            "insert": {},
            "update": {},
        }

        student_set_ids_map = defaultdict(list)
        for student_id, student_set_id in (
            TtStudentSetStudent.objects
            .values_list("student_id", "student_set_id")
            .all()
        ):
            student_set_ids_map[student_id].append(student_set_id)

        pos_module_map = defaultdict(list)
        for item in (
            TtStudentPosModule.objects
            .select_related(
                "pos_module_group_module",
                "pos_module_group_module__pos_module_group",
                "pos_module_group_module__pos_module_group__pos",
                "pos_module_group_module__module",
            )
            .values(
                "student_id",
                "pos_module_group_module__module_id",
                "pos_module_group_module__pos_module_group__pos_id",
            )
            .all()
        ):
            pos_module_map[item["student_id"]].append({
                "module_id": item["pos_module_group_module__module_id"],
                "pos_id": item["pos_module_group_module__pos_module_group__pos_id"],
            })
            
        ids = set()
        for data in all_data:
            ids.add(data.id)
            redis_table_data = {}
            for column in redis_column:
                match column:
                    case "module_ids":
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, "module").all()]) or []
                    case "academic_term_ids":
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, "academic_term").all()]) or []
                    case "pos_ids":
                        redis_table_data[column] = sorted([obj.id for obj in getattr(data, "pos").all()]) or []
                    case "pos_module":
                        redis_table_data[column] = pos_module_map.get(data.id, [])
                    case "student_set_ids":
                        # allocation only have student set ids
                        redis_table_data[column] = student_set_ids_map.get(data.id, [])
                    case "resource_map":
                        redis_table_data[column] = {}
                    case _:
                        redis_table_data[column] = getattr(data, column)
            redis_data["insert"].setdefault(redis_table, []).append(redis_table_data)

        # for update resource map only, activity can direct get at prefetch(activity)
        redis_data["update"][redis_table] = import_get_resource_map_redis_data("student", ids)
        bulk_sync_to_redis(redis_client, redis_data)

    

    def handle(self, *args, **options):
        try:
            self.redis_based_setting()
            self.redis_academic_term()
            self.redis_availability()
            self.redis_start_preference()
            self.redis_usage_preference()
            self.redis_time_block()
            self.redis_free_block()
            self.redis_resource_break()
            self.redis_maximum_workspan()
            self.redis_maximum_hour()
            self.redis_journey()
            self.redis_travel_table()
            self.redis_constraint_profile()
            self.redis_department()
            self.redis_zone()
            self.redis_staff()
            self.redis_location()
            self.redis_suitability()
            self.redis_module_group()
            self.redis_week_pattern()
            self.redis_pos()
            self.redis_module()
            self.redis_activity_template()
            self.redis_activity()
            self.redis_student_set()
            self.redis_student()
        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'])









