from datetime import date
from django.core.paginator import Paginator
from django.shortcuts import get_object_or_404, redirect, render,HttpResponse
from django.http import JsonResponse
from django.contrib.auth import logout
from django.db.models import F, Window, Avg, Sum, Q
from django.db.models import Sum, Max, F, Case, When, Value, IntegerField,ExpressionWrapper, fields, DateTimeField, DurationField
from dateutil.relativedelta import relativedelta
import openpyxl
import os
import uuid
from django.utils import timezone
import ast
import random
from django.utils.text import slugify
from django.core.files.base import ContentFile
from django.db.models.functions import Cast
from django.contrib import messages
from django.db.models.functions import Coalesce
# from django.core import serializers
from learn.models import Course_Academic,Resources,Coupon_courses,Section_Test_Result,Student_Answers,Test_Result,Study_material_batches, Category_Subjects, Course_Batches, Course_Exams, Study_Materials,Lecture_attendance,ContactUsEnquiries, Coupons, Enquiries, Lead_sourse, Lead_status, Lecture_Topics, Lecture_batches, Login_history, MockTestSection, Notifications_batches, Orders,OrderCourses,Testimonials,Notifications,Leads,Lead_Comments,Lead_Followup,Mock_Test_Series,Banners, Course, ExamCategory, MediaModule,Sections,Sub_Sections,Question_Bank,Topics,Mock_Test,Faculty,Batch_Management,Students,Blogs,Blog_category,Option, Exam_Master, Exam_Subjects,Live_Lecture,Student_Result, Vocab_Questions,Foundation_Videos_New,Add_On_Courses_Course_Ids,mock_sections,mock_exams,Foundation_Video_Categories,ExamSyllabus, CounselingAttendance, CounselingGroup, CounselingGroupStudents, CounselingSchedule,ZoomKeys,AppNotifications,AppNotifications_batches,ResourcesRc,NotificationRunLog,StudentActivity,BatchTopicTiming,MonthlyLectureCount,BatchTimeSlot,FacultyDetails,FacultyTimeSlot,Daily_speedMath_Score,Daily_Vocab_Score,CourseDiscount,UserRole,Feature,RolePermission,UserProfile,AcademicYear,ExamAcademic,FacultyLeaves
from django.contrib.auth import authenticate, login
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm
from learn.forms import UserRegisterForm
from django.db.models import Sum
from datetime import datetime, timedelta
from base64 import b64encode
import requests
from collections import defaultdict
import json
from docx import Document
from django.db.models import Prefetch
import random
from django.db.models import Count
from django.db.models.functions import Concat
from django.db.models import Value
from django.contrib.auth.decorators import login_required
from docx import Document as WordDocument
from io import BytesIO
import base64
from PIL import Image
import firebase_admin
from firebase_admin import credentials, messaging, exceptions as fb_exceptions
from celery import shared_task
from latex2mathml.converter import convert
from django.db.models import OuterRef, Subquery, Exists
from django.db import transaction
from docx import Document
from io import BytesIO
import re
from typing import Optional
import pytz
import re, json, base64
from io import BytesIO
from docx import Document
from docx.shared import Inches
from django.http import HttpResponse
from django.views.decorators.http import require_GET
from django.views.decorators.csrf import csrf_exempt
from django.http import JsonResponse, HttpResponseForbidden
from django.views.decorators.http import require_POST
from django.conf import settings
from admin_app.middleware import get_admin_landing_url

from docx.enum.text import WD_ALIGN_PARAGRAPH
from django.contrib.auth import get_user_model

User = get_user_model()


ADMIN_LOGIN_URL='/admin/login'


# Initialize Firebase using the path from settings
try:
    FIREBASE_KEY_PATH = os.path.join(os.path.dirname(__file__), 'firebase-credentials.json')

    if not firebase_admin._apps:
        cred = credentials.Certificate(FIREBASE_KEY_PATH)
        firebase_admin.initialize_app(cred)
except Exception as e:
    print(f"Error initializing Firebase: {str(e)}")
# # Initialize once
# push_service = FCMNotification(api_key=FCM_API_KEY)
#push_service = FCMNotification(service_account_file=settings.FIREBASE_CREDENTIALS_FILE, project_id="crackeverytest-a2037")

@require_GET
@csrf_exempt
def cron_notify_lectures(request):
    """
    Public endpoint for cron. No auth/token.
    Sends a push 10 minutes before lecture start.
    """
#    now = timezone.now()
    now = timezone.localtime(timezone.now())   # IST
    
    window_start_current = now + timedelta(minutes=5)
    window_end_current   = now + timedelta(minutes=20)

    lecture_id = request.GET.get("lecture_id")
    now = timezone.localtime()

    qs1 = (Live_Lecture.objects
          .filter(
              lacture_date=now.date(),
              start_time__gte=window_start_current.time(),
              start_time__lt=window_end_current.time()
          )
          .order_by('start_time')
    )[:1]
    processed = []
    total_sent = total_failed = 0
    invalid_tokens = []

    for lec in qs1.select_related("faculty").prefetch_related("batches"):
        students = Students.objects.filter(batch__in=lec.batches.all()).distinct()
        tokens = list(
            Login_history.objects.filter(
                student__in=students, device_token__isnull=False
            ).values_list("device_token", flat=True).distinct()
        )

        title = f"{lec.title} lecture begins in 10 mins, start your cameras and be on time."
        body  = "Don’t miss it"

        sent = failed = 0
        for t in tokens:
            try:
                if t:
                    msg = messaging.Message(
                        notification=messaging.Notification(title=title, body=body),
                        token=t
                    )
                    messaging.send(msg)
                    sent += 1
            except fb_exceptions.FirebaseError as e:
                failed += 1
                print(f"🔥 FCM Error for token {t[:10]}...: {e}")
        
        total_sent += sent
        total_failed += failed
#        if invalid_tokens:
#            Login_history.objects.filter(device_token__in=set(invalid_tokens)).update(device_token=None)
    
    window_start = now + timedelta(hours=23, minutes=55)
    window_end   = now + timedelta(hours=24, minutes=15)

    lecture_id = request.GET.get("lecture_id")
    now = timezone.localtime()

    qs = (Live_Lecture.objects
          .filter(
              lacture_date__gte=window_start.date(),
              lacture_date__lte=window_end.date(),
              start_time__gte=window_start.time(),
              start_time__lt=window_end.time()
          )
          .order_by('start_time')
    )[:1]
    processed = []
    invalid_tokens = []

    for lec in qs.select_related("faculty").prefetch_related("batches"):
        students = Students.objects.filter(batch__in=lec.batches.all()).distinct()
        tokens = list(
            Login_history.objects.filter(
                student__in=students, device_token__isnull=False
            ).values_list("device_token", flat=True).distinct()
        )

        title = f"{lec.title} lecture in 24 hours, brush up your basics."
        body  = "Be prepared and revise your key concepts."

#        sent = failed = 0
        for t in tokens:
            try:
                if t:
                    msg = messaging.Message(
                        notification=messaging.Notification(title=title, body=body),
                        token=t
                    )
                    messaging.send(msg)
                    sent += 1
            except fb_exceptions.FirebaseError as e:
                failed += 1
                print(f"🔥 FCM Error for token {t[:10]}...: {e}")

#        if invalid_tokens:
#            Login_history.objects.filter(device_token__in=set(invalid_tokens)).update(device_token=None)

        total_sent += sent
        total_failed += failed

        processed.append({
            "lecture_id": lec.id,
            "title": lec.title,
            "date": lec.lacture_date,
            "start_time": lec.start_time,
            "tokens": len(tokens),
            "sent": sent,
            "failed": failed,
            "invalid_pruned": len(invalid_tokens),
        })

    return JsonResponse({
        "ok": True,
        "now": now.isoformat(),
        "window": None if lecture_id else {
            "start": window_start.isoformat(),
            "end": window_end.isoformat(),
        },
        "window2": None if lecture_id else {
            "start": window_start_current.isoformat(),
            "end": window_end_current.isoformat(),
        },
        "date":now.date(),
        "start": window_start,
        "end": window_end,
        "count": len(processed),
        "processed": processed,
        "qs":qs1.count(),
        "totals": {"sent": total_sent, "failed": total_failed},
    })

def admin_login(request):
    next = request.GET.get('next')
    #return render(request, "login.html", {'next':next})
    
    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        next = request.POST.get('next')
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            landing_url = get_admin_landing_url(user)
            if landing_url:
                return redirect(landing_url)
            return HttpResponseForbidden('No admin module has been assigned to this user.')
        else:
            return render(request, 'login.html', {'error': 'Invalid credentials.','next':next})
    else:
        return render(request, 'login.html',{'error':'','next':next})

def login_process(request):
    # if request.method == 'POST':
  
    #     # AuthenticationForm_can_also_be_used__
  
    #     username = request.POST.get('username')
    #     password = request.POST.get('password')
    #     user = authenticate(request, username = username, password = password)
    #     if user is not None:
    #         form = login(request, user)
    #         messages.success(request, f' welcome {username} !!')
    #         return redirect('exam_category')
    #     else:
    #         messages.info(request, f'account done not exit plz sign in')
    # form = AuthenticationForm()

    if request.method == 'POST':
        username = request.POST.get('username')
        password = request.POST.get('password')
        user = authenticate(request, username=username, password=password)
        if user is not None:
            login(request, user)
            landing_url = get_admin_landing_url(user)
            if landing_url:
                return redirect(landing_url)
            return HttpResponseForbidden('No admin module has been assigned to this user.')
        else:
            return render(request, 'login.html', {'error': 'Invalid credentials.'})
    else:
        return render(request, 'login.html')

def registration(request):
    return render(request, "registration.html")

def admin_logout(request):
    logout(request)
    return redirect('/admin/login')
# def register_store(request):
#     if request.method == 'POST':
#         form = UserRegisterForm(request.POST)
#         if form.is_valid():
#             form.save()
#             username = form.cleaned_data.get('username')
#             email = form.cleaned_data.get('email')
#             ######################### mail system ####################################
#             htmly = get_template('user/Email.html')
#             d = { 'username': username }
#             subject, from_email, to = 'welcome', 'your_email@gmail.com', email
#             html_content = htmly.render(d)
#             msg = EmailMultiAlternatives(subject, html_content, from_email, [to])
#             msg.attach_alternative(html_content, "text/html")
#             msg.send()
#             ##################################################################
#             messages.success(request, f'Your account has been created ! You are now able to log in')
#             return redirect('login')
#     else:
#         form = UserRegisterForm()



@login_required(login_url=ADMIN_LOGIN_URL)
def dashboard(request): 
    today=date.today()
    paid_students = Students.objects.filter(order_courses__isnull=False).count()
    unpaid_students = Students.objects.filter(order_courses__isnull=True).count()
    new_leads = Leads.objects.filter(status=1).count()
    total_courses = Course.objects.count()
    total_sale = Orders.objects.aggregate(total=Sum('order_price'))['total']
    todays_lecture = Live_Lecture.objects.filter(lacture_date=today).values()
    todays_followup = Lead_Followup.objects.filter(follwup_date=today).select_related('student')
    #return JsonResponse({'paid_students':paid_students,'unpaid_students':unpaid_students,'new_leads':new_leads,'total_courses':total_courses,'total_sale':total_sale,'todays_lecture':list(todays_lecture),'todays_followup':list(todays_followup)})
    return render(request, "dashboard.html", {'paid_students':paid_students,'unpaid_students':unpaid_students,'new_leads':new_leads,'total_courses':total_courses,'total_sale':total_sale,'todays_lecture':todays_lecture,'todays_followup':todays_followup})




@login_required(login_url=ADMIN_LOGIN_URL)
# def dashboard(request):
#     today=date.today()
#     all_students = Students.objects.filter(is_lead=0).count()
#     all_leads = Leads.objects.filter().count()
#     all_courses = Course.objects.filter().count()
#     all_sales = Orders.objects.aggregate(total_amount=Sum('order_price'))['total_amount']
#     all_lectures = Live_Lecture.objects.filter(lacture_date=today).order_by('-id')[:10]
#     all_followup = Lead_Followup.objects.filter(follwup_date=today).order_by('-id')[:10]
#     sales = Orders.objects.annotate(month=TruncMonth('created_at')).values('month').annotate(total_sales=Sum('order_price')).order_by('month')
    
#     months = [sale['month'].strftime('%B %Y') for sale in sales]
#     # months = ["January", "February", "March", "April","May", "June", "July", "August","September", "October", "November", "December"]
#     totals = [sale['total_sales'] for sale in sales]

#     plt.bar(months, totals)
#     plt.xlabel('Month')
#     plt.ylabel('Sales')
#     plt.title('Monthly Sales')

#     # Save the graph to a file or render it directly
#     graph_path = 'admin_app/static/sales_graph/monthly_sales.png'
#     plt.savefig(graph_path)
   
#     # # return render(request, 'your_page.html', context_dict)
   
#     return render(request, "dashboard.html",{'all_students':all_students,'all_leads':all_leads,'all_courses':all_courses,'all_sales':all_sales,'all_lectures':all_lectures,'all_followup':all_followup})


@login_required(login_url=ADMIN_LOGIN_URL)
def exam_category(request):

    section_list = Sections.objects.all()
    all_batches = Batch_Management.objects.filter(status='Active').order_by('-id')

    name = request.GET.get('name', '').strip()
    status = request.GET.get('status', 'Active').strip()
    from_date = request.GET.get('from_date', '').strip()
    to_date = request.GET.get('to_date', '').strip()

    all_categories = ExamCategory.objects.all().order_by('-id')

    if name:
        all_categories = all_categories.filter(exam_category_name__icontains=name)

    if status:
        all_categories = all_categories.filter(status=status)

    if from_date:
        all_categories = all_categories.filter(created_at__date__gte=from_date)

    if to_date:
        all_categories = all_categories.filter(created_at__date__lte=to_date)

    # Counts for tabs
    total_count = ExamCategory.objects.count()
    active_count = ExamCategory.objects.filter(status='Active').count()
    inactive_count = ExamCategory.objects.filter(status='Inactive').count()

    # Keep filters during pagination
    query_params = request.GET.copy()
    query_params.pop('page', None)
    filter = '&' + query_params.urlencode() if query_params else ''

    # Tab URLs: keep name/date filters, only change status
    base_params = request.GET.copy()
    base_params.pop('page', None)

    all_tab_params = base_params.copy()
    all_tab_params.pop('status', None)

    active_tab_params = base_params.copy()
    active_tab_params['status'] = 'Active'

    inactive_tab_params = base_params.copy()
    inactive_tab_params['status'] = 'Inactive'

    paginator = Paginator(all_categories, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "exam-category.html", {
        'all_categories': page_obj,
        'all_batches': all_batches,
        'filter': filter,
        'page': page_number,
        'name': name,
        'status': status,
        'from_date': from_date,
        'to_date': to_date,
        'section_list': section_list,

        'total_count': total_count,
        'active_count': active_count,
        'inactive_count': inactive_count,

        'all_tab_url': all_tab_params.urlencode(),
        'active_tab_url': active_tab_params.urlencode(),
        'inactive_tab_url': inactive_tab_params.urlencode(),
    })


@login_required(login_url=ADMIN_LOGIN_URL)
def exam_category_detail(request, id):
    # Get Exam Category
    exam_category = get_object_or_404(ExamCategory, id=id)

    # Fetch Syllabus Data Efficiently
    syllabus = ExamSyllabus.objects.filter(exam_category=exam_category) \
        .select_related('section', 'sub_section', 'topic') \
        .prefetch_related('dependent_topics')

    # Structure Data for Display
    sections_data = {}

    for entry in syllabus:
        section = entry.section
        subsection = entry.sub_section
        topic = entry.topic
        dependent_topics = entry.dependent_topics.all()  # Many-to-Many related topics

        if section not in sections_data:
            sections_data[section] = {
                'subsections': {},
                's_strong': entry.s_strong,
                's_weak': entry.s_weak,
            }

        if subsection not in sections_data[section]['subsections']:
            sections_data[section]['subsections'][subsection] = {
                'topics': [],
                'ss_strong': entry.ss_strong,
                'ss_weak': entry.ss_weak,
            }

        # Append Topic and Dependent Topics
        sections_data[section]['subsections'][subsection]['topics'].append({
            'topic': topic,
            'dependent_topics': list(dependent_topics)  # Convert QuerySet to List
        })

    # Compute Rowspan for Table Merging
    for section, section_data in sections_data.items():
        section_data['rowspan'] = sum(
            len(subsection_data['topics']) for subsection_data in section_data['subsections'].values()
        )

        for subsection, subsection_data in section_data['subsections'].items():
            subsection_data['rowspan'] = len(subsection_data['topics'])

    # Render Template with Organized Data
    return render(request, 'exam-category-details.html', {
        'exam_category': exam_category,
        'sections_data': sections_data,
    })

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_examitem(request):
    if request.method == 'POST':
        item_id = request.POST.get('id')
        item_type = request.POST.get('type')
        
        try:
            # For section deletion, filter by section_id
            if item_type == 'section':
                section = ExamSyllabus.objects.filter(section_id=item_id)
                if section:
                    section.delete()  # Perform your soft delete logic
                    return JsonResponse({'status': 'success', 'msg': 'Section deleted successfully.'})
                else:
                    return JsonResponse({'status': 'error', 'msg': 'Section not found'})

            # For subsection deletion, filter by sub_section_id
            elif item_type == 'subsection':
                subsection = ExamSyllabus.objects.filter(sub_section_id=item_id)
                if subsection:
                    subsection.delete()  # Perform your soft delete logic
                    return JsonResponse({'status': 'success', 'msg': 'Subsection deleted successfully.'})
                else:
                    return JsonResponse({'status': 'error', 'msg': 'Subsection not found'})

            # For topic deletion, filter by topic_id
            elif item_type == 'topic':
                topic = ExamSyllabus.objects.filter(topic_id=item_id)
                if topic:
                    topic.delete()  # Perform your soft delete logic
                    return JsonResponse({'status': 'success', 'msg': 'Topic deleted successfully.'})
                else:
                    return JsonResponse({'status': 'error', 'msg': 'Topic not found'})

            else:
                return JsonResponse({'status': 'error', 'msg': 'Invalid item type'})

        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': f'Error: {str(e)}'})

    return JsonResponse({'status': 'error', 'msg': 'Invalid request method'})



#updated
@login_required(login_url=ADMIN_LOGIN_URL)
def add_exam_category(request):
    if request.method == 'POST':
        # Get the form data
        exam_category_name = request.POST.get('exam_category_name')
        batch_id = request.POST.get('batch_id')
        sections = request.POST.getlist('section_id')
        status = request.POST.get('status')
        image = request.FILES.get('image')
        section_wise_timer = request.POST.get('section_wise_timer')
        section_switching = request.POST.get('section_switching')
        calculator = request.POST.get('calculator')
        # section_durations = request.POST.getlist('section_durations[]')

        marks_per_question = request.POST.get('marks_per_question')
        negative_marks_per_question = request.POST.get('negative_marks_per_question')
        if(exam_category_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter exam category name.'})
        elif(marks_per_question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter marks per question'})
        elif(negative_marks_per_question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter negative marks per question'})
        elif(section_wise_timer==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select section wise timer'})
        elif(section_switching==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select section switching'})
        elif(calculator==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select calculator allowed'})
        elif(image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        else:
               
            # Create a new ExamCategory object
            if(request.FILES.get('image')):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})
                category = ExamCategory.objects.create(
                exam_category_name=exam_category_name,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
#                image=image,
                section_wise_timer=section_wise_timer,
                section_switching=section_switching,
                calculator=calculator,
                status=status,
                batch_id=batch_id
                )
            else:
                category = ExamCategory.objects.create(
                exam_category_name=exam_category_name,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
                section_wise_timer=section_wise_timer,
                section_switching=section_switching,
                calculator=calculator,
                status=status,
                batch_id=batch_id
                )

            category_id = category.id

            # if section_wise_timer == '1':
            for i in range(len(sections)):
                # for section_id in sections:
                    subject = Category_Subjects(
                        category_id=category_id,
                        section_id=sections[i],
                        # duration=section_durations[i],
                    )
                    subject.save()

            # Save the object to the database
            # category.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Exam category saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_exam_category(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        exam_category_name = request.POST.get('name')
        batch_id = request.POST.get('batch_id')
        sections = request.POST.getlist('section_id')
        section_wise_timer = request.POST.get('section_wise_timer')
        section_switching = request.POST.get('section_switching')
        calculator = request.POST.get('calculator')
        marks_per_question = request.POST.get('marks_per_question')
        negative_marks_per_question = request.POST.get('negative_marks_per_question')
        status = request.POST.get('status')
        image = request.FILES.get('image')
        if(exam_category_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter exam category name.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            if(request.FILES.get('image')):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})
                # Create a new ExamCategory object
                category = ExamCategory(
                    id=id,
                    exam_category_name=exam_category_name,
                    marks_per_question=marks_per_question,
                    negative_marks_per_question=negative_marks_per_question,
                    section_wise_timer=section_wise_timer,
                    section_switching=section_switching,
                    calculator=calculator,
                    image=image,
                    status=status,
                    batch_id=batch_id
                )

                # Save the object to the database
                category.save(update_fields=["exam_category_name","image","status","marks_per_question","negative_marks_per_question","section_wise_timer","section_switching","calculator","batch_id"])
                # category.save()
                if sections:
                    Category_Subjects.objects.filter(category_id=id).delete()
                
                category_id = category.id
           
                for section_id in sections:
                    subject = Category_Subjects(
                        category_id=category_id,
                        section_id=section_id
                    )
                    subject.save()
            else:
                 # Create a new ExamCategory object
                category = ExamCategory(
                    id=id,
                    exam_category_name=exam_category_name,
                    marks_per_question=marks_per_question,
                    negative_marks_per_question=negative_marks_per_question,
                    section_wise_timer=section_wise_timer,
                    section_switching=section_switching,
                    calculator=calculator,
                    status=status,
                    batch_id=batch_id
                )

                if sections:
                    Category_Subjects.objects.filter(category_id=id).delete()
                
                category_id = category.id
           
                for section_id in sections:
                    subject = Category_Subjects(
                        category_id=category_id,
                        section_id=section_id
                    )
                    subject.save()

                # Save the object to the database
                category.save(update_fields=["exam_category_name","status","marks_per_question","negative_marks_per_question","section_wise_timer","section_switching","calculator","batch_id"])
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Exam category saved successfully.'})

            
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_exam_category(request):
   
    id = request.POST.get('id')
    ExamCategory(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'Exam category deleted successfully.'})

# close updated

@login_required(login_url=ADMIN_LOGIN_URL)
def faculty_module(request):
    faculty_name=request.GET.get('faculty_name')
    faculty_email=request.GET.get('faculty_email')
    faculty_contact=request.GET.get('faculty_contact')
    status=request.GET.get('status')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    
    # from_date=request.GET.get('from_date')
    # to_date=request.GET.get('to_date')

    all_faculties = Faculty.objects.filter().order_by('-id')

    
    # student_list = Leads.objects.filter().order_by('-id')
    if faculty_name:
        all_faculties = all_faculties.filter(faculty_name__icontains=faculty_name)
        filter=filter+'&faculty_name='+faculty_name
    if faculty_email:
        all_faculties = all_faculties.filter(email__icontains=faculty_email)
        filter=filter+'&faculty_email='+faculty_email
    if faculty_contact:
        all_faculties = all_faculties.filter(contact__icontains=faculty_contact)
        filter=filter+'&faculty_contact='+faculty_contact
    if status:
        all_faculties = all_faculties.filter(status=status)
        filter=filter+'&status='+status
    if from_date:
        all_faculties = all_faculties.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_faculties = all_faculties.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
    # all_faculties = Faculty.objects.all()
    paginator = Paginator(all_faculties, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "faculty_module.html",{'all_faculties':page_obj,'filter':filter,
'page':page_number,'faculty_name':faculty_name,'status':status,'faculty_email':faculty_email,'faculty_contact':faculty_contact,'from_date':from_date,'to_date':to_date});

@login_required(login_url=ADMIN_LOGIN_URL)
def add_faculty(request):

    if request.method == 'POST':
        # Get the form data
        faculty_name = request.POST.get('faculty_name')
        email = request.POST.get('email')
        password = request.POST.get('password')
        contact = request.POST.get('contact')
        experience = request.POST.get('experience')
        date_of_birth = request.POST.get('date_of_birth')
        lecture_per_week = request.POST.get('lecture_per_week')
        status = request.POST.get('status')
       
        if(faculty_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter faculty name.'})
        elif(email==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter faculty email.'})
        elif(password==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter password.'})
        elif(contact==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter contact number.'})
        elif(experience==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter experience.'})
        elif(date_of_birth==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter date_of_birth.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            faculty = Faculty(
                faculty_name=faculty_name,
                email = email,
                password=password,
                contact= contact,
                experience= experience,
                date_of_birth = date_of_birth,
                lecture_per_week=lecture_per_week,
                status=status
            )

            # Save the object to the database
            faculty.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Faculty added successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_faculty(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        faculty_name = request.POST.get('name')
        email = request.POST.get('email')
        lecture_per_week = request.POST.get('lecture_per_week')
        password = request.POST.get('password')
        contact = request.POST.get('contact')
        experience = request.POST.get('experience')
        date_of_birth = request.POST.get('dob')
        status = request.POST.get('status')
       
        if(faculty_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter faculty name.'})
        elif(email==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter faculty email.'})
        elif(password==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter password.'})
        elif(contact==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter contact number.'})
        elif(experience==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter experience.'})
        elif(date_of_birth==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter date_of_birth.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            faculty = Faculty(
                id=id,
                faculty_name=faculty_name,
                email = email,
                password=password,
                contact= contact,
                experience= experience,
                date_of_birth = date_of_birth,
                status=status,
                lecture_per_week=lecture_per_week,
            )

            # Save the object to the database
            faculty.save(update_fields=["faculty_name","email","password","contact","experience","date_of_birth","status","lecture_per_week"])

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Faculty updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_faculty_by_Id(request):
     id = request.POST.get('id')
    #  print(id)
     Faculty(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'faculty deleted successfully.'})        
   
# =====================section module start ==================================

@login_required(login_url=ADMIN_LOGIN_URL)
def section(request):
    # all_section = Sections.objects.all();

    name=request.GET.get('name')
    # category=request.GET.get('category_id')
    status=request.GET.get('status')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    all_section = Sections.objects.filter().order_by('-id')

    if name:
        all_section = all_section.filter(section_name__icontains=name)
        filter=filter+'&name='+name
    # if category:
    #     all_exams = all_exams.filter(exam_category_id=category)
    if status:
        all_section = all_section.filter(status=status)
        filter=filter+'&status='+status
    if from_date:
        all_section = all_section.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_section = all_section.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    paginator = Paginator(all_section, 10)  # Show 25 contacts per page.

    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "section.html",{'all_section':page_obj,'page':page_number,'filter':filter,'from_date':from_date,'to_date':to_date,'name':name});

@login_required(login_url=ADMIN_LOGIN_URL)
def add_section(request):
    if request.method == 'POST':
        # Get the form data
        section_name = request.POST.get('section_name')
        status = request.POST.get('status')
        display_order = request.POST.get('display_order')
        if(section_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter section name.'})
        elif(display_order==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter display order.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            section = Sections(
                section_name=section_name,
                display_order=display_order,
                status=status
            )

            # Save the object to the database
            section.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Section created successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
  
@login_required(login_url=ADMIN_LOGIN_URL)  
def edit_section(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        section_name = request.POST.get('name')
        status = request.POST.get('status')
        display_order = request.POST.get('display_order')
        if(section_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter section name.'})
        elif(display_order==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Display order.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    

            section = Sections(
                    id=id,
                    section_name=section_name,
                    display_order=display_order,
                    status=status
                )
            section.save(update_fields=["section_name","display_order","status"])
           

            return JsonResponse({'status': 'success', 'msg': 'Section updated successfully.'})
    else:
       
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_section_by_Id(request):
     id = request.POST.get('id')
     Sections(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'section deleted successfully.'})


# ======================= end section module =============================

# ====================== start batch management ========================

@login_required(login_url=ADMIN_LOGIN_URL)
def batch_managements(request):
    batch_name = request.GET.get('batch_name', '').strip()
    course_name = request.GET.get('course_name', '').strip()
    status = request.GET.get('status', 'Active').strip()
    from_date = request.GET.get('from_date', '').strip()
    to_date = request.GET.get('to_date', '').strip()

    all_courses = Course.objects.all()
    exam_categories = ExamCategory.objects.all()

    all_batches = Batch_Management.objects.all().order_by('status', '-id')

    if batch_name:
        all_batches = all_batches.filter(batch_name__icontains=batch_name)

    if course_name:
        all_batches = all_batches.filter(couse_name__icontains=course_name)

    if status:
        all_batches = all_batches.filter(status=status)

    if from_date:
        all_batches = all_batches.filter(created_at__date__gte=from_date)

    if to_date:
        all_batches = all_batches.filter(created_at__date__lte=to_date)

    # Counts for pills
    total_count = Batch_Management.objects.count()
    active_count = Batch_Management.objects.filter(status='Active').count()
    inactive_count = Batch_Management.objects.filter(status='Inactive').count()

    # Keep filters during pagination
    query_params = request.GET.copy()
    query_params.pop('page', None)
    filter = '&' + query_params.urlencode() if query_params else ''

    # Pill URLs: keep batch/date/course filters, only change status
    base_params = request.GET.copy()
    base_params.pop('page', None)

    all_pill_params = base_params.copy()
    all_pill_params.pop('status', None)

    active_pill_params = base_params.copy()
    active_pill_params['status'] = 'Active'

    inactive_pill_params = base_params.copy()
    inactive_pill_params['status'] = 'Inactive'

    paginator = Paginator(all_batches, 20)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "batch-management.html", {
        'exam_categories': exam_categories,
        'all_batches': page_obj,
        'filter': filter,
        'all_courses': all_courses,
        'page': page_number,
        'batch_name': batch_name,
        'status': status,
        'course_name': course_name,
        'from_date': from_date,
        'to_date': to_date,

        'total_count': total_count,
        'active_count': active_count,
        'inactive_count': inactive_count,

        'all_pill_url': all_pill_params.urlencode(),
        'active_pill_url': active_pill_params.urlencode(),
        'inactive_pill_url': inactive_pill_params.urlencode(),
    })
@login_required(login_url=ADMIN_LOGIN_URL)
def add_batch(request):
    if request.method == 'POST':
        # Get form data
        batch_name = request.POST.get('batch_name')
        start_date = request.POST.get('start_date')
        end_date = request.POST.get('end_date')
        lecture_per_week = request.POST.get('lecture_per_week')
        exam_category_id = request.POST.get('exam_category_id')
        status = request.POST.get('status')
        merged_batch_ids = request.POST.getlist('merged_batches')  # Fetch multiple selected batch IDs

        # Validate required fields
        if not batch_name:
            return JsonResponse({'status': 'error', 'msg': 'Please enter batch name.'})
        elif not status:
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})

        # Create a new batch entry
        batch = Batch_Management(
            batch_name=batch_name,
            exam_category_id=exam_category_id,
            status=status,
            start_date=start_date,
            end_date=end_date,
            lecture_per_week=lecture_per_week
        )
        batch.save()

        # Handle merged batch relationships
        if merged_batch_ids:
            merged_batches = Batch_Management.objects.filter(id__in=merged_batch_ids)
            batch.merged_batches.set(merged_batches)  # Assign selected batches

        return JsonResponse({'status': 'success', 'msg': 'Created batch successfully.'})
    
    return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_batch(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        exam_category_id = request.POST.get('exam_category_id')
        batch_name = request.POST.get('name')
        lecture_per_week = request.POST.get('lecture_per_week')
        start_date = request.POST.get('start_date')
        end_date = request.POST.get('end_date')
        # exam_category_id = request.POST.get('exam_category_id')
        status = request.POST.get('status')
        merged_batch_ids = request.POST.getlist('merged_batches')
       
        if(batch_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter batch name.'})
        
        # elif(couse_name==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter course name.'})
        
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:

            batch = Batch_Management(
                    id=id,
                    batch_name=batch_name,
                    exam_category_id=exam_category_id,
                    # couse_name=couse_name,
                    status=status,
                    start_date=start_date,
                    end_date=end_date,
                    lecture_per_week=lecture_per_week
                )
            batch.save(update_fields=["batch_name","exam_category_id","status","start_date","end_date","lecture_per_week"])
            # Return a success response

            if merged_batch_ids:
                merged_batches = Batch_Management.objects.filter(id__in=merged_batch_ids)
                batch.merged_batches.set(merged_batches)  # Assign selected batches

            
            # 🧹 Remove invalid lecture links
            try:
                start_date_obj = datetime.strptime(str(start_date), "%Y-%m-%d").date() if start_date else None
                end_date_obj = datetime.strptime(str(end_date), "%Y-%m-%d").date() if end_date else None

                invalid_links = Lecture_batches.objects.filter(batches=batch).select_related('lecture')

                for link in invalid_links:
                    lecture = link.lecture
                    if not lecture or not lecture.lacture_date:
                        continue

                    if (start_date_obj and lecture.lacture_date < start_date_obj) or (end_date_obj and lecture.lacture_date > end_date_obj):
                        link.delete()

            except Exception as e:
                print(f"⚠️ Error cleaning up lectures: {e}")

            return JsonResponse({'status': 'success', 'msg': 'Batch updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_batch_by_Id(request):
     id = request.POST.get('id')
    #  print(id)
     Batch_Management(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'section deleted successfully.'})
# ==============================end batch_management===========================


@login_required(login_url=ADMIN_LOGIN_URL)
def section_selct(request): 
    section_id = request.GET.get('section')
    sub_section = Sub_Sections.objects.filter(section_id=section_id)
    return render(request, "select_sub_section.html" ,{'sub_section':sub_section})

@login_required(login_url=ADMIN_LOGIN_URL)
def subsection_select(request):  
    sub_section_id = request.GET.get('sub_section')
    topic = Topics.objects.filter(sub_section_id=sub_section_id)
    return render(request, "select_topic.html" ,{'topic':topic})

@login_required(login_url=ADMIN_LOGIN_URL)
def sub_section(request):
    section_list = Sections.objects.all()
    # Sub_Sections_list = Sub_Sections.objects.select_related('section')

    name=request.GET.get('name')
    section=request.GET.get('section_id')
    status=request.GET.get('status')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    Sub_Sections_list = Sub_Sections.objects.filter().order_by('-id')

    if name:
        Sub_Sections_list = Sub_Sections_list.filter(sub_section_name__icontains=name)
        filter=filter+'&name='+name
    if section:
        Sub_Sections_list = Sub_Sections_list.filter(section_id=section)
        filter=filter+'&section='+section
    if status:
        Sub_Sections_list = Sub_Sections_list.filter(status=status)
        filter=filter+'&status='+status
    if from_date:
        Sub_Sections_list = Sub_Sections_list.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        Sub_Sections_list = Sub_Sections_list.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    paginator = Paginator(Sub_Sections_list, 10)  # Show 25 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "sub-section.html",{'Sub_Sections_list':page_obj,'filter':filter,'section_list':section_list,'page':page_number,'name':name,'section':section,'status':status,'from_date':from_date,'to_date':to_date})

@login_required(login_url=ADMIN_LOGIN_URL)
def add_sub_section(request):
    if request.method == 'POST':
        # Get the form data
        section_name = request.POST.get('section_id')
        sub_section_name = request.POST.get('sub_section_name')
        display_order = request.POST.get('display_order')
        status = request.POST.get('status')
        # image = request.FILES.get('image')
        if(sub_section_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter sub section name.'})
        elif(display_order==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter display order.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            section = Sub_Sections(
                section_id=section_name,
                sub_section_name =sub_section_name,
                display_order=display_order,
                status=status
            )

            # Save the object to the database
            section.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Sub Section saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_sub_section(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        section=request.POST.get('section_id')
        sub_section_name = request.POST.get('sub_section_name')
        display_order = request.POST.get('display_order')
        status = request.POST.get('status')
       
        if(section==''):
            return JsonResponse({'status': 'error', 'msg': 'id is empty please pass the id '})
        if(sub_section_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter sub section name.'})
        elif(display_order==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter display order.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            sub_section = Sub_Sections(
                 id=id,
                sub_section_name = sub_section_name,
                section_id=section,
                display_order=display_order,
                status=status
            )

            # Save the object to the database
            sub_section.save(update_fields=["section_id","sub_section_name","display_order","status"])
            Topics.objects.filter(sub_section_id=id).update(section_id=section);
            Question_Bank.objects.filter(sub_section_id=id).update(section_id=section);
            ExamSyllabus.objects.filter(sub_section_id=id).update(section_id=section);

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Sub-section saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_sub_section_by_Id(request):
     id = request.POST.get('id')
    #  print(id)
     Sub_Sections(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'section deleted successfully.'})
  
@login_required(login_url=ADMIN_LOGIN_URL)     
def topic_list(request):
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    name=request.GET.get('name')
    section=request.GET.get('section_id')
    sub_section=request.GET.get('sub_section_id')
    status=request.GET.get('status')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''

    Topic_list = Topics.objects.filter().order_by('-id')
    if name:
        Topic_list = Topic_list.filter(topic_name__icontains=name)
        filter=filter+'&name='+name
    if section:
        Topic_list = Topic_list.filter(section_id=section)
        filter=filter+'&section_id='+section
    if sub_section:
        Topic_list = Topic_list.filter(sub_section_id=sub_section)
        filter=filter+'&sub_section_id='+sub_section
    if status:
        Topic_list = Topic_list.filter(status=status)
        filter=filter+'&status='+status
    if from_date:
        Topic_list = Topic_list.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        Topic_list = Topic_list.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    # Topic_list = Topics.objects.select_related('section', 'sub_section')
    paginator = Paginator(Topic_list, 10)  # Show 25 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "Topic-list.html",{'Topic_list':page_obj,'filter':filter,'Sub_Sections_list':Sub_Sections_list,'section_list':section_list,'page':page_number,'name':name,'section':section,'sub_section':sub_section,'status':status,'from_date':from_date,'to_date':to_date})

@login_required(login_url=ADMIN_LOGIN_URL)
def add_topic(request):
    if request.method == 'POST':
        # Get the form data
        section = request.POST.get('section')
        sub_section = request.POST.get('sub_section')
        topic_name = request.POST.get('topic_name')
        display_order = request.POST.get('display_order')
        status = request.POST.get('status')
        # image = request.FILES.get('image')
        if(section==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select section.'})
        elif(sub_section==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select sub section.'})
        elif(topic_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter topic name.'})
        elif(display_order==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter display order.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            section = Topics(
                section_id=section,
                sub_section_id=sub_section,
                topic_name=topic_name,
                display_order=display_order,
                status=status
            )

            # Save the object to the database
            section.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Topic saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_topic(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('topic_id')
        section = request.POST.get('edit_section')
        sub_section = request.POST.get('edit_sub_section')
        topic_name = request.POST.get('tname')
        display_order = request.POST.get('d_order')
        status = request.POST.get('edit_status')
        if(topic_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter topic name.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
           
                 # Create a new ExamCategory object
            topic = Topics(
                id=id,
                section_id=section,
                sub_section_id=sub_section,
                topic_name=topic_name,
                display_order=display_order,
                status=status
            )

            # Save the object to the database
            topic.save(update_fields=["section_id","sub_section_id","sub_section_id","topic_name","display_order","status"])
            Question_Bank.objects.filter(topic_id=id).update(section_id=section,sub_section_id=sub_section);
            ExamSyllabus.objects.filter(topic_id=id).update(section_id=section,sub_section_id=sub_section);
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Topic Updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_topic(request):   
    id = request.POST.get('id')
    Topics(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'Topic deleted successfully.'})


# ===================== start live lecture module ========================

# ===================== start live lecture module ========================

@login_required(login_url=ADMIN_LOGIN_URL)
def live_Lectures(request):
    batch_list = Batch_Management.objects.filter(status='Active')
    topics_list = Topics.objects.all()
    faculty_list = Faculty.objects.all()
    section_list = Sections.objects.all()
    batch = Batch_Management.objects.select_related('batch_name')
    topic = Lecture_Topics.objects.select_related('topics_id')
    faculty = Faculty.objects.select_related('faculty_name')

    title=request.GET.get('title')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    section=request.GET.get('section')
    batch=request.GET.get('batch')
    faculty=request.GET.get('faculty')
    filter = ''
    all_lectures = Live_Lecture.objects.prefetch_related('batches').filter().order_by('-id')
   

    if title:
        all_lectures = all_lectures.filter(title__icontains=title)
        filter=filter+'&title='+title
    if section:
        all_lectures = all_lectures.filter(section_id=section)
        filter=filter+'&section='+section
    if faculty:
        all_lectures = all_lectures.filter(faculty_id=faculty)
        filter=filter+'&faculty='+faculty
    if from_date:
        all_lectures = all_lectures.filter(lacture_date__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_lectures = all_lectures.filter(lacture_date__lte=to_date)
        filter=filter+'&to_date='+to_date
        
    if batch:
        if batch == "open_for_all":
            all_lectures = all_lectures.filter(lecture_type=batch)
        elif batch == "all_batches":
            all_lectures = all_lectures.filter(lecture_type=batch)
        else:
            all_lectures = all_lectures.filter(lecture_batches__batches_id=batch)
        filter=filter+'&batch='+batch
    # if batch:
    #     batches = Lecture_batches.objects.all().filter(batches_id=batch).values('lecture_id')
    #     batch1=batches[0]
    #     # res = list(batches.keys()).index('lecture_id')
    #     print(batch1["lecture_id"])
    #     all_lectures = all_lectures.filter(id=batch1["lecture_id"])
    # if from_date:
    #     all_lectures = all_lectures.filter(created_at__gte=from_date)
    # if to_date:
    #     all_lectures = all_lectures.filter(created_at__lte=to_date)


    today = timezone.now().date()
    upcoming_lectures = all_lectures.filter(lacture_date__gte=today).order_by('lacture_date', 'start_time')
    old_lectures = all_lectures.filter(lacture_date__lt=today).order_by('-lacture_date', 'start_time')

    paginator_old = Paginator(old_lectures, 10)
    page_number_old = request.GET.get("page")
    page_obj_old = paginator_old.get_page(page_number_old)
    
    zoom_keys = ZoomKeys.objects.all()
#    return JsonResponse({'status': 'error', 'msg': 'Please enter the title .','lacture_date':list(zoom_keys.values())})
    return render(request, "live_lecture.html",{
        'upcoming_lectures': upcoming_lectures,
        'old_lectures': page_obj_old,'filter':filter,
    'batch_list':batch_list,'topics_list':topics_list,'faculty_list':faculty_list,'faculty':faculty,'batch':batch,'topic':topic,'title':title,'page':page_number_old, 'section':section,'faculty':faculty,'from_date':from_date,'to_date':to_date,'section_list':section_list, 'zoom_keys':zoom_keys }
    );

@login_required(login_url=ADMIN_LOGIN_URL)   
def addLiveLecture(request):
    if request.method == 'POST':
        # Get the form data
        url = request.POST.get('url')
        title = request.POST.get('title')
        lecture_type = request.POST.get('lecture_type')
        batches = request.POST.getlist('batch')
        section = request.POST.get('section')
        topic = request.POST.get('topic')
        # image = request.FILES.get('image')
        faculty = request.POST.get('faculty')
        lacture_id = request.POST.get('lacture_id')
        lacture_date = request.POST.get('lacture_date')
        start_time = request.POST.get('start_time')
        end_time = request.POST.get('end_time')
        zoom_key_id = request.POST.get('zoom_key_id')
        # password = request.POST.get('password')
       
        if(title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter the title .'})
        elif(url==''):
            return JsonResponse({'status': 'error', 'msg': 'Please  enter the url '})    
        elif(lecture_type==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select lecture type '})
        elif(batches==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select batch.'})
        elif(section==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select subject.'})
        # elif(topic==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select topic.'})
        # elif(image==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(faculty==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select faculty.'})
        # elif(lacture_id==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter lacture id'})
        elif(lacture_date==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select lecture date'})
        elif(start_time==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select start time'})
        # elif(end_time==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select end time '})
        
        else:
        
            # Combine date + time
            dt = datetime.strptime(f"{lacture_date} {start_time}", "%Y-%m-%d %H:%M")
            start_time_iso = dt.isoformat()
            tz = pytz.timezone('Asia/Kolkata')

            start_dt_local = tz.localize(
                datetime.strptime(f"{lacture_date} {start_time}", "%Y-%m-%d %H:%M")
            )

            end_dt_local = tz.localize(
                datetime.strptime(f"{lacture_date} {end_time}", "%Y-%m-%d %H:%M")
            )

            # If the end time is earlier/equal (e.g., class crosses midnight), assume next day
            if end_dt_local <= start_dt_local:
                end_dt_local += timedelta(days=1)

            # Duration in whole minutes (min 1)
            duration = max(1, int((end_dt_local - start_dt_local).total_seconds() // 60))
            
            start_time_iso_local = start_dt_local.strftime("%Y-%m-%dT%H:%M:%S")
            meeting_id = ""
            password = ""
            meeting = []
            url = lacture_id
            if zoom_key_id:
                zoomkey = ZoomKeys.objects.filter(id=zoom_key_id).first();
                access_token = get_zoom_access_token(zoomkey.account_id,zoomkey.client_id,zoomkey.client_secret)
                zoom_user_id = "me"  # or your admin Zoom user email
                duration = int(request.POST.get('duration', 60))  # in minutes

                # Create meeting on Zoom
                headers = {
                    "Authorization": f"Bearer {access_token}",
                    "Content-Type": "application/json"
                }
                payload = {
                    "topic": title,
                    "type": 2,
                    "start_time": start_time_iso_local,
                    "duration": duration,
                    "timezone": "Asia/Kolkata",
                    "password": "123456",
                    "settings": {
                        "join_before_host": True,
                        "waiting_room": False
                    }
                }

                res = requests.post(
                    f"https://api.zoom.us/v2/users/{zoom_user_id}/meetings",
                    headers=headers,
                    json=payload
                )

                if res.status_code != 201:
                    return JsonResponse({"status": "error", "msg": res.json()})

                meeting = res.json()
                meeting_id = meeting["id"]
                password = meeting.get("password", "")
                url = meeting["join_url"]

            # Create a new ExamCategory object
            livelecture = Live_Lecture.objects.create(
                title=title,
#                url=url,
                zoom_key_id=zoom_key_id,
                url=url,
                meeting_id=meeting_id,
                password=password,
                meeting_data=json.dumps(meeting),
                lecture_type=lecture_type,
                section_id=section,
                topic_id=topic,
                faculty_id=faculty,
                # lacture_id=lacture_id,
                lacture_date=lacture_date,
                start_time=start_time,
                end_time=end_time,
                # password=password
            )


            # Save the object to the database
            # livelecture.save()
            lecture_id = livelecture.id
           
            
            for batch_id in batches:
                lecturebatch = Lecture_batches(
                    lecture_id=lecture_id,
                    batches_id=batch_id
                )
                lecturebatch.save()

            # for topic in topic:
            #     lecturetopic = Lecture_Topics(
            #         lecture_id=lecture_id,
            #         topic_id=topic
            #     )
            #     lecturetopic.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'live lecture info  saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def editLiveLecture(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        lacture_id = request.POST.get('lacture_id')
        title = request.POST.get('title')
        lecture_type = request.POST.get('lecture_type')
        batches = request.POST.getlist('batch')
        section = request.POST.get('section')
        topic = request.POST.get('topic')
        # image = request.FILES.get('image')
        faculty = request.POST.get('faculty')
        lacture_url = request.POST.get('lacture_id')
        lacture_date = request.POST.get('lacture_date')
        start_time = request.POST.get('start_time')
        end_time = request.POST.get('end_time')
        zoom_key_id = request.POST.get('zoom_key_id')
        # password = request.POST.get('password')
       
        if(title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter the title .'})
#        elif(url==''):
#            return JsonResponse({'status': 'error', 'msg': 'Please  enter the url '})    
        elif(lecture_type==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter lecture type '})
        elif(batches==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select batch.'})
        elif(section==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select subject.'})
        # elif(topic==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select topic.'})
        # elif(image==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(faculty==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select faculty.'})
        # elif(lacture_url==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter lacture url'})
        elif(lacture_date==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select lecture date'})
        elif(start_time==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select start time'})
        # elif(end_time==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select end time '})
        else:
        
            def trim_trailing_zero_seconds(t: Optional[str]) -> Optional[str]:
                """
                If time string is 'HH:MM:SS' with seconds == '00', return 'HH:MM'.
                Leaves other inputs unchanged. Trims surrounding whitespace.
                """
                if t is None:
                    return None
                t = t.strip()
                # Match exactly HH:MM:00 (24-hour)
                return re.sub(r'^([01]?\d|2[0-3]):([0-5]\d):00$', r'\1:\2', t)
            start_time = trim_trailing_zero_seconds(request.POST.get('start_time'))
            end_time   = trim_trailing_zero_seconds(request.POST.get('end_time'))
            dt = datetime.strptime(f"{lacture_date} {start_time}", "%Y-%m-%d %H:%M")
            start_time_iso = dt.isoformat()
            tz = pytz.timezone('Asia/Kolkata')

            start_dt_local = tz.localize(
                datetime.strptime(f"{lacture_date} {start_time}", "%Y-%m-%d %H:%M")
            )

            end_dt_local = tz.localize(
                datetime.strptime(f"{lacture_date} {end_time}", "%Y-%m-%d %H:%M")
            )

            # If the end time is earlier/equal (e.g., class crosses midnight), assume next day
            if end_dt_local <= start_dt_local:
                end_dt_local += timedelta(days=1)

            # Duration in whole minutes (min 1)
            duration = max(1, int((end_dt_local - start_dt_local).total_seconds() // 60))
            
            start_time_iso_local = start_dt_local.strftime("%Y-%m-%dT%H:%M:%S")

            lecture = Live_Lecture.objects.get(id=id)
            meeting_id = lecture.meeting_id  # e.g., "98765432101"
            zoom_user_id = "me"  # or your admin Zoom user email
#            duration = int(request.POST.get('duration', 60))  # in minutes
            join_url = lacture_id
            meeting_data=lecture.meeting_data
            meeting = []
            if zoom_key_id:
                zoomkey = ZoomKeys.objects.filter(id=zoom_key_id).first();
                access_token = get_zoom_access_token(zoomkey.account_id,zoomkey.client_id,zoomkey.client_secret)

                headers = {
                    "Authorization": f"Bearer {access_token}",
                    "Content-Type": "application/json"
                }

                update_payload = {
                    "topic": title,
                    "type": 2,  # scheduled meeting
                    "start_time": start_time_iso_local,        # e.g., "2025-08-12T10:30:00"
                    "duration": duration,                # minutes
                    "timezone": "Asia/Kolkata",
                    "password": "123456",
                    "settings": {
                        "join_before_host": True,
                        "waiting_room": False
                    }
                }

                update_res = requests.patch(
                    f"https://api.zoom.us/v2/meetings/{meeting_id}",
                    headers=headers,
                    json=update_payload
                )

                # Zoom usually responds with 204 No Content on success
                if update_res.status_code not in (204, 200):
                    # If it’s an error, Zoom returns JSON
                    try:
    #                    return JsonResponse({"status": "error", "msg": update_res.json()})
                        return JsonResponse({'status': 'success', 'msg': 'Failed to update past lecture.'})
                    except Exception:
    #                    return JsonResponse({"status": "error", "msg": update_res.text})
                        return JsonResponse({'status': 'success', 'msg': 'Failed to update past lecture.'})

                # (Optional) Fetch updated meeting details
                detail_res = requests.get(
                    f"https://api.zoom.us/v2/meetings/{meeting_id}",
                    headers=headers
                )
                if detail_res.status_code != 200:
    #                return JsonResponse({"status": "ok", "updated": True, "note": "Updated but failed to fetch details."})
                    return JsonResponse({'status': 'success', 'msg': 'Failed to update past lecture.'})
                else:
                    meeting = detail_res.json()
                    meeting_data=json.dumps(meeting)
                    join_url=meeting["join_url"]
            # Create a new ExamCategory object
            livelecture = Live_Lecture(
                id=id,
                title=title,
                meeting_data=meeting_data,
#                    url=url,
                url=join_url,
                lecture_type=lecture_type,
                section_id=section,
                topic_id=topic,
                faculty_id=faculty,
                # lacture_url=lacture_url,
                lacture_date=lacture_date,
                start_time=start_time,
                end_time=end_time
                # password=password
                )
            livelecture.save(update_fields=["title","lecture_type","section_id","faculty_id","lacture_date","start_time","end_time","topic_id","meeting_data","url"])

            # lecture_id = livelecture.id
           
            if batches:
                Lecture_batches.objects.filter(lecture_id=id).delete()
            Lecture_Topics.objects.filter(lecture_id=id).delete()

            for batch_id in batches:
                lecturebatch = Lecture_batches(
                    lecture_id=id,
                    batches_id=batch_id
                )
                lecturebatch.save()

            # for topic in topics:
            #     lecturetopic = Lecture_Topics(
                   
            #         lecture_id=id,
            #         topics_id=topic
            #     )
            #     lecturetopic.save()
            return JsonResponse({'status': 'success', 'msg': 'Lecture Details Updated successfully.'})
            # Return a success response
    else:
        # Return an error response
        return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def deleteLiveLecturebyId(request):
    id = request.POST.get('id')
    lecture = Live_Lecture.objects.get(id=id)
    meeting_id = lecture.meeting_id
    
    if not meeting_id:
#        return JsonResponse({"status": "error", "msg": "meeting_id is required"})
        Live_Lecture(id).soft_delete()
        return JsonResponse({'status': 'success', 'msg': 'lecture deleted successfully.'})
        
    zoomkey = ZoomKeys.objects.filter(id=lecture.zoom_key_id).first();
    access_token = get_zoom_access_token(zoomkey.account_id,zoomkey.client_id,zoomkey.client_secret)

    headers = {
        "Authorization": f"Bearer {access_token}",
        "Content-Type": "application/json"
    }


    update_res = requests.delete(
        f"https://api.zoom.us/v2/meetings/{meeting_id}",
        headers=headers
    )
    
    # Zoom usually responds with 204 No Content on success
#    if update_res.status_code not in (204, 200):
#        # If it’s an error, Zoom returns JSON
#        try:
#            return JsonResponse({"status": "error", "msg": update_res.json()})
#        except Exception:
#            return JsonResponse({"status": "error", "msg": update_res.text})

    #  print(id)
    Live_Lecture(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'lecture deleted successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def uploadVideobyLectureId(request):
    if request.method == 'POST':
        id = request.POST.get('id')
#        thumbnail = request.FILES.get('thumbnail', False)
        video_description = request.POST.get('video_description')
        videos = request.FILES.get('video-lecture', False)

#        if not thumbnail:
#            return JsonResponse({'status': 'error', 'msg': 'Please select thumbnail.'})
        if not video_description:
            return JsonResponse({'status': 'error', 'msg': 'Please enter video description.'})
        elif not videos:
            return JsonResponse({'status': 'error', 'msg': 'Please select video file to upload'})
#        elif thumbnail.size > 1048576:
#            return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data': thumbnail.size})

        # Generate unique file names
        def get_unique_filename(file, prefix):
            ext = os.path.splitext(file.name)[1]
            unique_name = f"{prefix}_{uuid.uuid4().hex[:8]}_{timezone.now().strftime('%Y%m%d%H%M%S')}{ext}"
            file.name = unique_name
            return file

#        thumbnail = get_unique_filename(thumbnail, 'thumb')
        videos = get_unique_filename(videos, 'video')

        # Update fields of existing Live_Lecture instance
        try:
            lecture = Live_Lecture.objects.get(id=id)
#            lecture.thumbnail = thumbnail
            lecture.video_description = video_description
            lecture.videos = videos
            lecture.save(update_fields=["video_description", "videos"])
        except Live_Lecture.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Lecture not found.'})

        return JsonResponse({'status': 'success', 'msg': 'Video uploaded successfully.'})
    else:
        return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

def upload_question_explaination_video(request):
    if request.method == "POST":
        quid = request.POST.get('quid')
        video = request.FILES.get('video')

        if not (quid and video):
            return JsonResponse({'error': 'Missing data'}, status=400)

        try:
            # Generate unique file name
            def get_unique_filename(file, prefix):
                ext = os.path.splitext(file.name)[1]
                unique_name = f"{prefix}_{uuid.uuid4().hex[:8]}_{timezone.now().strftime('%Y%m%d%H%M%S')}{ext}"
                file.name = unique_name
                return file

            video = get_unique_filename(video, 'explanation_video')

            # Get the base question
            question = Question_Bank.objects.get(id=quid)

            # Get tag value
            tag_value = question.tag
            if not tag_value:
                question.explanation_video = video
                question.save(update_fields=["explanation_video"])
                return JsonResponse({'success': True})

            # Update all questions with same tag
            Question_Bank.objects.filter(tag=tag_value).update(explanation_video=video)

            return JsonResponse({'success': True})

        except Question_Bank.DoesNotExist:
            return JsonResponse({'error': 'Question not found'}, status=404)

    return JsonResponse({'error': 'Invalid request'}, status=400)

@login_required(login_url=ADMIN_LOGIN_URL)
def Select_section_topic(request):
    section_id=request.GET.get('section')
    topics=Topics.objects.filter(section_id=section_id)
    return render(request, "select_section_topic.html" ,{'topics':topics})

# ===================== end live lecture module ========================


# ===================== end live lecture module ========================

@login_required(login_url=ADMIN_LOGIN_URL)
def examMaster(request):
    section_list = Sections.objects.all()
    all_categories = ExamCategory.objects.all()
    name=request.GET.get('name')
    category=request.GET.get('category_id')
    status=request.GET.get('status')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    all_exams = Exam_Master.objects.filter().order_by('-id')

    if name:
        all_exams = all_exams.filter(exam_name__icontains=name)
        filter=filter+'&name='+name
    if category:
        all_exams = all_exams.filter(exam_category_id=category)
        filter=filter+'&category='+category
    if status:
        all_exams = all_exams.filter(status=status)
        filter=filter+'&status='+status
    if from_date:
        all_exams = all_exams.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_exams = all_exams.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    # all_exams = Exam_Master.objects.all()
    paginator = Paginator(all_exams, 10)  # Show 25 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "exam-master.html",{'all_exams':page_obj,'filter':filter,'section_list':section_list,'all_categories':all_categories,'page':page_number,'name':name,'category':category,'status':status,'from_date':from_date,'to_date':to_date})

@login_required(login_url=ADMIN_LOGIN_URL)
def addExam(request):
    if request.method == 'POST':
        # Get the form data
        category_id= request.POST.get('category_id')
        exam_name = request.POST.get('exam_name')
        marks_per_question = request.POST.get('marks_per_question')
        negative_marks_per_question = request.POST.get('negative_marks_per_question')
        status = request.POST.get('status')
        image = request.FILES.get('image',False)
        banner = request.FILES.get('banner',False)
        if(exam_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter exam name.'})
        elif(category_id==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select category'})
        elif(marks_per_question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter marks per question'})
        elif(negative_marks_per_question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter negative marks per question'})
        elif(image== False):
            return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(banner== False):
            return JsonResponse({'status': 'error', 'msg': 'Please select banner.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            if( image != False and  banner == False ):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})    
            # Create a new ExamCategory object
                category = Exam_Master.objects.create(
                exam_category_id=category_id,
                exam_name=exam_name,
                image=image,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
                status=status
            )
            elif( image == False and  banner != False ):
                if banner.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select banner image size less than or equal to 1 MB.', 'data':banner.size, 'byter':banner.size})    
            # Create a new ExamCategory object
                category = Exam_Master.objects.create(
                exam_category_id=category_id,
                exam_name=exam_name,
                banner=banner,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
                status=status
            )
            elif( image != False and  banner != False ):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':banner.size, 'byter':banner.size})
                elif banner.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select banner image size less than or equal to 1 MB.', 'data':banner.size, 'byter':banner.size})
                            

                    
            # Create a new ExamCategory object
                category = Exam_Master.objects.create(
                exam_category_id=category_id,
                exam_name=exam_name,
                banner=banner,
                image=image,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
                status=status
            )
            else:
                category = Exam_Master.objects.create(
                exam_category_id=category_id,
                exam_name=exam_name,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
                status=status
            )

            # Save the object to the database
            # category.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Exam saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def editExam(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        category_id= request.POST.get('category_id')
        exam_name = request.POST.get('exam_name')
        marks_per_question = request.POST.get('marks_per_question')
        negative_marks_per_question = request.POST.get('negative_marks_per_question')
        status = request.POST.get('status')
        image = request.FILES.get('image',False)
        banner = request.FILES.get('banner',False)

        if(exam_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter exam name.'})
        elif(category_id==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select exam category'})
        elif(marks_per_question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter marks per question'})
        elif(negative_marks_per_question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter negative marks per question'})
        elif(image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(banner==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select banner.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            if( image != False and  banner == False ):
                    if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})
                # Create a new ExamCategory object
                    exam = Exam_Master(
                        id=id,
                        exam_category_id=category_id,
                        exam_name=exam_name,
                        image=image,
                        # banner=banner,
                        marks_per_question=marks_per_question,
                        negative_marks_per_question=negative_marks_per_question,
                        status=status
                    )

                # Save the object to the database
                    exam.save(update_fields=["exam_name","image","marks_per_question","negative_marks_per_question","status"])
                    # exam.save()
            elif( image == False and  banner != False ):
                if banner.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select banner image size less than or equal to 1 MB.', 'data':banner.size, 'byter':banner.size})    
            # Create a new ExamCategory object
                exam = Exam_Master(
                    id=id,
                exam_category_id=category_id,
                exam_name=exam_name,
                # image=image,
                banner=banner,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
                status=status
            )
                exam.save(update_fields=["exam_name","banner","marks_per_question","negative_marks_per_question","status"])
            
            elif( image != False and  banner != False ):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':banner.size, 'byter':banner.size})
                elif banner.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select banner image size less than or equal to 1 MB.', 'data':banner.size, 'byter':banner.size})
                exam = Exam_Master(
                    id=id,
                exam_category_id=category_id,
                exam_name=exam_name,
                image=image,
                banner=banner,
                marks_per_question=marks_per_question,
                negative_marks_per_question=negative_marks_per_question,
                status=status
            )
                exam.save(update_fields=["exam_name","banner","image","marks_per_question","negative_marks_per_question","status"])
            else:
                 # Create a new ExamCategory object
                    exam = Exam_Master(
                    id=id,
                    exam_category_id=category_id,
                    exam_name=exam_name,
                    marks_per_question=marks_per_question,
                    negative_marks_per_question=negative_marks_per_question,
                    status=status
                     )

                # Save the object to the database
                    exam.save(update_fields=["exam_name","marks_per_question","negative_marks_per_question","status"])


            # Return a success response
        return JsonResponse({'status': 'success', 'msg': 'Exam saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def deleteExamById(request):
     id = request.POST.get('id')
    #  print(id)
     Exam_Master(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Exam deleted successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def AddSubject(request):
     section_id = request.POST.getlist('section_id')
     id = request.POST.get('id')
     for section in section_id:
        subject = Exam_Subjects(
                exam_id=id,
                section_id=section,
               
        )
            # Save the object to the database
        subject.save()
    #  print(section_id,id)
     return JsonResponse({'status': 'success', 'msg': 'subject added successfully.'})

# =========================end exam master =============================


# ====================== start Blog category ========================

@login_required(login_url=ADMIN_LOGIN_URL)
def blog_category(request):
    # blog_category = Blog_category.objects.all()
    # all_blogs = Blogs.objects.all()
    category_name=request.GET.get('blog_title')
    status=request.GET.get('status')

    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    blog_category = Blog_category.objects.filter().order_by('-id')

    if category_name:
        blog_category = blog_category.filter(category_name__icontains=category_name)
        filter=filter+'&category_name='+category_name

    if status:
        blog_category = blog_category.filter(status=status)
        filter=filter+'&status='+status
    
    if from_date:
        blog_category = blog_category.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date

    if to_date:
        blog_category = blog_category.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
    paginator = Paginator(blog_category, 10)  # Show 10 contacts per page.

    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "blog_category.html",{'blog_category_list':page_obj,'filter':filter,'page':page_number,'category_name':category_name,'status':status,'from_date':from_date,'to_date':to_date});

@login_required(login_url=ADMIN_LOGIN_URL)
def add_blog_category(request):
    if request.method == 'POST':
        # Get the form data
        Blog_category_name = request.POST.get('Blog_category_name')
        meta_title = request.POST.get('meta_title')
        meta_discription = request.POST.get('meta_discription')
        slug = request.POST.get('slug')
        blog_image = request.FILES.get('image',False)
        status = request.POST.get('status')
       
        if(Blog_category_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter blog category name.'})
        elif(blog_image==False):
            return JsonResponse({'status': 'error', 'msg': 'Please select blog image.'})
        elif(meta_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meta title.'})
        elif(meta_discription==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meta discription.'})
        elif(slug==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter slug.'})
        
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            if blog_image.size > 1048576:
                    return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':blog_image.size, 'byter':blog_image.size})   
            # Create a new ExamCategory object
            blog = Blog_category(               
                category_name=Blog_category_name,
                meta_title=meta_title,
                meta_discription=meta_discription,
                slug=slug,
                blog_category_image=blog_image,
                status=status
            )

            # Save the object to the database
            blog.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Blog Category created  successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_blog_category(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        Blog_category_name = request.POST.get('blog_category_name')
        meta_title = request.POST.get('meta_title')
        meta_discription = request.POST.get('meta_discription')
        slug = request.POST.get('slug')
        blog_image = request.FILES.get('blog_image')
        status = request.POST.get('status')
       
        if(Blog_category_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter blog category name.'})
        
        elif(meta_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Meta title.'})
        elif(meta_discription==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meta description.'})
        elif(slug==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter slug.'})
        elif(blog_image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            if(request.FILES.get('blog_image')):
                if blog_image.size > 1048576:
                    return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':blog_image.size, 'byter':blog_image.size})
                batch = Blog_category(
                id=id,
                category_name=Blog_category_name,
                meta_title=meta_title,
                meta_discription=meta_discription,
                slug=slug,
                blog_category_image=blog_image,
                status=status
            )
                batch.save(update_fields=["category_name","meta_title","meta_discription","slug","blog_category_image","status"])
            else:
                batch = Blog_category(
                id=id,
                category_name=Blog_category_name,
                meta_title=meta_title,
                meta_discription=meta_discription,
                slug=slug,
                status=status
            )
                batch.save(update_fields=["category_name","meta_title","meta_discription","slug","status"])
            # Return a success response

            return JsonResponse({'status': 'success', 'msg': 'Blog category updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_blog_category(request):
     id = request.POST.get('id')
    #  print(id)
     Blog_category(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Blog Category deleted successfully.'})

# ==============================end Blog Category===========================
# ====================== start Blog Post ========================

@login_required(login_url=ADMIN_LOGIN_URL)
def blogs_list(request):
    blog_category = Blog_category.objects.all()
    # all_blogs = Blogs.objects.all()

    blog_title=request.GET.get('blog_title')
    blog_status=request.GET.get('blog_status')

    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    all_blogs = Blogs.objects.filter().order_by('-id')

    if blog_title:
        all_blogs = all_blogs.filter(blog_title__icontains=blog_title)
        filter=filter+'&blog_title='+blog_title
    if blog_status:
        all_blogs = all_blogs.filter(blog_status=blog_status)
        filter=filter+'&blog_status='+blog_status
    
    if from_date:
        all_blogs = all_blogs.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date

    if to_date:
        all_blogs = all_blogs.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    paginator = Paginator(all_blogs, 10)  # Show 10 contacts per page.

    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "blogs_list.html",{'all_blogs':page_obj,'filter':filter,'blog_category':blog_category,'page':page_number,'blog_title':blog_title,'blog_status':blog_status,'from_date':from_date,'to_date':to_date,});

@login_required(login_url=ADMIN_LOGIN_URL)
def add_blogs(request):
    if request.method == 'POST':
        # Get the form data
        blog_category = request.POST.get('blog_category')
        blog_title = request.POST.get('blog_title')
        blog_discription = request.POST.get('blog_discription')
        meta_title = request.POST.get('meta_title')
        meta_discription = request.POST.get('meta_discription')
        slug = request.POST.get('slug')
        blog_image = request.FILES.get('blog_image')
        status = request.POST.get('status')
       
        if(blog_category==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select blog category.'})
        elif(blog_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter blog title.'})
        # elif(blog_discription==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter blog discription.'})
        elif(meta_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meta title.'})
        elif(meta_discription==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meta discription.'})
        elif(slug==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter slug.'})
        elif(blog_image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select blog image.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            if blog_image.size > 1048576:
                return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':blog_image.size, 'byter':blog_image.size})   
            # Create a new ExamCategory object
            blog = Blogs(
                category_id=blog_category,
                blog_title=blog_title,
                blog_discription=blog_discription,
                meta_title=meta_title,
                meta_discription=meta_discription,
                slug=slug,
                blog_image=blog_image,
                blog_status=status
            )

            # Save the object to the database
            blog.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Blog post successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_blogs(request):
    if request.method == 'POST':
        # Get the form data
        blog_id = request.POST.get('blog_id')
        blog_category = request.POST.get('blog_category')
        blog_title = request.POST.get('blog_title')
        blog_discription = request.POST.get('blog_discription')
        meta_title = request.POST.get('meta_title')
        meta_discription = request.POST.get('meta_discription')
        slug = request.POST.get('slug')
        blog_image = request.FILES.get('blog_image')
        status = request.POST.get('status')
       
        if(blog_category==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select blog category.'})
        elif(blog_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter blog title.'})
        # elif(blog_discription==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter blog discription.'})
        elif(meta_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meta title.'})
        elif(meta_discription==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meta discription.'})
        elif(slug==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter slug.'})
        elif(blog_image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select blog image.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            
            batch = Blogs(
                    id=blog_id,
                    category_id=blog_category,
                    blog_title=blog_title,
                    blog_discription=blog_discription,
                    meta_title=meta_title,
                    meta_discription=meta_discription,
                    slug=slug,
                    blog_image=blog_image,
                    blog_status=status
                )
            if 'blog_image' in request.FILES:
                if blog_image.size > 1048576:
                    return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':blog_image.size, 'byter':blog_image.size})
                batch.save(update_fields=["category_id","blog_title","blog_discription","meta_title","meta_discription","slug","blog_image","blog_status"])
            else:
                batch.save(update_fields=["category_id","blog_title","blog_discription","meta_title","meta_discription","slug","blog_status"])
                
            # Return a success response

            return JsonResponse({'status': 'success', 'msg': 'Blog post updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_blog(request):
     id = request.POST.get('id')
     print(id)

     Blogs(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Blog post deleted successfully.'})

# ==============================end Blog Post ===========================

# ============================start banner =============================

@login_required(login_url=ADMIN_LOGIN_URL)
def banners(request):
    section_list = Sections.objects.all()
    all_banner = Banners.objects.all()
    paginator = Paginator(all_banner, 10)  # Show 25 contacts per page.

    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "banner.html",{'all_banner':page_obj,'section_list':section_list})

 
@login_required(login_url=ADMIN_LOGIN_URL)       
def add_banner(request):
    if request.method == 'POST':
        # Get the form data
        banner_image = request.FILES.get('image')
        position = request.POST.get('position')
        status = request.POST.get('status')
        redirect_url = request.POST.get('redirect_url')
        if(banner_image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select banner.', 'data': banner_image.FILES.get('image')})
        elif(position==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select banner position'})
        elif(redirect_url==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Redirect url.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            if banner_image.size > 1048576:
                return JsonResponse({'status': 'error', 'msg': 'Please select banner banner size less than or equal to 1 MB.', 'data':banner_image.size, 'byter':banner_image.size})
            banner = Banners(
                banner_image=banner_image,
                position=position,
                redirect_url=redirect_url,
                status=status
            )

            # Save the object to the database
            banner.save()

            # Return a success response
        return JsonResponse({'status': 'success', 'msg': 'Banner saved successfully.', 'data':banner_image.size})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
  
@login_required(login_url=ADMIN_LOGIN_URL)  
def edit_banner(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        banner_image = request.FILES.get('image')
        position = request.POST.get('position')
        status = request.POST.get('status')
        redirect_url = request.POST.get('redirect_url')
        if(banner_image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select banner.'})
        elif(position==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select banner position.'})
        elif(redirect_url==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Redirect url.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            if(request.FILES.get('image')):
                if banner_image.size > 1048576:
                    return JsonResponse({'status': 'error', 'msg': 'Please select banner banner size less than or equal to 1 MB.', 'data':banner_image.size, 'byter':banner_image.size})
                # Create a new ExamCategory object
                banner = Banners(
                    id=id,
                    banner_image=banner_image,
                    position=position,
                    redirect_url=redirect_url,
                    status=status,
                    # updated_at=date.now()
                )

                # Save the object to the database
                banner.save(update_fields=["banner_image","position","redirect_url","status"])
            else:
                 # Create a new ExamCategory object
                 banner= Banners(
                    id=id,
                    position=position,
                    redirect_url=redirect_url,
                    status=status,
                    # updated_at=date.now()
                )

                # Save the object to the database
            banner.save(update_fields=["position","redirect_url","status"])
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'banner saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_banner(request):  
   
    id = request.POST.get('id')
    Banners(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'banner deleted successfully.'})
# ============================end banner=============================

@login_required(login_url=ADMIN_LOGIN_URL)
def notification(request):
    # all_notification = Notifications.objects.all()
    batch_list = Batch_Management.objects.filter(status='Active')
    batch=request.GET.get('batch')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    all_notification = Notifications.objects.filter().order_by('-id')

    if batch:

        if batch == "open_for_all":
            all_notification = all_notification.filter(notification_type=batch)
            filter=filter+'&batch='+batch
        elif batch == "all_batches":
            all_notification = all_notification.filter(notification_type=batch)
            filter=filter+'&batch='+batch
        else:
            # all_notification =  Notifications_batches.objects.filter(batches_id=batch).select_related('Notification').values('Notification')
            # batch1=batches[0]
            # print(batches)
            all_notification = all_notification.filter(notifications_batches__batches_id=batch)
            filter=filter+'&batch='+batch

    if from_date:
        all_notification = all_notification.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_notification = all_notification.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    paginator = Paginator(all_notification, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "notification.html",{'all_notification':page_obj,'filter':filter,'page':page_number,'from_date':from_date,'to_date':to_date,'batch_list':batch_list,'batch':batch});

@login_required(login_url=ADMIN_LOGIN_URL)
def add_notification(request):
    if request.method == 'POST':
        # Get the form data
        message = request.POST.get('message')
        batches = request.POST.getlist('batch')
        notification_type = request.POST.get('notification_type')
        redirect_url = request.POST.get('redirect_url')
       
        if(message==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Message.'})
        elif(redirect_url==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter redirect url.'})
        else:    
            # Create a new ExamCategory object
            notification = Notifications(
                message=message,
                notification_type=notification_type,
                redirect_url=redirect_url
            )

            # Save the object to the database
            notification.save()

            notification = notification.id
           
            
            for batch_id in batches:
                lecturebatch = Notifications_batches(
                    Notification_id=notification,
                    batches_id=batch_id
                )
                lecturebatch.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Notifcation  created successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_notification(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        message = request.POST.get('message')
        batches = request.POST.getlist('batch')
        notification_type = request.POST.get('notification_type')
        redirect_url = request.POST.get('redirect_url')
       
        if(message==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Message.'})
        elif(redirect_url==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter redirect url.'})
        else:    
            # Create a new ExamCategory object
            notification = Notifications(
                id=id,
                message=message,
                notification_type=notification_type,
                redirect_url=redirect_url
            )
            notification.save(update_fields=["message","redirect_url","notification_type"])
    
            Notifications_batches.objects.filter(Notification_id=id).soft_delete()
            for batch_id in batches:
                    lecturebatch = Notifications_batches(
                        Notification_id=id,
                        batches_id=batch_id
                    )
                    lecturebatch.save()
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Notifcation  updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_notification(request):
    if request.method == 'POST':
        id = request.POST.get('id')
        Notifications(id).soft_delete()
        return JsonResponse({'status': 'success', 'msg': 'Notification deleted successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


# ======================end module notfications ========================

# ======================start Testimonial module ========================

@login_required(login_url=ADMIN_LOGIN_URL)
def testimonial(request):
    name=request.GET.get('name')
    course=request.GET.get('course')
    status=request.GET.get('status')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''

    all_testimonial = Testimonials.objects.filter().order_by('-id')

    if name:
        all_testimonial = all_testimonial.filter(name__icontains=name)
        filter=filter+'&name='+name
    if course:
        all_testimonial = all_testimonial.filter(course=course)
        filter=filter+'&course='+course
    if status:
        all_testimonial = all_testimonial.filter(status=status)
        filter=filter+'&status='+status
    if from_date:
        all_testimonial = all_testimonial.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_testimonial = all_testimonial.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    # all_testimonial=Testimonials.objects.all()
    paginator = Paginator(all_testimonial, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "testimonial.html",{'all_testimonial':page_obj ,'filter':filter,'name':name,'course':course,'status':status,'page':page_number,'from_date':from_date,'to_date':to_date});

@login_required(login_url=ADMIN_LOGIN_URL)
def add_testimonial(request):
    if request.method == 'POST':
        # Get the form data
        name = request.POST.get('name')
        course = request.POST.get('course')
        image = request.FILES.get('image')
        review = request.POST.get('review')
        status= request.POST.get('status')
       
        if(name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter name.'})
        elif(course==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course name.'})
        # elif(image==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(review==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter review.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status'})
        else:
            if(request.FILES.get('image')):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})  
            # Create a new ExamCategory object
                testimonial = Testimonials.objects.create(
                name=name,
                course=course,
                image=image,
                review=review,
                status=status
                )
            else:
                 # Create a new ExamCategory object
                testimonial = Testimonials.objects.create(
                name=name,
                course=course,
                review=review,
                status=status
                    )

             # Save the object to the database
                # testimonial.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'testimonial created successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_testimonial(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        name = request.POST.get('name')
        course = request.POST.get('course')
        image = request.FILES.get('image')
        review = request.POST.get('review')
        status= request.POST.get('status')
        if(name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter name.'})
        elif(course==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course name.'})
        elif(image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(review==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter review.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status'})
        else:    
            if(request.FILES.get('image')):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})
                # Create a new ExamCategory object
                testimonial = Testimonials(
                    id=id,
                    name=name,
                    course=course,
                    image=image,
                    review=review,
                    status=status
                )

                # Save the object to the database
                testimonial.save(update_fields=["name","course","image","review","status"])
               
            else:
                 # Create a new ExamCategory object
                testimonial = Testimonials(
                    id=id,
                    name=name,
                    course=course,
                    review=review,
                    status=status
                )

                # Save the object to the database
                testimonial.save(update_fields=["name","course","review","status"])
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Testimonial saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_testimonial(request):
    if request.method == 'POST':
        id = request.POST.get('id')
        Testimonials(id).soft_delete()
        return JsonResponse({'status': 'success', 'msg': 'Testimonial deleted successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
# ======================start Testimonial module ========================

# ==================seles module start ===================

@login_required(login_url=ADMIN_LOGIN_URL)
def Seles(request):
    msg = ''

    # 1) Handle POSTs
    if request.method == 'POST' and 'student_id' in request.POST:
        OrderCourses.objects.create(
            student_id=request.POST.get('student_id'),
            course_id=request.POST.get('course_id'),
            batch_id=request.POST.get('batch_id'),
            lectures=1
        )
        msg = "Batch assigned successfully."
    elif request.method == 'POST' and 'update_order_id' in request.POST:
        order_id = request.POST.get('update_order_id')
        action = request.POST.get('order_action')
        if order_id and action in ['refunded', 'canceled']:
            Orders.objects.filter(id=order_id).update(order_action=action, is_order=0)
            msg = f"Order {order_id} marked as {action.title()}."

    # 2) Filters (GET)
    order_id = request.GET.get('order_id')
    name     = request.GET.get('name')
    number   = request.GET.get('number')
    course_id= request.GET.get('course_id')
    from_date= request.GET.get('from_date')
    to_date  = request.GET.get('to_date')

    # Show only courses that actually exist in OrderCourses (better UX)
    all_courses = Course.objects.filter(ordercourses__isnull=False).distinct().order_by('course_name')
    all_batches = Batch_Management.objects.filter(status='Active').order_by('batch_name')

    # Base queryset — only successful/active orders
    all_sales = (
        Orders.objects
        .filter(is_order=1)
        .select_related('student')              # student FK
        .prefetch_related('ordercourses_set__course', 'ordercourses_set__batch')  # reverse relation
        .order_by('-id')
    )

    # Apply filters
    filter_qs = ''

    if order_id:
        all_sales = all_sales.filter(id=order_id)
        filter_qs += f'&order_id={order_id}'

    if name:
        # Use proper relation name "student__first_name/last_name"
        all_sales = all_sales.annotate(
            fullname=Concat('student__first_name', Value(' '), 'student__last_name')
        ).filter(fullname__icontains=name)
        filter_qs += f'&name={name}'

    if number:
        all_sales = all_sales.filter(student__mobile__icontains=number)
        filter_qs += f'&number={number}'

    if course_id:
        # IMPORTANT: filter by course THROUGH OrderCourses (not Orders.course)
        all_sales = all_sales.filter(ordercourses__course_id=course_id)
        filter_qs += f'&course_id={course_id}'

    if from_date:
        try:
            start_dt = make_aware(datetime.combine(datetime.fromisoformat(from_date).date(), time.min))
            all_sales = all_sales.filter(created_at__gte=start_dt)
            filter_qs += f'&from_date={from_date}'
        except Exception:
            pass

    if to_date:
        try:
            end_dt = make_aware(datetime.combine(datetime.fromisoformat(to_date).date(), time.max))
            all_sales = all_sales.filter(created_at__lte=end_dt)
            filter_qs += f'&to_date={to_date}'
        except Exception:
            pass

    # 3) Pagination
    paginator   = Paginator(all_sales, 10)
    page_number = request.GET.get("page")
    page_obj    = paginator.get_page(page_number)

    # 4) Build per-order aggregates for display

    # Attach display strings directly on each order (no template filters needed)
    for order in page_obj:
        # Courses in this order (via OrderCourses.order FK)
        oc_list = getattr(order, 'ordercourses_set').all()  # from prefetch cache
        course_names = [oc.course.course_name for oc in oc_list if oc.course]
        # Courses
        unique_courses = sorted(set(course_names))
        order.courses_str = (
            ''.join([f'<span class="badge bg-primary me-1">{c}</span>' for c in unique_courses])
            if unique_courses else 'N/A'
        )

        # Batches
        batch_names = [oc.batch.batch_name for oc in oc_list if oc.batch]
        unique_batches = sorted(set(batch_names))

        order.batches_str = (
            ''.join([f'<span class="badge bg-primary me-1">{b}</span>' for b in unique_batches])
            if unique_batches else 'N/A'
        )
    # 5) Render
    return render(
        request,
        "sales.html",
        {
            'msg': msg,
            'all_batches': all_batches,
            'filter': filter_qs,
            'course_id': course_id,
            'name': name,
            'number': number,
            'all_sales': page_obj,
            'page': page_number,
            'order_id': order_id,
            'from_date': from_date,
            'to_date': to_date,
            'all_courses': all_courses,  # filter dropdown sourced from OrderCourses
        }
    )

@login_required(login_url=ADMIN_LOGIN_URL)
def failed_orders(request):
    order_id=request.GET.get('order_id')
    name=request.GET.get('name')
    number=request.GET.get('number')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    course_id=request.GET.get('course_id')
    filter = ''
    all_courses = Course.objects.all()
    all_sales = Orders.objects.filter(is_order=0).select_related('student','course').order_by('-id')
    if order_id:
        all_sales = all_sales.filter(id=order_id)
        filter=filter+'&order_id='+order_id
    if name:
        all_sales = all_sales.annotate(fullname=Concat('student_id__first_name', Value(' '), 'student_id__last_name')).filter(fullname__icontains=name)
        filter=filter+'&name='+name
    if number:
        all_sales = all_sales.filter(student_id__mobile=number)
        filter=filter+'&number='+number
    if course_id:
         all_sales = all_sales.filter(course_id=course_id)
         filter=filter+'&course_id='+course_id
    if from_date:
        all_sales = all_sales.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_sales = all_sales.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
    # all_sales=Orders.objects.all()
    paginator = Paginator(all_sales, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "failed_orders.html",{'all_sales':page_obj,'page':page_number,'filter':filter,'order_id':order_id,'from_date':from_date,'to_date':to_date,'all_courses':all_courses ,'course_id':course_id,'name':name,'number':number});


@login_required(login_url=ADMIN_LOGIN_URL)
def Change_Status(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        payment_method = request.POST.get('payment_method')
        payment_status = request.POST.get('course_id')
       
        #     # Create a new ExamCategory object
        # order = Orders(
        #         payment_method=payment_method,
        #         payment_status=payment_status,
               
        #     )

        #     # Save the object to the database
        # order.save() 
        course = Orders.objects.filter(id=id)
        batch = Course.objects.filter(id=course[0].course_id)
        access=eval(batch[0].access)
        lecture=0
        recorded_lecture=0
        books=0
        area_mocks=0
        sectional_mocks=0
        mini_mocks=0
        full_mocks=0
        advance_mocks=0
        pyq_mocks=0
        if 'area_test' in access:
            area_mocks=1
        if 'sectional_test' in access:
            sectional_mocks=1
        if 'mini_mock_test' in access:
            mini_mocks=1
        if 'full_mock_test' in access:
            full_mocks=1
        if 'advance_mock_test' in access:
            advance_mocks=1
        if 'pyq_mock_test' in access:
            pyq_mocks=1
        if 'lecture' in access:
            lecture=1
        if 'books' in access:
            books=1
        batches = Course_Batches.objects.filter(course_id=course[0].course_id)
        Orders.objects.filter(id=id).update(
            payment_method = payment_method,
            payment_status = payment_status,
            is_order = 1)
        for cbatch in batches:
            OrderCourses.objects.create(
                student_id = course[0].student_id,
                course_id = course[0].course_id,
                batch_id = cbatch.batch_id,
                access = batch[0].access,
                area_mocks = area_mocks,
                sectional_mocks = sectional_mocks,
                mini_mocks = mini_mocks,
                full_mocks =full_mocks,
                lectures = lecture,
                books = books,
                advance_mocks=advance_mocks,
                pyq_mocks=pyq_mocks,
                order_id=id
            )
        return JsonResponse({'status': 'success', 'msg': 'Status changed successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
      
@login_required(login_url=ADMIN_LOGIN_URL)
def seles_Details(request,id):
    # course = Orders.objects.get(id=id)
    order = Orders.objects.prefetch_related('course','student').get(id=id)
    course=order
    ocourse = OrderCourses.objects.filter(student_id=course.student_id,order_id=course.id)
    # return JsonResponse({'status': 'error', 'msg': 'Something went wrong.','student_id':list(ocourse.values())})
    return render(request, 'sales-details.html',{'order':order,'ocourse':ocourse})


@login_required(login_url=ADMIN_LOGIN_URL)
def assign_batch(request,id):
    OrderCourses.objects.create(
        student_id = request.POST.get('student_id'),
        course_id = request.POST.get('course_id'),
        batch_id = request.POST.get('batch_id'),
        area_mocks = 0,
        sectional_mocks = 0,
        mini_mocks = 0,
        full_mocks =0,
        lectures = 1
    )
    return JsonResponse({'status': 'success', 'msg': 'Batch assigned successfully.'})

# ==================seles module start ===================
# ======================= end media module  ==============================


# ==============================Result module start====================

@login_required(login_url=ADMIN_LOGIN_URL)
def Results(request):
    name=request.GET.get('name')
    course=request.GET.get('course_id')
    status=request.GET.get('status')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    all_courses =Course.objects.all()
    filter = ''

    all_result = Student_Result.objects.filter().order_by('-id')

    if name:
        all_result = all_result.filter(name=name)
        filter=filter+'&name='+name
    if course:
        all_result = all_result.filter(course=course)
        filter=filter+'&course='+course
    if status:
        all_result = all_result.filter(status=status)
        filter=filter+'&status='+status
    if from_date:
        all_result = all_result.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_result = all_result.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    # all_result=Student_Result.objects.all()
    paginator = Paginator(all_result, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "result.html",{'all_result':page_obj,'page':page_number,'filter':filter,'name':name,'course_id':course,'status':status,'from_date':from_date,'to_date':to_date,'all_courses':all_courses});


@login_required(login_url=ADMIN_LOGIN_URL)
def add_result(request):
    if request.method == 'POST':
        # Get the form data
        name = request.POST.get('name')
        course = request.POST.get('course_id')
        image = request.FILES.get('image')
        status= request.POST.get('status')
       
        if(name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter name.'})
        elif(course==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course name.'})
        elif(image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status'})
        else:
            if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})    
            # Create a new ExamCategory object
            result = Student_Result(
                name=name,
                course_id=course,
                image=image,
                status=status
            )

            # Save the object to the database
            result.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Result Added successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def edit_result(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        name = request.POST.get('name')
        course = request.POST.get('course_id')
        image = request.FILES.get('image')
        status= request.POST.get('status')
        if(name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter name.'})
        elif(course==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course name.'})
        elif(image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select image.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status'})
        else:    
            if(request.FILES.get('image')):
                if image.size > 1048576:
                        return JsonResponse({'status': 'error', 'msg': 'Please select image size less than or equal to 1 MB.', 'data':image.size, 'byter':image.size})
                # Create a new ExamCategory object
                result = Student_Result(
                    id=id,
                    name=name,
                    course_id=course,
                    image=image,
                   
                    status=status
                )

                # Save the object to the database
                result.save(update_fields=["name","course_id","image","status"])
               
            else:
                 # Create a new ExamCategory object
                result = Student_Result(
                    id=id,
                    name=name,
                    course_id=course,
                    status=status
                )

                # Save the object to the database
                result.save(update_fields=["name","course_id","status"])
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Result saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_result(request):
    if request.method == 'POST':
        id = request.POST.get('id')
        Student_Result(id).soft_delete()
        return JsonResponse({'status': 'success', 'msg': 'Result deleted successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


# ==============================Result module End====================


# ====================== start coupon code module=======================

@login_required(login_url=ADMIN_LOGIN_URL)
def coupon_code(request):
    code=request.GET.get('code')
    course=request.GET.get('course')
    status=request.GET.get('status')
    valid_to=request.GET.get('valid_to')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''

    all_courses = Course.objects.all()
    all_coupon = Coupons.objects.prefetch_related('courses').filter().order_by('-id')

    if code:
        all_coupon = all_coupon.filter(code=code)
        filter=filter+'&code='+code
    # if course:
    #     all_coupon = all_coupon.filter(course_name=course)
    if status:
        all_coupon = all_coupon.filter(status=status)
        filter=filter+'&status='+status
    if valid_to:
        all_coupon = all_coupon.filter(validto__lte=valid_to)
        filter=filter+'&valid_to='+valid_to
    if from_date:
        all_coupon = all_coupon.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_coupon = all_coupon.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
   
    paginator = Paginator(all_coupon, 10)  # Show 25 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "coupons.html",{'all_coupon':page_obj ,'filter':filter,'all_courses':all_courses,'page':page_number,'code':code,'course':course,'valid_to':valid_to,'status':status,'from_date':from_date,'to_date':to_date})
    #  return render(request, "media.html")



@login_required(login_url=ADMIN_LOGIN_URL)
def add_coupons(request):

    if request.method == 'POST':
        # Get the form data
        code = request.POST.get('code')
        emails = request.POST.get('emails')
        course_name = request.POST.getlist('course_name')
        discount_type=request.POST.get('discount_type')
        minimum_amount = request.POST.get('minimum_amount')
        discount_amount=request.POST.get('discount')
        # discount_percent=request.POST.get('amount')
        validto = request.POST.get('validto')
        status = request.POST.get('status')
        usage_limit = request.POST.get('usage_limit')
        if(code==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter coupon code.'})
        elif(minimum_amount==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter minimum amount'})
        elif(discount_type==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select discount type'})
        # elif(discount_amount==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter discount amount'})
        # elif(discount_percent==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter discount percentage'})
        elif(validto==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select validity date and time'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            # Create a new ExamCategory object
            coupen = Coupons.objects.create(
                code=code,
                emails=emails,
                # course_name_id=course_name,
                discount_type=discount_type,
                minimum_amount=minimum_amount,
                discount = discount_amount,
                usage_limit=usage_limit,
                # discount_percent= discount_percent,
                validto= validto,
                status=status
            )
            coupen_id=coupen.id
            for course_id in course_name:
                Coupon_courses.objects.create(
                    coupon_id=coupen_id,
                    course_id=course_id
                )

            # Save the object to the database
            # coupen.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Coupon added successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def edit_coupon(request):

    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        code = request.POST.get('code')
        emails = request.POST.get('emails')
        course_name = request.POST.getlist('course_name')
        discount_type=request.POST.get('discount_type')
        minimum_amount = request.POST.get('minimum_amount')
        usage_limit = request.POST.get('usage_limit')
        discount_amount=request.POST.get('discount')
        # discount_percent=request.POST.get('amount')
        validto = request.POST.get('validto')
        status = request.POST.get('status')
       
        if(code==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter coupon code.'})
        elif(course_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select course'})
        elif(minimum_amount==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter minimum amount'})
        elif(discount_type==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select discount type'})
        elif(discount_amount==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter discount amount'})
        # elif(discount_percent==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter discount percentage'})
        elif(validto==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select validity date and time'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            # Create a new ExamCategory object
            coupen = Coupons(
                id=id,
                code=code,
                emails=emails,
                discount_type=discount_type,
                minimum_amount=minimum_amount,
                discount = discount_amount,
                usage_limit=usage_limit,
                # discount_percent= discount_percent,
                validto= validto,
                status=status
            )
            # Save the object to the database
            coupen.save(update_fields=["usage_limit","code","status","discount_type","discount" ,"validto" ,"minimum_amount","emails"])
            Coupon_courses.objects.filter(coupon_id=id).delete()
            for course_id in course_name:
                Coupon_courses.objects.create(
                    coupon_id=id,
                    course_id=course_id
                )

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Coupon updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})



@login_required(login_url=ADMIN_LOGIN_URL)
def delete_coupon(request):
     id = request.POST.get('id')
     Coupons(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'coupon deleted successfully.'})


# ====================== end coupen code module=======================

@login_required(login_url=ADMIN_LOGIN_URL)
def Course_enquiries(request):
    # enquiries = Enquiries.objects.all()
    name=request.GET.get('name')
    email=request.GET.get('email')
    contact=request.GET.get('contact')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''

    enquiries = Enquiries.objects.filter().order_by('-id')

    if name:
        enquiries = enquiries.filter(name=name)
        filter=filter+'&name='+name
    if email:
        enquiries = enquiries.filter(email=email)
        filter=filter+'&email='+email
    if contact:
        enquiries = enquiries.filter(mobile=contact)
        filter=filter+'&contact='+contact
    if from_date:
        enquiries = enquiries.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        enquiries = enquiries.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
    paginator = Paginator(enquiries, 10)  # Show 10 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "course-enquiries.html",{'enquiries':page_obj,'page':page_number,'filter':filter,'name':name,'from_date':from_date,'to_date':to_date,'email':email,'contact':contact})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_course_enquiries(request):
     id = request.POST.get('id')
     Enquiries(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Enquiry deleted successfully.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def Contact_us_enquiries(request):
    # enquiries = ContactUsEnquiries.objects.all()
    name=request.GET.get('name')
    email=request.GET.get('email')
    contact=request.GET.get('contact')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''

    enquiries = ContactUsEnquiries.objects.filter().order_by('-id')

    if name:
        enquiries = enquiries.annotate(fullname=Concat('first_name', Value(' '), 'last_name')).filter(fullname__icontains=name)
        filter=filter+'&name='+name
    if email:
        enquiries = enquiries.filter(email=email)
        filter=filter+'&email='+email
    if contact:
        enquiries = enquiries.filter(contact=contact)
        filter=filter+'&contact='+contact
    if from_date:
        enquiries = enquiries.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        enquiries = enquiries.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    paginator = Paginator(enquiries, 10)  # Show 10 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "contact-us-enquiries.html",{'enquiries':page_obj,'filter':filter,'page':page_number,'name':name,'from_date':from_date,'to_date':to_date,'email':email,'contact':contact})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_Contact_us_enquiries(request):
     id = request.POST.get('id')
     ContactUsEnquiries(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Enquiry deleted successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def Batch_Students_list(request,id):
    # batch= batch_management.objects.filter(id=id)
    student_list = OrderCourses.objects.filter(batch_id=id)
    paginator = Paginator(student_list, 10)  
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "batch-student-list.html",{'student_list':page_obj})


def safe_parse_access(access_str):
    try:
        # If it's a string and looks like a list (starts with [), use literal_eval
        if isinstance(access_str, str):
            if access_str.strip().startswith('['):
                return ast.literal_eval(access_str)
            else:
                return [x.strip() for x in access_str.split(',') if x.strip()]
        return []
    except Exception:
        return []

@login_required(login_url=ADMIN_LOGIN_URL)
def Course_Students_list(request, id):
    search = request.GET.get("search", "").strip()
    access_filter = request.GET.get("access_filter", "")
    date_from = request.GET.get("date_from", "")
    date_to = request.GET.get("date_to", "")

    student_list = OrderCourses.objects.filter(course_id=id).select_related("student").order_by('-id')
    course = Course.objects.get(id=id)

    # 🔍 Search Filter
    if search:
        student_list = student_list.filter(
            Q(student__first_name__icontains=search) |
            Q(student__last_name__icontains=search) |
            Q(student__email__icontains=search) |
            Q(student__mobile__icontains=search) |
            Q(student__whatsapp_number__icontains=search)
        )

    # 🔑 Access Type Filter
    if access_filter:
        student_list = student_list.filter(access__icontains=access_filter)

    # 📅 Date Filter
    if date_from:
        student_list = student_list.filter(created_at__gte=date_from)
    if date_to:
        student_list = student_list.filter(created_at__lte=date_to)

    # ---- Display Access ----
    for student in student_list:
        raw_access = safe_parse_access(student.access)
        student.display_access = ', '.join(i.replace('_', ' ').title() for i in raw_access if i.strip())
        
        # courses (through OrderCourses)
        course_ids = OrderCourses.objects.filter(student=student.student).values_list('course_id', flat=True).distinct()
        course_names = list(Course.objects.filter(id__in=course_ids).values_list('course_name', flat=True))
        student.courses = course_names if course_names else []
    
    static_features = [
        "books",
        "lecture",
        "recorded_lecture",
        "area_test",
        "sectional_test",
        "full_mock_test",
        "advance_mock_test",
        "pyq_mock_test",
        "basic_advance_mock_test"
    ]

    access_raw = ast.literal_eval(course.access) if course.access else []

    course_access = [
        {
            "value": 1 if feature in access_raw else 0,
            "label": feature.replace('_', ' ').title(),
            "key": feature
        }
        for feature in static_features
    ]

    # ---- Pagination ----
    paginator = Paginator(student_list, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(
        request,
        "course-students.html",
        {
            "student_list": page_obj,
            "course_id": id,
            "search": search,
            "access_filter": access_filter,
            "date_from": date_from,
            "date_to": date_to,
            "course_access": course_access
        }
    )

@login_required(login_url=ADMIN_LOGIN_URL)
def update_student_access(request):
    try:
        student_ids = json.loads(request.POST.get('student_ids', '[]'))
        access_list = json.loads(request.POST.get('access_list', '[]'))
        course_id = request.POST.get('course_id')

        # Save access as comma-separated string
        access_str = ','.join(access_list)

        # Map access keys to OrderCourses model fields
        access_field_map = {
            "books": "books",
            "lecture": "lectures",
            "recorded_lecture": "lectures",

            "area_test": "area_mocks",
            "sectional_test": "sectional_mocks",
            "full_mock_test": "full_mocks",
            "advance_mock_test": "advance_mocks",
            "pyq_mock_test": "pyq_mocks",
            "basic_advance_mock_test": "basic_advance_mocks",
        }

        for student_id in student_ids:
            order_course = OrderCourses.objects.filter(
                id=student_id
            ).first()

            if order_course:
                # Update access text
                order_course.access = access_str

                # First set all mapped fields to 0
                for field_name in set(access_field_map.values()):
                    setattr(order_course, field_name, 0)

                # Then set selected access fields to 1
                for access_key in access_list:
                    field_name = access_field_map.get(access_key)

                    if field_name:
                        setattr(order_course, field_name, 1)

                order_course.save()

        return JsonResponse({'status': 'success'})

    except Exception as e:
        return JsonResponse({
            'status': 'error',
            'message': str(e)
        }, status=500)


@login_required(login_url=ADMIN_LOGIN_URL)
def report(request):
    # all_sales=Orders.objects.all()
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''

    all_sales = Orders.objects.filter().order_by('-id')
    if from_date:
        all_sales = all_sales.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_sales = all_sales.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    paginator = Paginator(all_sales, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "reports.html",{'all_sales':page_obj,'page':page_number,'filter':filter,'from_date':from_date,'to_date':to_date});

def to_int(val, default=0):
    try:
        return int(val)
    except (TypeError, ValueError):
        return default


@login_required(login_url=ADMIN_LOGIN_URL)
def all_question_bank_list(request):
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    mock_test_list = Mock_Test.objects.all()

    # Mock type mapping
    mock_types = {
        1: "Area Mock",
        2: "Sectional Mock",
        3: "Weekly Mock",
        4: "Mini Mock",
        5: "Full Mock",
        8: "Advance Mock",
        9: "PYQ Mock"
    }

    # Default values
    section_id = to_int(request.GET.get("section_id", 0))
    sub_section_id = to_int(request.GET.get("sub_section_id", 0))
    topic_id = to_int(request.GET.get("topic_id", 0))
    mocktest_id = to_int(request.GET.get("mocktest_id", 0))
    mocktype_id = to_int(request.GET.get("mocktype_id", 0))
    sort_by = request.GET.get("sort_by", "")

    # Base Query
    Question_list = Question_Bank.objects.all()

    if section_id and section_id != "0":
        Question_list = Question_list.filter(section_id=section_id)

    if sub_section_id and sub_section_id != "0":
        Question_list = Question_list.filter(sub_section_id=sub_section_id)

    if topic_id and topic_id != "0":
        Question_list = Question_list.filter(topic_id=topic_id)

    # ✅ Filter by Mock Test
    if mocktest_id and mocktest_id != "0":
        Question_list = Question_list.filter(test_id=mocktest_id)

    # ✅ Filter by Mock Type
    if mocktype_id and mocktype_id != "0":
        Question_list = Question_list.filter(test__mock_type=mocktype_id)

    # ✅ Search in question text (optional)
    if sort_by == "question_title":
        Question_list = Question_list.order_by("question")   # alphabetical

    elif sort_by == "mock_test_desc":
        Question_list = Question_list.order_by("-test__id")   # newest mock test first
    
    elif sort_by == "mock_test_asc":
        Question_list = Question_list.order_by("test__id")    # oldest mock test first

    # Pagination
    paginator = Paginator(Question_list, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    filter = (
        f"&section_id={section_id}"
        f"&sub_section_id={sub_section_id}"
        f"&topic_id={topic_id}"
        f"&mocktest_id={mocktest_id}"
        f"&mocktype_id={mocktype_id}"
        f"&sort_by={sort_by}"
    )

    numbers = range(1, page_obj.paginator.count + 1)

    return render(
        request,
        "all-questions-bank-list1.html",
        {
            "display_order": list(page_obj.object_list.values_list("id", flat=True)),
            "numbers": numbers,
            "mock_test_list": mock_test_list,
            "Question_list": page_obj,
            "section_list": section_list,
            "Sub_Sections_list": Sub_Sections_list,
            "Topics_list": Topics_list,
            "filter": filter,
            "topic_id": topic_id,
            "section_id": section_id,
            "sub_section_id": sub_section_id,
            "mocktest_id": mocktest_id,
            "mocktype_id": mocktype_id,
            "sort_by": sort_by,
            "mock_types": mock_types,
        },
    )


@login_required(login_url=ADMIN_LOGIN_URL)
def all_question_bank(request):
    test_id = request.GET.get('test_id')
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    test_id = request.GET.get('test_id')
    section_id=0
    sub_section_id=0
    topic_id=0
    search=''
    all_mock_questions = list(
        Question_Bank.objects.filter(test_id=test_id).order_by('section_id', 'id')
    )
    question_number_by_id = {
        question.id: number
        for number, question in enumerate(all_mock_questions, start=1)
    }
    Question_list = Question_Bank.objects.filter(test_id=test_id).order_by('section_id','id')
    if request.GET.get('section_id'):
        if request.GET.get('section_id')!='0':
            section_id=int(request.GET.get('section_id'))
            Question_list=Question_list.filter(section_id=request.GET.get('section_id'))
    if request.GET.get('sub_section_id'):
        if request.GET.get('sub_section_id')!='0':
            sub_section_id=int(request.GET.get('sub_section_id'))
            Question_list=Question_list.filter(sub_section_id=request.GET.get('sub_section_id'))
    if request.GET.get('topic_id'):
        if request.GET.get('topic_id')!='0':
            topic_id=int(request.GET.get('topic_id'))
            Question_list=Question_list.filter(topic_id=request.GET.get('topic_id'))
    # Handle search for Nth question
    if request.GET.get('search') and request.GET.get('search') != 'None':
        search = int(request.GET.get('search'))
        Question_list=Question_list.filter(display_order=search)
#        if search.isdigit():
#            question_index = int(search) - 1  # Convert to zero-based index
#            if 0 <= question_index < Question_list.count():
#                Question_list = Question_list[question_index:question_index + 1]
#            else:
#                Question_list = Question_Bank.objects.none()  # No match, return empty queryset

    question_ids = Question_list.values_list('id', flat=True)
    mock_test = Mock_Test.objects.filter(id = test_id)
    paginator = Paginator(Question_list, 10)

    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    test = Mock_Test.objects.filter(id=test_id)
    for question in page_obj.object_list:
        question.video_question_number = question_number_by_id.get(question.id)
        question.video_filename = "{0}.mp4".format(question.video_question_number)

    video_folder = ""
    mock_test_object = test.first()
    if mock_test_object:
        from admin_app.question_video_service import mock_video_folder
        video_folder = "media/{0}/".format(mock_video_folder(mock_test_object))
#    filter='&test_id='+test_id+'&section_id='+str(section_id)+'&sub_section_id='+str(sub_section_id)+'&topic_id='+str(topic_id)+'&search='+str(search)
    filter = (
        '&test_id=' + str(test_id or '') +
        '&section_id=' + str(section_id or '') +
        '&sub_section_id=' + str(sub_section_id or '') +
        '&topic_id=' + str(topic_id or '') +
        '&search=' + str(search or '')
    )
    numbers = range(1, Question_list.count()+1)
    return render(request, "all-questions-bank-list.html",{'display_order': list(question_ids),'numbers':numbers,'mock_test':mock_test,'Question_list':page_obj,'section_list':section_list,'Sub_Sections_list':Sub_Sections_list,'Topics_list':Topics_list,'test_id':test_id,'filter':filter,'topic_id':topic_id,'section_id':section_id,'sub_section_id':sub_section_id,'search':search,'test':test,'video_folder':video_folder})


@login_required(login_url=ADMIN_LOGIN_URL)
def update_question_order(request):
    test_id = request.POST.get('test_id')
    order = request.POST.get('order')
    question_id = request.POST.get('question_id')
    Question_list = Question_Bank.objects.filter(test_id=test_id,display_order=order).all()
    if Question_list.count():
        return JsonResponse({'status': False, 'msg': 'Same order already exits.'})
    else:
        Question_Bank.objects.filter(id=question_id).update(display_order=order)
        return JsonResponse({'status': True, 'msg': 'Questions order updated successfully.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def add_mock_questions(request):
    test_id = request.GET.get('test_id')
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    Question_list = Question_Bank.objects.all()
    paginator = Paginator(Question_list, 100)  

    page_number = request.GET.get("page")
    test = Mock_Test.objects.get(id=test_id)
    page_obj = paginator.get_page(page_number)
    qids = json.loads(test.question_ids)
    return render(request, "add-mock-questions.html",{'qids':qids,'test':test,'test_id':test_id,'Question_list':page_obj,'section_list':section_list,'Sub_Sections_list':Sub_Sections_list,'Topics_list':Topics_list})

@login_required(login_url=ADMIN_LOGIN_URL)
def filter_questions(request):
    test_id = request.POST.get('test_id')
    qids = request.GET.get('qids')

    Question_list = Question_Bank.objects
    if request.GET.get('section'):
        Question_list=Question_list.filter(section_id=request.GET.get('section'))
    if request.GET.get('sub_section'):
        Question_list=Question_list.filter(sub_section_id=request.GET.get('sub_section'))
    if request.GET.get('topic'):
        Question_list=Question_list.filter(topic_id=request.GET.get('topic'))
    paginator = Paginator(Question_list, 100)  
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "filter-questions.html",{'Question_list':page_obj, 'qids':json.loads(qids)})


@login_required(login_url=ADMIN_LOGIN_URL)
def save_mock_questions(request):  
   
    id = request.POST.get('test_id')
    qids = request.POST.get('qids')
    Mock_Test.objects.filter(id=id).update(question_ids=qids)
    return JsonResponse({'status': 'success', 'msg': 'Questions added successfully successfully.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def add_question(request):
    test_id = request.GET.get('test_id')
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    return render(request, "add-question.html",{'section_list':section_list,'Sub_Sections_list':Sub_Sections_list,'Topics_list':Topics_list,'test_id':test_id})

@login_required(login_url=ADMIN_LOGIN_URL)
def upload_data(request):
    if request.method == 'POST' and request.FILES['excelFile']:
        test_id = request.POST.get('test_id')
        exam_category_id = request.POST.get('exam_category_id')
        excel_file = request.FILES['excelFile']
        category = ExamCategory.objects.filter(id=exam_category_id)

        # Load the Excel file
        wb = openpyxl.load_workbook(excel_file)
        sheet = wb.active

        questions_data = []
        options_data = []
        options = []
                
        marks = 1
        negative_marks = 0
        if category[0].marks_per_question:
            marks=category[0].marks_per_question
        if category[0].negative_marks_per_question:
            negative_marks=category[0].negative_marks_per_question
        # Iterate over the rows in the Excel file
        for row in sheet.iter_rows(min_row=2, values_only=True):
            section_id = row[0]
            subsection_id = row[1]
            topic_id = row[2]
            question_type = row[3]
            question = row[4]
            paragraph = row[5]
            option_1 = row[6]
            option_2 = row[7]
            option_3 = row[8]
            option_4 = row[9]
            option_5 = row[10]
            correct_answer= row[11]
            answer_explaination = row[12]
            difficulty_level = row[13]
            marks = marks
            negative_marks = negative_marks
            #  time_to_spent = row[7]
            status = 'Active'
            # options = row[12:]
            print(section_id)
            
            option_data = [option_1, option_2, option_3, option_4, option_5]
            if question_type=="mcq":
                if option_1==correct_answer:
                    correct_answer="1"
                if option_2==correct_answer:
                    correct_answer="2"
                if option_3==correct_answer:
                    correct_answer="3"
                if option_4==correct_answer:
                    correct_answer="4"
                if option_5==correct_answer:
                    correct_answer="5"
            # Create a question object and append it to the questions_data list
            if section_id:
                question_obj = Question_Bank(section_id=section_id, sub_section_id=subsection_id, topic_id=topic_id,
                                        question_type=question_type, question=question, paragraph=paragraph, options=json.dumps(option_data),
                                        difficulty_level=difficulty_level, marks=marks, negative_marks=negative_marks, explanation=answer_explaination,correct_answer=correct_answer, status=status,test_id=test_id
                                       )
                questions_data.append(question_obj)
            # option_data = [option_1, option_2, option_3, option_4, option_5]
            # options.append(option_data)

        # Bulk insert the questions data
        Question_Bank.objects.bulk_create(questions_data)
        question_ids = list(Question_Bank.objects.filter(
            created_at__in=[question.created_at for question in questions_data]
        ).values_list('id', flat=True))

        # Retrieve the primary keys of the inserted questions
        #all_question_ids = [question.id for question in question_ids]

        # options_data = []

        # # Iterate over the rows again to get options data
        # is_correct
        # for index, row in enumerate(sheet.iter_rows(min_row=2, values_only=True)):
            
        #     question_id = question_ids[index]
        #     option_1 = row[6]
        #     option_2 = row[7]
        #     option_3 = row[8]
        #     option_4 = row[9]
        #     option_5 = row[10]
        #     # Create option objects and append them to the options_data list
        #     if option_1:
        #         if correct_answer==option_1:
        #             is_correct=1
        #         else:
        #             is_correct=0
        #         option_obj = Option(
        #             question_id=question_id,
        #             option=option_1,
        #             is_correct=is_correct
        #         )
        #         options_data.append(option_obj)   
        #     if option_2:
        #         if correct_answer==option_2:
        #             is_correct=1
        #         else:
        #             is_correct=0
        #         option_obj = Option(
        #             question_id=question_id,
        #             option=option_2,
        #             is_correct=is_correct
        #         )
        #         options_data.append(option_obj)  
        #     if option_3:
        #         if correct_answer==option_3:
        #             is_correct=1
        #         else:
        #             is_correct=0
        #         option_obj = Option(
        #             question_id=question_id,
        #             option=option_3,
        #             is_correct=is_correct
        #         )
        #         options_data.append(option_obj)  
        #     if option_4:
        #         if correct_answer==option_4:
        #             is_correct=1
        #         else:
        #             is_correct=0
        #         option_obj = Option(
        #             question_id=question_id,
        #             option=option_4,
        #             is_correct=is_correct
        #         )
        #         options_data.append(option_obj) 
        #     if option_5:
        #         if correct_answer==option_5:
        #             is_correct=1
        #         else:
        #             is_correct=0
        #         option_obj = Option(
        #             question_id=question_id,
        #             option=option_5,
        #             is_correct=is_correct
        #         )
        #         options_data.append(option_obj)  

        # # Bulk insert the options data
        # Option.objects.bulk_create(options_data)
        return JsonResponse({'message': 'Data uploaded successfully.', 'section': question_ids})

    return render(request, 'upload.html')

def extract_formatted_text(paragraph):
    """Extract formatted text with bold, italic, underline, strike-through, superscript, subscript, and inline images."""
    formatted_text = ""
    # Explicit namespaces (IMPORTANT)
    namespaces = {
        'a': 'http://schemas.openxmlformats.org/drawingml/2006/main',
        'r': 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
    }

    for run in paragraph.runs:
        # Process text formatting
        text = run.text

        if run.bold:
            text = f"<b>{text}</b>"
        if run.italic:
            text = f"<i>{text}</i>"
        if run.underline:
            text = f"<u>{text}</u>"
        if run.font.strike:
            text = f"<s>{text}</s>"
        if run.font.superscript:
            text = f"<sup>{text}</sup>"
        if run.font.subscript:
            text = f"<sub>{text}</sub>"

        # Append formatted text
        formatted_text += text


        # ---- INLINE IMAGES ----
        blips = run._element.findall('.//a:blip', namespaces)

        for blip in blips:
            image_id = blip.get('{%s}embed' % namespaces['r'])
            if not image_id:
                continue

            image_part = run.part.related_parts.get(image_id)
            if not image_part:
                continue

            image_bytes = image_part.blob
            base64_image = base64.b64encode(image_bytes).decode('utf-8')

            img_tag = f'<img src="data:image/png;base64,{base64_image}" alt="Image"/>'
            formatted_text += img_tag

    return formatted_text


def strip_html_tags(text):
    """Remove HTML tags from the given text."""
    clean = re.compile('<.*?>')
    return re.sub(clean, '', text)

IMG_TAG_RE = re.compile(r'<img[^>]+src="([^"]+)"[^>]*>', re.I)

import re
import html  # built-in for &radic; &ndash; etc.

# maps for superscript / subscript
_SUPERSCRIPT_MAP = str.maketrans({
    "0": "⁰",
    "1": "¹",
    "2": "²",
    "3": "³",
    "4": "⁴",
    "5": "⁵",
    "6": "⁶",
    "7": "⁷",
    "8": "⁸",
    "9": "⁹",
    "+": "⁺",
    "-": "⁻",
    "=": "⁼",
    "(": "⁽",
    ")": "⁾",
})

_SUBSCRIPT_MAP = str.maketrans({
    "0": "₀",
    "1": "₁",
    "2": "₂",
    "3": "₃",
    "4": "₄",
    "5": "₅",
    "6": "₆",
    "7": "₇",
    "8": "₈",
    "9": "₉",
    "+": "₊",
    "-": "₋",
    "=": "₌",
    "(": "₍",
    ")": "₎",
})


def _to_superscript(text: str) -> str:
    # convert digits and basic math signs; leave other chars as-is
    return text.translate(_SUPERSCRIPT_MAP)


def _to_subscript(text: str) -> str:
    return text.translate(_SUBSCRIPT_MAP)


def strip_tags(html_text: str) -> str:
    """
    Remove HTML tags, decode HTML entities, keep line breaks,
    and convert <sup>/<sub> + common math entities to a readable math format.
    """
    if not html_text:
        return ""

    # 1) Handle <sup> and <sub> BEFORE stripping tags
    #    so we can convert their inner text properly.
    #    We run these on the raw HTML.
    def sup_repl(match):
        inner = match.group(1)
        inner = html.unescape(inner)  # decode entities inside sup
        return _to_superscript(inner)

    def sub_repl(match):
        inner = match.group(1)
        inner = html.unescape(inner)
        return _to_subscript(inner)

    # (?i) for case-insensitive, DOTALL for multiline inner
    html_text = re.sub(
        r'(?is)<\s*sup[^>]*>(.*?)</\s*sup\s*>',
        lambda m: sup_repl(m),
        html_text,
    )
    html_text = re.sub(
        r'(?is)<\s*sub[^>]*>(.*?)</\s*sub\s*>',
        lambda m: sub_repl(m),
        html_text,
    )

    # 2) Turn <br> into newline
    html_text = re.sub(r'<\s*br\s*/?>', "\n", html_text, flags=re.I)

    # 3) Closing block tags -> newline
    html_text = re.sub(r'</\s*(p|div)\s*>', "\n", html_text, flags=re.I)

    # 4) Opening block tags -> remove (structure captured by closing tags)
    html_text = re.sub(r'<\s*(p|div)[^>]*>', "", html_text, flags=re.I)

    # 5) Remove remaining tags (span, b, i, etc. – formatting already handled where needed)
    html_text = re.sub(r"<[^<]+?>", "", html_text)

    # 6) Decode ALL HTML entities (&radic;, &ndash;, &le; etc.)
    html_text = html.unescape(html_text)

    # 7) Extra math-specific replacements if some entities remain or came as text
    extra_math = {
        "&radic;": "√",
        "&times;": "×",
        "&divide;": "÷",
        "&minus;": "−",
        "&frasl;": "/",
        "&le;": "≤",
        "&ge;": "≥",
        "&ne;": "≠",
        "&plusmn;": "±",
        "&sum;": "∑",
        "&prod;": "∏",
        "&int;": "∫",
        "&infin;": "∞",
    }
    for k, v in extra_math.items():
        html_text = html_text.replace(k, v)

    # 8) Normalise whitespace and newlines
    html_text = html_text.replace("\r", "")
    html_text = html_text.replace("&nbsp;", " ")
    # collapse 3+ newlines to max 2
    html_text = re.sub(r"\n{3,}", "\n\n", html_text)

    return html_text.strip()



def iter_text_and_images(html: str):
    """
    Yields ('text', '...') and ('img', bytes) chunks in document order from a small subset of HTML
    (we only care about <img> and text). Text is plain; images are raw bytes.
    """
    if not html:
        return
    parts = IMG_TAG_RE.split(html)  # splits into [text, src, text, src, text, ...]
    # parts indexes: even = text, odd = image src
    for idx, chunk in enumerate(parts):
        if idx % 2 == 0:
            text = strip_tags(chunk)
            if text:
                yield ('text', text)
        else:
            src = chunk.strip()
            # data URI
            if src.startswith('data:image'):
                # e.g. data:image/png;base64,AAA...
                try:
                    b64 = src.split('base64,', 1)[1]
                    yield ('img', base64.b64decode(b64))
                except Exception:
                    # ignore bad image
                    pass
            # http/https – try to download (optional)
            elif src.startswith('http://') or src.startswith('https://'):
                try:
                    import requests
                    r = requests.get(src, timeout=7)
                    if r.ok:
                        yield ('img', r.content)
                except Exception:
                    pass
            # anything else → ignore
            else:
                pass

def add_html_block_to_cell(cell, html: str, prefix: str = None, picture_width_in=2.5):
    """
    Appends the HTML (text + <img>) to a table cell, preserving order.
    If prefix (like 'Directions:' or 'Q-') is present, it is prepended once.
    Images are inserted with given width (inches).
    """
    first_text = True
    for kind, payload in iter_text_and_images(html):
        if kind == 'text':
            # prefix on first text piece only
            if first_text and prefix:
                para = cell.add_paragraph(f"{prefix} {payload}")
            else:
                para = cell.add_paragraph(payload)
            first_text = False
        elif kind == 'img':
            # keep image visually separated
            para = cell.add_paragraph()
            try:
                cell._tc.get_or_add_tcPr()  # ensure cell ready
                para.add_run()  # ensure a run exists (not strictly required for add_picture)
                para = cell.add_paragraph()  # picture will be after previous para
                para._element  # touch
                # add picture
                stream = BytesIO(payload)
                # width can be tuned; or omit width to use native
                para = cell.add_paragraph()
                run = para.add_run()
                run.add_picture(stream, width=Inches(picture_width_in))
            except Exception:
                # if add_picture in run fails, fallback to doc-level and then move on
                pass

def add_html_block_to_doc(doc: Document, html: str, prefix: str = None, picture_width_in=2.5):
    """
    Same as above but appends at document (not in a table cell).
    """
    first_text = True
    for kind, payload in iter_text_and_images(html):
        if kind == 'text':
            if first_text and prefix:
                doc.add_paragraph(f"{prefix} {payload}")
            else:
                doc.add_paragraph(payload)
            first_text = False
        elif kind == 'img':
            try:
                stream = BytesIO(payload)
                doc.add_picture(stream, width=Inches(picture_width_in))
            except Exception:
                pass

from docx import Document
from docx.shared import Inches
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.section import WD_ORIENT

@login_required(login_url=ADMIN_LOGIN_URL)
def download_questions_word(request):
    test_id = request.GET.get('test_id')

    qs = (
        Question_Bank.objects
        .filter(test_id=test_id, status='Active')
        .select_related('section', 'sub_section', 'topic')
        .order_by('section_id', 'id')
    )

    doc = Document()

    # ---------- Page setup: bigger width (landscape) + smaller margins ----------
    section = doc.sections[0]
    section.orientation = WD_ORIENT.LANDSCAPE
    # swap width & height when changing orientation
    old_width, old_height = section.page_width, section.page_height
    section.page_width = old_height
    section.page_height = old_width

    # reduce side margins to give more room to the table
    section.left_margin = Inches(0.5)
    section.right_margin = Inches(0.5)
    section.top_margin = Inches(0.7)
    section.bottom_margin = Inches(0.7)

    # ---------- Title ----------
    title = doc.add_heading(f'Questions Export (Test ID: {test_id})', level=1)
    title.alignment = WD_ALIGN_PARAGRAPH.CENTER

    # ---------- Table ----------
    table = doc.add_table(rows=1, cols=6)
    table.style = 'Table Grid'
    table.autofit = False  # so our widths are respected

    hdr = table.rows[0].cells
    hdr[0].text = 'S.No'
    hdr[1].text = 'Content (Directions, Q-, A)-E), Answer, Solution)'
    hdr[2].text = 'Topic'
    hdr[3].text = 'Section'
    hdr[4].text = 'Sub-Section'
    hdr[5].text = 'Difficulty (e.g., Easy/Medium/Hard)'

    # Set column widths – make Content column much bigger
    col_widths = [
        Inches(0.6),  # S.No
        Inches(6.0),  # Content
        Inches(1.4),  # Topic
        Inches(1.4),  # Section
        Inches(1.4),  # Sub-Section
        Inches(1.4),  # Difficulty
    ]

    for col_idx, width in enumerate(col_widths):
        for cell in table.columns[col_idx].cells:
            cell.width = width

    labels = ['A', 'B', 'C', 'D', 'E']

    for idx, q in enumerate(qs, start=1):
        row = table.add_row().cells
        row[0].text = str(idx)
        content = row[1]
        content.text = ''  # important: clear default empty paragraph

        # We will insert blank paragraphs between major chunks
        has_any_content = False
        options_started = False

        # Directions / paragraph (with images)
        if q.paragraph:
            if has_any_content:
                content.add_paragraph('')  # blank line
            add_html_block_to_cell(content, q.paragraph, prefix='Directions:')
            has_any_content = True

        # Question (with images)
        if q.question:
            if has_any_content:
                content.add_paragraph('')  # blank line
            add_html_block_to_cell(content, q.question, prefix='Q-')
            has_any_content = True

        # Options (with images)
        try:
            opts = json.loads(q.options or '[]')
        except Exception:
            opts = []

        for i, opt in enumerate(opts[:5]):
            # add one blank paragraph before the whole options block
            if not options_started:
                if has_any_content:
                    content.add_paragraph('')  # blank line before A)
                options_started = True

            add_html_block_to_cell(content, opt, prefix=f'{labels[i]})')
            has_any_content = True

        # Answer
        if q.correct_answer:
            if has_any_content:
                content.add_paragraph('')  # blank line before Answer
            letter = {1: 'A', 2: 'B', 3: 'C', 4: 'D', 5: 'E'}.get(int(q.correct_answer), '')
            if letter:
                content.add_paragraph(f"Answer: {letter}")
                has_any_content = True

        # Solution (with images)
        if q.explanation:
            if has_any_content:
                content.add_paragraph('')  # blank line before Solution
            add_html_block_to_cell(content, q.explanation, prefix='Solution:')
            has_any_content = True

        # Meta
        row[2].text = getattr(getattr(q, 'topic', None), 'topic_name', '') or ''
        row[3].text = getattr(getattr(q, 'section', None), 'section_name', '') or ''
        row[4].text = getattr(getattr(q, 'sub_section', None), 'sub_section_name', '') or ''
        row[5].text = q.difficulty_level or ''

    bio = BytesIO()
    doc.save(bio)
    bio.seek(0)
    resp = HttpResponse(
        bio.getvalue(),
        content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
    )
    resp['Content-Disposition'] = f'attachment; filename="questions_test_{test_id}.docx"'
    return resp

@login_required(login_url=ADMIN_LOGIN_URL)
def odownload_questions_word(request):
    # Fetch questions for the given test_id
    test_id = request.GET.get('test_id')
    questions = Question_Bank.objects.filter(test_id=test_id, status='Active')

    # Create a new Word Document
    doc = Document()

    for q in questions:
        # Add Question
        if q.paragraph:
            paragraph = strip_html_tags(q.paragraph)
            doc.add_paragraph(f"Directions- {paragraph}", style='List Bullet')

        question_text = strip_html_tags(q.question)
        doc.add_paragraph(f"Q- {question_text}", style='List Bullet')

        # Add Options if MCQ
        options = json.loads(q.options)
        option_labels = ['A)', 'B)', 'C)', 'D)', 'E)']
        for idx, option in enumerate(options):
            label = option_labels[idx] if idx < len(option_labels) else f"{chr(65+idx)})"
            option_text = strip_html_tags(option)
            doc.add_paragraph(f"{label} {option_text}", style='List Bullet 2')
        # Determine the answer display based on question_type
        if q.question_type == 'mcq':
            # Convert correct_answer to int and map to letter
            try:
                correct_ans_int = int(q.correct_answer)
                answer_map = {1:'A', 2:'B', 3:'C', 4:'D', 5:'E'}
                answer_letter = answer_map.get(correct_ans_int, '')
            except (ValueError, TypeError):
                answer_letter = ''
            answer_display = answer_letter
        else:
            # For non-MCQ, display correct_answer as is
            answer_display = q.correct_answer

        doc.add_paragraph(f"Answer: {answer_display}")

        # Add Explanation
        if q.explanation:
            explanation_text = strip_html_tags(q.explanation)
            doc.add_paragraph(f"Solution: {explanation_text}")

        # Add a separator
        doc.add_paragraph('')  # blank line for separation

    # Save the document into a BytesIO stream
    buffer = BytesIO()
    doc.save(buffer)
    buffer.seek(0)

    # Prepare HTTP response
    response = HttpResponse(
        buffer.getvalue(),
        content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document'
    )
    response['Content-Disposition'] = f'attachment; filename=questions_test_{test_id}.docx'
    return response

import json
import re
import uuid
import html

from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.core.files.base import ContentFile
from django.core.files.storage import default_storage
from django.http import JsonResponse
from docx import Document
from lxml import etree

# =========================================================
# CONFIG
# =========================================================

EXTRA_MATH = {
    "&radic;": "√",
    "&times;": "×",
    "&divide;": "÷",
    "&minus;": "−",
    "&frasl;": "/",
    "&le;": "≤",
    "&ge;": "≥",
    "&ne;": "≠",
    "&plusmn;": "±",
    "&sum;": "∑",
    "&prod;": "∏",
    "&int;": "∫",
    "&infin;": "∞",
}

# =========================================================
# OMML -> LaTeX (Word equation -> LaTeX)
# Supports: fraction, roots, superscripts/subscripts, n-ary (sum/prod/int)
# =========================================================
def _ln(el):
    return etree.QName(el).localname

def _children(el):
    return list(el) if el is not None else []

def _find_child(el, name):
    for c in _children(el):
        if _ln(c) == name:
            return c
    return None

def normalize_entities(s: str) -> str:
    if not s:
        return ""
    s = html.unescape(s)
    for k, v in EXTRA_MATH.items():
        s = s.replace(k, v)
    return s

LATEX_TEXT_WRAP_RE = re.compile(r"[A-Za-z].*\s+.*[A-Za-z]")  # letters + spaces + letters

def latex_escape_math(s: str) -> str:
    """
    Escapes text that will live in math mode (not inside \\text{...}).
    Keep operators and backslashes safe.
    """
    if not s:
        return ""
    s = normalize_entities(s)
    s = normalize_sqrt_unicode_to_latex(s)

    # escape only what commonly breaks math
    s = s.replace("\\", r"\\")
    s = s.replace("{", r"\{").replace("}", r"\}")
    s = s.replace("%", r"\%").replace("#", r"\#")
    s = s.replace("&", r"\&").replace("$", r"\$")
    return s

def latex_escape_textmode(s: str) -> str:
    """
    Escapes text that will live inside \\text{...}.
    In textmode we should escape underscores etc too.
    """
    if not s:
        return ""
    s = normalize_entities(s)
    # DO NOT convert √ to \\sqrt{} inside \\text{...}
    # keep it as unicode √ if it appears in plain text

    s = s.replace("\\", r"\\")
    s = s.replace("{", r"\{").replace("}", r"\}")
    s = s.replace("%", r"\%").replace("#", r"\#")
    s = s.replace("&", r"\&").replace("$", r"\$")
    s = s.replace("_", r"\_")
    return s

def omml_text_to_latex(text: str) -> str:
    """
    If the OMML text looks like real sentence-ish text, wrap as \\text{...}.
    """
    t = (text or "")
    t_stripped = t.strip()

    # Heuristic: if it has letters AND whitespace (or looks like a phrase), treat as text.
    if LATEX_TEXT_WRAP_RE.search(t_stripped):
        return r"\text{" + latex_escape_textmode(t_stripped) + "}"
    return latex_escape_math(t_stripped)


def normalize_sqrt_unicode_to_latex(s: str) -> str:
    """
    √2 -> \\sqrt{2}, √x -> \\sqrt{x}
    """
    if not s:
        return ""
    return re.sub(r"√\s*([0-9A-Za-z]+)", r"\\sqrt{\1}", s)

def latex_escape_text(s: str) -> str:
    if not s:
        return ""
    s = normalize_entities(s)
    s = normalize_sqrt_unicode_to_latex(s)
    s = s.replace("\\", r"\\")
    s = s.replace("%", r"\%").replace("#", r"\#")
    return s

def omml_to_latex(el) -> str:
    if el is None:
        return ""

    name = _ln(el)

    # containers
    if name in ("oMathPara", "oMath", "e", "num", "den", "sup", "sub", "deg"):
        return "".join(omml_to_latex(c) for c in _children(el))

    # run container
    if name == "r":
        return "".join(omml_to_latex(c) for c in _children(el))

    # text leaf
    if name == "t":
        return omml_text_to_latex(el.text or "")

    # fraction
    if name == "f":
        num = omml_to_latex(_find_child(el, "num"))
        den = omml_to_latex(_find_child(el, "den"))
        if num and den:
            return r"\frac{" + num + "}{" + den + "}"
        return num or den

    # superscript
    if name == "sSup":
        base = omml_to_latex(_find_child(el, "e"))
        sup  = omml_to_latex(_find_child(el, "sup"))
        if base and sup:
            return base + "^{" + sup + "}"
        return base or sup

    # subscript
    if name == "sSub":
        base = omml_to_latex(_find_child(el, "e"))
        sub  = omml_to_latex(_find_child(el, "sub"))
        if base and sub:
            return base + "_{" + sub + "}"
        return base or sub

    # sub+sup
    if name == "sSubSup":
        base = omml_to_latex(_find_child(el, "e"))
        sub  = omml_to_latex(_find_child(el, "sub"))
        sup  = omml_to_latex(_find_child(el, "sup"))
        out = base
        if sub:
            out += "_{" + sub + "}"
        if sup:
            out += "^{" + sup + "}"
        return out

    # root
    if name == "rad":
        deg = omml_to_latex(_find_child(el, "deg"))
        e   = omml_to_latex(_find_child(el, "e"))
        if deg:
            return r"\sqrt[" + deg + "]{" + e + "}"
        return r"\sqrt{" + e + "}"

    # delimiter/group (best-effort)
    if name == "d":
        inner = omml_to_latex(_find_child(el, "e"))
        return r"\left(" + inner + r"\right)" if inner else ""

    # n-ary operators
    if name == "nary":
        naryPr = _find_child(el, "naryPr")
        chr_el = _find_child(naryPr, "chr") if naryPr is not None else None
        op = omml_to_latex(chr_el) if chr_el is not None else r"\sum"
        op_map = {"∑": r"\sum", "∏": r"\prod", "∫": r"\int"}
        op = op_map.get(op, op)

        sub = omml_to_latex(_find_child(el, "sub"))
        sup = omml_to_latex(_find_child(el, "sup"))
        body = omml_to_latex(_find_child(el, "e"))

        out = op
        if sub:
            out += "_{" + sub + "}"
        if sup:
            out += "^{" + sup + "}"
        if body:
            out += " " + body
        return out

    # fallback
    return "".join(omml_to_latex(c) for c in _children(el))

def extract_math_from_omml(math_elem) -> str:
    """
    Return LaTeX WITHOUT wrapper.
    We'll wrap inline as \\( ... \\) for Flutter.
    """
    return re.sub(r"\s+", " ", omml_to_latex(math_elem)).strip()

def latex_inline(eq: str) -> str:
    if not eq:
        return ""
    # inline latex tag
    return f"<latex-inline>{eq}</latex-inline>"

def latex_display(eq: str) -> str:
    if not eq:
        return ""
    # block latex tag
    return f"<latex-block>{eq}</latex-block>"

# =========================================================
# TEXT superscript/subscript (Word formatted text, not OMML)
# Example: (x+6√2)^(1/2) is often stored as run with vertAlign=superscript
# =========================================================
def get_run_vert_align(run_elem) -> str:
    va = run_elem.xpath('.//*[local-name()="vertAlign"]')
    if not va:
        return ""
    val = va[0].get("{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val") or va[0].get("val")
    return (val or "").lower()  # "superscript" / "subscript"

# =========================================================
# IMAGES
# =========================================================
def get_image_rel_ids_from_run(run_elem):
    rel_ids = []

    for blip in run_elem.xpath('.//*[local-name()="blip"]'):
        rid = blip.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed")
        if rid:
            rel_ids.append(rid)
        rid_link = blip.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}link")
        if rid_link:
            rel_ids.append(rid_link)

    for imagedata in run_elem.xpath('.//*[local-name()="imagedata"]'):
        rid = imagedata.get("{http://schemas.openxmlformats.org/officeDocument/2006/relationships}id")
        if rid:
            rel_ids.append(rid)

    out = []
    for x in rel_ids:
        if x not in out:
            out.append(x)
    return out

def upload_docx_image_to_spaces(document: Document, rel_id: str) -> str:
    try:
        part = document.part
        if rel_id not in part.related_parts:
            return ""

        image_part = part.related_parts[rel_id]
        blob = image_part.blob

        partname = str(getattr(image_part, "partname", "image.png"))
        ext = partname.split(".")[-1].lower() if "." in partname else "png"
        if ext not in ("png", "jpg", "jpeg", "gif", "webp", "bmp", "tif", "tiff"):
            ext = "png"

        filename = f"question_word/{uuid.uuid4().hex}.{ext}"
        saved_path = default_storage.save(filename, ContentFile(blob))
        return default_storage.url(saved_path)
    except Exception:
        return ""

# =========================================================
# Paragraph -> HTML for DB (Flutter-friendly)
# - Always returns HTML as <p>...</p>
# - OMML equations inserted as LaTeX inline: \( ... \)
# - If a paragraph contains ONLY equations, they are stored in their own <p> as display math: \[...\]
# - Superscript/subscript runs converted to ^{...} / _{...} (when not OMML)
# =========================================================
def extract_paragraph_html(paragraph, document: Document) -> str:
    p = paragraph._p
    parts = []
    has_text_or_img = False
    eqs = []  # equation latex (raw) found in this paragraph

    for child in p:
        tag = etree.QName(child).localname

        if tag == "r":
            vert = get_run_vert_align(child)

            # text in the run
            texts = child.xpath('.//*[local-name()="t"]')
            run_text = "".join((t.text or "") for t in texts)
            run_text = run_text.replace("\u00a0", " ")  # NBSP -> space
            run_text = normalize_entities(run_text)
            run_text = normalize_sqrt_unicode_to_latex(run_text)

            # images
            imgs = []
            for rid in get_image_rel_ids_from_run(child):
                img_url = upload_docx_image_to_spaces(document, rid)
                if img_url:
                    imgs.append(f'<img src="{img_url}" style="max-width:100%;height:auto;" />')

            if run_text.strip():
                has_text_or_img = True
                if vert == "superscript":
                    parts.append("^{" + run_text.strip() + "}")
                elif vert == "subscript":
                    parts.append("_{" + run_text.strip() + "}")
                else:
                    parts.append(run_text)

            if imgs:
                has_text_or_img = True
                parts.extend(imgs)

        elif tag in ("oMath", "oMathPara"):
            eq = extract_math_from_omml(child)
            if eq:
                eqs.append(eq)
                # inline equation inside paragraph (Word-like)
                parts.append(latex_inline(eq))

    # Paragraph is only equations
    if (not has_text_or_img) and eqs:
        # each equation on separate line (Flutter renders well)
        return "".join([f"<p>{latex_display(eq)}</p>" for eq in eqs])

    line = "".join(parts).strip()
    return f"<p>{line}</p>" if line else ""

# =========================================================
# Structure detection (use raw text)
# =========================================================
def is_option_start(text: str) -> bool:
    return bool(re.match(r"^\s*[A-E]\)", (text or "").strip()))

def is_structural_break(raw_text: str) -> bool:
    t = (raw_text or "").strip()
    return (
        t.startswith("Q-")
        or t.startswith("Directions:")
        or t.startswith("Answer:")
        or t.startswith("Solution:")
        or is_option_start(t)
    )

# =========================================================
# Strip label from FIRST <p> only (keeps math/images in later <p>)
# =========================================================
def strip_label_from_first_p(html_block: str, label: str) -> str:
    if not html_block:
        return ""
    # remove "Label" after <p> and optional spaces
    return re.sub(rf"^<p>\s*{re.escape(label)}\s*", "<p>", html_block, count=1)

def strip_option_label_from_first_p(html_block: str) -> str:
    if not html_block:
        return ""
    return re.sub(r"^<p>\s*[A-E]\)\s*", "<p>", html_block, count=1)

# =========================================================
# VIEW (your upload)
# =========================================================
@login_required(login_url=ADMIN_LOGIN_URL)
def upload_question_word(request):
    if request.method != "POST":
        return JsonResponse({"success": False, "message": "Invalid request method."})

    if "excelFile" not in request.FILES:
        return JsonResponse({"success": False, "message": "File not found in request (excelFile)."})

    uploaded_file = request.FILES["excelFile"]
    test_id = request.POST.get("test_id")
    exam_category_id = request.POST.get("exam_category_id")

    document = Document(uploaded_file)

    # defaults
    marks = 1
    negative_marks = 0

    # NOTE: keep your existing ExamCategory logic
    if exam_category_id and exam_category_id != "None":
        category = ExamCategory.objects.filter(id=exam_category_id).first()
        if category:
            marks = category.marks_per_question or marks
            negative_marks = category.negative_marks_per_question or negative_marks

    questions_data = []
    topic_data = []
    section_data = []
    sub_section_data = []

    for table in document.tables:
        q_no = 1

        for row_index, row in enumerate(table.rows):
            if row_index == 0:
                continue

            if len(row.cells) < 6:
                return JsonResponse({"success": False, "message": f"Row {row_index + 1} has insufficient columns."})

            topic = (row.cells[2].text or "").replace("\n", " ").strip()
            section = (row.cells[3].text or "").replace("\n", " ").strip()
            sub_section = (row.cells[4].text or "").replace("\n", " ").strip()
            difficulty_level = (row.cells[5].text or "").replace("\n", " ").strip()

            topics = Topics.objects.filter(topic_name=topic).first()
            if not topics:
                return JsonResponse({
                    "success": False,
                    "message": f"Question No. {q_no}) {topic} topic does not exists.",
                    "section": section,
                    "sub_section": sub_section,
                    "topic": topic
                })

            topic_id = topics.id
            sub_section_id = topics.sub_section_id
            section_id = topics.section_id

            topic_data.append(topic)
            section_data.append(section)
            sub_section_data.append(sub_section)

            paragraphs = row.cells[1].paragraphs

            current_question = {
                "paragraph": "",
                "question_text": "",
                "options": [],
                "correct_answer": 0,
                "explanation": "",
                "section_id": section_id,
                "sub_section_id": sub_section_id,
                "topic_id": topic_id,
                "difficulty_level": difficulty_level,
                "question_type": "mcq",
            }

            i = 0
            while i < len(paragraphs):
                p = paragraphs[i]
                raw = (p.text or "").strip()

                # Directions
                if raw.startswith("Directions:"):
                    block = extract_paragraph_html(p, document)
                    block = strip_label_from_first_p(block, "Directions:")
                    current_question["paragraph"] += block

                    j = i + 1
                    while j < len(paragraphs):
                        nxt_raw = (paragraphs[j].text or "").strip()
                        if nxt_raw.startswith("Q-"):
                            break
                        current_question["paragraph"] += extract_paragraph_html(paragraphs[j], document)
                        j += 1

                    i = j
                    continue

                # Question
                if raw.startswith("Q-"):
                    block = extract_paragraph_html(p, document)
                    block = strip_label_from_first_p(block, "Q-")
                    current_question["question_text"] += block

                    j = i + 1
                    while j < len(paragraphs):
                        nxt_raw = (paragraphs[j].text or "").strip()
                        if is_option_start(nxt_raw) or nxt_raw.startswith(("Answer:", "Solution:", "Q-")):
                            break
                        current_question["question_text"] += extract_paragraph_html(paragraphs[j], document)
                        j += 1

                    i = j
                    continue

                # Options
                if is_option_start(raw):
                    block = extract_paragraph_html(p, document)
                    block = strip_option_label_from_first_p(block)
                    option_html = block

                    j = i + 1
                    while j < len(paragraphs):
                        nxt_raw = (paragraphs[j].text or "").strip()
                        if is_structural_break(nxt_raw) and (
                            is_option_start(nxt_raw) or nxt_raw.startswith(("Answer:", "Solution:", "Q-", "Directions:"))
                        ):
                            break
                        option_html += extract_paragraph_html(paragraphs[j], document)
                        j += 1

                    current_question["options"].append(option_html)
                    i = j
                    continue

                # Answer
                if raw.startswith("Answer:"):
                    ans = raw.replace("Answer:", "", 1).strip()
                    ans = ans[:1].upper() if ans else ""
                    current_question["correct_answer"] = {"A": 1, "B": 2, "C": 3, "D": 4, "E": 5}.get(ans, 0)
                    i += 1
                    continue

                # Solution / Explanation
                if raw.startswith("Solution:"):
                    block = extract_paragraph_html(p, document)
                    block = strip_label_from_first_p(block, "Solution:")
                    current_question["explanation"] += block

                    j = i + 1
                    while j < len(paragraphs):
                        nxt_raw = (paragraphs[j].text or "").strip()
                        if nxt_raw.startswith("Q-"):
                            break
                        current_question["explanation"] += extract_paragraph_html(paragraphs[j], document)
                        j += 1

                    i = j
                    continue

                i += 1

            if current_question["question_text"]:
                current_question["question_type"] = "mcq" if current_question["options"] else "oneliner"
                questions_data.append(current_question)

            q_no += 1

    # =============================
    # Bulk insert to your Question_Bank
    # (assumes model exists in scope)
    # =============================
    paragraph_map = {}
    q_counter = 1

    objects = []

    for q in questions_data:
        paragraph = q.get("paragraph")

        # assign same number for same paragraph
        if paragraph not in paragraph_map:
            paragraph_map[paragraph] = q_counter
            q_counter += 1

        tag = f"T-{test_id}-{paragraph_map[paragraph]}"

        objects.append(
            Question_Bank(
                section_id=q["section_id"],
                sub_section_id=q["sub_section_id"],
                topic_id=q["topic_id"],
                question_type=q["question_type"],
                question=q["question_text"],
                paragraph=paragraph,
                options=json.dumps(q["options"]),
                difficulty_level=q["difficulty_level"],
                marks=marks,
                negative_marks=negative_marks,
                explanation=q["explanation"],
                correct_answer=q["correct_answer"],
                status="Active",
                test_id=test_id,
                tag=tag   # ✅ added here
            )
        )

    Question_Bank.objects.bulk_create(objects)
    return JsonResponse({
        "success": True,
        "message": "Questions uploaded successfully.",
        "topic_data": topic_data,
        "section_data": section_data,
        "sub_section_data": sub_section_data,
        "inserted": len(questions_data),
    })

    
@login_required(login_url=ADMIN_LOGIN_URL)
def oupload_question_word(request):
    if request.method == 'POST' and request.FILES['excelFile']:
        uploaded_file = request.FILES['excelFile']
        test_id = request.POST.get('test_id')
        exam_category_id = request.POST.get('exam_category_id')
        document = WordDocument(uploaded_file)
        marks = 1
        negative_marks = 0
        if exam_category_id != 'None':
            category = ExamCategory.objects.filter(id=exam_category_id)
            if category[0].marks_per_question:
                marks=category[0].marks_per_question
            if category[0].negative_marks_per_question:
                negative_marks=category[0].negative_marks_per_question
        # Find and extract equations from the LaTeX content
        equations = []
        questions_data = []
        paragraph_data = []
        section_data = []
        sub_section_data = []
        topic_data = []
        questions=[]
        current_question = {
            'paragraph':'',
            'question_text': '',
            'options': [],
            'correct_answer': '',
            'explanation': '',
            'section_id': '',
            'sub_section_id': '',
            'topic_id': '',
            'difficulty_level': ''
        }
        mathml_equations = []
        for table in document.tables:
            i=1;
            for row in table.rows:
                topic=row.cells[2].text.replace('\n', ' ')
                sub_section=row.cells[4].text.replace('\n', ' ')
                section=row.cells[3].text.replace('\n', ' ')
                difficulty_level=row.cells[5].text.replace('\n', ' ')
                topic_id = Topics.objects.filter(topic_name=topic).values_list('id', flat=True)
                sub_section_id = Sub_Sections.objects.filter(sub_section_name=sub_section).values_list('id', flat=True)
                section_id = Sections.objects.filter(section_name=section).values_list('id', flat=True)
                
                # Validate the retrieved IDs
                if not topic_id:
                    return JsonResponse({
                        "success": False,
                        "message": f"Topic '{topic}' not found in question no. {i}. Please check spelling or spacing.",
                        'section': section_data,
                        'sub_section': sub_section_data,
                        'topic': topic_data
                    })

                if not sub_section_id:
                    return JsonResponse({
                        "success": False,
                        "message": f"Sub-section '{sub_section}' not found in question no. {i}. Please check spelling or spacing.",
                        'section': section_data,
                        'sub_section': sub_section_data,
                        'topic': topic_data
                    })

                if not section_id:
                    return JsonResponse({
                        "success": False,
                        "message": f"Section '{section}' not found in question no. {i}. Please check spelling or spacing.",
                        'section': section_data,
                        'sub_section': sub_section_data,
                        'topic': topic_data
                    })

                # Append valid data
                topic_data.append(topic)
                section_data.append(section)
                sub_section_data.append(sub_section)
                paragraphs = row.cells[1].paragraphs
                current_question = {
                    'paragraph': '',
                    'question_text': '',
                    'options': [],
                    'correct_answer': '',
                    'explanation': '',
                    'section_id': section_id,
                    'sub_section_id': sub_section_id,
                    'topic_id': topic_id,
                    'difficulty_level':difficulty_level,
                    'question_type': 'mcq'
                }
                for paragraph in paragraphs:
                    paragraph_data.append(paragraph.text)
                    text = paragraph.text
                    if paragraph.text.startswith('Directions:'):
                        current_question['paragraph'] = paragraph.text.lstrip('Directions:').strip()
                        for next_paragraph in paragraphs[paragraphs.index(paragraph) + 1:]:
                            if next_paragraph.text.startswith(('Q-')):
                                    break
                            else:
                                if next_paragraph.text.startswith("$$") and next_paragraph.text.endswith("$$"):
                                    latex_equation = next_paragraph.text[2:-2].strip()  # Remove $$ delimiters
                                    current_question['paragraph'] +='<p>'+convert(latex_equation)+'</p>'
                                else:
                                    current_question['paragraph'] += '<p>' + next_paragraph.text.strip()+'</p>'
                                for run in next_paragraph.runs:
                                    for drawing in run._element.findall('.//a:blip', namespaces=run.part._element.nsmap):
                                        image_id = drawing.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                                        image_part = document.part.related_parts[image_id]
                                        image_bytes = image_part.blob

                                        # Save the image to a file or process it as needed
                                        # For demonstration, we encode it as base64 and append to the images list
                                        base64_image = base64.b64encode(image_bytes).decode('utf-8')
                                        # current_question['images'].append(f'data:image/png;base64,{base64_image}')
                                        current_question['paragraph'] += f'<img src="data:image/png;base64,{base64_image}"/>'

                    # Check if the paragraph starts with 'Q-'
                    elif paragraph.text.startswith('Q-'):
                        # Save the previous question data (if any)
                        current_question['question_text']='<p>'+paragraph.text.lstrip('Q-').strip()+'</p>'
#                        if current_question['question_text']:
#                            questions_data.append(current_question.copy())
#                        # Start a new question
#                        current_question = {
#                            'paragraph': current_question['paragraph'],  # Keep the paragraph info
#                            'question_text': '<p>'+paragraph.text.lstrip('Q-').strip()+'</p>',
#                            'options': [],  # Reset options for a new question
#                            'correct_answer': '',
#                            'explanation': '',
#                            'section_id': section_id,
#                            'sub_section_id': sub_section_id,
#                            'topic_id': topic_id,
#                            'difficulty_level':difficulty_level,
#                            'question_type': 'mcq'
#                        }
                        
                        for next_paragraph in paragraphs[paragraphs.index(paragraph) + 1:]:
                            if next_paragraph.text.startswith(('A)', 'B)', 'C)', 'D)', 'E)')):
                                break
                            else:
                                if next_paragraph.text.startswith("$$") and next_paragraph.text.endswith("$$"):
                                    latex_equation = next_paragraph.text[2:-2].strip()  # Remove $$ delimiters
                                    current_question['question_text'] +='<p>'+convert(latex_equation)+'</p>'
                                else:
                                    current_question['question_text'] += '<p>' + next_paragraph.text.strip()+'</p>'
                                for run in next_paragraph.runs:
                                    for drawing in run._element.findall('.//a:blip', namespaces=run.part._element.nsmap):
                                        image_id = drawing.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                                        image_part = document.part.related_parts[image_id]
                                        image_bytes = image_part.blob
                                        # Save the image to a file or process it as needed
                                        # For demonstration, we encode it as base64 and append to the images list
                                        base64_image = base64.b64encode(image_bytes).decode('utf-8')
                                        # current_question['images'].append(f'data:image/png;base64,{base64_image}')
                                        current_question['question_text'] += f'<img src="data:image/png;base64,{base64_image}"/>'
                    # Check if the paragraph starts with options ('A)', 'B)', 'C)', 'D)', 'E)')
                    elif paragraph.text.startswith(('A)', 'B)', 'C)', 'D)', 'E)')):
                        if paragraph.text.lstrip('ABCDE)').strip().startswith("$$") and paragraph.text.lstrip('ABCDE)').strip().endswith("$$"):
                            latex_equation = paragraph.text.lstrip('ABCDE)').strip()[2:-2]  # Remove $$ delimiters
                            current_question['options'].append('<p>'+convert(latex_equation)+'</p>')
                        else:
                            current_question['options'].append(paragraph.text.lstrip('ABCDE)').strip())

                        # Add images to the current option
                        for run in paragraph.runs:
                            for drawing in run._element.findall('.//a:blip', namespaces=run.part._element.nsmap):
                                image_id = drawing.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                                image_part = document.part.related_parts[image_id]
                                image_bytes = image_part.blob

                                # Save the image to a file or process it as needed
                                # For demonstration, we encode it as base64 and append to the images list
                                base64_image = base64.b64encode(image_bytes).decode('utf-8')
                                current_question['options'][-1] += f'<img src="data:image/png;base64,{base64_image}"/>'
                    elif paragraph.text.startswith('Answer:'):
                        if paragraph.text.lstrip('Answer:').strip()=='A':
                            current_question['correct_answer'] = 1
                        elif paragraph.text.lstrip('Answer:').strip()=='B':
                            current_question['correct_answer'] = 2
                        elif paragraph.text.lstrip('Answer:').strip()=='C':
                            current_question['correct_answer'] = 3
                        elif paragraph.text.lstrip('Answer:').strip()=='D':
                            current_question['correct_answer'] = 4
                        elif paragraph.text.lstrip('Answer:').strip()=='E':
                            current_question['correct_answer'] = 5
                    elif paragraph.text.startswith('Solution:'):
                        # Start capturing explanation from the next line until the last text
                        current_question['explanation'] = paragraph.text.lstrip('Solution:').strip()
                        for next_paragraph in paragraphs[paragraphs.index(paragraph) + 1:]:
                            if next_paragraph.text.startswith('Q-'):
                                break
                            if next_paragraph.text.startswith("$$") and next_paragraph.text.endswith("$$"):
                                latex_equation = next_paragraph.text[2:-2].strip()  # Remove $$ delimiters
                                current_question['explanation'] +=f'<p>'+convert(latex_equation)+'</p>'
                            else:
                                current_question['explanation'] += '<p>' + next_paragraph.text.strip()+'</p>'
                            for run in next_paragraph.runs:
                                for drawing in run._element.findall('.//a:blip', namespaces=run.part._element.nsmap):
                                    image_id = drawing.get('{http://schemas.openxmlformats.org/officeDocument/2006/relationships}embed')
                                    image_part = document.part.related_parts[image_id]
                                    image_bytes = image_part.blob

                                    # Save the image to a file or process it as needed
                                    # For demonstration, we encode it as base64 and append to the images list
                                    base64_image = base64.b64encode(image_bytes).decode('utf-8')
                                    # current_question['images'].append(f'data:image/png;base64,{base64_image}')
                                    current_question['explanation'] += f'<img src="data:image/png;base64,{base64_image}"/>'
                
                if current_question['question_text']:
                    if current_question['options'].count:
                        current_question['question_type']='mcq'
                    else:
                        current_question['question_type']='oneliner'
                       
                    questions_data.append(current_question.copy())
                else:
                    return JsonResponse({"success": False, "message":"Something wrong with question no."+str(i)+"."})
                    
                i=i+1
                    
        for current_question in questions_data:
            question_obj = Question_Bank(section_id=current_question['section_id'], sub_section_id=current_question['sub_section_id'], topic_id=current_question['topic_id'],
                            question_type=current_question['question_type'], question=current_question['question_text'], paragraph=current_question['paragraph'], options=json.dumps(current_question['options']),
                            difficulty_level=current_question['difficulty_level'], marks=marks, negative_marks=negative_marks, explanation=current_question['explanation'],correct_answer=current_question['correct_answer'], status='Active',test_id=test_id
                           )
            questions.append(question_obj)
            mathml_equations.append(1)
        # Bulk insert the questions data
        Question_Bank.objects.bulk_create(questions)
            
        # Save the last question data (if any)
        # if current_question['question_text']:
        #     questions_data.append(current_question)

        # Save data to the database
        # for data in questions_data:
        #     QuizQuestion.objects.create(
        #         question_text=data['question_text'],
        #         option_a=data['options']['a'],
        #         option_b=data['options']['b'],
        #         option_c=data['options']['c'],
        #         option_d=data['options']['d'],
        #         correct_answer=data['correct_answer'],
        #         explanation=data['explanation'],
        #     )

        return JsonResponse({"success": True, "message":"Questions uploaded and data stored successfully.","topic_data":topic_data,"section_data":section_data,"sub_section_data":sub_section_data})
    
    # return render(request, 'upload_quiz_questions.html')


def convert_to_mathml(latex_equation):
    try:
        mathml = convert(latex_equation)
        return mathml
    except Exception as e:
        print(f"Error converting LaTeX to MathML: {e}")
        return None

@login_required(login_url=ADMIN_LOGIN_URL)
def editquestion(request, id): 
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all() 
    Question_list = Question_Bank.objects.get(id=id) 
    options = json.loads(Question_list.options)
    #return JsonResponse({'status': 'success', 'msg': 'Sub Section saved successfully.', 'options':options})
    return render(request,'edit-question.html', {'Question_list':Question_list,'section_list':section_list,'Sub_Sections_list':Sub_Sections_list,'Topics_list':Topics_list, 'options':options})

@login_required(login_url=ADMIN_LOGIN_URL)
def create_course(request):
    return render(request, "create-course.html");



@login_required(login_url=ADMIN_LOGIN_URL)
def mock_test_list(request):
    # Get parameters from GET request
    series_id = request.GET.get('series_id')
    mock_type = request.GET.get('type')

    # Fetch necessary lists for rendering the page
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    all_categories = ExamCategory.objects.all()

    # Get filter parameters from request
    name = request.GET.get('name')
    section1 = request.GET.get('section1')
    sub1 = request.GET.get('sub1')
    cat1 = request.GET.get('cat1')
    mock_level = request.GET.get('mock_level')
    is_accessible = request.GET.get('is_accessible')
    section_wise_timer = request.GET.get('section_wise_timer')
    section_switching = request.GET.get('section_switching')
    calculator = request.GET.get('calculator')

    # Initialize the filter and queryset
    mock_test = Mock_Test.objects.filter(mock_type=mock_type)
    # Apply ordering only for specific mock types
    filter_string = ''

    # Apply filters
    if name:
        mock_test = mock_test.filter(mock_title__icontains=name)  # Using icontains for case-insensitive partial match
        filter_string += f'&name={name}'

    if section1:
        mock_test = mock_test.filter(section=section1)
        filter_string += f'&section1={section1}'

    if sub1:
        mock_test = mock_test.filter(sub_section=sub1)
        filter_string += f'&sub1={sub1}'
        
    if cat1:
        mock_test = mock_test.filter(
            mock_exams__exam_category_id = cat1
        )
        filter_string += f"&cat1={cat1}"

    if mock_level:
        mock_test = mock_test.filter(mock_level=mock_level)
        filter_string += f'&mock_level={mock_level}'

    if is_accessible:
        mock_test = mock_test.filter(is_accessible=is_accessible)
        filter_string += f'&is_accessible={is_accessible}'

    if section_wise_timer:
        mock_test = mock_test.filter(section_wise_timer=section_wise_timer)
        filter_string += f'&section_wise_timer={section_wise_timer}'

    if section_switching:
        mock_test = mock_test.filter(section_switching=section_switching)
        filter_string += f'&section_switching={section_switching}'

    if calculator:
        mock_test = mock_test.filter(calculator=calculator)
        filter_string += f'&calculator={calculator}'
    
    if mock_type in ['1', '2', '7', '8']:
        mock_test = mock_test.order_by('section_id')
    # Paginate results
    paginator = Paginator(mock_test, 10)  # Show 10 items per page
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    for mock in page_obj.object_list:
        mock.video_question_count = Question_Bank.objects.filter(test_id=mock.id).count()
        mock.uploaded_video_count = Question_Bank.objects.filter(
            test_id=mock.id,
            explanation_video__isnull=False,
        ).exclude(explanation_video="").count()

    # Render the page with filtered data
    return render(request, "mock-test.html", {
        'mock_test_list': page_obj,
        'all_categories': all_categories,
        'series_id': series_id,
        'mock_type': mock_type,
        'section_list': section_list,
        'Sub_Sections_list': Sub_Sections_list,
        'Topics_list': Topics_list,
        'name': name,
        'mock_level': mock_level,
        'is_accessible': is_accessible,
        'section_wise_timer': section_wise_timer,
        'section_switching': section_switching,
        'calculator': calculator,
        'filter': filter_string,  # Passing the filter string if you use it in the template
    })

@login_required(login_url=ADMIN_LOGIN_URL)
def update_mock_statisctic(request):
    today=date.today()
    # Fetch mocks and pre-loaded test results and questions
    mocks = Mock_Test.objects.filter(mock_type__in=[5,8],test_date__lte=today).prefetch_related('question_bank_set').order_by('-id')[30:60]
    
    # Loop through each mock test
    for mock in mocks:
        exam_id = mock.id
        
        # Fetch total students and top 20 students in one query
        top_20_students = Test_Result.objects.filter(mock_test_id=exam_id, not_accessible=0, is_end=1) \
            .order_by('-marks')[:10]
        
        top_20_students_ids = list(top_20_students.values_list('student_id', flat=True))
        
        total_students_count = Test_Result.objects.filter(mock_test_id=exam_id, not_accessible=0, is_end=1).distinct().count()

        # Fetch student answers aggregation in batch
        student_stats = (Student_Answers.objects.filter(question__test_id=exam_id,result__not_accessible=0, result__is_end=1)
            .values('question_id')
            .annotate(total_attempted=Count('id', filter=Q(answer_id__isnull=False)),
                      correct_attempts=Count('id', filter=Q(is_right=True)),
                      total_time_taken=Sum('time_taken'),
                      toppers_attempted=Count('id', filter=Q(student_id__in=top_20_students_ids, answer_id__isnull=False)),
                      toppers_correct=Count('id', filter=Q(student_id__in=top_20_students_ids, is_right=True)),
                      total_toppers=Count('id', filter=Q(student_id__in=top_20_students_ids)),
                      toppers_time_taken_total=Sum('time_taken', filter=Q(student_id__in=top_20_students_ids))
                      )
            )

        # Prepare to hold updated questions
        updated_questions = []

        # Iterate through questions and aggregate to update the question model
        questions = mock.question_bank_set.all()  # prefetch related questions
        for question in questions:
            question_stats = next((stat for stat in student_stats if stat['question_id'] == question.id), None)

            if question_stats:
                total_attempted = question_stats['total_attempted']
                correct_attempts = question_stats['correct_attempts']
                total_time_taken = question_stats['total_time_taken'] or 0

                toppers_attempted = question_stats['toppers_attempted']
                toppers_correct = question_stats['toppers_correct']
                total_toppers = question_stats['total_toppers']
                toppers_time_taken_total = question_stats['toppers_time_taken_total'] or 0

                # Calculate percentages
                overall_attempt_percentage = (total_attempted / total_students_count * 100) if total_students_count > 0 else 0
                overall_accuracy_percentage = (correct_attempts / total_attempted * 100) if total_attempted > 0 else 0
                toppers_attempt_percentage = (toppers_attempted / total_toppers * 100) if total_toppers > 0 else 0
                toppers_accuracy_percentage = (toppers_correct / toppers_attempted * 100) if toppers_attempted > 0 else 0
                toppers_time_taken_avg = (toppers_time_taken_total / total_toppers) if total_toppers > 0 else 0
                overall_time_taken_avg = (total_time_taken / total_students_count) if total_students_count > 0 else 0

                # Update the question model with the aggregated statistics
                question.total_attempted = total_attempted
                question.correct_attempts = correct_attempts
                question.total_time_taken = total_time_taken
                question.toppers_attempted = toppers_attempted
                question.toppers_correct_attempts = toppers_correct
                question.total_toppers = total_toppers
                question.toppers_time_taken_total = toppers_time_taken_total
                question.overall_attempt_percentage = overall_attempt_percentage
                question.overall_accuracy_percentage = overall_accuracy_percentage
                question.toppers_attempt_percentage = toppers_attempt_percentage
                question.toppers_accuracy_percentage = toppers_accuracy_percentage
                question.toppers_time_taken_avg = toppers_time_taken_avg
                question.overall_time_taken_avg = overall_time_taken_avg
                
                updated_questions.append(question)

        # Bulk update questions to reduce the number of database hits
        Question_Bank.objects.bulk_update(updated_questions, [
            'total_attempted',
            'correct_attempts',
            'total_time_taken',
            'toppers_attempted',
            'toppers_correct_attempts',
            'total_toppers',
            'toppers_time_taken_total',
            'overall_attempt_percentage',
            'overall_accuracy_percentage',
            'toppers_attempt_percentage',
            'toppers_accuracy_percentage',
            'toppers_time_taken_avg',
            'overall_time_taken_avg',
        ])

    return JsonResponse({"status": "success"})
    
@login_required(login_url=ADMIN_LOGIN_URL)
def calculate_fullmock_statisctic(request):
    today=date.today()
    # Fetch mocks and pre-loaded test results and questions
    mocks = Mock_Test.objects.filter(mock_type__in=[5],test_date__lte=today).prefetch_related('question_bank_set').order_by('-id')
    
    # Loop through each mock test
    for mock in mocks:
        exam_id = mock.id
        
        # Fetch total students and top 20 students in one query
        top_20_students = Test_Result.objects.filter(mock_test_id=exam_id, not_accessible=0, is_end=1) \
            .order_by('-marks')[:10]
        
        top_20_students_ids = list(top_20_students.values_list('student_id', flat=True))
        
        total_students_count = Test_Result.objects.filter(mock_test_id=exam_id, not_accessible=0, is_end=1).distinct().count()

        # Fetch student answers aggregation in batch
        student_stats = (Student_Answers.objects.filter(question__test_id=exam_id,result__not_accessible=0, result__is_end=1)
            .values('question_id')
            .annotate(total_attempted=Count('id', filter=Q(answer_id__isnull=False)),
                      correct_attempts=Count('id', filter=Q(is_right=True)),
                      total_time_taken=Sum('time_taken'),
                      toppers_attempted=Count('id', filter=Q(student_id__in=top_20_students_ids, answer_id__isnull=False)),
                      toppers_correct=Count('id', filter=Q(student_id__in=top_20_students_ids, is_right=True)),
                      total_toppers=Count('id', filter=Q(student_id__in=top_20_students_ids)),
                      toppers_time_taken_total=Sum('time_taken', filter=Q(student_id__in=top_20_students_ids))
                      )
            )

        # Prepare to hold updated questions
        updated_questions = []

        # Iterate through questions and aggregate to update the question model
        questions = mock.question_bank_set.all()  # prefetch related questions
        for question in questions:
            question_stats = next((stat for stat in student_stats if stat['question_id'] == question.id), None)

            if question_stats:
                total_attempted = question_stats['total_attempted']
                correct_attempts = question_stats['correct_attempts']
                total_time_taken = question_stats['total_time_taken'] or 0

                toppers_attempted = question_stats['toppers_attempted']
                toppers_correct = question_stats['toppers_correct']
                total_toppers = question_stats['total_toppers']
                toppers_time_taken_total = question_stats['toppers_time_taken_total'] or 0

                # Calculate percentages
                overall_attempt_percentage = (total_attempted / total_students_count * 100) if total_students_count > 0 else 0
                overall_accuracy_percentage = (correct_attempts / total_attempted * 100) if total_attempted > 0 else 0
                toppers_attempt_percentage = (toppers_attempted / total_toppers * 100) if total_toppers > 0 else 0
                toppers_accuracy_percentage = (toppers_correct / toppers_attempted * 100) if toppers_attempted > 0 else 0
                toppers_time_taken_avg = (toppers_time_taken_total / total_toppers) if total_toppers > 0 else 0
                overall_time_taken_avg = (total_time_taken / total_students_count) if total_students_count > 0 else 0

                # Update the question model with the aggregated statistics
                question.total_attempted = total_attempted
                question.correct_attempts = correct_attempts
                question.total_time_taken = total_time_taken
                question.toppers_attempted = toppers_attempted
                question.toppers_correct_attempts = toppers_correct
                question.total_toppers = total_toppers
                question.toppers_time_taken_total = toppers_time_taken_total
                question.overall_attempt_percentage = overall_attempt_percentage
                question.overall_accuracy_percentage = overall_accuracy_percentage
                question.toppers_attempt_percentage = toppers_attempt_percentage
                question.toppers_accuracy_percentage = toppers_accuracy_percentage
                question.toppers_time_taken_avg = toppers_time_taken_avg
                question.overall_time_taken_avg = overall_time_taken_avg
                
                updated_questions.append(question)

        # Bulk update questions to reduce the number of database hits
        Question_Bank.objects.bulk_update(updated_questions, [
            'total_attempted',
            'correct_attempts',
            'total_time_taken',
            'toppers_attempted',
            'toppers_correct_attempts',
            'total_toppers',
            'toppers_time_taken_total',
            'overall_attempt_percentage',
            'overall_accuracy_percentage',
            'toppers_attempt_percentage',
            'toppers_accuracy_percentage',
            'toppers_time_taken_avg',
            'overall_time_taken_avg',
        ])

    return JsonResponse({"status": "success"})

@login_required(login_url=ADMIN_LOGIN_URL)
def calculate_advancemock_statisctic(request):
    today=date.today()
    # Fetch mocks and pre-loaded test results and questions
    mocks = Mock_Test.objects.filter(mock_type__in=[8],test_date__lte=today).prefetch_related('question_bank_set').order_by('-id')
    
    # Loop through each mock test
    for mock in mocks:
        exam_id = mock.id
        
        # Fetch total students and top 20 students in one query
        top_20_students = Test_Result.objects.filter(mock_test_id=exam_id, not_accessible=0, is_end=1) \
            .order_by('-marks')[:10]
        
        top_20_students_ids = list(top_20_students.values_list('student_id', flat=True))
        
        total_students_count = Test_Result.objects.filter(mock_test_id=exam_id, not_accessible=0, is_end=1).distinct().count()

        # Fetch student answers aggregation in batch
        student_stats = (Student_Answers.objects.filter(question__test_id=exam_id,result__not_accessible=0, result__is_end=1)
            .values('question_id')
            .annotate(total_attempted=Count('id', filter=Q(answer_id__isnull=False)),
                      correct_attempts=Count('id', filter=Q(is_right=True)),
                      total_time_taken=Sum('time_taken'),
                      toppers_attempted=Count('id', filter=Q(student_id__in=top_20_students_ids, answer_id__isnull=False)),
                      toppers_correct=Count('id', filter=Q(student_id__in=top_20_students_ids, is_right=True)),
                      total_toppers=Count('id', filter=Q(student_id__in=top_20_students_ids)),
                      toppers_time_taken_total=Sum('time_taken', filter=Q(student_id__in=top_20_students_ids))
                      )
            )

        # Prepare to hold updated questions
        updated_questions = []

        # Iterate through questions and aggregate to update the question model
        questions = mock.question_bank_set.all()  # prefetch related questions
        for question in questions:
            question_stats = next((stat for stat in student_stats if stat['question_id'] == question.id), None)

            if question_stats:
                total_attempted = question_stats['total_attempted']
                correct_attempts = question_stats['correct_attempts']
                total_time_taken = question_stats['total_time_taken'] or 0

                toppers_attempted = question_stats['toppers_attempted']
                toppers_correct = question_stats['toppers_correct']
                total_toppers = question_stats['total_toppers']
                toppers_time_taken_total = question_stats['toppers_time_taken_total'] or 0

                # Calculate percentages
                overall_attempt_percentage = (total_attempted / total_students_count * 100) if total_students_count > 0 else 0
                overall_accuracy_percentage = (correct_attempts / total_attempted * 100) if total_attempted > 0 else 0
                toppers_attempt_percentage = (toppers_attempted / total_toppers * 100) if total_toppers > 0 else 0
                toppers_accuracy_percentage = (toppers_correct / toppers_attempted * 100) if toppers_attempted > 0 else 0
                toppers_time_taken_avg = (toppers_time_taken_total / total_toppers) if total_toppers > 0 else 0
                overall_time_taken_avg = (total_time_taken / total_students_count) if total_students_count > 0 else 0

                # Update the question model with the aggregated statistics
                question.total_attempted = total_attempted
                question.correct_attempts = correct_attempts
                question.total_time_taken = total_time_taken
                question.toppers_attempted = toppers_attempted
                question.toppers_correct_attempts = toppers_correct
                question.total_toppers = total_toppers
                question.toppers_time_taken_total = toppers_time_taken_total
                question.overall_attempt_percentage = overall_attempt_percentage
                question.overall_accuracy_percentage = overall_accuracy_percentage
                question.toppers_attempt_percentage = toppers_attempt_percentage
                question.toppers_accuracy_percentage = toppers_accuracy_percentage
                question.toppers_time_taken_avg = toppers_time_taken_avg
                question.overall_time_taken_avg = overall_time_taken_avg
                
                updated_questions.append(question)

        # Bulk update questions to reduce the number of database hits
        Question_Bank.objects.bulk_update(updated_questions, [
            'total_attempted',
            'correct_attempts',
            'total_time_taken',
            'toppers_attempted',
            'toppers_correct_attempts',
            'total_toppers',
            'toppers_time_taken_total',
            'overall_attempt_percentage',
            'overall_accuracy_percentage',
            'toppers_attempt_percentage',
            'toppers_accuracy_percentage',
            'toppers_time_taken_avg',
            'overall_time_taken_avg',
        ])

    return JsonResponse({"status": "success"})


@login_required(login_url=ADMIN_LOGIN_URL)
def add_mock_test(request):
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    all_categories = ExamCategory.objects.all()
    all_exam = Exam_Master.objects.all()
    # series_id = request.GET.get('series_id')
    mock_type = request.GET.get('type')
    # mock_type = 3
    # print(mock_type)
    return render(request, "mock-test-list-add.html",{'all_categories':all_categories,'section_list':section_list,'Sub_Sections_list':Sub_Sections_list,'Topics_list':Topics_list, 'all_exam':all_exam, 'mock_type': mock_type})


@login_required(login_url=ADMIN_LOGIN_URL)
def search_mock(request):
    search = request.GET.get('search')
    mock_test_list = Mock_Test.objects.filter(mock_title__startswith=search)
    # print(section_list)
    return render(request, "search_mock.html" ,{'mock_test_list':mock_test_list})

@login_required(login_url=ADMIN_LOGIN_URL)
def add_mocktest(request):
    if request.method == 'POST':
        # Get the form data
        mock_title = request.POST.get('mock_title')
        mock_type = request.POST.get('mock_type')
        print(mock_type)
        # series_id = request.POST.get('series_id')
        section_wise_timer = request.POST.get('section_wise_timer')
        # print("section_wise_timer",section_wise_timer)
        section_switching = request.POST.get('section_switching')
        section_id = request.POST.get('section_id')
        sub_section_id = request.POST.get('sub_section_id')
        topic_id = request.POST.get('topic_id')
        exam_category_id = request.POST.get('exam_category_id')
        exam_master_id = request.POST.get('exam_master_id')
        mock_level = request.POST.get('mock_level')
        total_questions = request.POST.get('total_questions')
        total_marks = request.POST.get('total_marks')
        mock_duration = request.POST.get('mock_duration')
        # pass_per = request.POST.get('pass_per')
        is_accessible = request.POST.get('is_accessible')
        send_promotional_message = request.POST.get('send_promotional_message') == '1'
        test_date = request.POST.get('test_date')
        test_time = request.POST.get('test_time')
        negative_marks = request.POST.get('negative_marks')
        status = request.POST.get('status')
        calculator = request.POST.get('calculator')
        question_type = request.POST.get('question_type')

        exam_category_ids = request.POST.getlist('exam_category_ids')
        # print("exam_category_ids",exam_category_ids)
        
        section_ids = request.POST.getlist('section_ids[]')
        # print("section_ids",section_ids)

        section_durations = request.POST.getlist('section_durations[]')
        # print("section_durations",section_durations)
        # image = request.FILES.get('image')
        if(mock_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Mock Title.'})
        elif(mock_type==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter mock type.'})
        elif(total_questions==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter total questions.'})
        elif(total_marks==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter total marks.'})
        elif(mock_duration==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter mock duration.'})
        # elif(pass_per==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter pass Percentage.'})
        elif(is_accessible==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter accessible number.'})
        # elif(test_date==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select test date.'})
        elif(negative_marks==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter negative marks.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        # elif(section_wise_timer==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select section_wise_timer'})
        # elif(section_switching==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select section_switching'})
        
        else:
            if (mock_type=='1'):
            # Create a new ExamCategory object
                mock = Mock_Test.objects.create(
                    mock_title=mock_title,
                    mock_type=mock_type,
                    # series_id=series_id,
                    section_id=section_id,
                    sub_section_id=sub_section_id,
                    topic_id=topic_id,
#                    examcategory_id=exam_category_id,
                    exam_master_id=exam_master_id,
                    question_ids=json.dumps([]),
                    mock_level=mock_level,
                    total_questions=total_questions,
                    total_marks=total_marks,
                    # pass_per=pass_per,
                    negative_marks=negative_marks,
                    status=status,
                    test_date=test_date,
                    is_accessible=is_accessible,
                    mock_duration=mock_duration,
                    calculator=calculator
                )

                # mock_id = mock.id  # Use the same variable 'mock'
                # print("topic mock_id ", mock_id)

                # # Loop through the category IDs and create records
                # for category_id in exam_category_ids:
                #     mock_exams.objects.create(
                #         mock_test_id=mock_id,
                #         exam_category_id=category_id
                #     )

               

            elif (mock_type=='2'):
                mock = Mock_Test.objects.create(
                    mock_title=mock_title,
                    mock_type=mock_type,
                    # series_id=series_id,
                    section_id=section_id,
#                    sub_section_id=sub_section_id,
                    # topic_id=topic_id,
#                    examcategory_id=exam_category_id,
                    exam_master_id=exam_master_id,
                    question_ids=json.dumps([]),
                    mock_level=mock_level,
                    total_questions=total_questions,
                    total_marks=total_marks,
                    # pass_per=pass_per,
                    negative_marks=negative_marks,
                    status=status,
                    test_date=test_date,
                    test_time=test_time,
                    is_accessible=is_accessible,
                    mock_duration=mock_duration,
                    calculator=calculator
                )
                mock_id = mock.id  # Use the same variable 'mock'
                # Loop through the category IDs and create records
                for category_id in exam_category_ids:
                    mock_exams.objects.create(
                        mock_test_id=mock_id,
                        exam_category_id=category_id
                    )

            elif (mock_type=='6' or mock_type=='7'):
                mock = Mock_Test.objects.create(
                    mock_title=mock_title,
                    mock_type=mock_type,
                    # series_id=series_id,
                    section_id=section_id,
#                    sub_section_id=sub_section_id,
                    # topic_id=topic_id,
#                    examcategory_id=exam_category_id,
                    exam_master_id=exam_master_id,
                    question_ids=json.dumps([]),
                    mock_level=mock_level,
                    total_questions=total_questions,
                    total_marks=total_marks,
                    # pass_per=pass_per,
                    status=status,
                    is_accessible=is_accessible,
                    mock_duration=mock_duration
                )
                mock_id = mock.id  # Use the same variable 'mock'
                print("topic mock_id ", mock_id)
                # mock_exams=''
                # Loop through the category IDs and create records
                for category_id in exam_category_ids:
                    mock_exams.objects.create(
                        mock_test_id=mock_id,
                        exam_category_id=category_id
                    )
            else:
                mock = Mock_Test.objects.create(
                    mock_title=mock_title,
                    mock_type=mock_type,
                    # series_id=series_id,
                    # section_id=section_id,
                    # sub_section_id=sub_section_id,
                    # topic_id=topic_id,
                    examcategory_id=exam_category_id,
                    exam_master_id=exam_master_id,
                    question_ids=json.dumps([]),
                    mock_level=mock_level,
                    total_questions=total_questions,
                    total_marks=total_marks,
                    # pass_per=pass_per,
                    negative_marks=negative_marks,
                    status=status,
                    test_date=test_date,
                    test_time=test_time,
                    is_accessible=is_accessible,
                    mock_duration=mock_duration,
                    section_wise_timer=section_wise_timer,
                    section_switching=section_switching,
                    question_type=question_type,
                    calculator=calculator

                )
            mock.send_promotional_message = send_promotional_message
            mock.save(update_fields=["send_promotional_message"])
            id= mock.id;
                            
            # Loop through the category IDs and create records
            for category_id in exam_category_ids:
                mock_exams.objects.create(
                    mock_test_id=id,
                    exam_category_id=category_id
                )
            # print("mock id =",id)

            # print("section_wise_timer valu=",section_wise_timer)

          
            # if section_wise_timer == '1':
            for i in range(len(section_ids)):
                if section_durations[i]=='':
                    section_durations[i]=0
                # Ensure you're passing a single value, not a list
                mock_sections.objects.create(
                    mock_test_id=id,
                    section_id=section_ids[i],  # Pass individual section_id
                    duration=section_durations[i],  # Pass individual duration
                )



                

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Mock Test saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
 

@login_required(login_url=ADMIN_LOGIN_URL)
def add_question_store(request):
    test_id= request.GET.get('test_id')
    if request.method == 'POST':
        # Get the form data
        
        section_id = request.POST.get('section_id')
        question_id = request.POST.get('question_id')
        sub_section_id = request.POST.get('sub_section_id')
        topic = request.POST.get('topic')
        test_id= request.POST.get('test_id')
        question_type = request.POST.get('question_type')
        difficulty_level = request.POST.get('difficulty_level')
        time_to_speed = request.POST.get('time_to_speed')
        mark = request.POST.get('mark')
        negative_mark = request.POST.get('negative_mark')
        question = request.POST.get('question')
        tag = request.POST.get('tag')
        correct = request.POST.get('correct')
        paragraph = request.POST.get('paragraph')
        explanation = request.POST.get('editor2')
        # status = request.POST.get('status')
        question_file = request.FILES.get('question_file')
        pquestion_id = request.POST.get('pquestion_id')
        pparagraph = request.POST.get('pparagraph')
        option_1 = request.POST.get('option_1')
        option_2 = request.POST.get('option_2')
        option_3 =request.POST.get('option_3')
        option_4 = request.POST.get('option_4')
        option_5 = request.POST.get('option_5')
        correct_answer = request.POST.get('correct_answer')
        options_data = []
        # Create option objects and append them to the options_data list
        if option_1:
            options_data.append(option_1)
        if option_2:
            options_data.append(option_2)
        if option_3:
            options_data.append(option_3)
        if option_4:
            options_data.append(option_4)
        if option_5:
            options_data.append(option_5)
        # image = request.FILES.get('image')
        # question_id=0
        if(section_id==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Section name.'})
        if(question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter question.'})
        # elif(status==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            if question_type=='mcq':
                if option_1=='' and option_2=='' and option_3=='' and option_4=='' and option_5=='':
                    return JsonResponse({'status': 'error', 'msg': 'Please enter atleast 2 options.'})
            correct_a = correct
            if question_id:
                if question_type!='oneliner':
                    correct_a = correct_answer
                section = Question_Bank.objects.filter(id=question_id).update(
                    parent_question_id=0,
                    section_id=section_id,
                    sub_section_id=sub_section_id,
                    topic_id=topic,
                    paragraph=paragraph,
                    tag=tag,
                    difficulty_level=difficulty_level,
                    time_to_speed=time_to_speed,
                    question=question,
                    explanation=explanation,
                    question_file=question_file,
                    status='Active',
                    marks=mark,
                    negative_marks=negative_mark,
                    options=json.dumps(options_data),
                    correct_answer=correct_a,
                    is_edited=1
                )
                return JsonResponse({'status': 'success', 'msg': 'Question data updated successfully.', 'question_id':question_id, 'paragraph':paragraph,'test_id':test_id, 'question_id':question_id})
            else:
                if question_type!='oneliner':
                    correct_a = correct_answer
                section = Question_Bank.objects.create(
                    parent_question_id=0,
                    section_id=section_id,
                    sub_section_id=sub_section_id,
                    topic_id=topic,
                    test_id=test_id,
                    tag=tag,
                    question_type=question_type,
                    paragraph=paragraph,
                    difficulty_level=difficulty_level,
                    time_to_speed=time_to_speed,
                    question=question,
                    explanation=explanation,
                    question_file=question_file,
                    status='Active',
                    marks=mark,
                    negative_marks=negative_mark,
                    options=json.dumps(options_data),
                    correct_answer=correct_a
                )
                return JsonResponse({'status': 'success', 'msg': 'Question data added successfully.', 'question_id':question_id, 'paragraph':paragraph,'test_id':test_id, 'type':'create'})
    # if question_type=='paragraph':
            #     # if paragraph!=pparagraph:
            #     #     pquestion_id=0
            #     if pquestion_id:
            #         section = Question_Bank.objects.create(
            #             parent_question_id=pquestion_id,
            #             section_id=section_id,
            #             sub_section_id=sub_section_id,
            #             topic_id=topic,
            #             test_id=test_id,
            #             question_type=question_type,
            #             paragraph=paragraph,
            #             difficulty_level=difficulty_level,
            #             time_to_speed=time_to_speed,
            #             question=question,
            #             explanation=explanation,
            #             question_file=question_file,
            #             # status=status,
            #             marks=mark,
            #             negative_marks=negative_mark,
            #             options=json.dumps(options_data),
            #             correct_answer=correct_answer
            #         )
            #         question_id = section.id
            #     else:
            #         section = Question_Bank.objects.create(
            #             section_id=section_id,
            #             sub_section_id=sub_section_id,
            #             topic_id=topic,
            #             test_id=test_id,
            #             question_type=question_type,
            #             paragraph=paragraph,
            #             difficulty_level=difficulty_level,
            #             time_to_speed=time_to_speed,
            #             question=question,
            #             explanation=explanation,
            #             question_file=question_file,
            #             # status=status,
            #             marks=mark,
            #             negative_marks=negative_mark,
            #             options=json.dumps(options_data),
            #             correct_answer=correct_answer
            #         )
            #         pquestion_id=section.id
            #         subquestion = Question_Bank.objects.create(
            #             parent_question_id=section.id,
            #             section_id=section_id,
            #             sub_section_id=sub_section_id,
            #             topic_id=topic,
            #             test_id=test_id,
            #             question_type=question_type,
            #             paragraph=paragraph,
            #             difficulty_level=difficulty_level,
            #             time_to_speed=time_to_speed,
            #             question=question,
            #             explanation=explanation,
            #             question_file=question_file,
            #             # status=status,
            #             marks=mark,
            #             negative_marks=negative_mark,
            #             options=json.dumps(options_data),
            #             correct_answer=correct_answer
            #         )
            #         question_id = subquestion.id
            # else:
            #     section = Question_Bank.objects.create(
            #         section_id=section_id,
            #         sub_section_id=sub_section_id,
            #         topic_id=topic,
            #         test_id=test_id,
            #         question_type=question_type,
            #         paragraph=paragraph,
            #         difficulty_level=difficulty_level,
            #         time_to_speed=time_to_speed,
            #         question=question,
            #         explanation=explanation,
            #         question_file=question_file,
            #         # status=status,
            #         marks=mark,
            #         negative_marks=negative_mark,
            #         options=json.dumps(options_data),
            #         correct_answer=correct_answer
            #     )
            #     question_id = section.id
            #     pparagraph=''
            #     pquestion_id=''
            
            # Save the object to the database
            # section.save()

    else:
        # Return an error response
        return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def oadd_question_store(request):
    test_id= request.GET.get('test_id')
    if request.method == 'POST':
        # Get the form data
        
        section_id = request.POST.get('section_id')
        question_id = request.POST.get('question_id')
        sub_section_id = request.POST.get('sub_section_id')
        topic = request.POST.get('topic')
        test_id= request.POST.get('test_id')
        question_type = request.POST.get('question_type')
        difficulty_level = request.POST.get('difficulty_level')
        time_to_speed = request.POST.get('time_to_speed')
        mark = request.POST.get('mark')
        negative_mark = request.POST.get('negative_mark')
        question = request.POST.get('question')
        correct = request.POST.get('correct')
        paragraph = request.POST.get('paragraph')
        explanation = request.POST.get('editor2')
        # status = request.POST.get('status')
        question_file = request.FILES.get('question_file')
        pquestion_id = request.POST.get('pquestion_id')
        pparagraph = request.POST.get('pparagraph')
        option_1 = request.POST.get('option_1')
        option_2 = request.POST.get('option_2')
        option_3 =request.POST.get('option_3')
        option_4 = request.POST.get('option_4')
        option_5 = request.POST.get('option_5')
        correct_answer = request.POST.get('correct_answer')
        options_data = []
        # Create option objects and append them to the options_data list
        if option_1:
            options_data.append(option_1)   
        if option_2:
            options_data.append(option_2)   
        if option_3:
            options_data.append(option_3)    
        if option_4:
            options_data.append(option_4)
        if option_5:
            options_data.append(option_5)   
        # image = request.FILES.get('image')
        # question_id=0
        if(section_id==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Section name.'})
        if(question==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter question.'})
        # elif(status==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else: 
            if question_type=='mcq':
                if option_1=='' and option_2=='' and option_3=='' and option_4=='' and option_5=='':
                    return JsonResponse({'status': 'error', 'msg': 'Please enter atleast 2 options.'})
            correct_a = correct
            if question_id:
                if question_type!='oneliner':
                    correct_a = correct_answer
                section = Question_Bank.objects.filter(id=question_id).update(
                    parent_question_id=0,
                    section_id=section_id,
                    sub_section_id=sub_section_id,
                    topic_id=topic,
                    paragraph=paragraph,
                    difficulty_level=difficulty_level,
                    time_to_speed=time_to_speed,
                    question=question,
                    explanation=explanation,
                    question_file=question_file,
                    status='Active',
                    marks=mark,
                    negative_marks=negative_mark,
                    options=json.dumps(options_data),
                    correct_answer=correct_a,
                    is_edited=1
                )
                return JsonResponse({'status': 'success', 'msg': 'Question data updated successfully.', 'question_id':question_id, 'paragraph':paragraph,'test_id':test_id, 'question_id':question_id})
            else:
                if question_type!='oneliner':
                    correct_a = correct_answer
                section = Question_Bank.objects.create(
                    parent_question_id=0,
                    section_id=section_id,
                    sub_section_id=sub_section_id,
                    topic_id=topic,
                    test_id=test_id,
                    question_type=question_type,
                    paragraph=paragraph,
                    difficulty_level=difficulty_level,
                    time_to_speed=time_to_speed,
                    question=question,
                    explanation=explanation,
                    question_file=question_file,
                    status='Active',
                    marks=mark,
                    negative_marks=negative_mark,
                    options=json.dumps(options_data),
                    correct_answer=correct_a
                )
                return JsonResponse({'status': 'success', 'msg': 'Question data added successfully.', 'question_id':question_id, 'paragraph':paragraph,'test_id':test_id, 'type':'create'})
    # if question_type=='paragraph':
            #     # if paragraph!=pparagraph:
            #     #     pquestion_id=0
            #     if pquestion_id:
            #         section = Question_Bank.objects.create(
            #             parent_question_id=pquestion_id,
            #             section_id=section_id,
            #             sub_section_id=sub_section_id,
            #             topic_id=topic,
            #             test_id=test_id,
            #             question_type=question_type,
            #             paragraph=paragraph,
            #             difficulty_level=difficulty_level,
            #             time_to_speed=time_to_speed,
            #             question=question,
            #             explanation=explanation,
            #             question_file=question_file,
            #             # status=status,
            #             marks=mark,
            #             negative_marks=negative_mark,
            #             options=json.dumps(options_data),
            #             correct_answer=correct_answer
            #         )
            #         question_id = section.id
            #     else:
            #         section = Question_Bank.objects.create(
            #             section_id=section_id,
            #             sub_section_id=sub_section_id,
            #             topic_id=topic,
            #             test_id=test_id,
            #             question_type=question_type,
            #             paragraph=paragraph,
            #             difficulty_level=difficulty_level,
            #             time_to_speed=time_to_speed,
            #             question=question,
            #             explanation=explanation,
            #             question_file=question_file,
            #             # status=status,
            #             marks=mark,
            #             negative_marks=negative_mark,
            #             options=json.dumps(options_data),
            #             correct_answer=correct_answer
            #         )
            #         pquestion_id=section.id
            #         subquestion = Question_Bank.objects.create(
            #             parent_question_id=section.id,
            #             section_id=section_id,
            #             sub_section_id=sub_section_id,
            #             topic_id=topic,
            #             test_id=test_id,
            #             question_type=question_type,
            #             paragraph=paragraph,
            #             difficulty_level=difficulty_level,
            #             time_to_speed=time_to_speed,
            #             question=question,
            #             explanation=explanation,
            #             question_file=question_file,
            #             # status=status,
            #             marks=mark,
            #             negative_marks=negative_mark,
            #             options=json.dumps(options_data),
            #             correct_answer=correct_answer
            #         )
            #         question_id = subquestion.id
            # else: 
            #     section = Question_Bank.objects.create(
            #         section_id=section_id,
            #         sub_section_id=sub_section_id,
            #         topic_id=topic,
            #         test_id=test_id,
            #         question_type=question_type,
            #         paragraph=paragraph,
            #         difficulty_level=difficulty_level,
            #         time_to_speed=time_to_speed,
            #         question=question,
            #         explanation=explanation,
            #         question_file=question_file,
            #         # status=status,
            #         marks=mark,
            #         negative_marks=negative_mark,
            #         options=json.dumps(options_data),
            #         correct_answer=correct_answer
            #     )
            #     question_id = section.id
            #     pparagraph=''
            #     pquestion_id=''
            
            # Save the object to the database
            # section.save()

    else:
        # Return an error response
        return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def upload_excel(request):
    if request.method == 'POST':

        # excel_file = request.FILES["excel_file"]
        excel_file = request.FILES.get('excel_file')
        print(excel_file)
        # you may put validations here to check extension or file size

        wb = openpyxl.load_workbook(excel_file)

        # getting a particular sheet by name out of many sheets
        worksheet = wb["Sheet1"]
        print(worksheet)

        excel_data = list()
        # iterating over the rows and
        # getting value from each cell in row
        for row in worksheet.iter_rows():
            row_data = list()
            # print(row)
            for cell in row:
                print(cell.value)
                row_data.append(str(cell.value))
            excel_data.append(row_data)
            # print(row_data)
            return HttpResponse('<h2> form submitted.</h2>')
        # data = serializers.serialize('xml/json', excel_data)    
        # print(data)
        # Get the form data
        # section_id = request.POST.get('section_id')
        # sub_section_id = request.POST.get('sub_section_id')
        # topic = request.POST.get('topic')
        # question_type = request.POST.get('question_type')
        # difficulty_level = request.POST.get('difficulty_level')
        # time_to_speed = request.POST.get('time_to_speed')
        # question = request.POST.get('editor1')
        # explanation = request.POST.get('editor2')
        # status = request.POST.get('status')
        # question_file = request.FILES.get('question_file')
        # # image = request.FILES.get('image')
        # if(section_id==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please enter Section name.'})
        # elif(status==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        # else:    
        #     # Create a new ExamCategory object
        #     section = Question_Bank(
        #         section_id=section_id,
        #         sub_section_id=sub_section_id,
        #         topic_id=topic,
        #         question_type=question_type,
        #         difficulty_level=difficulty_level,
        #         time_to_speed=time_to_speed,
        #         question=question,
        #         explanation=explanation,
        #         question_file=question_file,
        #         status=status
        #     )

        #     # Save the object to the database
        #     section.save()

            # Return a success response
        # return JsonResponse(worksheet)
            # return JsonResponse({'status': 'success', 'msg': 'Exam Section saved successfully.'})
    else:
        # Return an error response
        return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
  
@login_required(login_url=ADMIN_LOGIN_URL)
def edit_mock_test(request, mock_id):
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    all_categories = ExamCategory.objects.all()
    all_exam = Exam_Master.objects.all()
    # series_id = request.GET.get('series_id')
    mock_type = request.GET.get('type')
    # Fetch the mock test object based on the ID
    mock = get_object_or_404(Mock_Test, id=mock_id)
    # Fetch associated sections
    mock_section = mock_sections.objects.filter(mock_test=mock)
    mock_exam = mock_exams.objects.filter(mock_test=mock)

    # Extract category IDs
    selected_exam_ids = list(mock_exam.values_list('exam_category_id', flat=True))

    # print(mock_type)
    return render(request, "edit_mock.html",{'mock':mock,'mock_sections':mock_section,'mock_exams':mock_exam,'all_categories':all_categories,'section_list':section_list,'Sub_Sections_list':Sub_Sections_list,'Topics_list':Topics_list, 'all_exam':all_exam, 'mock_type': mock_type,'selected_exam_ids':selected_exam_ids})
  
@login_required(login_url=ADMIN_LOGIN_URL)
def edit_mock(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('mock_id')
        mock_title = request.POST.get('mock_title')
        mock_type = request.POST.get('mock_type')
        # print(mock_type ,"mock_type")
        # exam_category = request.POST.get('exam_category')
        exam_category_id = request.POST.get('exam_category_id')
        section_wise_timer = request.POST.get('section_wise_timer')
        # print("section_wise_timer",section_wise_timer)
        section_switching = request.POST.get('section_switching')
        section_id = request.POST.get('section_id')
        sub_section_id = request.POST.get('sub_section_id')
        topic_id = request.POST.get('topic_id')
        total_questions = request.POST.get('total_questions')
        total_marks = request.POST.get('total_marks')
        mock_level = request.POST.get('mock_level')
        mock_duration = request.POST.get('mock_duration')
        pass_per = request.POST.get('pass_per')
        is_accessible = request.POST.get('is_accessible')
        send_promotional_message = request.POST.get('send_promotional_message') == '1'
        test_date = request.POST.get('test_date')
        test_time = request.POST.get('test_time')
        negative_marks = request.POST.get('negative_marks')
        status = request.POST.get('status')
        question_type = request.POST.get('question_type')
        exam_category_ids = request.POST.getlist('exam_category_ids')
        # print("exam_category_ids",exam_category_ids)
        section_ids = request.POST.getlist('section_ids[]')
        # print("section_ids",section_ids)
        calculator = request.POST.get('calculator')
        section_durations = request.POST.getlist('section_durations[]')
        if(mock_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Section name.'})
        elif(total_questions==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter total questions.'})
        elif(exam_category_id==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select Exam Category.'})
        # elif(section_id==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please Select Section.'})
        # elif(sub_section_id==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please Select Sub-section.'})
        elif(total_marks==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter total marks.'})
        elif(mock_duration==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter mock duration.'})
        elif(pass_per==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter pass Percentage.'})
        elif(is_accessible==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter accessible number.'})
        # elif(test_date==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select test date.'})
        elif(negative_marks==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter negative marks.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
                 # Create a new ExamCategory object
            mock = Mock_Test(
                id=id,
                mock_title=mock_title,
                mock_type=mock_type,
                # examcategory_id=exam_category,
                section_id=section_id,
                sub_section_id=sub_section_id,
                topic_id=topic_id,
                examcategory_id=exam_category_id,
                total_questions=total_questions,
                total_marks=total_marks,
                mock_level='Easy',
                # pass_per=pass_per,
                is_accessible=is_accessible,
                send_promotional_message=send_promotional_message,
                test_date=test_date,
                test_time=test_time,
                negative_marks=negative_marks,
                status=status,
                mock_duration=mock_duration,
                section_wise_timer=section_wise_timer,
                section_switching=section_switching,
                calculator=calculator,
                question_type=question_type
            )

            mock_type = mock.mock_type
            # print("mock_type ", mock_type)
            # mock_exams=''
            mock_exams.objects.filter(mock_test_id=id).delete()
            for category_id in exam_category_ids:
                mock_exams.objects.create(
                        mock_test_id=id,
                        exam_category_id=category_id
                    )
            if mock_type== '6' or mock_type== '7' :
                mock.save(update_fields=["mock_title","mock_type","total_questions","total_marks","mock_level","is_accessible","send_promotional_message","status","mock_duration","section_id","sub_section_id","topic_id"])

            elif mock_type== '3' or mock_type== '4' or mock_type== '5' or mock_type== '8' or mock_type== '9':
                mock_sections.objects.filter(mock_test_id=id).delete()
                for i in range(len(section_ids)):
                    # Ensure you're passing a single value, not a list
                    mock_sections.objects.create(
                        mock_test_id=id,
                        section_id=section_ids[i],  # Pass individual section_id
                        duration=section_durations[i] if section_durations[i] else 0,  # Pass individual duration
                    )
                mock.save(update_fields=["mock_title","mock_type","total_questions","total_marks","mock_level","negative_marks","is_accessible","send_promotional_message","test_date","status","mock_duration","section_id","sub_section_id","section_wise_timer","section_switching","calculator","question_type"])
            # Save the object to the database
            else:
                mock.save(update_fields=["mock_title","mock_type","total_questions","total_marks","mock_level","negative_marks","is_accessible","send_promotional_message","test_date","status","mock_duration","examcategory_id","section_id","sub_section_id"])
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Mock Test Updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def upload_lead_excel(request):
    if request.method == 'POST' and request.FILES['excelFile']:
        excel_file = request.FILES['excelFile']
        # Load the Excel file
        wb = openpyxl.load_workbook(excel_file)
        sheet = wb.active
        leads_data = []
        # Iterate over the rows in the Excel file
        for row in sheet.iter_rows(min_row=2, values_only=True):
            first_name = row[0]
            last_name = row[1]
            email = row[2]
            mobile = row[3]
            location = row[4]
            lead_type = row[5]
            source = row[6]
            remark = row[7]
            #lead_date = row[8]
            status = 'New'
            # options = row[12:]
            # Create a question object and append it to the questions_data list
            lead_obj = Leads(first_name=first_name,
                last_name=last_name,
                email=email,
                mobile=mobile,
                source=source,
                remark=remark,
                address=location,
                status=status)
            leads_data.append(lead_obj)
        # Bulk insert the questions data
        Leads.objects.bulk_create(leads_data)
    return JsonResponse({'status': 'success', 'msg': 'Leads uploaded successfully.'})
 
@login_required(login_url=ADMIN_LOGIN_URL)   
def delete_mock(request):  
    id = request.POST.get('id')
    Mock_Test(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'Mock Test deleted successfully.'})

# =============================start student module ===============================

@login_required(login_url=ADMIN_LOGIN_URL)
def Students_list(request):
    msg = ''
    if request.method == 'POST':
        OrderCourses.objects.create(
            student_id=request.POST.get('student_id'),
            batch_id=request.POST.get('batch_id'),
            area_mocks=0,
            sectional_mocks=0,
            mini_mocks=0,
            full_mocks=0,
            lectures=1
        )
        msg = "Batch assigned successfully."

    all_batches = Batch_Management.objects.filter(status='Active')
    student_list = Students.objects.filter(is_lead=0).order_by('-id')

    # Filters
    name = request.GET.get('name')
    number = request.GET.get('number')
    status = request.GET.get('status')
    batch = request.GET.get('batch')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    filter = ''

    if name:
        student_list = student_list.annotate(
            fullname=Concat('first_name', Value(' '), 'last_name')
        ).filter(fullname__icontains=name)
        filter += f'&name={name}'
    if number:
        student_list = student_list.filter(mobile__icontains=number)
        filter += f'&number={number}'
    if status:
        student_list = student_list.filter(status=status)
        filter += f'&status={status}'
    if batch:
        student_list = student_list.filter(ordercourses__batch_id=batch,ordercourses__lectures=1)
        filter += f'&batch={batch}'
    if from_date:
        student_list = student_list.filter(created_at__gte=from_date)
        filter += f'&from_date={from_date}'
    if to_date:
        student_list = student_list.filter(created_at__lte=to_date)
        filter += f'&to_date={to_date}'
    # Export Excel
    if request.GET.get('export') == 'excel':

        wb = openpyxl.Workbook()
        ws = wb.active
        ws.title = "Students"

        headers = [
            'SR No',
            'Name',
            'Name on Aadhar',
            'Email',
            'Mobile',
            'Whatsapp Number',
            'Aadhar Number',
            'Batches',
            'Courses',
            'Added Date'
        ]

        ws.append(headers)

        for index, student in enumerate(student_list, start=1):

            # batches
            batch_names = list(
                student.batch.filter(
                    ordercourses__student=student,
                    ordercourses__lectures=1
                ).values_list('batch_name', flat=True)
            )

            # courses
            course_ids = OrderCourses.objects.filter(
                student=student
            ).values_list('course_id', flat=True).distinct()

            course_names = list(
                Course.objects.filter(
                    id__in=course_ids
                ).values_list('course_name', flat=True)
            )

            ws.append([
                index,
                f"{student.first_name} {student.last_name}",
                student.aadhar_name,
                student.email,
                student.mobile,
                student.whatsapp_number,
                student.aadhar_number,
                ", ".join(batch_names) if batch_names else '-',
                ", ".join(course_names) if course_names else '-',
                str(student.created_at),
            ])

        # Optional Styling
        for cell in ws[1]:
            cell.font = openpyxl.styles.Font(bold=True)

        response = HttpResponse(
            content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
        )

        response['Content-Disposition'] = 'attachment; filename=students.xlsx'

        wb.save(response)

        return response
    paginator = Paginator(student_list, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    search = request.GET.get('search', '')

    # Collect batches + courses for each student
    student_batches = {}
    student_courses = {}

    for student in page_obj:
        # batches (ManyToMany direct relation)
        batch_names = list(student.batch.filter(ordercourses__student=student, ordercourses__lectures=1).values_list('batch_name', flat=True))
        student.batches = batch_names if batch_names else []

        # courses (through OrderCourses)
        course_ids = OrderCourses.objects.filter(student=student).values_list('course_id', flat=True).distinct()
        course_names = list(Course.objects.filter(id__in=course_ids).values_list('course_name', flat=True))
        student.courses = course_names if course_names else []

    return render(
        request,
        "student_module.html",
        {
            'msg': msg,
            'all_batches': all_batches,
            'student_list': page_obj,
            'filter': filter,
            'search': search,
            'name': name,
            'number': number,
            'status': status,
            'batch': batch,
            'from_date': from_date,
            'to_date': to_date,
            'student_batches': student_batches,
            'student_courses': student_courses,
        }
    )
    
    
@login_required(login_url=ADMIN_LOGIN_URL)
def batch_students(request, id):
    try:
        batch = Batch_Management.objects.get(id=id)
    except Batch_Management.DoesNotExist:
        return render(request, "batch-students.html", {"error": "Batch not found"})

    student_list = OrderCourses.objects.filter(
        batch_id=id,
        lectures=1,
    ).select_related('student', 'course').order_by('student__first_name', 'student__last_name')
    all_courses = Course.objects.filter(status='Active').order_by('course_name')
    exam_category = batch.exam_category
    # Subquery to get no_of_lectures from BatchTopicTiming
    lecture_subquery = BatchTopicTiming.objects.filter(
        batch=batch,
        topic=OuterRef('topic')
    ).values('no_of_lectures')[:1]

    exam_syllabus = ExamSyllabus.objects.filter(exam_category=exam_category).select_related(
        'section', 'sub_section', 'topic'
    ).annotate(
        no_of_lectures=Coalesce(Subquery(lecture_subquery, output_field=IntegerField()), 0)
    )

    all_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

    if batch.start_date and batch.end_date:
        batch_months = get_month_range(batch.start_date, batch.end_date)
    else:
        batch_months = []
        
    # Query caps for this batch across those months
    caps_qs = (
        MonthlyLectureCount.objects
        .filter(batch=batch, month__in=batch_months)
        .values('month')
        .annotate(no_of_lectures=Sum('no_of_lectures'))   # Sum in case duplicates exist
    )

    # Build a month->count dict (default 0 where missing)
    monthly_caps = {}
    for row in caps_qs:
        monthly_caps[str(row['month'])] = ( row['no_of_lectures'] or 0 )
    rows = [ { "month": str(m), "count": monthly_caps.get(str(m), 0) } for m in batch_months ]
    # Retrieve time slots for each day
    time_slots_list = [(day, BatchTimeSlot.objects.filter(day=day,batch=id)) for day in all_days]

    # Handle form submission
    if request.method == "POST":
        for topic_id, lectures in request.POST.items():
            if topic_id.startswith("lectures_"):  # Ensure correct input names
                topic_id = int(topic_id.replace("lectures_", ""))
                no_of_lectures = int(lectures) if lectures.isdigit() else 0
                
                # Update or create the batch-topic lecture entry
                BatchTopicTiming.objects.update_or_create(
                    batch=batch, topic_id=topic_id,
                    defaults={"no_of_lectures": no_of_lectures}
                )

        return redirect(request.path)  # Refresh page after saving
        

    return render(request, "batch-students.html", {
        "student_list": student_list,
        "exam_category": exam_category,
        "exam_syllabus": exam_syllabus,
        "batch": batch,
        "time_slots_list": time_slots_list,
        "batch_months": batch_months,
        "monthly_rows": rows,
        "all_courses": all_courses,
    })


@login_required(login_url=ADMIN_LOGIN_URL)
def get_course_students_for_batch(request):
    course_id = request.GET.get('course_id')
    batch_id = request.GET.get('batch_id')
    search = request.GET.get('q', '').strip()

    if not course_id or not batch_id:
        return JsonResponse({
            'status': 'error',
            'msg': 'Course and batch are required.',
            'students': [],
        }, status=400)

    enrolled_student_ids = OrderCourses.objects.filter(
        course_id=course_id,
    ).values_list('student_id', flat=True)

    assigned_student_ids = OrderCourses.objects.filter(
        course_id=course_id,
        batch_id=batch_id,
        lectures=1,
    ).values_list('student_id', flat=True)

    students = Students.objects.filter(
        id__in=enrolled_student_ids,
        is_lead=0,
    ).exclude(id__in=assigned_student_ids).order_by('first_name', 'last_name')

    if search:
        students = students.filter(
            Q(first_name__icontains=search)
            | Q(last_name__icontains=search)
            | Q(email__icontains=search)
            | Q(mobile__icontains=search)
        )

    data = [
        {
            'id': student.id,
            'name': f'{student.first_name or ""} {student.last_name or ""}'.strip(),
            'email': student.email or '',
            'mobile': student.mobile or '',
        }
        for student in students
    ]

    return JsonResponse({'status': 'success', 'students': data})


@login_required(login_url=ADMIN_LOGIN_URL)
def bulk_assign_batch_students(request, id):
    if request.method != 'POST':
        return JsonResponse({
            'status': 'error',
            'msg': 'Invalid request method.',
        }, status=405)

    batch = get_object_or_404(Batch_Management, id=id)
    course_id = request.POST.get('course_id')
    student_ids = list(dict.fromkeys(request.POST.getlist('student_ids')))

    if not course_id:
        return JsonResponse({'status': 'error', 'msg': 'Please select a course.'}, status=400)

    if not student_ids:
        return JsonResponse({'status': 'error', 'msg': 'Please select at least one student.'}, status=400)

    course = Course.objects.filter(id=course_id).first()
    if not course:
        return JsonResponse({'status': 'error', 'msg': 'Selected course was not found.'}, status=404)

    enrolled_ids = set(
        OrderCourses.objects.filter(
            course=course,
            student_id__in=student_ids,
        ).values_list('student_id', flat=True)
    )
    if any(not str(student_id).isdigit() for student_id in student_ids):
        return JsonResponse({'status': 'error', 'msg': 'Invalid student selection.'}, status=400)

    requested_ids = {int(student_id) for student_id in student_ids}

    if enrolled_ids != requested_ids:
        return JsonResponse({
            'status': 'error',
            'msg': 'One or more selected students are not enrolled in this course.',
        }, status=400)

    assigned_count = 0

    with transaction.atomic():
        for student_id in requested_ids:
            assignment = OrderCourses.objects.filter(
                student_id=student_id,
                course=course,
                batch=batch,
            ).first()

            if assignment:
                if assignment.lectures != 1:
                    assignment.lectures = 1
                    assignment.save(update_fields=['lectures', 'updated_at'])
                    assigned_count += 1
                continue

            assignment = OrderCourses.objects.filter(
                student_id=student_id,
                course=course,
                batch__isnull=True,
            ).order_by('-id').first()

            if assignment:
                assignment.batch = batch
                assignment.lectures = 1
                assignment.save(update_fields=['batch', 'lectures', 'updated_at'])
            else:
                OrderCourses.objects.create(
                    student_id=student_id,
                    course=course,
                    batch=batch,
                    lectures=1,
                )

            assigned_count += 1

    return JsonResponse({
        'status': 'success',
        'msg': f'{assigned_count} student(s) assigned to the batch for lectures.',
        'assigned_count': assigned_count,
    })

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_student(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('student_id')
        first_name = request.POST.get('fname')
        last_name = request.POST.get('lname')
        email = request.POST.get('email')
        mobile = request.POST.get('contact')
        address = request.POST.get('address')
        city = request.POST.get('city')
        state = request.POST.get('state')
        pincode = request.POST.get('pincode')
        whatsapp_number = request.POST.get('whatsapp_number')
        if(first_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter First name.'})
        else:    
           
                 # Create a new ExamCategory object
            mock = Students(
                id=id,
                first_name=first_name,
                last_name=last_name,
                email=email,
                mobile=mobile,
                address=address,
                city=city,
                state=state,
                pincode=pincode,
                whatsapp_number=whatsapp_number
            )

            # Save the object to the database
            mock.save(update_fields=["first_name","last_name","email","mobile","address","city","state","pincode","whatsapp_number"])
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Student Updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_student(request):  
   
    id = request.POST.get('id')    
    Students(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'Student deleted successfully.'})
 
@login_required(login_url=ADMIN_LOGIN_URL)
def delete_lead(request):  
   
    id = request.POST.get('id')    
    Leads(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'Lead deleted successfully.'})
 
@login_required(login_url=ADMIN_LOGIN_URL)
def block_student(request):  
   
    # id = request.GET.get('id')  
    id = request.POST.get('student_id')
    student_list = Students.objects.get(id=id)
    remark = request.POST.get('remark')
    if student_list.status == 'Active':
        status = 'Inactive' 
    else:
        status = 'Active' 
    student = Students(
                id=id,
                remark=remark,
                status= status
            )
    student.save(update_fields=["remark","status"])
            # Return a success response
    return JsonResponse({'status': 'success', 'msg': 'Student Updated successfully.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def search_student(request):
    search = request.GET.get('search')
    dates = request.GET.get('dates')
    source = request.GET.get('dates')
    # student_list = Students.objects.filter( is_lead=1 ,first_name__startswith=search)

    student_list = Students.objects.filter( is_lead=1 ,source__startswith=search)

    # if search != '' and  dates!='':
    #     student_list = Students.objects.filter( is_lead=0,first_name__startswith=search,
    #     source__startswith=search,created_at=datetime.date(dates))
    # elif search != '' and  dates=='':
    #      student_list = Students.objects.filter( is_lead=0,first_name__startswith=search, source__startswith=search)
    # elif search == '' and  dates!='':
    #      student_list = Students.objects.filter( is_lead=0,created_at=datetime.date(dates))
    return render(request, "search_student.html" ,{'student_list':student_list})

@login_required(login_url=ADMIN_LOGIN_URL)
def Student_Profile(request,id):
    student_list = Students.objects.get(id=id)
    return render(request, "student_profile.html",{'student_list':student_list})
# =================================end student module ========================


# ==================== start lead management =============================

@login_required(login_url=ADMIN_LOGIN_URL)
def Lead_Management(request):
    all_source=Lead_sourse.objects.all()
    all_status=Lead_status.objects.all()
    lead_type=request.GET.get('lead_type')
    name=request.GET.get('name')
    number=request.GET.get('number')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    source=request.GET.get('source')
    status=request.GET.get('status')
    filter = ''

    student_list = Leads.objects.filter().order_by('-id')
    if lead_type:
        student_list = student_list.filter(lead_type=lead_type)
        filter=filter+'&lead_type='+lead_type
    if name:
        student_list = student_list.annotate(fullname=Concat('first_name', Value(' '), 'last_name')).filter(fullname__icontains=name)
        filter=filter+'&name='+name
    if number:
        student_list = student_list.filter(mobile=number)
        filter=filter+'&number='+number
    if source:
        student_list = student_list.filter(source_id=source)
        filter=filter+'&source='+source
    if status:
        student_list = student_list.filter(status_id=status)
        filter=filter+'&status='+status
    if from_date:
        student_list = student_list.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        student_list = student_list.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
    # Batch_Management = Batch_Management.objects.all()
    paginator = Paginator(student_list, 10)  

    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "lead_management.html",{'student_list':page_obj,'page':page_number,'filter':filter,'source':source,'status':status,'from_date':from_date,'to_date':to_date,'lead_type':lead_type,'all_source':all_source,'all_status':all_status,'name':name,'number':number})

@login_required(login_url=ADMIN_LOGIN_URL)
def app_leads(request):
    student_list = Students.objects.order_by('-id').filter(order_courses__isnull=True).exclude(order_courses__is_order=0)
    # Batch_Management = Batch_Management.objects.all()
    if request.GET.get('search'):
        student_list= student_list.filter(mobile=request.GET.get('search'))
    paginator = Paginator(student_list, 50)

    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    search=''
    if request.GET.get('search'):
        search = request.GET.get('search')
    return render(request, "app-leads.html",{'student_list':page_obj, 'search':search})

@login_required(login_url=ADMIN_LOGIN_URL)
def Lead_View(request):
    student_list = Leads.objects.get(id=id)
    return render(request, "lead_view.html",{'student_list':student_list})

@login_required(login_url=ADMIN_LOGIN_URL)
def Lead_Student_Profile(request,id):
    today=date.today()
    student_list = Leads.objects.get(id=id)
    all_task=Lead_Followup.objects.filter(student_id=id)
    due_task =Lead_Followup.objects.filter(student_id=id,follwup_date__lt=today)
    upcoming_task =Lead_Followup.objects.filter(student_id=id,follwup_date__gte=today)

    all_comment=Lead_Comments.objects.filter(student_id=id)
    return render(request, "lead-student-profile.html",{'student_list':student_list ,'all_task':all_task,'all_comment':all_comment ,'due_task':due_task,'upcoming_task':upcoming_task})
  
@login_required(login_url=ADMIN_LOGIN_URL)
def addLead(request):
    all_source=Lead_sourse.objects.all()
    all_status=Lead_status.objects.all()
    return render(request, "add-lead.html",{'all_source':all_source,'all_status':all_status})

@login_required(login_url=ADMIN_LOGIN_URL)
def saveLead(request):
    if request.method == 'POST':
        # Get the form data
        first_name = request.POST.get('first_name')
        last_name = request.POST.get('last_name')
        email = request.POST.get('email')
        mobile = request.POST.get('mobile')
        lead_type = request.POST.get('lead_type')
        source = request.POST.get('source')
        status = request.POST.get('status')
        address = request.POST.get('location')
        remark = request.POST.get('remark')
        if(first_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter first name.'})
        elif(last_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter last name.'})
        elif(email==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter email address.'})
        elif(mobile==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter mobile number.'})
        elif(address==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter location.'})
        elif(source==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select source.'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        elif(remark==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter remark.'})
        else:    
            # Create a new ExamCategory object
            followup = Leads(
                first_name=first_name,
                last_name=last_name,
                email=email,
                mobile=mobile,
                lead_type=lead_type,
                source_id=source,
                remark=remark,
                address=address,
                status_id=status
            )

            # Save the object to the database
            followup.save()
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Lead Created successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
   

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_lead(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('student_id')
        first_name = request.POST.get('fname')
        last_name = request.POST.get('lname')
        email = request.POST.get('email')
        mobile = request.POST.get('contact')
        address = request.POST.get('address')
        source = request.POST.get('source')
        remark = request.POST.get('remark')
        status = request.POST.get('status')
        if(first_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter first name.'})
        elif(last_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter last name.'})
        elif(email==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter email address.'})
        elif(mobile==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter mobile number.'})
        elif(address==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter location.'})
        elif(source==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select source.'})
        elif(remark==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter remark.'})
        else:    
           
                 # Create a new ExamCategory object
            mock = Leads(
                id=id,
                first_name=first_name,
                last_name=last_name,
                email=email,
                mobile=mobile,
                address=address,
                remark=remark,
                status_id=status,
                source_id=source
            )

            # Save the object to the database
            mock.save(update_fields=["first_name","last_name","email","mobile","address","source_id","status_id","remark"])
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Lead Updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def addFollowUp(request):
    if request.method == 'POST':
        # Get the form data
        student_id = request.POST.get('id')
        task_title = request.POST.get('task_title')
        date = request.POST.get('date')
        time= request.POST.get('time')
        is_completed= request.POST.get('is_completed')
        if(task_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter task title.'})
        elif(date==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select date.'})
        elif(time==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select time.'})
        else:    
            # Create a new ExamCategory object
            followup = Lead_Followup(
                student_id=student_id,
                task_title=task_title,
                follwup_date=date,
                follwup_time=time,
                is_completed=is_completed
            )

            # Save the object to the database
            followup.save()
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'follow up task saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
    
@login_required(login_url=ADMIN_LOGIN_URL)
def Lead_followup(request):
    today=date.today()
    all_followup=Lead_Followup.objects.filter(follwup_date__gte=today).select_related('student')
    return render(request, "lead-followup.html",{'all_followup':all_followup, })

@login_required(login_url=ADMIN_LOGIN_URL)
def addComments(request):
    if request.method == 'POST':
        # Get the form data
        student_id = request.POST.get('id')
        message = request.POST.get('message')
       
        if(message==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter message.'})
       
        else:    
            # Create a new ExamCategory object
            comment = Lead_Comments(
                student_id=student_id,
                message=message,
               
            )

            # Save the object to the database
            comment.save()
            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'comment saved successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

# ======================== create course =================

@login_required(login_url=ADMIN_LOGIN_URL)
def courses(request):
    all_categories = ExamCategory.objects.select_related('exam_category')
    all_exams = Exam_Master.objects.select_related('exam_master')
    active_courses = Course.objects.filter(status='Active').order_by('-id')
    inactive_courses = Course.objects.filter(status='Inactive').order_by('-id')

    return render(request, "course-list.html",{'active_courses': active_courses,
        'inactive_courses': inactive_courses,'all_categories':all_categories,'all_exams':all_exams})

@login_required(login_url=ADMIN_LOGIN_URL)
def course_create(request):
    all_categories = ExamCategory.objects.all()
    all_exams = Exam_Master.objects.all()
    all_academics = AcademicYear.objects.all()
    batches = Batch_Management.objects.filter(status='Active')
    all_courses = Course.objects.all()
    if request.method == 'POST':
        # Process form data and create a new course
        course_id = request.POST.get('course_id')
        batch_id = request.POST.getlist('batch_id')
        course_name = request.POST.get('course_name')
        subtitle = request.POST.get('editor3')
        image = request.FILES.get('image')
        d_image = request.FILES.get('d_image')
        academic_id = request.POST.getlist('academic_id')
        exam_type_id = request.POST.getlist('exam_type')
        exam_masters = request.POST.getlist('exam_master')
        features = request.POST.get('editor2')
        meta_title = request.POST.get('meta_title')
        meta_description = request.POST.get('meta_description')
        slug = request.POST.get('slug')
        access=request.POST.getlist('access')

        duration = request.POST.get('duration')
        # batch_id = request.POST.get('batch_id')
        description = request.POST.get('editor1')

        area_price = request.POST.get('area_price')
        sectional_price = request.POST.get('sectional_price')
        minimock_price = request.POST.get('minimock_price')
        fullmock_price = request.POST.get('fullmock_price')
        lectures_price = request.POST.get('lectures_price')
        wp_link = request.POST.get('wp_link')
        full_price = request.POST.get('full_price')
        discount_price = request.POST.get('discount_price')
        display_order = request.POST.get('display_order')
        c_price = request.POST.get('c_price')
        course_type = request.POST.get('course_type')
        status = request.POST.get('status')

        course_ids = request.POST.getlist('course_id[]')
        prices = request.POST.getlist('price[]')
        course_name1 = request.POST.getlist('course_name[]')
        course_info = request.POST.getlist('course_info[]')
        discount_prices = request.POST.getlist('discount_price[]')
        is_verified_str = request.POST.get('is_verified', 'false')
        is_verified = True if is_verified_str == 'true' else False

        if(course_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course name'})
        elif(subtitle==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Enter Subtitle'})
        elif(exam_type_id==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select exam category'})
        # elif(exam_master_id==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select exam master'})
        elif(features==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course description'})
        elif(meta_title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course meta title'})
        elif(meta_description==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course meta description'})
        elif(slug==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course slug'})
        elif(duration==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course duration'})
        elif(description==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter course description'})
        elif(area_price==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter area price'})
        elif(sectional_price==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter sectional price'})
        elif(minimock_price==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter minimock price'})
        elif(fullmock_price==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter full mock price'})
        elif(lectures_price==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter lecture price'})
        elif(full_price==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter full price'})
        elif(c_price==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter counseling price'})
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:
            image = image      # thumb (already set via request.FILES or similar)
            d_image = d_image  # detail course_image

            # ---------- CREATE ----------
            if course_id == '':
                if image == '':
                    return JsonResponse({'status': 'error', 'msg': 'Please select thumbnail image'})
                if d_image == '':
                    return JsonResponse({'status': 'error', 'msg': 'Please select course detail image'})
                if image:
                    ext = os.path.splitext(image.name)[1]        # .jpg, .png, etc.
                    new_name = datetime.now().strftime("%Y%m%d%H%M%S") + ext
                    image.name = new_name
                if d_image:
                    ext = os.path.splitext(d_image.name)[1]
                    new_name = datetime.now().strftime("%Y%m%d%H%M%S") + "_detail" + ext
                    d_image.name = new_name
                course = Course.objects.create(
                    course_name=course_name,
                    subtitle=subtitle,
                    features=features,
                    image=image,
                    course_image=d_image,
                    wp_link=wp_link,
                    meta_title=meta_title,
                    meta_description=meta_description,
                    slug=slug,
                    access=access,
                    duration=duration,
                    description=description,
                    area_price=area_price,
                    sectional_price=sectional_price,
                    minimock_price=minimock_price,
                    fullmock_price=fullmock_price,
                    lectures_price=lectures_price,
                    full_price=full_price,
                    discount_price=discount_price,
                    course_type=course_type,
                    c_price=c_price,
                    display_order=display_order,
                    is_verified=is_verified,
                    status=status,
                )

                courseid = course.id

                # Batches
                for batch in batch_id:
                    Course_Batches.objects.create(
                        course_id=courseid,
                        batch_id=batch
                    )

                # Exams
                for exam in exam_type_id:
                    Course_Exams.objects.create(
                        course_id=courseid,
                        exam_id=exam
                    )

                for academic in academic_id:
                    Course_Academic.objects.create(
                        course_id=courseid,
                        academic_year_id=academic
                    )

                # Add-on courses
                for i in range(len(course_ids)):
                    Add_On_Courses_Course_Ids.objects.create(
                        add_on_course_id=courseid,
                        course_id=course_ids[i],
                        course_name=course_name1[i],
                        course_info=course_info[i],
                        price=prices[i],
                        discount_price=discount_prices[i],
                    )

                return JsonResponse({'status': 'success', 'msg': 'Course created successfully.'})

            # ---------- UPDATE ----------
            else:
                course = get_object_or_404(Course, pk=course_id)

                # Common fields
                course.course_name = course_name
                course.subtitle = subtitle
                course.features = features
                course.duration = duration
                course.wp_link=wp_link
                course.description = description
                course.meta_title = meta_title
                course.meta_description = meta_description
                course.slug = slug
                course.area_price = area_price
                course.sectional_price = sectional_price
                course.minimock_price = minimock_price
                course.fullmock_price = fullmock_price
                course.lectures_price = lectures_price
                course.full_price = full_price
                course.discount_price = discount_price
                course.display_order = display_order
                course.course_type = course_type
                course.c_price = c_price
                course.status = status
                course.access = access
                course.is_verified = is_verified

                    
                if image:
                    ext = os.path.splitext(image.name)[1]        # .jpg, .png, etc.
                    new_name = datetime.now().strftime("%Y%m%d%H%M%S") + ext
                    image.name = new_name
                    course.image = image

                # ✅ NOW also update course detail image if new one uploaded
                if d_image:
                    ext = os.path.splitext(d_image.name)[1]
                    new_name = datetime.now().strftime("%Y%m%d%H%M%S") + "_detail" + ext
                    d_image.name = new_name
                    course.course_image = d_image

                course.save()

                # Refresh relations
                Course_Batches.objects.filter(course_id=course_id).delete()
                Course_Exams.objects.filter(course_id=course_id).delete()
                Course_Academic.objects.filter(course_id=course_id).delete()
                Add_On_Courses_Course_Ids.objects.filter(add_on_course_id=course_id).delete()

                for batch in batch_id:
                    Course_Batches.objects.create(
                        course_id=course_id,
                        batch_id=batch
                    )

                for exam in exam_type_id:
                    Course_Exams.objects.create(
                        is_deleted=0,
                        course_id=course_id,
                        exam_id=exam
                    )

                for academic in academic_id:
                    Course_Academic.objects.create(
                        course_id=course_id,
                        academic_year_id=academic
                    )

                for i in range(len(course_ids)):
                    Add_On_Courses_Course_Ids.objects.create(
                        add_on_course_id=course_id,
                        course_id=course_ids[i],
                        course_name=course_name1[i],
                        course_info=course_info[i],
                        price=prices[i],
                        discount_price=discount_prices[i],
                    )

                return JsonResponse({'status': 'success', 'msg': 'Course updated successfully.', 'exam_type_id': exam_type_id})

    else:
        
        # Render the add form
        return render(request, 'course_form.html', {'batches':batches,'all_academics':all_academics,'form_title': 'Add Course','submit_button': 'Add Course','all_categories':all_categories,'all_exams':all_exams ,'status': 'error', 'msg': 'Something went wrong.','all_courses':all_courses})

@login_required(login_url=ADMIN_LOGIN_URL)
def course_edit(request, course_id):
    course = get_object_or_404(Course, pk=course_id)
    c_batches = Course_Batches.objects.filter(course_id=course_id)
    c_exams = Course_Exams.objects.filter(course_id=course_id)
    c_academics = Course_Academic.objects.filter(course_id=course_id)
    # Fetch already added add-ons for the course
    addons = Add_On_Courses_Course_Ids.objects.filter(add_on_course_id=course_id)
    courses = Course.objects.all()
    course_access=course.access
    # print(course_access)
    # course_access = json.loads(course[0].access )
    # course_access = json.loads(course.access)
    print(course_access)
    # print(course[0].access)
    all_categories = ExamCategory.objects.all()
    all_exams = Exam_Master.objects.all()
    batches = Batch_Management.objects.filter(status='Active')
    all_academics = AcademicYear.objects.all()
    return render(request, 'course_form.html', {'all_courses':courses,'all_academics':all_academics,'batches':batches,'form_title': 'Edit Course','submit_button': 'Save Course', 'course': course, 'c_batches':c_batches,'all_categories':all_categories,'all_exams':all_exams, 'c_exams':c_exams,'course_access':course_access,'addons':addons,'c_academics':c_academics})



@login_required(login_url=ADMIN_LOGIN_URL)
def select_exam_category(request): 
    exam_category_id = request.GET.get('exam_category_id')
    exam_master = Exam_Master.objects.filter(exam_category_id=exam_category_id)
    return render(request, "select_exam_master.html" ,{'exam_master':exam_master})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_course(request):
     id = request.POST.get('course_id')
    #  print(id)
     Course(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Course deleted successfully.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def change_course_status(request):
     id = request.POST.get('course_id')
     status = request.POST.get('status')
     Course.objects.filter(id=id).update(status=status)
     return JsonResponse({'status': 'success', 'msg': 'Course status updated successfully.'})
     
@login_required(login_url=ADMIN_LOGIN_URL)
def change_batch_status(request):
     id = request.POST.get('batch_id')
     status = request.POST.get('status')
     Batch_Management.objects.filter(id=id).update(status=status)
     return JsonResponse({'status': 'success', 'msg': 'Batch status updated successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def change_category_status(request):
     id = request.POST.get('category_id')
     status = request.POST.get('status')
     ExamCategory.objects.filter(id=id).update(status=status)
     return JsonResponse({'status': 'success', 'msg': 'Category status updated successfully.'})


# =============================end courses module========================

# ======================= start media module ==============================

@login_required(login_url=ADMIN_LOGIN_URL)
def media(request):
    # all_categories = ExamCategory.objects.all()
    all_media = MediaModule.objects.all()
    paginator = Paginator(all_media, 20)  # Show 25 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "media.html",{'all_media':page_obj})
    #  return render(request, "media.html")

@login_required(login_url=ADMIN_LOGIN_URL)
def addMedia(request):
    if request.method == 'POST':
        image = request.FILES.getlist('image')
        print(image)
        if(image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select images'})
        else:    
            for media_image in image:
                media = MediaModule(
                    image=media_image,   
                )
                media.save()

                # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Images Uploaded successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def editMedia(request):
    if request.method == 'POST':
        id = request.POST.get('id')
        image = request.FILES.get('image')
        if(image==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select images'})
        else:    
            # Create a new ExamCategory object
            media = MediaModule(
                id=id,
                image=image,   
            )

            media.save(update_fields=["image"])

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Images Uploaded successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def deleteMediabyId(request):
     id = request.POST.get('id')
    #  print(id)
     MediaModule(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Media deleted successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def MockTestSeries(request,series_type):
    title="Area Mock Test"
    if series_type==2:
        title="Sectional Mock Test"
    all_section = Sections.objects.all();
    all_categories = ExamCategory.objects.all()
    all_exams = Exam_Master.objects.all()

    all_mocks = Mock_Test_Series.objects.filter(series_type=series_type)
    paginator = Paginator(all_mocks, 10)  # Show 10 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "mock-test-series.html",{'all_mocks':page_obj ,'title': title,'series_type':series_type ,'all_section':all_section,'all_categories':all_categories,'all_exams':all_exams})  

# def select_category(request): 
#     category = request.GET.get('category')
#     exam = Exam_Master.objects.filter(exam_category_id=category)
#     return render(request, "select_exam_master.html" ,{'sub_section':sub_section})

@login_required(login_url=ADMIN_LOGIN_URL)
def addTestSeries(request):
    if request.method == 'POST':
        # Get the form data
        title = request.POST.get('title')
        # image = request.FILES.get('image')
        sections = request.POST.getlist('section')
        series_type=request.POST.get('series_type')
        exam_type=request.POST.get('exam_type')
        exam_master=request.POST.get('exam_master')
        total_tests=request.POST.get('total_tests')
        release_date=request.POST.get('release_date')
        status = request.POST.get('status')
       
        if(title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter coupen code.'})
        # elif(image==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select course'})
        elif(series_type==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter minimum amount'})
       
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            # Create a new ExamCategory object
            mock = Mock_Test_Series(
                title=title,
                # image=image,
                series_type=series_type,
                exam_category_id=exam_type,
                exam_master_id=exam_master,
                total_tests=total_tests,
                release_date=release_date,
                status=status
            )
            mock.save()

            series_id = mock.id
           
            for section in sections:
                section = MockTestSection(
                    series_id=series_id,
                    sections_id=section
                )
                section.save()
            # Save the object to the database

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Area Mock Test series Created   successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})
    


@login_required(login_url=ADMIN_LOGIN_URL)
def editTestSeries(request):

    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        title = request.POST.get('title')
        # image = request.FILES.get('image')
        sections = request.POST.getlist('section')
        series_type=request.POST.get('series_type')
        exam_type=request.POST.get('exam_type')
        exam_master=request.POST.get('exam_master')
        total_tests=request.POST.get('total_tests')
        release_date=request.POST.get('release_date')
        status = request.POST.get('status')
       
        if(title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter coupen code.'})
        # elif(image==''):
        #     return JsonResponse({'status': 'error', 'msg': 'Please select course'})
        elif(series_type==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter minimum amount'})
       
        elif(status==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select status.'})
        else:    
            if(request.FILES.get('image')):
            # Create a new ExamCategory object
                mock = Mock_Test_Series(
                    id=id,
                    title=title,
                    # image=image,
                    series_type=series_type,
                    exam_category_id=exam_type,
                    exam_master_id=exam_master,
                    total_tests=total_tests,
                    release_date=release_date,
                    status=status
                )

                # Save the object to the database
                mock.save(update_fields=["title","image","series_type","exam_category_id","exam_master_id","total_tests","release_date","status"])
            else:
            # Create a new ExamCategory object
                mock = Mock_Test_Series(
                    id=id,
                    title=title,
                    # image=image,
                    series_type=series_type,
                    exam_category_id=exam_type,
                    exam_master_id=exam_master,
                    total_tests=total_tests,
                    release_date=release_date,
                    status=status
                )

                # Save the object to the database
                mock.save(update_fields=["title","series_type","exam_category_id","exam_master_id","total_tests","release_date","status"])
                
            series_id=mock.id
            MockTestSection.objects.filter(series=id).soft_delete()
                
            for section in sections:
                section = MockTestSection(
                    series_id=id,
                    sections_id=section
                )
                section.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Area Mock Test series Created   successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def deleteMockTestSeries(request):
    if request.method == 'POST':
        id = request.POST.get('id')
        Mock_Test_Series(id).soft_delete()
        return JsonResponse({'status': 'success', 'msg': 'Mock Test Series deleted successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})



@login_required(login_url=ADMIN_LOGIN_URL)
def lead_status(request):
    # all_sales=Orders.objects.all()

    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    status_name=request.GET.get('status_name')
    bg_color=request.GET.get('bg_color')
    filter = ''

    status=Lead_status.objects.filter().order_by('-id')

    if from_date:
        status = status.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        status = status.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
    if status_name:
        status = status.filter(status_name__startswith=status_name)
        filter=filter+'&status_name='+status_name
    if bg_color:
        status = status.filter(bg_color__startswith=bg_color)
        filter=filter+'&bg_color='+bg_color

    paginator = Paginator(status, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "lead-status.html",{'status':page_obj,'filter':filter,
})

@login_required(login_url=ADMIN_LOGIN_URL)
def add_lead_status(request):
    if request.method == 'POST':
        # Get the form data
        status_name = request.POST.get('status_name')
        bg_color = request.POST.get('bg_color')
      
        if(status_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select blog category.'})
        elif(bg_color==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter blog title.'})       
        else:
            
            lead_status = Lead_status(
                status_name=status_name,
                bg_color=bg_color
            )

            # Save the object to the database
            lead_status.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Blog post successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_lead_status(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        status_name = request.POST.get('status_name')
        bg_color = request.POST.get('bg_color')
       
        if(status_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select blog category.'})
        elif(bg_color==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter blog title.'})       
        else:
            
            lead_status = Lead_status(
                id=id,
                status_name=status_name,
                bg_color=bg_color
            )
           
            lead_status.save(update_fields=["status_name","bg_color"])
                
            # Return a success response

            return JsonResponse({'status': 'success', 'msg': 'Blog post updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_lead_status(request):
     id = request.POST.get('id')
     print(id)

     Lead_status(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Blog post deleted successfully.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def lead_source(request):
    # all_sales=Orders.objects.all()
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    search=request.GET.get('source_name')
    filter = ''

    source=Lead_sourse.objects.filter().order_by('-id')

    if from_date:
        source = source.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        source = source.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date
    if search:
        source = source.filter(source_name__startswith=search)
        filter=filter+'&search='+search

    paginator = Paginator(source, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "lead-source.html",{'source':page_obj,'filter':filter,})

@login_required(login_url=ADMIN_LOGIN_URL)
def add_lead_source(request):
    if request.method == 'POST':
        # Get the form data
        source_name = request.POST.get('source_name')
      
      
        if(source_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select blog category.'})
       
        else:
            
            lead_status = Lead_sourse(
                source_name=source_name
            )

            # Save the object to the database
            lead_status.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'Blog post successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_lead_source(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        source_name = request.POST.get('source_name')
      
      
        if(source_name==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Select blog category.'}) 
        else:
            
            lead_status = Lead_sourse(
                id=id,
                source_name=source_name
            )
           
            lead_status.save(update_fields=["source_name"])
                
            # Return a success response

            return JsonResponse({'status': 'success', 'msg': 'Blog post updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_lead_source(request):
     
     id = request.POST.get('id')
     print(id)

     Lead_sourse(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'Blog post deleted successfully.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def vocabulary_questions(request):
    all_vocab_questions=Vocab_Questions.objects.all()
    paginator = Paginator(all_vocab_questions, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "vocab-questions.html",{'all_vocab_questions':page_obj})

@login_required(login_url=ADMIN_LOGIN_URL)
def add_vocab_question(request):
    if request.method == 'POST':
        # Get the form data
        word = request.POST.get('word')
        meaning = request.POST.get('meaning')
      
        if(word==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter word.'})
        elif(meaning==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter meaning.'})       
        else:
            
            vocab = Vocab_Questions.objects.create(
                word=word,
                meaning=meaning
            )

            # Save the object to the database
            # lead_status.save()

            # Return a success response
            return JsonResponse({'status': 'success', 'msg': 'vocab created successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_vocab_question(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        word = request.POST.get('word')
        meaning = request.POST.get('meaning')
       
        if(word==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Enter Word.'})
        elif(meaning==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter Meaning .'})       
        else:
            
            vocab = Vocab_Questions(
                id=id,
                word=word,
                meaning=meaning
            )
           
            vocab.save(update_fields=["word","meaning"])
                
            # Return a success response

            return JsonResponse({'status': 'success', 'msg': 'vocab updated successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_vocab_question(request):
     id = request.POST.get('id')
     Vocab_Questions(id).soft_delete()
     return JsonResponse({'status': 'success', 'msg': 'vocab deleted successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def upload_vocab_excel(request):
    if request.method == 'POST' and request.FILES['excelFile']:
        excel_file = request.FILES['excelFile']

        # Load the Excel file
        wb = openpyxl.load_workbook(excel_file)
        sheet = wb.active
        print(wb)

        vocab_data = []
        # Iterate over the rows in the Excel file
        for row in sheet.iter_rows(min_row=2, values_only=True):
            word = row[0]
            meaning = row[1]
            lead_obj = Vocab_Questions(
                word=word,
                meaning=meaning,
            )
                
            vocab_data.append(lead_obj)

        # Bulk insert the questions data
        Vocab_Questions.objects.bulk_create(vocab_data)
    return JsonResponse({'status': 'success', 'msg': 'vocab uploaded successfully.'})



@login_required(login_url=ADMIN_LOGIN_URL)
def Device_login_history(request):
    login_history=Login_history.objects.order_by('-id').all()
    paginator = Paginator(login_history, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "device-login-history.html",{'login_history':page_obj})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_login_is_lecture(request):
    if request.method == 'POST':
        # Get the form data
        id = request.POST.get('id')
        is_lecture = request.POST.get('is_lecture')
        if(is_lecture==''):
            return JsonResponse({'status': 'error', 'msg': 'Please Enter select lecture authority.'})     
        else:
            
            vocab = Login_history(
                id=id,
                is_lecture=is_lecture, 
                is_admin=1,    
            )
           
            vocab.save(update_fields=["is_lecture","is_admin"])
                
            # Return a success response

            return JsonResponse({'status': 'success', 'msg': 'Lecture device info successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def Student_Profile(request,id):
    student_list = Students.objects.get(id=id)
    all_sales = Orders.objects.filter(student_id=id,is_order=1).order_by('-id')
    # all_courses = Orders.objects.filter(student_id=id).order_by('-id')
    all_batch_list = Batch_Management.objects.filter(status='Active')
    all_batches =OrderCourses.objects.filter(student_id=id).exclude(batch_id__isnull=True).order_by('-id')
    # Student's enrolled batch IDs
    student_batch_ids = all_batches.values_list('batch_id', flat=True)
    # Student's enrolled batch IDs
    student_batch_ids = all_batches.values_list('batch_id', flat=True)

    # Lecture IDs assigned to student's batches
    lecture_ids = Lecture_batches.objects.filter(
        batches_id__in=student_batch_ids
    ).values_list('lecture_id', flat=True).distinct()

    # All lectures student should attend
    all_lectures = Live_Lecture.objects.filter(
        id__in=lecture_ids
    ).order_by('-lacture_date')

    # Attendance records of this student
    attendance_records = Lecture_attendance.objects.filter(
        student_id=id,
        lecture_id__in=lecture_ids
    ).order_by('-id')

    # Map lecture_id with attendance object
    attendance_map = {}

    for attendance in attendance_records:
        if attendance.lecture_id not in attendance_map:
            attendance_map[attendance.lecture_id] = attendance

    # Final lecture-wise attendance data
    lecture_wise_attendance = []

    for lecture in all_lectures:
        attendance = attendance_map.get(lecture.id)

        is_present = True if attendance and attendance.join_time else False

        lecture_wise_attendance.append({
            'lecture': lecture,
            'status': 'Present' if is_present else 'Absent',
            'join_time': attendance.join_time if attendance and attendance.join_time else None,
            'leave_time': attendance.leave_time if attendance and attendance.leave_time else None,
            'duration': attendance.duration if attendance and attendance.duration else None,
        })
#    all_attendance=Lecture_attendance.objects.filter(student_id=id).order_by('-id')
    logins=Login_history.objects.filter(student_id=id).order_by('-id')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    filter_condition = {}
    if from_date:
        filter_condition['created_at__gte'] = from_date
    if to_date:
        filter_condition['created_at__lte'] = to_date
    # mock_type = request.GET.get('type')
    sections = Sections.objects.all()
    # If you want to filter or order the sections, you can do so using queryset methods.
    # For example, to order by section_name:
    # sections = Sections.objects.all().order_by('section_name')

    # If you want to get the section names only, you can use values_list
    section_wise_stats = {}

# # Iterate over sections and fetch section-wise statistics
    area_wise_stats = (
        Test_Result.objects
        .filter(student_id=id,is_end=1,mock_test__mock_type=1).annotate(fullname=Concat('student__first_name', Value(' '), 'student__last_name')).filter(**filter_condition)
        .values('mock_test__id', 'mock_test__mock_title', 'student_id', 'student__first_name', 'student__last_name','created_at')
        .annotate(
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration')  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(attempted__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
        .order_by('-created_at')  # Add this line to order results by student_id
    )
    
    rank_percentile=[]
    for exam in area_wise_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],is_end=1).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(list(rank_percentile)[0],2)
        exam['rank']=list(rank_percentile)[1]
    
    section_wise_stats = (
        Test_Result.objects
        .filter(student_id=id,is_end=1,mock_test__mock_type=2).annotate(fullname=Concat('student__first_name', Value(' '), 'student__last_name')).filter(**filter_condition)
        .values('mock_test__id', 'mock_test__mock_title', 'student_id', 'student__first_name', 'student__last_name','created_at')
        .annotate(
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration')  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(attempted__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
        .order_by('-created_at')  # Add this line to order results by student_id
    )
    
    rank_percentile=[]
    for exam in section_wise_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id']).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(list(rank_percentile)[0],2)
        exam['rank']=list(rank_percentile)[1]
    mini_wise_stats = (
        Test_Result.objects
        .filter(student_id=id,is_end=1,mock_test__mock_type=4).annotate(fullname=Concat('student__first_name', Value(' '), 'student__last_name')).filter(**filter_condition)
        .values('mock_test__id', 'mock_test__mock_title', 'student_id', 'student__first_name', 'student__last_name','created_at')
        .annotate(
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration')  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(attempted__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
        .order_by('-created_at')  # Add this line to order results by student_id
    )
    
    rank_percentile=[]
    for exam in mini_wise_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],is_end=1).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(list(rank_percentile)[0],2)
        exam['rank']=list(rank_percentile)[1]
    full_wise_stats = (
        Test_Result.objects
        .filter(student_id=id,is_end=1,mock_test__mock_type=5).annotate(fullname=Concat('student__first_name', Value(' '), 'student__last_name')).filter(**filter_condition)
        .values('mock_test__id', 'mock_test__mock_title', 'student_id', 'student__first_name', 'student__last_name','created_at')
        .annotate(
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration')  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(attempted__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
        .order_by('-created_at')  # Add this line to order results by student_id
    )
    
    rank_percentile=[]
    for exam in full_wise_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],is_end=1).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(list(rank_percentile)[0],2)
        exam['rank']=list(rank_percentile)[1]
    advance_stats = (
        Test_Result.objects
        .filter(student_id=id,is_end=1,mock_test__mock_type=8).annotate(fullname=Concat('student__first_name', Value(' '), 'student__last_name')).filter(**filter_condition)
        .values('mock_test__id', 'mock_test__mock_title', 'student_id', 'student__first_name', 'student__last_name','created_at')
        .annotate(
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration')  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(attempted__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
        .order_by('-created_at')  # Add this line to order results by student_id
    )
    
    rank_percentile=[]
    for exam in advance_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],is_end=1).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(list(rank_percentile)[0],2)
        exam['rank']=list(rank_percentile)[1]
    pyq_stats = (
        Test_Result.objects
        .filter(student_id=id,is_end=1,mock_test__mock_type=9).annotate(fullname=Concat('student__first_name', Value(' '), 'student__last_name')).filter(**filter_condition)
        .values('mock_test__id', 'mock_test__mock_title', 'student_id', 'student__first_name', 'student__last_name','created_at')
        .annotate(
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration')  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(attempted__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
        .order_by('-created_at')  # Add this line to order results by student_id
    )
    
    rank_percentile=[]
    for exam in pyq_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],is_end=1).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(list(rank_percentile)[0],2)
        exam['rank']=list(rank_percentile)[1]
        

    student_activities = StudentActivity.objects.filter(
        student_id=id
    ).select_related("student").order_by("-created_at")

    # -------- Exam Map --------
    exam_ids = set(student_activities.values_list("exam_id", flat=True))

    exam_map = {
        e.id: e.exam_category_name
        for e in ExamCategory.objects.filter(id__in=exam_ids)
    }

    # -------- GROUPING --------
    grouped = defaultdict(lambda: {
        "name": "",
        "contact": "",
        "email": "",
        "exam": set(),
        "activity_logs": []
    })

    for act in student_activities:

        key = act.created_at.date()  # 👈 group by DATE only (since single student)

        resource_name = ""
        score = ""
        exam_name = exam_map.get(act.exam_id, "")

        # -------- Resource Mapping --------

        if act.activity_type in ["notes", "downloads"]:
            res = Resources.objects.filter(id=act.resource_id).first()
            if res:
                resource_name = res.title

        elif act.activity_type in [
            'area_mock','sectional_mock','mini_mock',
            'full_mock','advance_mock','pyq_mock',
            'basic_concept_test','advance_concept_test'
        ]:
            mock = Mock_Test.objects.filter(id=act.resource_id).first()
            result = Test_Result.objects.filter(
                student=act.student,
                mock_test_id=act.resource_id
            ).order_by('-created_at').first()

            if result:
                if mock:
                    resource_name = mock.mock_title

                score = f"{result.marks}/{getattr(result, 'total_marks', '')}".strip('/')

        elif act.activity_type == "daily_speedMath":
            resource_name = "Daily Speed Math"
            sm = Daily_speedMath_Score.objects.filter(
                student=act.student
            ).order_by('-date').first()
            if sm:
                score = str(sm.score)

        elif act.activity_type == "daily_vocab":
            resource_name = "Daily Vocabulary"
            vocab = Daily_Vocab_Score.objects.filter(
                student=act.student
            ).order_by('-date').first()
            if vocab:
                score = str(vocab.score)

        # -------- FORMAT --------

        if resource_name and score:
            activity_text = f"{resource_name} ({score})"
        elif resource_name:
            activity_text = f"{act.get_activity_type_display()}: {resource_name}"
        else:
            activity_text = act.get_activity_type_display()

        time_str = act.created_at.strftime("%I:%M %p")

        formatted_block = f"{activity_text}\n🕒 {time_str}"

        # -------- STORE --------

        grouped[key]["name"] = f"{act.student.first_name} {act.student.last_name}"
        grouped[key]["contact"] = act.student.mobile
        grouped[key]["email"] = act.student.email

        if exam_name:
            grouped[key]["exam"].add(exam_name)

        grouped[key]["activity_logs"].append({
            "text": formatted_block,
            "time": act.created_at
        })

    # -------- FINAL OUTPUT --------

    rows = []

    for date, data in grouped.items():

        logs = sorted(data["activity_logs"], key=lambda x: x["time"])

        rows.append({
            "name": data["name"],
            "contact": data["contact"],
            "email": data["email"],
            "date": date,
            "exam": ", ".join(data["exam"]),
            "activity": "\n\n".join([l["text"] for l in logs])
        })

    # -------- PAGINATION --------

    paginator = Paginator(rows, 10)
    page_number = request.GET.get("page")
    student_activities = paginator.get_page(page_number)

    # if sort_by == 'percentile':
    #     exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'])
    # elif sort_by == '-percentile':
    #     exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'], reverse=True)
    # elif sort_by == '-rank':
    #     exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'], reverse=True)
    # elif sort_by == 'rank':
    #     exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'])
    return render(request, "student_profile.html",{'student_list':student_list,'all_sales':all_sales,'all_batches':all_batches,'lecture_wise_attendance':lecture_wise_attendance,'all_batch_list':all_batch_list,'logins':logins,'area_wise_stats':list(area_wise_stats),'section_wise_stats':list(section_wise_stats),'mini_wise_stats':list(mini_wise_stats),'full_wise_stats':list(full_wise_stats),'from_date':from_date,
        'to_date':to_date,'student_activities':student_activities,'advance_stats':advance_stats,'pyq_stats':pyq_stats})

def student_analysis(request):
    student_id = request.GET.get('id')
    total_attempted = Test_Result.objects.filter(student_id=student_id,mock_test__mock_type=5).aggregate(total_attempted=Sum('attempted'))['total_attempted']
    total_correct = Test_Result.objects.filter(student_id=student_id,mock_test__mock_type=5).aggregate(total_correct=Sum('right_answered'))['total_correct']
    overall_accuracy = (total_correct / total_attempted) * 100 if total_attempted > 0 else 0
    student_score = Test_Result.objects.filter(student_id=student_id,mock_test__mock_type=5).aggregate(student_score=Sum('marks'))['student_score']

    # Initialize dictionaries to store statistics for sections, subsections, and topics
    section_statistics = defaultdict(lambda: defaultdict(int))
    subsection_statistics = defaultdict(lambda: defaultdict(int))
    topic_statistics = defaultdict(lambda: defaultdict(int))
    section_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id,mock_test__mock_type=5)
        .values('question__section_id', 'question__section__section_name')  # Group by section_id and select section_name
        .annotate(
            total_question=Count('question_id'),  # Count total questions
            attempted=Sum('attempted'),  # Sum of attempted
            right=Sum(
                Case(
                    When(is_right=1, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of right (is_right=1)
            wrong=Sum(
                Case(
                    When(is_right=0, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of wrong (is_right=0)
            marks=Sum('marks'),  # Sum of marks
        )
    )
    sub_section_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id,mock_test__mock_type=5)
        .values('question__sub_section_id', 'question__sub_section__sub_section_name')  # Group by sub_section_id and select sub_section_name
        .annotate(
            total_question=Count('question_id'),  # Count total questions
            attempted=Sum('attempted'),  # Sum of attempted
            right=Sum(
                Case(
                    When(is_right=1, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of right (is_right=1)
            wrong=Sum(
                Case(
                    When(is_right=0, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of wrong (is_right=0)
            marks=Sum('is_right'),  # Sum of marks
        )
        .annotate(
            toppers_marks=Max('question__marks'),  # Max marks of the highest-scoring question
        )
    )
    topic_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id,mock_test__mock_type=5)
        .values('question__topic_id', 'question__topic__topic_name')  # Group by topic_id and select topic_name
        .annotate(
            total_question=Count('question_id'),  # Count total questions
            attempted=Sum('attempted'),  # Sum of attempted
            right=Sum(
                Case(
                    When(is_right=1, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of right (is_right=1)
            wrong=Sum(
                Case(
                    When(is_right=0, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of wrong (is_right=0)
            marks=Sum('is_right'),  # Sum of marks
        )
        .annotate(
            toppers_marks=Max('question__marks'),  # Max marks of the highest-scoring question
        )
    )
    sections = Sections.objects.all()

    # If you want to filter or order the sections, you can do so using queryset methods.
    # For example, to order by section_name:
    # sections = Sections.objects.all().order_by('section_name')

    # If you want to get the section names only, you can use values_list
    section_wise_stats = {}

# # Iterate over sections and fetch section-wise statistics
    exam_wise_stats = (
        Test_Result.objects
        .filter(student_id=student_id,mock_test__mock_type=5)
        .values('mock_test__id', 'mock_test__mock_title')
        .annotate(
            total_question=F('total_question'),
            attempted=F('attempted'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(attempted__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                ) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
    )
    for exam in exam_wise_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],mock_test__mock_type=5).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=rank_percentile[0]
        exam['rank']=rank_percentile[1]
        
    context = {
        # 'overall_accuracy':overall_accuracy,
        # 'percentile':percentile,
        # 'section_statistics':list(section_statistics),
        # 'sub_section_statistics':list(sub_section_statistics),
        # 'topic_statistics':list(topic_statistics),
        # 'section_wise_stats':(section_wise_stats),
        # 'section_wise_stats':section_wise_stats,
        'exam_wise_stats':list(exam_wise_stats),
        'sections':list(sections.values()),
        # 'section_wise_percentile':list(section_wise_percentile),
        'student_id':student_id
    }
    # return JsonResponse(context)
    return render(request,'overall-analysis.html',context)

@login_required(login_url=ADMIN_LOGIN_URL)
def remove_batch(request):  
    id = request.POST.get('id')    
    OrderCourses.objects.filter(id=id).update(batch_id='',lectures=0)
    return JsonResponse({'status': 'success', 'msg': 'Batch removed successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def assign_batch(request):
    if request.method == 'POST':
        # Get the form data
        student_id = request.POST.get('student_id')
        batch = request.POST.get('batch_id')
        course = request.POST.get('course_id')
        # meaning = request.POST.get('meaning')
      
        if(batch==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select batch.'})    
        else:
            
            vocab = OrderCourses.objects.create(
                student_id=student_id,
                batch_id=batch,
                course_id=course,
                lectures = 1
            )
            return JsonResponse({'status': 'success', 'msg': 'batch assigned successfully.'})
    else:
        # Return an error response
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def Lecture_Details(request, id):
    lecture = Live_Lecture.objects.get(id=id)

    # Get batches assigned to this lecture
    batch_ids = Lecture_batches.objects.filter(
        lecture_id=id
    ).values_list('batches_id', flat=True).distinct()

    # Get all students enrolled in those batches
    student_ids = OrderCourses.objects.filter(
        batch_id__in=batch_ids
    ).values_list('student_id', flat=True).distinct()

    students = Students.objects.filter(
        id__in=student_ids
    ).order_by('first_name', 'last_name')

    # Get attendance records for this lecture
    attendance_records = Lecture_attendance.objects.filter(
        lecture_id=id,
        student_id__in=student_ids
    ).order_by('-id')

    # Create student_id based attendance map
    attendance_map = {}

    for attendance in attendance_records:
        if attendance.student_id not in attendance_map:
            attendance_map[attendance.student_id] = attendance

    # Final student-wise attendance data
    lecture_attendance_list = []

    for student in students:
        attendance = attendance_map.get(student.id)

        is_present = True if attendance and attendance.join_time else False

        lecture_attendance_list.append({
            'student': student,
            'status': 'Present' if is_present else 'Absent',
            'duration': attendance.duration if attendance and attendance.duration else None,
            'join_time': attendance.join_time if attendance and attendance.join_time else None,
            'leave_time': attendance.leave_time if attendance and attendance.leave_time else None,
        })

    return render(request, "lecture-details.html", {
        'lecture': lecture,
        'lecture_attendance_list': lecture_attendance_list
    })
# study material

@login_required(login_url=ADMIN_LOGIN_URL)
def StudyMaterial(request):
    all_study_materials=Study_Materials.objects.order_by('-id').all()
    all_batch_list = Batch_Management.objects.filter(status='Active')
    paginator = Paginator(all_study_materials, 10)  # Show 2 contacts per page.
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "study.html",{'all_study_materials':page_obj,'all_batch_list':all_batch_list,})

@login_required(login_url=ADMIN_LOGIN_URL)
def AddStudyMaterial(request):
    if request.method == 'POST':
        study_materials = request.FILES.getlist('materials')
        batch = request.POST.getlist('batch')

        # Validation
#        if not batch:
#            return JsonResponse({'status': 'error', 'msg': 'Please select batch.'})
        if not study_materials:
            return JsonResponse({'status': 'error', 'msg': 'Please select Materials.'})

        counter = 1
        for study_material in study_materials:
            # Keep original name for DB display
            original_name = study_material.name

            # Extract extension
            ext = os.path.splitext(study_material.name)[1]  # e.g. ".pdf"

            # Generate unique datetime filename for storage
            new_filename = datetime.now().strftime("%Y%m%d%H%M%S") + f"_{counter}" + ext

            # Rename the uploaded file for storage
            study_material.name = new_filename

            # Save model: original name in DB, renamed file in storage
            studymaterial = Study_Materials.objects.create(
                name=original_name,       # ✅ original file name
                materials=study_material, # ✅ renamed file saved
            )

            # Save batch linking
            for batch_id in batch:
                Study_material_batches.objects.create(
                    study_id=studymaterial.id,
                    batch_id=batch_id
                )

            counter += 1

        return JsonResponse({'status': 'success', 'msg': 'Study Materials uploaded successfully.'})

    # Fallback for non-POST
    return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def EditStudyMaterial(request):
    if request.method == 'POST':

        study_id = request.POST.get('id')
        uploaded_files = request.FILES.getlist('materials')
        batch_ids = request.POST.getlist('batch')

#        if not batch_ids:
#            return JsonResponse({'status': 'error', 'msg': 'Please select batch'})

        # Fetch record
        study_obj = get_object_or_404(Study_Materials, pk=study_id)

        # Handle file update (only 1 file allowed when editing)
        if uploaded_files:
            file = uploaded_files[0]                # Only first file
            original_name = file.name               # Keep original filename in DB

            # Rename file only for saving on server
            ext = os.path.splitext(file.name)[1]
            new_file_name = datetime.now().strftime("%Y%m%d%H%M%S") + ext
            file.name = new_file_name

            # Update fields
            study_obj.name = original_name          # ✔ Original name saved
            study_obj.materials = file              # ✔ Updated file
            study_obj.save(update_fields=["name", "materials"])
        else:
            # No file update, only batch update
            study_obj.save(update_fields=[])

        # Update batches
        Study_material_batches.objects.filter(study_id=study_id).delete()
        for b in batch_ids:
            Study_material_batches.objects.create(study_id=study_id, batch_id=b)

        return JsonResponse({'status': 'success', 'msg': 'Study Material updated successfully'})

    return JsonResponse({'status': 'error', 'msg': 'Something went wrong'})


@login_required(login_url=ADMIN_LOGIN_URL)
def deleteMaterials(request):
    id = request.POST.get('id')
    Study_Materials(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'deleted successfully.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def deleteQuestion(request):
    id = request.POST.get('id')
    Question_Bank(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'deleted successfully.'})

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger

@login_required(login_url=ADMIN_LOGIN_URL)
def mock_test_result(request):
    test_id = request.GET.get('test_id')
    sort_by = request.GET.get('sort_by')
    search = request.GET.get('search')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    page = request.GET.get('page', 1)  # Get the page number from the request
    per_page = request.GET.get('per_page', 10)  # Default to 10 items per page

    filter_condition = {}
    if search:
        filter_condition['fullname__icontains'] = search
    if from_date:
        filter_condition['created_at__gte'] = from_date
    if to_date:
        filter_condition['created_at__lte'] = to_date

    sections = Sections.objects.all()

    exam_wise_stats = (
        Test_Result.objects
        .filter(mock_test_id=test_id, is_end=1)
        .annotate(
            fullname=Concat('student__first_name', Value(' '), 'student__last_name')
        )
        .filter(**filter_condition)
        .values(
            'mock_test__id', 'mock_test__mock_title', 'student_id', 'student__first_name',
            'student__last_name', 'created_at'
        )
        .annotate(
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration')
        )
        .annotate(
            marks_per_minute=Case(
                When(
                    attempted__gt=0,
                    then=ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),
                        output_field=fields.FloatField(),
                    )
                ),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
        .order_by('-created_at')
    )

    rank_percentile = []
    for exam in exam_wise_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id']).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile'] = round(list(rank_percentile)[0], 2)
        exam['rank'] = list(rank_percentile)[1]

    # Sorting
    if sort_by == 'percentile':
        exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'])
    elif sort_by == '-percentile':
        exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'], reverse=True)
    elif sort_by == '-rank':
        exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'], reverse=True)
    elif sort_by == 'rank':
        exam_wise_stats = sorted(exam_wise_stats, key=lambda x: x['percentile'])

    # Pagination
    paginator = Paginator(exam_wise_stats, per_page)
    try:
        paginated_stats = paginator.page(page)
    except PageNotAnInteger:
        paginated_stats = paginator.page(1)
    except EmptyPage:
        paginated_stats = paginator.page(paginator.num_pages)

    test = Mock_Test.objects.filter(id=test_id)
    context = {
        'status': 'Success',
        'exam_wise_stats': list(paginated_stats),  # Use the paginated data
        'sections': list(sections.values()),
        'test_id': test_id,
        'sort_by': sort_by,
        'search': search,
        'from_date': from_date,
        'to_date': to_date,
        'rank_percentile': list(rank_percentile),
        'test': list(test.values()),
        'current_page': paginated_stats.number,
        'total_pages': paginator.num_pages,
        'per_page': per_page,
        'page_range': range(1, paginator.num_pages + 1),
    }
    return render(request, "mock-test-result.html", context)



@login_required(login_url=ADMIN_LOGIN_URL)
def view_mock_solution(request):
    student_id = request.GET.get('student_id')
    exam_id = request.GET.get('exam_id')
    section_id = request.GET.get('section_id')
    ftype = request.GET.get('type')
    sections = Sections.objects.exclude(id__exact=23).all()
    filter_condition = {}
    if ftype=='bookmarked':
        filter_condition['bookmarked'] = 1
        filter_condition['answer_id__isnull'] = True
    elif ftype=='correct':
        filter_condition['is_right'] = 1
    elif ftype=='incorrect':
        filter_condition['is_right'] = 0
        filter_condition['answer_id__isnull'] = False
    elif ftype=='notviewed':
        filter_condition['bookmarked'] = 0
        filter_condition['answer_id__isnull'] = True

    if section_id:
        section_statistics = (
            Student_Answers.objects
            .filter(student_id=student_id, mock_test_id=exam_id, question__section_id=section_id, **filter_condition)
            .values('question__id', 'question__section__section_name', 'question__sub_section__sub_section_name', 'question__topic__topic_name', 'question__question', 'question__paragraph', 'question__explanation', 'question__marks', 'question__negative_marks', 'question__correct_answer', 'question__options','question__difficulty_level','answer_id','is_right','bookmarked','attempted','question__question_type', 'time_taken')  # Group by sub_section_id and select sub_section_name
        )
    else:
        section_statistics = (
            Student_Answers.objects
            .filter(student_id=student_id, mock_test_id=exam_id, **filter_condition)
            .values('question__id', 'question__section__section_name', 'question__sub_section__sub_section_name', 'question__topic__topic_name', 'question__question', 'question__paragraph', 'question__explanation', 'question__marks', 'question__negative_marks', 'question__correct_answer', 'question__options','question__difficulty_level','answer_id','is_right','bookmarked','attempted','question__question_type', 'time_taken')  # Group by sub_section_id and select sub_section_name
    )
    section_result=[]
    if section_id:
        section_result = list(Section_Test_Result.objects.filter(mock_test_id=exam_id,student_id=student_id, section_id=section_id).values())
        section_id=int(section_id)
    result = Test_Result.objects.filter(mock_test_id=exam_id,student_id=student_id)
    student = Students.objects.filter(id=student_id)
    context = {
        'status': 'Success',
        'sections':list(sections.values()),
        'question_answers':list(section_statistics),
        'section_result':section_result,
        'result':list(result.values()),
        'section_id':section_id,
        'test_id':exam_id,
        'student_id':student_id,
        'student':list(student.values())
    }
    
    return render(request, "view-solution.html", context)

@login_required(login_url=ADMIN_LOGIN_URL)
def mock_analysis(request):
    student_id = request.GET.get('student_id')
    test_id= request.GET.get('exam_id')
    student = Students.objects.filter(id=student_id)
    total_attempted = Test_Result.objects.filter(student_id=student_id,is_end=1,not_accessible=0).aggregate(total_attempted=Sum('attempted'))['total_attempted']
    total_correct = Test_Result.objects.filter(student_id=student_id,is_end=1,not_accessible=0).aggregate(total_correct=Sum('right_answered'))['total_correct']
    overall_accuracy = (total_correct / total_attempted) * 100 if total_attempted > 0 else 0
    student_score = Test_Result.objects.filter(student_id=student_id,is_end=1,not_accessible=0).aggregate(student_score=Sum('marks'))['student_score']


    sections = Sections.objects.all()

    # If you want to filter or order the sections, you can do so using queryset methods.
    # For example, to order by section_name:
    # sections = Sections.objects.all().order_by('section_name')

    # If you want to get the section names only, you can use values_list
    section_wise_stats = {}

# # Iterate over sections and fetch section-wise statistics
    exam_wise_stats = (
        Test_Result.objects
        .filter(student_id=student_id,mock_test_id=test_id,is_end=1,not_accessible=0)
        .values('mock_test__id', 'mock_test__mock_title')
        .annotate(
            attempted_at=F('created_at'),
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),
            time_left=F('time_left'),
            duration=F('mock_test__mock_duration'),  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(time_taken__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / (F('right')+F('wrong'))) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
    )
    difficulty_wise_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id,mock_test_id=test_id,result__is_end=1,result__not_accessible=0)
        .values('question__difficulty_level')  # Group by section_id and select section_name
        .annotate(
            total_question=Count('question_id'),  # Count total questions
            attempted=Value(0, IntegerField()),  # Sum of attempted
            right=Sum(
                Case(
                    When(is_right=1, then=1), default=0, output_field=IntegerField()
                )
            ),
            wrong=Sum(
                Case(
                    When(is_right=0, answer_id__isnull=False, then=1),
                    default=0,
                    output_field=IntegerField()
                )
            ),  # Sum of wrong (is_right=0)
            marks=Sum('marks'),  # Sum of marks
        )
    )
    rank_percentile=[]
    for exam in exam_wise_stats:
        rank_percentile = calculate_percentile(
            Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],is_end=1).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(rank_percentile[0], 2)
        exam['rank']=rank_percentile[1]
    context = {
        # 'overall_accuracy':overall_accuracy,
        # 'percentile':percentile,
        # 'section_statistics':list(section_statistics),
        # 'sub_section_statistics':list(sub_section_statistics),
        # 'topic_statistics':list(topic_statistics),
        # 'section_wise_stats':(section_wise_stats),
        # 'section_wise_stats':section_wise_stats,
        'exam_wise_stats':list(exam_wise_stats),
        'sections':list(sections.values()),
        # 'section_wise_percentile':list(section_wise_percentile),
        'student_id':student_id,
        'test_id':test_id,
        'difficulty_wise_statistics':list(difficulty_wise_statistics),
        'student':list(student.values())
    }
#     return JsonResponse(context)
    return render(request,'mock-exam-analysis.html',context)


@login_required(login_url=ADMIN_LOGIN_URL)
def abc_analysis(request):
    student_id = request.POST.get('student_id')
    section_id = request.POST.get('section_id')
    exam_id = request.POST.get('exam_id')
    filter_type = request.GET.get('filter_type')
    filter_condition = {}
    filter_condition['result__is_end'] = 1
    filter_condition['result__not_accessible'] = 0
    if exam_id:
        filter_condition['mock_test__id'] = exam_id
    if section_id:
        filter_condition['question__section_id'] = section_id
    topic_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id,answer_id__isnull=False, **filter_condition)
        .values('question__sub_section_id', 'question__topic__topic_name', 'question__sub_section__sub_section_name')  # Group by sub_section_id and select sub_section_name
        .annotate(
                marks=Sum('marks'),
                time_taken=Sum('time_taken'),
                marks_per_minute=Case(
                When(time_taken__gt=0, then=(
                    ExpressionWrapper(
                        F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
                        output_field=fields.FloatField(),
                    )
                )),
                default=0,
                output_field=fields.FloatField(),
            ),
            question__sub_section__strong=F('question__sub_section__strong'),
            question__sub_section__weak=F('question__sub_section__weak')
        )
    )
    # subsection_statistics = (
    #     Student_Answers.objects
    #     .filter(student_id=student_id, **filter_condition)
    #     .values('question__sub_section_id', 'question__sub_section__sub_section_name','question__sub_section__strong','question__sub_section__weak')  # Group by sub_section_id and select sub_section_name
    #     .annotate(
    #         marks_per_minute=Case(
    #         When(attempted__gt=0, then=(
    #             ExpressionWrapper(
    #                 F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
    #                 output_field=fields.FloatField(),
    #             )
    #         ) * 100),
    #         default=0,
    #         output_field=fields.FloatField(),
    #     )
    #     )
    # )
    # section_statistics = (
    #     Student_Answers.objects
    #     .filter(student_id=student_id, **filter_condition)
    #     .values('question__section_id', 'question__section__section_name','question__section__strong','question__section__weak')  # Group by sub_section_id and select sub_section_name
    #     .annotate(
    #         marks_per_minute=Case(
    #         When(attempted__gt=0, then=(
    #             ExpressionWrapper(
    #                 F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
    #                 output_field=fields.FloatField(),
    #             )
    #         ) * 100),
    #         default=0,
    #         output_field=fields.FloatField(),
    #     )
    #     )
    # )
    context = {
        'topic_statistics':list(topic_statistics),
        'filter_type':filter_type,
        'section_id':section_id,
        'exam_id':exam_id
        #   'topic_statistics':list(topic_statistics),
    }
#    return JsonResponse(context)
    return render(request,'mock-abc-analysis.html',context)


def calculate_percentile(scores, target_score):
    # Sort the list of scores in ascending order
    sorted_scores = sorted(scores)[::-1]
    # print(len(sorted_scores))
    # return sorted_scores
    # Find the index of the target_score in the sorted list
    percentile = 100
    rank = 1
    if len(sorted_scores) > 1:
        rank = sorted_scores.index(target_score)+1
        lower = len([score for score in sorted_scores if score < target_score])
        # Calculate the percentile based on the index
        #percentile = (index / len(sorted_scores)) * 100
        percentile = (lower/(len(sorted_scores)-1))*100
    # (no. of people behind me /total -1) *100
    rank_percentile = [percentile, rank]
    return rank_percentile


@login_required(login_url=ADMIN_LOGIN_URL)
def exam_section_analysis(request):
    student_id = request.POST.get('student_id')
    section_id = request.POST.get('section_id')
    sub_section_id = request.POST.get('sub_section_id')
    exam_id = request.POST.get('exam_id')
    topic_id = request.POST.get('topic_id')
    filter_condition = {}
    filter_condition['result__is_end'] = 1
    filter_condition['result__not_accessible'] = 0
    if exam_id:
        filter_condition['mock_test__id'] = exam_id
    if section_id:
        filter_condition['question__section_id'] = section_id
    if sub_section_id:
        filter_condition['question__sub_section_id'] = sub_section_id
    if topic_id:
        filter_condition['question__topic_id'] = topic_id
    exam_wise_stats = (
        Section_Test_Result.objects
        .filter(student_id=student_id,section_id=section_id,mock_test__id=exam_id,result__is_end=1,result__not_accessible=0)
        .values('mock_test__id', 'mock_test__mock_title')
        .annotate(
            section_id=F('section_id'),
            total_question=F('total_question'),
            attempted=F('right_answered') + F('wrong_answered'),
            right=F('right_answered'),
            wrong=F('wrong_answered'),
            marks=F('marks'),
            percentage=F('percentage'),
            time_taken=F('time_taken'),  # Time taken for each question
        )
        .annotate(
            marks_per_minute=Case(
                When(time_taken__gt=0, then=(F('marks') / (F('time_taken') / 60))),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_questions')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
    )
    for exam in exam_wise_stats:
        rank_percentile = calculate_percentile(
            Section_Test_Result.objects.filter(mock_test_id=exam['mock_test__id'],section_id=exam['section_id'],result__is_end=1).values_list('marks', flat=True),
            exam['marks']
        )
        exam['percentile']=round(rank_percentile[0], 2)
        exam['rank']=rank_percentile[1]
    section_statistics = (
    Student_Answers.objects
    .filter(student_id=student_id, question__section_id=section_id,mock_test__id=exam_id,result__is_end=1,result__not_accessible=0)
    .values('question__sub_section__id', 'question__sub_section__sub_section_name')
    .annotate(total_question=Count('question__sub_section__id'))
    .annotate(
        # total_question=Count('question__sub_section_id'),  # Count total questions
        attempted=Sum('attempted'),  # Sum of attempted
        right=Sum(
            Case(
                When(is_right=1, then=1),
                default=0,
                output_field=IntegerField()
            )
        ),  # Sum of right (is_right=1)
        wrong=Sum(
            Case(
                When(is_right=0, answer_id__isnull=False, then=1),
                default=0,
                output_field=IntegerField()
            )
        ),  # Sum of wrong (is_right=0)
        marks=Sum('marks'),
        time_taken=Sum('time_taken'),  # Time taken for each question
        marks_per_minute=Case(
            When(time_taken__gt=0, then=(F('marks') / (F('time_taken') / 60))),
            default=0,
            output_field=fields.FloatField(),
        ),
        right_accuracy=Case(
            When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
            default=0,
            output_field=fields.FloatField(),
        ),
        avg_attempt=Case(
            When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
            default=0,
            output_field=fields.FloatField(),
        )
    )
)
    
    # topic_statistics = (
    #     Student_Answers.objects
    #     .filter(student_id=student_id)
    #     .values('question__topic_id', 'question__topic__topic_name')  # Group by topic_id and select topic_name
    #     .annotate(
    #         total_question=Count('question_id'),  # Count total questions
    #         attempted=Sum('attempted'),  # Sum of attempted
    #         right=Sum(
    #             Case(
    #                 When(is_right=1, then=1), default=0, output_field=IntegerField()
    #             )
    #         ),  # Sum of right (is_right=1)
    #         wrong=Sum(
    #             Case(
    #                 When(is_right=0, then=1), default=0, output_field=IntegerField()
    #             )
    #         ),  # Sum of wrong (is_right=0)
    #         marks=Sum('marks'),
    #         time_taken=F('time_taken'),  # Time taken for each question
    #         )
    #         .annotate(
    #             marks_per_minute=ExpressionWrapper(
    #                 F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
    #                 output_field=fields.FloatField(),
    #             ),
    #             right_accuracy=Case(
    #                 When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
    #                 default=0,
    #                 output_field=fields.FloatField(),
    #             ),
    #             avg_attempt=Case(
    #                 When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
    #                 default=0,
    #                 output_field=fields.FloatField(),
    #             )
    #         )
    # )
    difficulty_wise_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id, **filter_condition)
        .values('question__difficulty_level')  # Group by section_id and select section_name
        .annotate(
            total_question=Count('question_id'),  # Count total questions
            attempted=Value(0, IntegerField()),  # Sum of attempted
            right=Sum(
                Case(
                    When(is_right=1, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of right (is_right=1)
            wrong=Sum(
                Case(
                    When(is_right=0, answer_id__isnull=False, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of wrong (is_right=0)
            marks=Sum('marks'),  # Sum of marks
        )
    )
    section_data = (
        Student_Answers.objects
        .filter(student_id=student_id, question__section_id=section_id,mock_test__id=exam_id,result__is_end=1,result__not_accessible=0)
        .values('question__sub_section__id', 'question__sub_section__sub_section_name')
        .annotate(total_question=Count('question__sub_section__id'))
    )
    context = {
        'status': 'Success',
        'section_statistics':list(section_statistics),
        'section_id':section_id,
        'exam_id':exam_id,
        'student_id':student_id,
        'section_stats':list(exam_wise_stats),
        'difficulty_wise_statistics':list(difficulty_wise_statistics),
        # 'topic_statistics':list(topic_statistics),
        'filter_condition':filter_condition,
        'section_data':list(section_data)
    }
    
    # return JsonResponse(context)
    # return JsonResponse(context)
    return render(request,'admin/exam_section_analysis.html',context)

def exam_filter_analysis(request):
    student_id = request.POST.get('student_id')
    section_id = request.POST.get('section_id')
    sub_section_id = request.POST.get('sub_section_id')
    exam_id = request.POST.get('exam_id')
    topic_id = request.POST.get('topic_id')
    filter_type = request.POST.get('filter_type')
    filter_condition = {}
    filter_condition['result__is_end'] = 1
    filter_condition['result__not_accessible'] = 0
    if exam_id:
        filter_condition['mock_test__id'] = exam_id
    if section_id:
        filter_condition['question__section_id'] = section_id
    if sub_section_id:
        filter_condition['question__sub_section_id'] = sub_section_id
    if topic_id:
        filter_condition['question__topic_id'] = topic_id

    if filter_type == 'Topic':
        section_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id, **filter_condition)
        .values('question__topic__id', 'question__topic__topic_name')
        .annotate(
            total_question=Count('question__topic__id'),  # Count total questions per topic
            attempted=Sum('attempted'),  # Sum of attempted
            right=Sum(
                Case(
                    When(is_right=1, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of right (is_right=1)
            wrong=Sum(
                Case(
                    When(is_right=0, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of wrong (is_right=0)
            marks=Sum('marks'),
            time_taken=Sum('time_taken'),  # Time taken for each question
            marks_per_minute=Case(
                When(time_taken__gt=0, then=(F('marks') / (F('time_taken') / 60))),
                default=0,
                output_field=fields.FloatField(),
            ),
            right_accuracy=Case(
                When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                default=0,
                output_field=fields.FloatField(),
            ),
            avg_attempt=Case(
                When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                default=0,
                output_field=fields.FloatField(),
            )
        )
    )
    else:
        section_statistics = (
        Student_Answers.objects
        .filter(student_id=student_id, **filter_condition)
        .values('question__sub_section_id', 'question__sub_section__sub_section_name')
        .annotate(
            total_question=Count('question__sub_section_id'),  # Count total questions
            attempted=Sum('attempted'),  # Sum of attempted
            right=Sum(
                Case(
                    When(is_right=1, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of right (is_right=1)
            wrong=Sum(
                Case(
                    When(is_right=0, then=1), default=0, output_field=IntegerField()
                )
            ),  # Sum of wrong (is_right=0)
            marks=Sum('marks'),
            time_taken=Sum('time_taken'),  # Time taken for each question
            )
            .annotate(
                marks_per_minute=Case(
                When(time_taken__gt=0, then=(F('marks') / (F('time_taken') / 60))),
                default=0,
                output_field=fields.FloatField(),
            ),
                right_accuracy=Case(
                    When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
                    default=0,
                    output_field=fields.FloatField(),
                ),
                avg_attempt=Case(
                    When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
                    default=0,
                    output_field=fields.FloatField(),
                )
            )
            # .values('question__sub_section_id', 'question__sub_section__sub_section_name', 'total_question', 'attempted', 'right', 'wrong', 'marks', 'time_taken', 'marks_per_minute', 'right_accuracy', 'avg_attempt')
            # .distinct()  # Add this line to ensure distinct topics in the result
  
        )
    
    # topic_statistics = (
    #     Student_Answers.objects
    #     .filter(student_id=student_id)
    #     .values('question__topic_id', 'question__topic__topic_name')  # Group by topic_id and select topic_name
    #     .annotate(
    #         total_question=Count('question_id'),  # Count total questions
    #         attempted=Sum('attempted'),  # Sum of attempted
    #         right=Sum(
    #             Case(
    #                 When(is_right=1, then=1), default=0, output_field=IntegerField()
    #             )
    #         ),  # Sum of right (is_right=1)
    #         wrong=Sum(
    #             Case(
    #                 When(is_right=0, then=1), default=0, output_field=IntegerField()
    #             )
    #         ),  # Sum of wrong (is_right=0)
    #         marks=Sum('marks'),
    #         time_taken=F('time_taken'),  # Time taken for each question
    #         )
    #         .annotate(
    #             marks_per_minute=ExpressionWrapper(
    #                 F('marks') / (F('time_taken') / 60),  # Calculate marks per minute for each question
    #                 output_field=fields.FloatField(),
    #             ),
    #             right_accuracy=Case(
    #                 When(attempted__gt=0, then=(F('right') / F('attempted')) * 100),
    #                 default=0,
    #                 output_field=fields.FloatField(),
    #             ),
    #             avg_attempt=Case(
    #                 When(attempted__gt=0, then=(F('attempted') / F('total_question')) * 100),
    #                 default=0,
    #                 output_field=fields.FloatField(),
    #             )
    #         )
    # )
    context = {
        'status': 'success',
        'section_statistics':list(section_statistics),
        'filter_type':filter_type,
        'section_id':section_id,
        'exam_id':exam_id
        # 'topic_statistics':list(topic_statistics),
    }
    # return JsonResponse(context)
    return render(request,'dashboard/exam_filter_analysis.html',context)

def reset_student_exam(request):
    student_id = request.POST.get('student_id')
    exam_id = request.POST.get('exam_id')
    Student_Answers.objects.filter(student_id=student_id,mock_test_id=exam_id).delete()
    Section_Test_Result.objects.filter(student_id=student_id,mock_test_id=exam_id).delete()
    Test_Result.objects.filter(student_id=student_id,mock_test_id=exam_id).delete()
    context = {
        'status': 'success'
    }
    return JsonResponse(context)

# resources module

@login_required(login_url=ADMIN_LOGIN_URL)
def resources(request):
    name = request.GET.get('name', '')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')

    all_resources = Resources.objects.all()

    # Apply filters
    if name:
        all_resources = all_resources.filter(title__icontains=name)


    if from_date:
        all_resources = all_resources.filter(created_at__gte=from_date)
    if to_date:
        all_resources = all_resources.filter(created_at__lte=to_date)

    all_resources = all_resources.order_by('-id')

    # Pagination
    paginator = Paginator(all_resources, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    context = {
        'all_resources': page_obj,
        'name': name,
        'from_date': from_date,
        'to_date': to_date,
    }

    return render(request, "resources.html", context)

@login_required(login_url=ADMIN_LOGIN_URL)
def addResources(request):
    if request.method == 'POST':
        title = request.POST.get('name', '').strip()
        resource = request.FILES.get('resource')

        # Validation
        if not title:
            return JsonResponse({'status': 'error', 'msg': 'Please enter a title'})
        if not resource:
            return JsonResponse({'status': 'error', 'msg': 'Please select a resource'})

        # Generate unique datetime filename
        ext = os.path.splitext(resource.name)[1]  # e.g. ".pdf"
        new_filename = datetime.now().strftime("%Y%m%d%H%M%S") + ext

        # Apply new filename
        resource.name = new_filename

        # Save model
        Resources.objects.create(
            title=title,
            resources=resource,
        )

        return JsonResponse({'status': 'success', 'msg': 'Resource uploaded successfully.'})

    # For non-POST
    return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})



@login_required(login_url=ADMIN_LOGIN_URL)
def editResources(request):
    if request.method == 'POST':
        title = request.POST.get('name', '').strip()
        res_id = request.POST.get('id')
        resource_file = request.FILES.get('resource')  # may be None

        # Basic validation
        if not title:
            return JsonResponse({'status': 'error', 'msg': 'Please enter a Title'})

        # Fetch existing resource
        resource_obj = get_object_or_404(Resources, pk=res_id)

        # Update title
        resource_obj.title = title

        # If new file uploaded, rename and replace
        if resource_file:
            ext = os.path.splitext(resource_file.name)[1]  # e.g. ".pdf"
            new_filename = datetime.now().strftime("%Y%m%d%H%M%S") + ext
            resource_file.name = new_filename
            resource_obj.resources = resource_file

        # Save only changed fields
        if resource_file:
            resource_obj.save(update_fields=["title", "resources"])
        else:
            resource_obj.save(update_fields=["title"])

        return JsonResponse({'status': 'success', 'msg': 'Resource updated successfully.'})

    return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def deleteResources(request):
    id = request.POST.get('id')
    Resources(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'deleted successfully.'})


#new code


# Foundation videos
@login_required(login_url=ADMIN_LOGIN_URL)
def foundation_videos_list(request):
    topic_id = request.GET.get('topic_id')
    videos = FoundationVideos.objects.filter(topic_id=topic_id).all()
    topics = Topics.objects.all()
    return render(request, 'foundation_videos_list.html', {'videos': videos, 'topics': topics, 'topic_id':topic_id})
    
# @login_required(login_url=ADMIN_LOGIN_URL)
# def add_foundation_video(request):
#     if request.method == 'POST':
#         title = request.POST.get('title')
#         topic_id = request.POST.get('topic_id')
#         video = request.FILES.get('video')
#         if title and video:
#             video_duration = 0
#             # try:
#             #     video_clip = VideoFileClip(video.temporary_file_path())
#             #     video_duration = video_clip.duration  # duration in seconds
#             #     video_clip.close()
#             # except Exception as e:
#             #     return JsonResponse({'status': 'error', 'msg': f'Error processing video: {str(e)}'})

#             FoundationVideos.objects.create(
#                 topic_id=topic_id,
#                 video_title=title,
#                 video_file=video,
#                 video_duration=video_duration
#                 )
#             return JsonResponse({'status': 'success'}, status=200)
#         else:
#             return JsonResponse({'status': 'error', 'errors': 'All fields are required'}, status=400)
#     return JsonResponse({'status': 'invalid_method'}, status=405)

# @login_required(login_url=ADMIN_LOGIN_URL)
# def edit_foundation_video(request):
#     if request.method == 'POST':
#         video_id = request.POST.get('id')
#         title = request.POST.get('title')
#         topic_id = request.POST.get('topic_id')
#         video_file = request.FILES.get('video')
#         video = get_object_or_404(FoundationVideos, id=video_id)
#         if title:
#             video.video_title = title
#             video.topic_id = topic_id
#             video_duration = 0
#             # try:
#             #     video_clip = VideoFileClip(video.temporary_file_path())
#             #     video_duration = video_clip.duration  # duration in seconds
#             #     video_clip.close()
#             # except Exception as e:
#             #     return JsonResponse({'status': 'error', 'msg': f'Error processing video: {str(e)}'})

#             if video_file:
#                 video.video_file = video_file
#                 video.video_duration=video_duration
#             video.save()
#             return JsonResponse({'status': 'success'}, status=200)
#         else:
#             return JsonResponse({'status': 'error', 'errors': 'Title and description are required'}, status=400)
#     return JsonResponse({'status': 'invalid_method'}, status=405)

# @login_required(login_url=ADMIN_LOGIN_URL)
# def delete_foundation_video(request):
#     if request.method == 'POST':
#         video_id = request.POST.get('id')
#         video = get_object_or_404(FoundationVideos, id=video_id)
#         video.delete()
#         return JsonResponse({'status': 'success'}, status=200)
#     return JsonResponse({'status': 'invalid_method'}, status=405)



@login_required(login_url=ADMIN_LOGIN_URL)
def exam_syllabus(request, id):
    id=id
    # Get a list of sections where category_id matches the provided id
    section_list = Category_Subjects.objects.filter(category_id=id)
    topics = Topics.objects.all()
    
    # Fetch sub-sections for each section in section_list
    sub_section_dict = {}
    for section in section_list:
        sub_sections = Sub_Sections.objects.filter(section_id=section.section.id)
        sub_section_dict[section.section.id] = sub_sections

    return render(request, 'add-exam-syllabus.html', {
        'section_list': section_list,
        'sub_section_dict': sub_section_dict,
        'topics':topics,
        'id':id
    })




def get_sub_sections(request, section_id):
    # Fetch sub-sections where section matches section_id
    sub_sections = Sub_Sections.objects.filter(section_id=section_id).values('id', 'sub_section_name')
    print(sub_sections.sub_section_name)
    # Return the sub-sections as a JSON response
    return JsonResponse(list(sub_sections), safe=False)




@login_required(login_url=ADMIN_LOGIN_URL)
def get_topics(request, sub_section_id):
    # Fetch topics based on the selected sub-section
    topics = Topics.objects.filter(sub_section_id=sub_section_id)  # Adjust based on your model structure
    topic_list = [{"id": topic.id, "topic_name": topic.topic_name} for topic in topics]
    return JsonResponse(topic_list, safe=False)


def add_exam_syllabus(request):
    if request.method != "POST":
        return JsonResponse({"status": "error", "msg": "Invalid request method"}, status=405)

    exam_category_id = request.POST.get("exam_category_id") or request.POST.get("exam-category-id")
    payload_str = request.POST.get("syllabus_payload", "[]")

    try:
        payload = json.loads(payload_str)
    except json.JSONDecodeError:
        return JsonResponse({"status": "error", "msg": "Invalid syllabus payload JSON"})

    if not payload:
        return JsonResponse({"status": "error", "msg": "No syllabus mappings provided"})

    try:
        created_count = 0
        updated_count = 0

        for row in payload:
            section_id = row.get("section_id")
            sub_section_id = row.get("sub_section_id")
            topic_ids = row.get("topic_ids", []) or []
            dependency_ids = row.get("dependent_topic_ids", []) or []

            if not section_id or not sub_section_id or not topic_ids:
                continue

            section = Sections.objects.get(id=section_id)
            sub_section = Sub_Sections.objects.get(id=sub_section_id)

            dep_qs = Topics.objects.filter(id__in=dependency_ids)

            for topic_id in topic_ids:
                topic_id = str(topic_id).strip()

                obj = ExamSyllabus.objects.filter(
                    exam_category_id=exam_category_id,
                    section=section,
                    sub_section=sub_section,
                    topic_id=topic_id
                ).first()

                if obj:
                    # ✅ add only new deps (M2M ignores duplicates)
                    obj.dependent_topics.add(*dep_qs)
                    updated_count += 1
                else:
                    obj = ExamSyllabus.objects.create(
                        exam_category_id=exam_category_id,
                        section=section,
                        sub_section=sub_section,
                        topic_id=topic_id
                    )
                    if dependency_ids:
                        obj.dependent_topics.add(*dep_qs)
                    created_count += 1

        return JsonResponse({
            "status": "success",
            "msg": f"Saved. New topics: {created_count}, Updated dependencies: {updated_count}"
        })

    except Sections.DoesNotExist:
        return JsonResponse({"status": "error", "msg": "Invalid section id in payload"})
    except Sub_Sections.DoesNotExist:
        return JsonResponse({"status": "error", "msg": "Invalid sub-section id in payload"})
    except Exception as e:
        return JsonResponse({"status": "error", "msg": str(e)})

# @login_required(login_url=ADMIN_LOGIN_URL)
# def foundation_videos_list(request):
#     # all_section = Sections.objects.all();

#     video_title=request.GET.get('video_title')
#     category_id=request.GET.get('category_id')
#     section_id=request.GET.get('section_id')
#     sub_section_id=request.GET.get('sub_section_id')
#     topic_id=request.GET.get('topic_id')
#     status=request.GET.get('status')
#     from_date=request.GET.get('from_date')
#     to_date=request.GET.get('to_date')
#     filter = ''

#     categories = ExamCategory.objects.all()
#     sections = Sections.objects.all()
#     sub_sections = Sub_Sections.objects.all()
#     topics = Topics.objects.all()
#     all_foundation_videos = Foundation_Videos_New.objects.filter().order_by('-id')

#     if video_title:
#         all_section = all_section.filter(video_title__icontains=video_title)
#         filter=filter+'&name='+video_title
#     if category_id:
#         all_exams = all_exams.filter(exam_category_id=category_id)
#     if status:
#         all_section = all_section.filter(status=status)
#         filter=filter+'&status='+status
#     if from_date:
#         all_section = all_section.filter(created_at__gte=from_date)
#         filter=filter+'&from_date='+from_date
#     if to_date:
#         all_section = all_section.filter(created_at__lte=to_date)
#         filter=filter+'&to_date='+to_date

#     paginator = Paginator(all_foundation_videos, 10)  # Show 25 contacts per page.

#     page_number = request.GET.get("page")
#     page_obj = paginator.get_page(page_number)

#     return render(request, "foundation_videos.html",{'all_foundation_videos':page_obj,'page':page_number,'filter':filter,'from_date':from_date,'to_date':to_date,'name':name,'categories':categories,'sections':sections,'sub_sections':sub_sections,'topics':topics});


@login_required(login_url=ADMIN_LOGIN_URL)
def foundation_videos_list(request):
    video_title = request.GET.get('video_title')
    category_id = request.GET.get('category_id')
    section_id = request.GET.get('section_id')
    sub_section_id = request.GET.get('sub_section_id')
    topic_id = request.GET.get('topic_id')
    # status = request.GET.get('status')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    filter_params = ''

    # Fetch all categories, sections, sub-sections, and topics for filters
    categories = ExamCategory.objects.all()
    sections = Sections.objects.all()
    sub_sections = Sub_Sections.objects.all()
    topics = Topics.objects.all()

    # Query all foundation videos, ordered by ID descending
    all_foundation_videos = Foundation_Videos_New.objects.all().order_by('-id')

    # Apply filters based on user input
    if video_title:
        all_foundation_videos = all_foundation_videos.filter(video_title__icontains=video_title)
        filter_params += '&video_title=' + video_title

    if category_id:
        all_foundation_videos = all_foundation_videos.filter(
            foundation_video_categories__category_id=category_id
        )
        filter_params += '&category_id=' + category_id

    if section_id:
        all_foundation_videos = all_foundation_videos.filter(section_id=section_id)
        filter_params += '&section_id=' + section_id

    if sub_section_id:
        all_foundation_videos = all_foundation_videos.filter(sub_section_id=sub_section_id)
        filter_params += '&sub_section_id=' + sub_section_id

    if topic_id:
        all_foundation_videos = all_foundation_videos.filter(topic_id=topic_id)
        filter_params += '&topic_id=' + topic_id

    # if status:
    #     all_foundation_videos = all_foundation_videos.filter(status=status)
    #     filter_params += '&status=' + status

    if from_date:
        all_foundation_videos = all_foundation_videos.filter(created_at__gte=from_date)
        filter_params += '&from_date=' + from_date

    if to_date:
        all_foundation_videos = all_foundation_videos.filter(created_at__lte=to_date)
        filter_params += '&to_date=' + to_date

    # Pagination
    paginator = Paginator(all_foundation_videos, 10)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    # Render the template with the filtered data
    return render(request, "foundation_videos.html",
        {'all_foundation_videos': page_obj,
        'page': page_number,
        'filter': filter_params,
        'from_date': from_date,
        'to_date': to_date,
        'video_title': video_title,
        'categories': categories,
        'sections': sections,
        'sub_sections': sub_sections,
        'topics': topics,
        'category_id':category_id,
        'section_id':section_id,
        'video_title':video_title,
        'sub_section_id':sub_section_id,
        'topic_id':topic_id,
        'from_date':from_date,
        'to_date':to_date
    })


# @login_required(login_url=ADMIN_LOGIN_URL)
# def category_selct(request):
#     category_id = request.GET.get('category')
#     sections = ExamSyllabus.objects.filter(exam_category_id=category_id)
#     return render(request, "select_section.html" ,{'sections':sections})


# @login_required(login_url=ADMIN_LOGIN_URL)
# def add_foundation_video(request):
#     if request.method == 'POST':
#         title = request.POST.get('title')
#         category_id = request.POST.get('category_id')
#         section_id = request.POST.get('section_id')
#         sub_section_id = request.POST.get('sub_section_id')
#         topic_id = request.POST.get('topic')
#         video = request.FILES.get('video')
#         if title and video:
#             video_duration = 0
#             # try:
#             #     video_clip = VideoFileClip(video.temporary_file_path())
#             #     video_duration = video_clip.duration  # duration in seconds
#             #     video_clip.close()
#             # except Exception as e:
#             #     return JsonResponse({'status': 'error', 'msg': f'Error processing video: {str(e)}'})

#             Foundation_Videos_New.objects.create(
#                 topic_id=topic_id,
#                 video_title=title,
#                 category_id=category_id,
#                 section_id=section_id,
#                 sub_section_id=sub_section_id,
#                 video_file=video,
#                 video_duration=video_duration
#                 )
#             return JsonResponse({'status': 'success'}, status=200)
#         else:
#             return JsonResponse({'status': 'error', 'errors': 'All fields are required'}, status=400)
#     return JsonResponse({'status': 'invalid_method'}, status=405)



@login_required(login_url=ADMIN_LOGIN_URL)
def add_foundation_video(request):
    if request.method == 'POST':
        title = request.POST.get('title')
        category_ids = request.POST.getlist('category_id[]')  # Retrieve multiple category IDs
        section_id = request.POST.get('section_id')
        sub_section_id = request.POST.get('sub_section_id')
        topic_id = request.POST.get('topic')
        video = request.FILES.get('video')

        if title and video and category_ids:
            video_duration = 0
            # Uncomment the video processing code if needed
             
            # Create the video record
            foundation_video = Foundation_Videos_New.objects.create(
                topic_id=topic_id,
                video_title=title,
                section_id=section_id,
                sub_section_id=sub_section_id,
                video_file=video,
                video_duration=video_duration
            )

            foundation_video_id=foundation_video.id

            # Add multiple categories to the ManyToManyField
            for cat_id in category_ids:
                    Foundation_Video_Categories.objects.create(
                        foundation_video_id=foundation_video_id,
                        category_id=cat_id
                    )

            
        


            # foundation_video.category.set(category_ids)

            return JsonResponse({'status': 'success'}, status=200)
        else:
            return JsonResponse({'status': 'error', 'errors': 'All fields are required'}, status=400)

    return JsonResponse({'status': 'invalid_method'}, status=405)


@login_required(login_url=ADMIN_LOGIN_URL)
def edit_foundation_video(request):
    if request.method == 'POST':
        video_id = request.POST.get('id')
        title = request.POST.get('title')
        # category_id = request.POST.get('category_id')
        category_ids = request.POST.getlist('category_id[]')
        section_id = request.POST.get('section_id')
        sub_section_id = request.POST.get('sub_section_id')
        topic_id = request.POST.get('topic')
        video_file = request.FILES.get('video')
        video = get_object_or_404(Foundation_Videos_New, id=video_id)
        if title:
            video.video_title = title
            # video.category_id = category_id
            video.section_id = section_id
            video.sub_section_id = sub_section_id
           
            video.topic_id = topic_id
            video_duration = 0
            # try:
            #     video_clip = VideoFileClip(video.temporary_file_path())
            #     video_duration = video_clip.duration  # duration in seconds
            #     video_clip.close()
            # except Exception as e:
            #     return JsonResponse({'status': 'error', 'msg': f'Error processing video: {str(e)}'})
            
            foundation_video_id=video_id
            Foundation_Video_Categories.objects.filter(foundation_video_id=foundation_video_id).delete()
            # Add multiple categories to the ManyToManyField
            for cat_id in category_ids:
                    Foundation_Video_Categories.objects.create(
                        foundation_video_id=foundation_video_id,
                        category_id=cat_id
                    )

            if video_file:
                video.video_file = video_file
                video.video_duration=video_duration
                video.save(update_fields=["video_file", "video_duration","video_title","section_id","topic_id","sub_section_id"])
            else:
                video.save(update_fields=["video_title","section_id","topic_id","sub_section_id"])
            
            return JsonResponse({'status': 'success'}, status=200)
        else:
            return JsonResponse({'status': 'error', 'errors': 'Title and description are required'}, status=400)
    return JsonResponse({'status': 'invalid_method'}, status=405)


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_foundation_video(request):
    if request.method == 'POST':
        video_id = request.POST.get('id')
        video = get_object_or_404(Foundation_Videos_New, id=video_id)
        video.soft_delete()
        return JsonResponse({'status': 'success'}, status=200)
    return JsonResponse({'status': 'invalid_method'}, status=405)


# def category_selct(request):
#     categories = request.GET.getlist('categories[]')  # Get the list of selected categories
#     sections_list = Sections.objects.all()
#     print("mutiple exm categories :- ",categories)
#     section = Category_Subjects.objects.filter(category__in=categories).distinct()  # Query for common sections

#     section_options = ''.join([f'<option value="{section.id}">{section.section_name}</option>' for section in sections_list])
    
#     return JsonResponse(section_options, safe=False)




def category_selct(request):
    categories = request.GET.getlist('categories[]')  # Get the list of selected categories
    print("mutiple exm categories :- ",categories)
    if categories:
        # Get sections that are linked to all selected categories
        sections = Category_Subjects.objects.filter(category__in=categories) \
            .values('section') \
            .annotate(category_count=Count('category')) \
            .filter(category_count=len(categories))  # Filter sections appearing in all selected categories
        
        # Fetch section names and IDs
        section_ids = [item['section'] for item in sections]
        sections_data = Sections.objects.filter(id__in=section_ids)
        
        # Build options for the dropdown
        section_options = ''.join([f'<option value="{section.id}">{section.section_name}</option>' for section in sections_data])
        
        return JsonResponse(section_options, safe=False)
    
    return JsonResponse('', safe=False)


@login_required(login_url=ADMIN_LOGIN_URL)
def question_tag_list(request):
    tag_title = request.GET.get('tag_title')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    filter_params = ''
   
    all_question_tags = Questions_Tags.objects.all().order_by('-id')

    if tag_title:
        all_question_tags = all_question_tags.filter(tag_title__icontains=tag_title)
        filter_params += '&tag_title=' + tag_title


    if from_date:
        all_question_tags = all_question_tags.filter(created_at__gte=from_date)
        filter_params += '&from_date=' + from_date

    if to_date:
        all_question_tags = all_question_tags.filter(created_at__lte=to_date)
        filter_params += '&to_date=' + to_date

    # Pagination
    paginator = Paginator(all_question_tags, 10)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    # Render the template with the filtered data
    return render(request, "question_tag_list.html",
        {'all_question_tags': page_obj,
        'page': page_number,
         'filter': filter_params,
        'from_date': from_date,
        'to_date': to_date,
        'tag_title': tag_title,

    })


@login_required(login_url=ADMIN_LOGIN_URL)
def add_question_tag(request):
    if request.method == 'POST':
        tag_title = request.POST.get('title')
        video = request.FILES.get('video')
        q_tag = Questions_Tags.objects.create(
                tag_title=tag_title,
                video_file=video,
            )

          
            # foundation_video.category.set(category_ids)

        return JsonResponse({'status': 'success'}, status=200)
        

    return JsonResponse({'status': 'invalid_method'}, status=405)


@login_required(login_url=ADMIN_LOGIN_URL)
def edit_question_tag(request):
    if request.method == 'POST':
        # Retrieve ID of the tag to be edited
        tag_id = request.POST.get('id')
        
        # Check if the tag exists
        try:
            q_tag = Questions_Tags.objects.get(id=tag_id)
        except Questions_Tags.DoesNotExist:
            return JsonResponse({'status': 'not_found'}, status=404)

        # Update fields if provided in the POST request
        tag_title = request.POST.get('title')
        video = request.FILES.get('video')

        if tag_title:
            q_tag.tag_title = tag_title
        if video:
            q_tag.video_file = video

        # Save the updated fields to the database
        q_tag.save(update_fields=["tag_title", "video_file"])

        return JsonResponse({'status': 'success'}, status=200)

    return JsonResponse({'status': 'invalid_method'}, status=405)


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_question_tag(request):
    if request.method == 'POST':
        tag_id = request.POST.get('id')
        
        # Attempt to retrieve and delete the tag
        try:
            q_tag = Questions_Tags.objects.get(id=tag_id)
            q_tag.soft_delete()
            return JsonResponse({'status': 'success'}, status=200)
        except Questions_Tags.DoesNotExist:
            return JsonResponse({'status': 'not_found'}, status=404)

    return JsonResponse({'status': 'invalid_method'}, status=405)




@login_required(login_url=ADMIN_LOGIN_URL)
def add_on_course_list(request):

    name = request.GET.get('name')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    filter_params = ''

    course_list = Add_On_Courses.objects.all().order_by('-id')
   
    if name:
        course_list = course_list.filter(name__icontains=name)
        filter_params += '&name=' + name


    if from_date:
        course_list = course_list.filter(created_at__gte=from_date)
        filter_params += '&from_date=' + from_date

    if to_date:
        course_list = course_list.filter(created_at__lte=to_date)
        filter_params += '&to_date=' + to_date

    
    # Pagination
    paginator = Paginator(course_list, 10)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    # Render the template with the filtered data
    return render(request, "add-on-course.html",
        {'course_list': page_obj,
        'page': page_number,
        })


@login_required(login_url=ADMIN_LOGIN_URL)
def create_add_on_course(request):
    all_courses = Course.objects.all()
    if request.method == 'POST':
        name = request.POST.get('course_name')
        course_ids = request.POST.getlist('course_ids')
        full_price = request.POST.get('full_price')
        status = request.POST.get('status')
        prices = request.POST.getlist('price[]')

        # Create the main Add-On Course object
        course = Add_On_Courses.objects.create(
            name=name,
            price=full_price,
            status=status
        )

        # Save each related course with its price
        for i in range(len(course_ids)):
            Add_On_Courses_Course_Ids.objects.create(
                add_on_course=course,
                course_id=course_ids[i],
                price=prices[i]
            )

        # Add success message
        messages.success(request, 'Add-On Course created successfully.')

        return redirect('add_on_course_list')

    return render(request, 'create-add-on-course.html', {'all_courses': all_courses})  # foundation_video.category.set(category_ids)






@login_required(login_url=ADMIN_LOGIN_URL)
def delete_add_on_course(request):
    if request.method == 'POST':
        tag_id = request.POST.get('id')
        
        # Attempt to retrieve and delete the tag
        try:
            id = Add_On_Courses.objects.get(id=tag_id)
            id.soft_delete()
            return JsonResponse({'status': 'success'}, status=200)
        except Add_On_Courses.DoesNotExist:
            return JsonResponse({'status': 'not_found'}, status=404)

    return JsonResponse({'status': 'invalid_method'}, status=405)



@login_required(login_url=ADMIN_LOGIN_URL)
def section_selct1(request):
    exam_id = request.GET.getlist('exam_id[]')
    sections = Category_Subjects.objects.filter(category_id__in=exam_id).distinct()
    section_ids = Category_Subjects.objects.filter(
        category_id__in=exam_id
    ).values_list('section_id', flat=True).distinct()
    sections = Sections.objects.filter(id__in=section_ids)

    print(sections)
    return render(request, "section_for_timer.html" ,{'sections':sections})




#Zoom keys

@login_required(login_url=ADMIN_LOGIN_URL)
def zoomkeys(request):
    all_zoomkeys = ZoomKeys.objects.filter().order_by('-id')
    paginator = Paginator(all_zoomkeys, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "zoomkeys.html",{'all_zoomkeys':page_obj,'page':page_number})

@login_required(login_url=ADMIN_LOGIN_URL)
def add_zoomkey(request):
    if request.method == 'POST':
    
        meet_id = request.POST.get('meet_id')
        account_id = request.POST.get('account_id')
        nick_name = request.POST.get('nick_name')
        client_id = request.POST.get('client_id')
        client_secret = request.POST.get('client_secret')
        sdk_key = request.POST.get('sdk_key')
        sdk_secret = request.POST.get('sdk_secret')
        app_sdk_key = request.POST.get('app_sdk_key')
        app_sdk_secret = request.POST.get('app_sdk_secret')
        status = request.POST.get('status')

        try:
            zoomkey = ZoomKeys.objects.create(
                meet_id=meet_id,
                account_id=account_id,
                nick_name=nick_name,
                client_id=client_id,
                client_secret=client_secret,
                sdk_key=sdk_key,
                sdk_secret=sdk_secret,
                app_sdk_key=app_sdk_key,
                app_sdk_secret=app_sdk_secret,
                status=status
            )

            return JsonResponse({'status': 'success', 'msg': 'Zoom Key Added Successfully'})

        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)})

    return JsonResponse({'status': 'error', 'msg': 'Invalid Request Method'})
  
@login_required(login_url=ADMIN_LOGIN_URL)
def edit_zoomkey(request):
    if request.method == 'POST':
        zoomkey_id = request.POST.get('id')
        meet_id = request.POST.get('meet_id')
        account_id = request.POST.get('account_id')
        nick_name = request.POST.get('nick_name')
        client_id = request.POST.get('client_id')
        client_secret = request.POST.get('client_secret')
        sdk_key = request.POST.get('sdk_key')
        sdk_secret = request.POST.get('sdk_secret')
        app_sdk_key = request.POST.get('app_sdk_key')
        app_sdk_secret = request.POST.get('app_sdk_secret')
        status = request.POST.get('status')

        # Get the ZoomKey object
        try:
            zoomkey = ZoomKeys.objects.get(id=zoomkey_id)

            # Update the fields
            zoomkey.meet_id = meet_id
            zoomkey.account_id = account_id
            zoomkey.nick_name = nick_name
            zoomkey.client_id = client_id
            zoomkey.client_secret = client_secret
            zoomkey.sdk_key = sdk_key
            zoomkey.sdk_secret = sdk_secret
            zoomkey.app_sdk_key = app_sdk_key
            zoomkey.app_sdk_secret = app_sdk_secret
            zoomkey.status = status
            zoomkey.save()  # Save the updated ZoomKey

            return JsonResponse({'status': 'success', 'msg': 'ZoomKey Updated Successfully'})

        except ZoomKeys.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'ZoomKey not found'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)})

    return JsonResponse({'status': 'error', 'msg': 'Invalid Request Method'})




@login_required(login_url=ADMIN_LOGIN_URL)
def counseling_groups(request):
    all_groups = CounselingGroup.objects.all().order_by('-id')
    exams = ExamCategory.objects.all()
    print(exams)
    paginator = Paginator(all_groups, 10)  # 10 items per page
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "counseling-group.html", {'all_groups': page_obj, 'page': page_number,'exams': exams})


@login_required(login_url=ADMIN_LOGIN_URL)
def add_counseling_group(request):
    if request.method == 'POST':
        title = request.POST.get('title')
        min_marks = request.POST.get('min_marks')
        max_marks = request.POST.get('max_marks')
        attempted_mocks = request.POST.get('attempted_mocks')
        mock_deadline = request.POST.get('mock_deadline')
        exam_id = request.POST.get('exam')
        status = request.POST.get('status')

        if not (title and min_marks and max_marks and attempted_mocks and mock_deadline and exam_id):
            return JsonResponse({'status': 'error', 'msg': 'Please fill all required fields.'})

        try:
            exam = ExamCategory.objects.get(id=exam_id)
            CounselingGroup.objects.create(
                title=title,
                min_marks=min_marks,
                max_marks=max_marks,
                attempted_mocks=attempted_mocks,
                exam=exam,
                mock_deadline=mock_deadline,
                status=status
            )
            return JsonResponse({'status': 'success'})
        except ExamCategory.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Invalid Exam selected.'})
    return JsonResponse({'status': 'error', 'msg': 'Invalid Request'})

@login_required(login_url=ADMIN_LOGIN_URL)
def edit_counseling_group(request):
    if request.method == 'POST':
        group_id = request.POST.get('id')
        title = request.POST.get('title')
        min_marks = request.POST.get('min_marks')
        max_marks = request.POST.get('max_marks')
        attempted_mocks = request.POST.get('attempted_mocks')
        mock_deadline = request.POST.get('mock_deadline')
        exam_id = request.POST.get('exam')
        status = request.POST.get('status')

        try:
            group = CounselingGroup.objects.get(id=group_id)

            group.title = title
            group.min_marks = min_marks
            group.max_marks = max_marks
            group.attempted_mocks = attempted_mocks
            group.mock_deadline = mock_deadline
            group.exam_id = exam_id
            group.status = status
            group.save()

            return JsonResponse({'status': 'success', 'msg': 'Counseling Group Updated Successfully'})

        except CounselingGroup.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Counseling Group not found'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)})

    return JsonResponse({'status': 'error', 'msg': 'Invalid Request Method'})
    
#@login_required(login_url=ADMIN_LOGIN_URL)
def app_notification(request):
    # all_notification = Notifications.objects.all()
    batch_list = Batch_Management.objects.filter(status='Active')
    batch=request.GET.get('batch')
    from_date=request.GET.get('from_date')
    to_date=request.GET.get('to_date')
    filter = ''
    all_notification = AppNotifications.objects.filter().order_by('-id')
    exams = ExamCategory.objects.all()
    courses = Course.objects.all()

    if batch:

        if batch == "open_for_all":
            all_notification = all_notification.filter(notification_type=batch)
            filter=filter+'&batch='+batch
        elif batch == "all_batches":
            all_notification = all_notification.filter(notification_type=batch)
            filter=filter+'&batch='+batch
        else:
            all_notification = all_notification.filter(appnotifications_batches__batches_id=batch)
            filter=filter+'&batch='+batch

    if from_date:
        all_notification = all_notification.filter(created_at__gte=from_date)
        filter=filter+'&from_date='+from_date
    if to_date:
        all_notification = all_notification.filter(created_at__lte=to_date)
        filter=filter+'&to_date='+to_date

    paginator = Paginator(all_notification, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    return render(request, "app-notification.html",{'all_notification':page_obj,'filter':filter,'page':page_number,'from_date':from_date,'to_date':to_date,'batch_list':batch_list,'batch':batch ,'exams':exams ,'courses':courses})

#@login_required(login_url=ADMIN_LOGIN_URL)
def add_app_notification(request):
    if request.method == 'POST':
        title = request.POST.get('title')
        message = request.POST.get('message')
        batches = request.POST.getlist('batch')
        exams = request.POST.getlist('exams')
        courses = request.POST.getlist('courses')
        notification_type = request.POST.get('notification_type')
        redirect_url = request.POST.get('redirect_url')

        if not message:
            return JsonResponse({'status': 'error', 'msg': 'Please enter Message.'})

        notification = AppNotifications.objects.create(
            message=message,
            notification_type=notification_type,
            redirect_url=redirect_url
        )

        # Save each batch (independently)
        for batch_id in batches:
            AppNotifications_batches.objects.create(
                Notification=notification,
                batches_id=batch_id
            )

        # Save each exam (independently)
        for exam_id in exams:
            AppNotifications_batches.objects.create(
                Notification=notification,
                exams_id=exam_id
            )

        # Save each course (independently)
        for course_id in courses:
            AppNotifications_batches.objects.create(
                Notification=notification,
                courses_id=course_id
            )
        # Normalize incoming IDs to ints (ignore blanks)
        batch_ids  = [int(b) for b in batches  if str(b).strip().isdigit()]
        exam_ids   = [int(e) for e in exams    if str(e).strip().isdigit()]
        course_ids = [int(c) for c in courses  if str(c).strip().isdigit()]

        # OR-style targeting (match ANY of the selected filters)
        qs = Students.objects.all()

        if batch_ids:
            qs = qs.filter(batch__id__in=batch_ids)

        if course_ids:
            qs = qs.filter(
                Exists(
                    OrderCourses.objects.filter(
                        course_id__in=course_ids,
                        student_id=OuterRef('pk')
                    )
                )
            )

        if exam_ids:
            qs = qs.filter(
                Exists(
                    OrderCourses.objects.filter(
                        course__exams__id__in=exam_ids,
                        student_id=OuterRef('pk')
                    )
                )
            )

        students = qs.distinct()
        # Annotate each student with their latest Android device token
        students_with_tokens = students.annotate(
            latest_token=Subquery(
                Login_history.objects.filter(
                    student=OuterRef('pk'),
                    device_token__isnull=False,
                ).order_by('-id').values('device_token')[:1]
            )
        ).values_list('latest_token', flat=True).exclude(latest_token__isnull=True)

        tokens = list(filter(None, students_with_tokens))  # filter out blanks just in case
                
#        return JsonResponse({'status': 'error', 'msg': 'No valid tokens found', 'tokens':tokens})
        if tokens:
            success = 0
            failure = 0
            failed_tokens = []

            for token in tokens:
                try:
                    message_obj = messaging.Message(
                        notification=messaging.Notification(
                            title=title,
                            body=message,
                        ),
                        token=token,
                    )
                    response = messaging.send(message_obj)
                    success += 1
                except firebase_admin.exceptions.FirebaseError as e:
                    print(f"Failed to send to {token}: {e}")
                    failed_tokens.append(token)
                    failure += 1

            return JsonResponse({
                'status': 'success',
                'msg': f"Sent to {success} users. Failed: {failure}",
                'failed_tokens': failed_tokens , # optional, for debugging
                'tokens':tokens
            })

        else:
            return JsonResponse({'status': 'error', 'msg': 'No valid tokens found'})

    
    return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_counseling_group_by_id(request):
    if request.method == 'POST':
        group_id = request.POST.get('id')
        try:
            group = get_object_or_404(CounselingGroup, id=group_id)
            group.soft_delete()  # or group.status = 'inactive'; group.save()
            return JsonResponse({'status': 'success', 'msg': 'Counseling Group deleted successfully.'})
        except CounselingGroup.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Counseling Group not found.'})
    return JsonResponse({'status': 'error', 'msg': 'Invalid request method.'})


# @login_required(login_url=ADMIN_LOGIN_URL)
# def counseling_groups_detail(request, id):
#     counseling_detail = get_object_or_404(CounselingGroup, id=id)
#     counseling_students_list=CounselingGroupStudents.objects.filter(counseling_group_id=id).all()
#     exams = ExamCategory.objects.all()
#     courses = Course.objects.all()
#     all_students = Students.objects.all()

#     paginator = Paginator(counseling_students_list, 10)
#     page_number = request.GET.get("page")
#     counseling_students = paginator.get_page(page_number)
    
#     return render(request, "counseling-group-details.html", {
#         'counseling_detail': counseling_detail,
#         'courses': courses,
#         'counseling_students': counseling_students,
#         'exams': exams,
#         'all_students': all_students
#     })




@login_required(login_url=ADMIN_LOGIN_URL)
def counseling_groups_detail(request, id):
    counseling_detail = get_object_or_404(CounselingGroup, id=id)
    exams = ExamCategory.objects.all()
    courses = Course.objects.all()
    all_students = Students.objects.all()
    # Fetch counseling group students
    counseling_students_list = CounselingGroupStudents.objects.filter(counseling_group_id=id)

    paid_students = []
    unpaid_students = []

    for entry in counseling_students_list:
        student = entry.student
        is_paid = OrderCourses.objects.filter(student=student).exists()
        student_info = {
            'id': student.id,
            'name': student.first_name+' '+student.last_name,
            'is_admin': entry.is_admin,
            'paid': is_paid,
            'marks':entry.marks
        }
        if is_paid:
            paid_students.append(student_info)
        else:
            unpaid_students.append(student_info)
    return render(request, "counseling-group-details.html", {
        'counseling_detail': counseling_detail,
        'courses': courses,
        'exams': exams,
        'all_students': all_students,
        'unpaid_students':unpaid_students,
        'paid_students':paid_students
    })
    
def process_counseling_group(counseling_group):
    """
    Process students for a single counseling group:
    - Calculate recent test results.
    - Check if within min/max marks.
    - Add students to the group if criteria match.
    """
    valid_mock_types = [7, 8]
    students = Students.objects.all()

    for student in students:
        recent_results = (
            Test_Result.objects
            .filter(
                student=student,
                mock_test__examcategory=counseling_group.exam,
                mock_test__mock_type__in=valid_mock_types,
                not_accessible=0,
                is_end=1
            )
            .order_by('-created_at')[:counseling_group.attempted_mocks]
        )

        if recent_results.exists():
            total_marks = sum([res.marks for res in recent_results])
            count = recent_results.count()
            avg_marks = total_marks / count if count > 0 else 0

            if (
                counseling_group.min_marks is not None
                and counseling_group.max_marks is not None
                and counseling_group.min_marks <= avg_marks <= counseling_group.max_marks
            ):
                CounselingGroupStudents.objects.get_or_create(
                    counseling_group=counseling_group,
                    exam=counseling_group.exam,
                    student=student,
                    marks=avg_marks,
                    defaults={'is_admin': False}
                )
                
def counseling_group_students(request):
    groups = CounselingGroup.objects.all()

    for group in groups:
        process_counseling_group(group)

    return JsonResponse({'status': 'success', 'msg': 'All groups processed successfully.'})

def add_counseling_students(request):
    if request.method == 'POST':
        try:
            group_id = request.POST.get('counseling_group_id')
            course_id = request.POST.get('course')
            exam_id = request.POST.get('exam')
            student_ids = request.POST.getlist('student')
           

            if not group_id or not student_ids:
                return JsonResponse({'status': 'error', 'msg': 'Missing required fields.'})

            counseling_group = CounselingGroup.objects.get(id=group_id)
            course = Course.objects.get(id=course_id) if course_id else None
            exam = ExamCategory.objects.get(id=exam_id) if exam_id else None
            for student_id in student_ids:
                # Create CounselingGroupDetails entry
                group_detail = CounselingGroupStudents.objects.create(
                    counseling_group=counseling_group,
                    course=course,
                    exam=exam,
                    is_admin=True,
                    student_id=student_id
                )
#                group_detail.student.set(student_ids)
                group_detail.save()

            return JsonResponse({'status': 'success'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)})
    return JsonResponse({'status': 'error', 'msg': 'Invalid request method'})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_counseling_student_by_id(request):
    if request.method == 'POST':
        id = request.POST.get('id')
        print(id)
        try:
            CounselingGroupStudents.objects.get(student_id=id).delete()
            return JsonResponse({'status': 'success', 'msg': 'Student removed successfully.'})
        except CounselingGroupStudents.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Record not found.'})
    return JsonResponse({'status': 'error', 'msg': 'Invalid request.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_counseling_student_by_id(request):
    if request.method == 'POST':
        student_id = request.POST.get('id')
        counseling_group_id = request.POST.get('counseling_group_id')  # You need to send counseling_group_id in the modal
        try:
            group_detail = get_object_or_404(
                CounselingGroupStudents,
                counseling_group_id=counseling_group_id,  # Filter by counseling group
                student__id=student_id  # Filter by student ID
            )
            group_detail.student.remove(student_id)  # Remove the student from the many-to-many field
            group_detail.save()  # Save the change
            if not group_detail.student.exists():
                group_detail.soft_delete()
            return JsonResponse({'status': 'success', 'msg': 'Student removed from counseling group successfully.'})
        except CounselingGroupStudents.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Student detail not found in this counseling group.'})
    
    return JsonResponse({'status': 'error', 'msg': 'Invalid request method.'})


def get_zoom_access_token(account_id,client_id,client_secret):
    url = f"https://zoom.us/oauth/token?grant_type=account_credentials&account_id={account_id}"
    auth_header = b64encode(f"{client_id}:{client_secret}".encode()).decode()
    headers = {
        "Authorization": f"Basic {auth_header}"
    }

    response = requests.post(url, headers=headers)
    response.raise_for_status()
    return response.json()["access_token"]

@login_required(login_url=ADMIN_LOGIN_URL)
def counseling_schedule_list(request):
    all_schedules = CounselingSchedule.objects.all().order_by('-id')
    counseling_groups = CounselingGroup.objects.all()
    zoom_keys = ZoomKeys.objects.all()
    paginator = Paginator(all_schedules, 10)
    page_number = request.GET.get('page')
    all_schedules = paginator.get_page(page_number)

    return render(request, 'counseling-schedule.html', {
        'all_schedules': all_schedules,
        'paginator': paginator,
        'counseling_groups': counseling_groups,
        'zoom_keys': zoom_keys,
    })
   

@login_required(login_url=ADMIN_LOGIN_URL)
def add_counseling_schedule(request):
    if request.method == "POST":
        try:
            # Form inputs
            counseling_group_id = request.POST.get('counseling_group')
            status = request.POST.get('status')
            zoom_key_id = request.POST.get('zoom_key')  # Optional if needed
            counseling_date = request.POST.get('counseling_date')  # "YYYY-MM-DD"
            counseling_time = request.POST.get('counseling_time')  # "HH:MM"
            topic = request.POST.get('topic', 'Counseling Session')
            duration = int(request.POST.get('duration', 60))  # in minutes

            # Combine date + time
            dt = datetime.strptime(f"{counseling_date} {counseling_time}", "%Y-%m-%d %H:%M")
            start_time_iso = dt.isoformat()
            zoomkey = ZoomKeys.objects.filter(id=zoom_key_id).first();
            access_token = get_zoom_access_token(zoomkey.account_id,zoomkey.client_id,zoomkey.client_secret)
            zoom_user_id = "me"  # or your admin Zoom user email

            # Create meeting on Zoom
            headers = {
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json"
            }
            payload = {
                "topic": topic,
                "type": 2,
                "start_time": start_time_iso,
                "duration": duration,
                "timezone": "Asia/Kolkata",
                "password": "123456",
                "settings": {
                    "join_before_host": True,
                    "waiting_room": False
                }
            }

            res = requests.post(
                f"https://api.zoom.us/v2/users/{zoom_user_id}/meetings",
                headers=headers,
                json=payload
            )

            if res.status_code != 201:
                return JsonResponse({"status": "error", "zoom_error": res.json()})

            meeting = res.json()

            # Save to DB
            CounselingSchedule.objects.create(
                counseling_group_id=counseling_group_id,
                zoom_key_id=zoom_key_id,
                meeting_id=meeting["id"],
                password=meeting.get("password", ""),
                counseling_date=counseling_date,
                counseling_time=counseling_time,
                meeting_data=json.dumps(meeting),
                status=status
            )
            return JsonResponse({'status': 'success'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)})
    return JsonResponse({'status': 'error', 'msg': 'Invalid request method'})


@login_required(login_url=ADMIN_LOGIN_URL)
def edit_counseling_schedule(request):
    if request.method == 'POST':
        schedule_id = request.POST.get('id')

        try:
            schedule = CounselingSchedule.objects.get(id=schedule_id)

            # Zoom-related data
            meeting_id = schedule.meeting_id
            topic = request.POST.get('topic', 'Updated Counseling Session')
            date = request.POST.get('counseling_date')
            time = request.POST.get('counseling_time')
            zoom_key_id = request.POST.get('zoom_key')
            duration = int(request.POST.get('duration', 60))  # fallback if not passed
            start_time = datetime.strptime(f"{date} {time}", "%Y-%m-%d %H:%M").isoformat()
            
            zoomkey = ZoomKeys.objects.filter(id=zoom_key_id).first();
            access_token = get_zoom_access_token(zoomkey.account_id,zoomkey.client_id,zoomkey.client_secret)

            headers = {
                "Authorization": f"Bearer {access_token}",
                "Content-Type": "application/json"
            }

            payload = {
                "topic": topic,
                "start_time": start_time,
                "duration": duration,
                "timezone": "Asia/Kolkata",
                "password": schedule.password,
                "settings": {
                    "join_before_host": True,
                    "waiting_room": False
                }
            }

            zoom_response = requests.patch(
                f"https://api.zoom.us/v2/meetings/{meeting_id}",
                headers=headers,
                json=payload
            )

            if zoom_response.status_code != 204:  # 204 = Success for PATCH
                return JsonResponse({'status': 'error', 'msg': 'Failed to update meeting on Zoom', 'zoom_response': zoom_response.json()})

            # Update local DB
            schedule.zoom_key_id = schedule.zoom_key_id
            schedule.meeting_id = meeting_id
            schedule.password = schedule.password
            schedule.counseling_date = date
            schedule.counseling_time = time
            schedule.status = request.POST.get('status', schedule.status)
            schedule.save()

            return JsonResponse({'status': 'success'})

        except CounselingSchedule.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Schedule not found.'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)})

    return JsonResponse({'status': 'error', 'msg': 'Invalid request method'})

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_counseling_schedule_by_id(request):
    if request.method == 'POST':
        schedule_id = request.POST.get('id')
        try:
            schedule = get_object_or_404(CounselingSchedule, id=schedule_id)

            # Step 1: Delete from Zoom
            if schedule.meeting_id:
                try:
                    zoomkey = ZoomKeys.objects.filter(id=zoom_key_id).first();
                    access_token = get_zoom_access_token(zoomkey.account_id,zoomkey.client_id,zoomkey.client_secret)
                    
                    headers = {
                        "Authorization": f"Bearer {access_token}"
                    }

                    zoom_url = f"https://api.zoom.us/v2/meetings/{schedule.meeting_id}"
                    zoom_res = requests.delete(zoom_url, headers=headers)

                    if zoom_res.status_code not in [204, 404]:  # 204 = success, 404 = already deleted
                        return JsonResponse({
                            'status': 'error',
                            'msg': 'Failed to delete meeting from Zoom',
                            'zoom_response': zoom_res.json()
                        })
                except Exception as zoom_error:
                    return JsonResponse({'status': 'error', 'msg': f'Zoom deletion failed: {zoom_error}'})

            # Step 2: Soft delete from DB
            schedule.soft_delete()  # If using SoftDeleteModel
            # OR: schedule.status = 'inactive'; schedule.save()

            return JsonResponse({'status': 'success', 'msg': 'Counseling Schedule deleted successfully.'})

        except CounselingSchedule.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'Counseling Schedule not found.'})

    return JsonResponse({'status': 'error', 'msg': 'Invalid request method.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def counseling_schedule_detail(request, id):
    counseling_detail = get_object_or_404(CounselingSchedule, id=id)
    counseling_students_list=CounselingAttendance.objects.filter(counseling_schedule_id=id).all()
    paginator = Paginator(counseling_students_list, 10)
    page_number = request.GET.get("page")
    counseling_students = paginator.get_page(page_number)
    
    return render(request, "counseling-schedule-details.html", {
        'counseling_detail': counseling_detail,
        'counseling_students': counseling_students
    })

@login_required(login_url=ADMIN_LOGIN_URL)
def delete_zoomkey_by_Id(request):
    if request.method == 'POST':
        zoomkey_id = request.POST.get('id')
        try:
            zoomkey = get_object_or_404(ZoomKeys, id=zoomkey_id)
            zoomkey.soft_delete()
            return JsonResponse({'status': 'success', 'msg': 'ZoomKey deleted successfully.'})
        except ZoomKeys.DoesNotExist:
            return JsonResponse({'status': 'error', 'msg': 'ZoomKey not found.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def resourcesRc(request):
    name = request.GET.get('name', '')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')

    all_resources = ResourcesRc.objects.all()

    # Apply filters
    if name:
        all_resources = all_resources.filter(title__icontains=name)

    if from_date:
        all_resources = all_resources.filter(release_date__gte=from_date)
    if to_date:
        all_resources = all_resources.filter(release_date__lte=to_date)
        
    all_resources = all_resources.order_by('-id')

    # Pagination
    paginator = Paginator(all_resources, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    context = {
        'all_resources': page_obj,
        'name': name,
        'from_date': from_date,
        'to_date': to_date,
    }

    return render(request, "resources-rc.html", context)

@login_required(login_url=ADMIN_LOGIN_URL)
def addResourcesRc(request):
    if request.method == 'POST':
        title= request.POST.get('name')
        resource = request.FILES.get('resource')
        release_date = request.POST.get('release_date')
        
        if(title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter a title'})
        elif(resource==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select resource'})
        else:
           
            ResourcesRc.objects.create(
                title=title,
                resources=resource,
                release_date=release_date
            )
            
           
            return JsonResponse({'status': 'success', 'msg': 'Resources uploaded successfully.', })
    else:
       
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})

@login_required(login_url=ADMIN_LOGIN_URL)
def editResourcesRc(request):
    if request.method == 'POST':

        title = request.POST.get('name')
        id=request.POST.get('id')
        resource = request.FILES.get('resource')
        release_date = request.POST.get('release_date')
       
        if(title==''):
            return JsonResponse({'status': 'error', 'msg': 'Please enter a Title'})
        elif(resource==''):
            return JsonResponse({'status': 'error', 'msg': 'Please select Resource'})
        else:
            
            resource = ResourcesRc(
                    id=id,
                 title=title,
                resources=resource,
                release_date=release_date
                    
                )
            resource.save(update_fields=["title","resources" ,"release_date"])
               
           
            return JsonResponse({'status': 'success', 'msg': 'Resources uploaded successfully.'})
    else:
       
            return JsonResponse({'status': 'error', 'msg': 'Something went wrong.'})


@login_required(login_url=ADMIN_LOGIN_URL)
def deleteResourcesRc(request):
    id = request.POST.get('id')
    ResourcesRc(id).soft_delete()
    return JsonResponse({'status': 'success', 'msg': 'deleted successfully.'})


def get_zoom_recording(meeting_id, access_token):
    url = f"https://api.zoom.us/v2/meetings/{meeting_id}/recordings"
    headers = {
        "Authorization": f"Bearer {access_token}"
    }

    response = requests.get(url, headers=headers)
    return response.json()
    if response.status_code == 200:
        data = response.json()
        return data.get('recording_files', [])  # list of files (MP4, M4A, transcript, etc.)
    else:
        print("Error fetching recording:", response.json())
        return None


import subprocess



# ✅ FFmpeg Compression Function
def compress_video(input_path, output_path):
    command = [
        "/usr/bin/ffmpeg",   # 🔥 use full path
        "-y",                # overwrite if exists
        "-i", input_path,
        "-vcodec", "libx264",
        "-crf", "28",        # quality (lower = better)
        "-preset", "fast",
        "-acodec", "aac",
        "-b:a", "128k",
        output_path
    ]

    subprocess.run(command, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)


# Main API: queue the long Zoom download/FFmpeg/Spaces upload and respond now.
def uploadZoomRecording(request):
    if request.method != 'GET':
        return JsonResponse({
            'status': 'error',
            'msg': 'Only GET request allowed',
        }, status=405)

    from admin_app.zoom_recording_jobs import queue_zoom_recording_job

    job, created = queue_zoom_recording_job()
    return JsonResponse({
        'status': 'queued' if created else 'already_running',
        'msg': (
            'Zoom recording processing was queued in the background.'
            if created else
            'A Zoom recording processing job is already running.'
        ),
        'job': job,
    }, status=202)


def zoomRecordingJobStatus(request, job_id):
    if request.method != 'GET':
        return JsonResponse({'status': 'error', 'msg': 'Only GET request allowed'}, status=405)

    from admin_app.zoom_recording_jobs import read_zoom_recording_job

    job = read_zoom_recording_job(job_id)
    if job is None:
        return JsonResponse({'status': 'error', 'msg': 'Zoom recording job not found.'}, status=404)
    return JsonResponse({'status': 'success', 'job': job})

import unicodedata
from django.utils.dateparse import parse_datetime

IGNORED_ZOOM_NAMES = {
    "crack every test",
}


def clean_name(name):
    """
    Normalize names for case-insensitive matching.

    Examples:
        Ruman Shaikh   -> ruman shaikh
        RUMAN SHAIKH   -> ruman shaikh
        Ruman-Shaikh   -> ruman shaikh
    """
    if not name:
        return ""

    name = unicodedata.normalize("NFKC", str(name))
    name = name.casefold()

    # Replace special characters with spaces
    name = re.sub(r"[^a-z0-9\s]", " ", name)

    # Remove extra spaces
    return " ".join(name.split())


def build_student_index():
    """
    Build the student-name index once before processing all lectures.
    """
    students = Students.objects.exclude(
        aadhar_name__isnull=True
    ).exclude(
        aadhar_name=""
    )

    student_index = []

    for student in students:
        aadhar_name = clean_name(student.aadhar_name)

        if not aadhar_name:
            continue

        tokens = set(aadhar_name.split())

        student_index.append({
            "student": student,
            "aadhar_name": aadhar_name,
            "tokens": tokens,
        })

    return student_index


def get_unique_student(index_items):
    """
    Return a student only when the matches resolve to exactly one student.
    """
    unique_students = {}

    for item in index_items:
        student = item["student"]
        unique_students[student.pk] = student

    if len(unique_students) == 1:
        return next(iter(unique_students.values()))

    return None


def find_student_fast(zoom_name, student_index):
    """
    Match a Zoom participant name against Students.aadhar_name.

    Priority:
    1. Exact full name
    2. All Zoom tokens exist in Aadhaar name
    3. All Aadhaar tokens exist in Zoom name
    4. Unique single-token name
    5. First + last token match
    """
    zoom_name_clean = clean_name(zoom_name)

    if not zoom_name_clean:
        return None

    zoom_tokens_list = zoom_name_clean.split()
    zoom_tokens = set(zoom_tokens_list)

    # 1. Exact name match
    exact_matches = [
        item
        for item in student_index
        if zoom_name_clean == item["aadhar_name"]
    ]

    student = get_unique_student(exact_matches)

    if student is not None:
        return student

    # 2. All Zoom tokens are present in Aadhaar name
    #
    # Zoom:
    # Ruman Shaikh
    #
    # Aadhaar:
    # Ruman Umarfaruk Shaikh
    subset_matches = [
        item
        for item in student_index
        if zoom_tokens.issubset(item["tokens"])
    ]

    student = get_unique_student(subset_matches)

    if student is not None:
        return student

    # 3. Aadhaar tokens are present in a longer Zoom name
    reverse_subset_matches = [
        item
        for item in student_index
        if item["tokens"].issubset(zoom_tokens)
    ]

    student = get_unique_student(reverse_subset_matches)

    if student is not None:
        return student

    # 4. Single-name Zoom participant, for example "Viraj"
    # Only return it when exactly one Aadhaar name contains that token.
    if len(zoom_tokens_list) == 1:
        token = zoom_tokens_list[0]

        single_name_matches = [
            item
            for item in student_index
            if token in item["tokens"]
        ]

        student = get_unique_student(single_name_matches)

        if student is not None:
            return student

    # 5. Match first and last Zoom tokens
    #
    # Useful when a Zoom name includes extra middle content.
    if len(zoom_tokens_list) >= 2:
        first_token = zoom_tokens_list[0]
        last_token = zoom_tokens_list[-1]

        first_last_matches = [
            item
            for item in student_index
            if first_token in item["tokens"]
            and last_token in item["tokens"]
        ]

        student = get_unique_student(first_last_matches)

        if student is not None:
            return student

    return None


def get_zoom_participants(meeting_id, access_token):
    url = (
        f"https://api.zoom.us/v2/"
        f"past_meetings/{meeting_id}/participants"
    )

    headers = {
        "Authorization": f"Bearer {access_token}",
    }

    participants = []
    next_page_token = ""

    while True:
        params = {
            "page_size": 300,
        }

        if next_page_token:
            params["next_page_token"] = next_page_token

        try:
            response = requests.get(
                url,
                headers=headers,
                params=params,
                timeout=30,
            )
        except requests.RequestException as exc:
            print(
                f"Zoom participants request failed "
                f"for meeting {meeting_id}: {exc}"
            )
            break

        if response.status_code != 200:
            print({
                "meeting_id": meeting_id,
                "zoom_status_code": response.status_code,
                "zoom_response": response.text,
            })
            break

        data = response.json()

        participants.extend(
            data.get("participants", [])
        )

        next_page_token = data.get(
            "next_page_token",
            ""
        )

        if not next_page_token:
            break

    return participants


def parse_zoom_datetime(value):
    """
    Parse Zoom's UTC datetime and convert it to Django's local timezone.
    """
    if not value:
        return None

    parsed_value = parse_datetime(value)

    if parsed_value is None:
        return None

    if timezone.is_naive(parsed_value):
        parsed_value = timezone.make_aware(
            parsed_value,
            timezone.utc,
        )

    return timezone.localtime(parsed_value)


@csrf_exempt
def record_zoom_attendance(request):
    if request.method != "GET":
        return JsonResponse(
            {
                "status": "error",
                "msg": "Only GET allowed",
            },
            status=405,
        )

    today = date.today()

    # Currently this fetches today and yesterday.
    start_date = today - timedelta(days=1)

    lectures = Live_Lecture.objects.filter(
        lacture_date__gte=start_date,
        lacture_date__lte=today,
        zoom_key__isnull=False,
    ).exclude(
        meeting_id__isnull=True
    ).exclude(
        meeting_id=""
    ).select_related(
        "zoom_key"
    ).order_by(
        "start_time"
    )

    student_index = build_student_index()

    total_saved = 0
    total_created = 0
    total_updated = 0
    total_existing = 0

    unmatched = []
    ignored = []
    errors = []

    participants_debug = []
    matched_debug = []

    for lecture in lectures:
        zoom_key = lecture.zoom_key

        try:
            access_token = get_zoom_access_token(
                zoom_key.account_id,
                zoom_key.client_id,
                zoom_key.client_secret,
            )
        except Exception as exc:
            errors.append({
                "lecture_id": lecture.id,
                "meeting_id": lecture.meeting_id,
                "stage": "access_token",
                "error": str(exc),
            })
            continue

        if not access_token:
            errors.append({
                "lecture_id": lecture.id,
                "meeting_id": lecture.meeting_id,
                "stage": "access_token",
                "error": "Empty Zoom access token",
            })
            continue

        participants = get_zoom_participants(
            lecture.meeting_id,
            access_token,
        )

        for participant in participants:
            zoom_name = str(
                participant.get("name") or ""
            ).strip()

            zoom_email = str(
                participant.get("user_email") or ""
            ).strip()

            if not zoom_name:
                ignored.append({
                    "lecture_id": lecture.id,
                    "zoom_name": "",
                    "reason": "Empty Zoom name",
                })
                continue

            cleaned_zoom_name = clean_name(zoom_name)

            if cleaned_zoom_name in IGNORED_ZOOM_NAMES:
                ignored.append({
                    "lecture_id": lecture.id,
                    "zoom_name": zoom_name,
                    "reason": "Ignored host/system account",
                })
                continue

            join_time_raw = participant.get("join_time")
            leave_time_raw = participant.get("leave_time")

            try:
                duration_seconds = int(
                    participant.get("duration") or 0
                )
            except (TypeError, ValueError):
                duration_seconds = 0

            participants_debug.append({
                "lecture_id": lecture.id,
                "lecture_name": getattr(
                    lecture,
                    "title",
                    None,
                ),
                "zoom_name": zoom_name,
                "zoom_email": zoom_email,
                "join_time_raw": join_time_raw,
                "leave_time_raw": leave_time_raw,
                "duration_raw": duration_seconds,
            })

            student = find_student_fast(
                zoom_name,
                student_index,
            )

            if student is None:
                unmatched.append(zoom_name)
                continue

            join_time = parse_zoom_datetime(
                join_time_raw
            )

            leave_time = parse_zoom_datetime(
                leave_time_raw
            )

            matched_debug.append({
                "lecture_id": lecture.id,
                "zoom_name": zoom_name,
                "student_id": student.pk,
                "aadhar_name": student.aadhar_name,
            })

            try:
                attendance, created = (
                    Lecture_attendance.objects.update_or_create(
                        lecture=lecture,
                        student=student,
                        defaults={
                            "email": zoom_email,
                            "join_time": join_time,
                            "leave_time": leave_time,

                            # Zoom duration is returned in seconds.
                            "duration": timedelta(
                                seconds=duration_seconds
                            ),
                        },
                    )
                )

                total_saved += 1

                if created:
                    total_created += 1
                else:
                    total_updated += 1
                    total_existing += 1

            except Exception as exc:
                errors.append({
                    "lecture_id": lecture.id,
                    "meeting_id": lecture.meeting_id,
                    "student_id": student.pk,
                    "aadhar_name": student.aadhar_name,
                    "zoom_name": zoom_name,
                    "stage": "save_attendance",
                    "error": str(exc),
                })

    unique_unmatched = list(
        dict.fromkeys(unmatched)
    )

    response_status = (
        "success"
        if not errors
        else "partial_success"
    )

    return JsonResponse({
        "status": response_status,
        "lectures": lectures.count(),
        "student_index_count": len(student_index),

        "saved_records": total_saved,
        "created_records": total_created,
        "updated_records": total_updated,
        "existing_records": total_existing,

        "unmatched_count": len(unique_unmatched),
        "unmatched_samples": unique_unmatched[:20],

        "ignored_count": len(ignored),
        "ignored_samples": ignored[:20],

        "error_count": len(errors),
        "errors": errors[:20],

        "matched_count": len(matched_debug),
        "matched_samples": matched_debug[:20],

        "participants_count": len(participants_debug),
        "participants": participants_debug,
    })
def ouploadZoomRecording(request):
    if request.method == 'GET':
        yesterday = date.today()

        # last 3 days range
        today = date.today()
        three_days_ago = today - timedelta(days=3)

        todays_lecture_list = Live_Lecture.objects.filter(
#            lacture_date__gte=three_days_ago,
#            lacture_date__lte=today,
#            videos='',
#            zoom_key__isnull=False
            id=1294
        ).order_by('start_time')

        for lecture in todays_lecture_list:
            zoom_key = lecture.zoom_key  # Assumes ForeignKey relation to ZoomKeys
            access_token = get_zoom_access_token(
                zoom_key.account_id,
                zoom_key.client_id,
                zoom_key.client_secret
            )

            recordings_response = get_zoom_recording(lecture.meeting_id, access_token)
            return JsonResponse({'status': 'success', 'msg': 'Zoom recordings saved for today\'s lectures.','yesterday':yesterday, 'todays_lecture_list':list(todays_lecture_list.values()),'recordings':recordings_response})
            
            if not recordings_response:
                continue

            for rec in recordings_response:
                if rec.get("file_extension") == "MP4":
                    file_url = rec["download_url"]
                    if not file_url:
                        continue

                    full_url = f"{file_url}?access_token={access_token}"
                    response = requests.get(full_url)

                    if response.status_code == 200:
                        file_data = response.content
                        filename = f"{uuid.uuid4().hex}.mp4"
                        file_content = ContentFile(file_data, name=filename)

                        lecture.videos = file_content
                        lecture.save(update_fields=["videos"])

        return JsonResponse({'status': 'success', 'msg': 'Zoom recordings saved for today\'s lectures.','yesterday':yesterday, 'todays_lecture_list':list(todays_lecture_list.values())})

    return JsonResponse({'status': 'error', 'msg': 'Only GET request is allowed.'})


# -----------------------------
# Common helpers
# -----------------------------

FCM_CHUNK = 500

def annotate_latest_tokens(student_qs):
    """Attach latest non-null device_token per student (fast)."""
    return (
        student_qs.annotate(
            latest_token=Subquery(
                Login_history.objects
                .filter(student=OuterRef('pk'), device_token__isnull=False)
                .order_by('-id').values('device_token')[:1]
            )
        )
        .values_list('latest_token', flat=True)
        .exclude(latest_token__isnull=True)
    )
def send_multicast(tokens, title, body, data=None, sleep_ms=0):
    """
    Sends one FCM message per token (sequential).
    - tokens: iterable of strings
    - sleep_ms: optional pause between sends to avoid throttling (e.g., 5–20ms)
    Returns: (success_count, failure_count, failed_tokens:list)
    """
    data = {k: str(v) for k, v in (data or {}).items()}
    success = failure = 0
    failed_tokens = []

    for t in tokens:
        try:
            if t:
                msg = messaging.Message(
                    notification=messaging.Notification(title=title, body=body),
                    token=t,
                    data=data
                )
                messaging.send(msg)  # synchronous single send
                success += 1
        except Exception as e:
            # optional: log e for this token
            failed_tokens.append(t)
            failure += 1
        if sleep_ms:
            time.sleep(sleep_ms / 1000.0)

    return {"success": success, "failure": failure, "failed_tokens": failed_tokens}

def osend_multicast(tokens, title, body, data=None, sleep_ms=0):
    """
    Efficient FCM send using MulticastMessage (<=500 tokens per request).
    Returns dict with counts + failed tokens list.
    """
    if not tokens:
        return {"success": 0, "failure": 0, "failed_tokens": []}

    payload_data = {str(k): str(v) for k, v in (data or {}).items()}
    success = failure = 0
    failed_tokens = []

    # chunk tokens for multicast
    for i in range(0, len(tokens), FCM_CHUNK):
        batch = tokens[i:i+FCM_CHUNK]
        msg = messaging.MulticastMessage(
            notification=messaging.Notification(title=title, body=body),
            data=payload_data,
            tokens=batch,
        )
        resp = messaging.send_multicast(msg)
        success += resp.success_count
        failure += resp.failure_count

        # collect failures (optional: purge invalid tokens)
        for t, r in zip(batch, resp.responses):
            if not r.success:
                failed_tokens.append(t)

        if sleep_ms:
            time.sleep(sleep_ms / 1000.0)

    return {"success": success, "failure": failure, "failed_tokens": failed_tokens}


def is_paid_q(student_alias='pk'):
    """Subquery: student has at least one paid OrderCourses row after May of the current year."""
    may_start = date(date.today().year, 5, 1)  # 1st May current year
    return Exists(
        OrderCourses.objects.filter(
            student_id=OuterRef(student_alias),
            created_at__date__gte=may_start  # or order_date__gte=may_start
        )
    )

def already_sent(key: str) -> bool:
    return NotificationRunLog.objects.filter(key=key).exists()

def mark_sent(key: str):
    NotificationRunLog.objects.get_or_create(key=key)

# -----------------------------
# Template engine (very small)
# -----------------------------

def render_text(text: str, ctx: dict) -> str:
    """
    Safe format: leaves unknown placeholders as-is.
    Example: "Starts at {time}" with ctx={'time':'7pm'}
    """
    class _D(dict):
        def __missing__(self, k):
            return '{' + k + '}'
    return text.format_map(_D(**{k: ('' if v is None else v) for k, v in ctx.items()}))

# High-level templates with placeholders.
LECTURE_TEMPLATES = [
    dict(
        key="lecture_tminus10",
        title="Expert session",
        body="Expert session on {topic} starts in 10 minutes! Don’t miss it",
        data={"type": "lecture", "deeplink": "app://lectures/upcoming"},
    ),
    dict(
        key="lecture_reminder",
        title="Reminder",
        body="Lecture begins in 10 mins: {topic} — be on time",
        data={"type": "lecture", "deeplink": "app://lectures/upcoming"},
    ),
]

MOCK_TEMPLATES = [
    dict(
        key="mock_release",
        title="New mock",
        body="New mock released: {mock_name}. Try it now!",
        data={"type": "mock", "deeplink": "app://mocks/{mock_id}"},
    ),
    dict(
        key="mock_nudge",
        title="Challenge",
        body="Challenge yourself: Beat yesterday’s score in today’s test: {mock_name}",
        data={"type": "mock", "deeplink": "app://mocks/{mock_id}"},
    ),
]

MOTIVATION_COMMON = [
    dict(key="mot_target",  title="Target", body="Stop scrolling, you need to study!", data={}),
    dict(key="mot_check",   title="Check",  body="Finished your daily targets yet?", data={"deeplink":"app://targets"}),
    dict(key="mot_vocab",   title="Vocab",  body="Strengthen your vocabulary with a 5-min quiz", data={"deeplink":"app://tests/vocab"}),
]

def pick(seq):
    return random.choice(seq)

# -----------------------------
# CRON 1: Upcoming lectures (T-10 min)
# -----------------------------

@require_GET
@csrf_exempt
def ocron_notify_lectures(request):
    """
    Hit every 1 or 5 minutes.
    - If ?lecture_id=123 present, sends for that lecture only (debug).
    - Otherwise finds lectures starting in [now+10min, now+11min).
    De-dup key: lecture:<id>:tminus10
    """
    now = timezone.now()
    window_start = now + timedelta(minutes=10)
    window_end   = now + timedelta(minutes=11)

    lecture_id = request.GET.get("lecture_id")
    if lecture_id:
        lectures = Live_Lecture.objects.filter(pk=lecture_id)
    else:
        # Your Live_Lecture has date & start_time? If it's a DateTime, adjust filter accordingly.
        lectures = Live_Lecture.objects.filter(
            lacture_date=window_start.date(),
            start_time__gte=window_start.time(),
            start_time__lt=window_end.time(),
            is_cancel=False
        )

    processed = []
    for lec in lectures:
        key = f"lecture:{lec.id}:tminus10"
        if already_sent(key):
            continue

        # audience: all students in any of the lecture's batches
        students = Students.objects.filter(batch__in=lec.batches.all()).distinct()
        tokens = list(annotate_latest_tokens(students))

        ctx = {
            "topic": getattr(lec, "title", "your session"),
            "date": lec.lacture_date.strftime("%Y-%m-%d") if getattr(lec, "lacture_date", None) else "",
            "time": lec.start_time.strftime("%H:%M") if getattr(lec, "start_time", None) else "",
            "batch_names": " ".join(b.batch_name for b in lec.batches.all()),
            "lecture_id": lec.id,
        }
        tpl = LECTURE_TEMPLATES[0]  # pick the “tminus10” template
        title = render_text(tpl["title"], ctx)
        body  = render_text(tpl["body"], ctx)
        data  = {k: render_text(v, ctx) for k, v in tpl["data"].items()}

        stats = send_multicast(tokens, title, body, data)
        mark_sent(key)
        processed.append({"lecture_id": lec.id, "sent": stats["success"], "failed": stats["failure"]})

    return JsonResponse({"ok": True, "processed": processed})

# -----------------------------
# CRON 2: Mock events (dynamic)
# -----------------------------

@require_GET
@csrf_exempt
def cron_mock_events(request):
    """
    Send pushes for newly released/featured mocks.
    Use one of:
      - ?mock_id=123            (send for that mock only)
      - ?released_since=YYYY-MM-DD (send for all mocks released since this date)
    De-dup key: mock:<id>:release
    Segments: paid & unpaid (optional tweak deeplinks per segment).
    """
    mock_id = request.GET.get("mock_id")
    released_since = request.GET.get("released_since")

    if mock_id:
        mocks = Mock_Test.objects.filter(pk=mock_id, status='Active')
    elif released_since:
        try:
            since = timezone.datetime.strptime(released_since, "%Y-%m-%d").date()
        except ValueError:
            return HttpResponseBadRequest("Bad released_since")
        mocks = Mock_Test.objects.filter(status='Active', test_date=since)
    else:
        # default: last 24h
        since = timezone.localdate() - timedelta(days=1)
        mocks = Mock_Test.objects.filter(status='Active', test_date=since)
#    return JsonResponse({"ok": True, "mocks": list(mocks.values())})

    results = []

    for m in mocks:
        key = f"mock:{m.id}:release"
        if already_sent(key):
            continue

        ctx = {"mock_name": m.mock_title, "mock_id": m.id}

        # Paid segment
        paid_students = Students.objects.filter(is_paid_q())
        paid_tokens = list(annotate_latest_tokens(paid_students))
        tpl = MOCK_TEMPLATES[0]  # "mock_release"
        title = render_text(tpl["title"], ctx)
        body  = render_text(tpl["body"], ctx)
        data  = {k: render_text(v, ctx) for k, v in tpl["data"].items()}
        stats_paid = send_multicast(paid_tokens, title, body, data)

        # Unpaid segment (optional: different copy/deeplink)
        unpaid_students = Students.objects.exclude(Exists(OrderCourses.objects.filter(student_id=OuterRef('pk'))))
        unpaid_tokens = list(annotate_latest_tokens(unpaid_students))
        data_unpaid = {**data, "utm": "upsell"}  # example tweak
        stats_unpaid = send_multicast(paid_tokens if False else unpaid_tokens, title, body, data_unpaid)

        mark_sent(key)
        results.append({
            "mock_id": m.id,
            "paid": stats_paid,
            "unpaid": stats_unpaid,
        })

    return JsonResponse({"ok": True, "mocks": results})

# -----------------------------
# CRON 3: Motivation / generic nudges (uses static templates)
# -----------------------------

@require_GET
@csrf_exempt
def cron_motivation(request):
    """
    Daily long nudge for paid + unpaid, plus one short random nudge per hour.
    De-dup keys:
      motivation:<YYYY-MM-DD>:paid
      motivation:<YYYY-MM-DD>:unpaid
      motivation_short:<YYYY-MM-DD>:<HH>
    """
    today = timezone.localdate()

    # Paid
    k_paid = f"motivation:{today}:paid"
    if not already_sent(k_paid):
        title = "Keep going"
        body  = "90 days from now, you'll thank yourself for studying today. Get started now!"
        data  = {"deeplink": "app://mocks"}  # paid target
        students = Students.objects.filter(is_paid_q())
        tokens   = list(annotate_latest_tokens(students))
        send_multicast(tokens, title, body, data)
        mark_sent(k_paid)

    # Unpaid
    k_unpaid = f"motivation:{today}:unpaid"
    if not already_sent(k_unpaid):
        title = "Keep going"
        body  = "90 days from now, you'll thank yourself for studying today. Get started now!"
        data  = {"deeplink": "app://courses"}  # upsell target
        students = Students.objects.exclude(Exists(OrderCourses.objects.filter(student_id=OuterRef('pk'))))
        tokens   = list(annotate_latest_tokens(students))
        send_multicast(tokens, title, body, data)
        mark_sent(k_unpaid)

    # Short random (hourly)
    hour_key = f"motivation_short:{today}:{timezone.now().hour:02d}"
    if not already_sent(hour_key):
        tpl  = pick(MOTIVATION_COMMON)
        title = tpl["title"]
        body  = tpl["body"]
        data  = tpl.get("data", {})
        tokens = list(annotate_latest_tokens(Students.objects.all()))
        send_multicast(tokens, title, body, data)
        mark_sent(hour_key)

    return JsonResponse({"ok": True})

def to_int(val, default=0):
    try:
        return int(val)
    except (TypeError, ValueError):
        return default


@login_required(login_url=ADMIN_LOGIN_URL)
def all_question_bank_list(request):
    section_list = Sections.objects.all()
    Sub_Sections_list = Sub_Sections.objects.all()
    Topics_list = Topics.objects.all()
    mock_test_list = Mock_Test.objects.all()

    # Mock type mapping
    mock_types = {
        1: "Area Mock",
        2: "Sectional Mock",
        3: "Weekly Mock",
        4: "Mini Mock",
        5: "Full Mock",
        8: "Advance Mock",
        9: "PYQ Mock"
    }

    # Default values
    section_id = to_int(request.GET.get("section_id", 0))
    sub_section_id = to_int(request.GET.get("sub_section_id", 0))
    topic_id = to_int(request.GET.get("topic_id", 0))
    mocktest_id = to_int(request.GET.get("mocktest_id", 0))
    mocktype_id = to_int(request.GET.get("mocktype_id", 0))
    sort_by = request.GET.get("sort_by", "")

    # Base Query
    Question_list = Question_Bank.objects.all()

    if section_id and section_id != "0":
        Question_list = Question_list.filter(section_id=section_id)

    if sub_section_id and sub_section_id != "0":
        Question_list = Question_list.filter(sub_section_id=sub_section_id)

    if topic_id and topic_id != "0":
        Question_list = Question_list.filter(topic_id=topic_id)

    # ✅ Filter by Mock Test
    if mocktest_id and mocktest_id != "0":
        Question_list = Question_list.filter(test_id=mocktest_id)

    # ✅ Filter by Mock Type
    if mocktype_id and mocktype_id != "0":
        Question_list = Question_list.filter(test__mock_type=mocktype_id)

    # ✅ Search in question text (optional)
    if sort_by == "question_title":
        Question_list = Question_list.order_by("question")   # alphabetical

    elif sort_by == "mock_test_desc":
        Question_list = Question_list.order_by("-test__id")   # newest mock test first
    
    elif sort_by == "mock_test_asc":
        Question_list = Question_list.order_by("test__id")    # oldest mock test first

    # Pagination
    paginator = Paginator(Question_list, 10)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    filter = (
        f"&section_id={section_id}"
        f"&sub_section_id={sub_section_id}"
        f"&topic_id={topic_id}"
        f"&mocktest_id={mocktest_id}"
        f"&mocktype_id={mocktype_id}"
        f"&sort_by={sort_by}"
    )

    numbers = range(1, page_obj.paginator.count + 1)

    return render(
        request,
        "all-questions-bank-list1.html",
        {
            "display_order": list(page_obj.object_list.values_list("id", flat=True)),
            "numbers": numbers,
            "mock_test_list": mock_test_list,
            "Question_list": page_obj,
            "section_list": section_list,
            "Sub_Sections_list": Sub_Sections_list,
            "Topics_list": Topics_list,
            "filter": filter,
            "topic_id": topic_id,
            "section_id": section_id,
            "sub_section_id": sub_section_id,
            "mocktest_id": mocktest_id,
            "mocktype_id": mocktype_id,
            "sort_by": sort_by,
            "mock_types": mock_types,
        },
    )

@login_required(login_url=ADMIN_LOGIN_URL)
def get_mocktests_by_type(request):
    mocktype_id = request.GET.get("mocktype_id")
    if not mocktype_id:
        return JsonResponse({"error": "Missing mocktype_id"}, status=400)

    mock_tests = Mock_Test.objects.filter(mock_type=mocktype_id).values("id", "mock_title")
    return JsonResponse(list(mock_tests), safe=False)

MSG91_URL = "https://api.msg91.com/api/v5/whatsapp/whatsapp-outbound-message/bulk/"

@csrf_exempt
@require_POST
def send_whatsapp_template(request):
    """
    POST (JSON or form-encoded):
    {
      "numbers": ["9198XXXXXXXX","9170XXXXXXXX"],  # or "9198...,9170..."
      "template_name": "otp",                      # default "otp"
      "language_code": "en",                       # default "en"
      "body_1": "value1",                          # maps to {{1}} in template
      "button_url": "https://example.com/path"     # optional; fills button_1 url
    }
    """
    # 1) Parse input (JSON or form)
    try:
        if request.content_type and request.content_type.startswith("application/json"):
            data = json.loads(request.body.decode("utf-8"))
        else:
            data = request.POST.dict()
    except Exception:
        return JsonResponse({"success": False, "error": "Invalid JSON payload"}, status=400)

    # 2) Pull/normalize fields
    numbers = data.get("numbers") or data.get("to")
    if isinstance(numbers, str):
        numbers = [n.strip() for n in numbers.split(",") if n.strip()]
    if not numbers or not isinstance(numbers, list):
        return JsonResponse({"success": False, "error": "Provide 'numbers' as list or comma-separated string."}, status=400)

    template_name = data.get("template_name", "otp")
    language_code = data.get("language_code", "en")
    body_1       = data.get("body_1")
    button_url   = data.get("button_url")

    # 3) Build MSG91 payload (mirrors your curl)
    components = {}
    if body_1 is not None:
        components["body_1"] = {"type": "text", "value": str(body_1)}
    if button_url:
        components["button_1"] = {"subtype": "url", "type": "text", "value": str(button_url)}

    payload = {
        "integrated_number": getattr(settings, "MSG91_INTEGRATED_NUMBER", "917977041874"),
        "content_type": "template",
        "payload": {
            "messaging_product": "whatsapp",
            "type": "template",
            "template": {
                "name": template_name,
                "language": {
                    "code": language_code,
                    "policy": "deterministic"
                },
                "namespace": getattr(settings, "MSG91_NAMESPACE", "1fa9599b_fd7a_46b6_91e9_e91419535c4f"),
                "to_and_components": [{
                    "to": numbers,
                    "components": components
                }]
            }
        }
    }

    headers = {
        "Content-Type": "application/json",
        "authkey": getattr(settings, "MSG91_AUTHKEY", "475291AMVy0hIO6901fca1P1"),
    }

    # 4) Call MSG91
    try:
        resp = requests.post(MSG91_URL, headers=headers, json=payload, timeout=20)
    except requests.RequestException as e:
        return JsonResponse({"success": False, "error": f"Network error: {e}"}, status=502)

    # 5) Return clean result
    try:
        resp_json = resp.json()
    except ValueError:
        resp_json = {"raw": resp.text}

    ok = 200 <= resp.status_code < 300
    return JsonResponse(
        {"success": ok, "status_code": resp.status_code, "response": resp_json},
        status=200 if ok else 400
    )
# views_generate_lectures.py
# --------------------------------------------
# CLEAN LECTURE GENERATOR (strict, debuggable, supports DB-merge first, faculty availability check)
# --------------------------------------------
from datetime import date, datetime, time, timedelta
from calendar import monthrange
from typing import Dict, Tuple, List, Optional, Set

from django.http import JsonResponse
from django.shortcuts import render
from django.db.models import Q

import json
import traceback


# ----------------
# Config
# ----------------
DEFAULT_FACULTY_WEEK_LIMIT = 8
MAX_LECTURES_PER_DAY_PER_BATCH = 2
MAX_LECTURES_PER_DAY_PER_FACULTY = 2  # Optional: restrict to 2 per day
ONE_LECTURE_MINUTES = 60
OVERLAP_STEP_MINUTES = 30

_fac_week_limit_cache: Dict[int, int] = {}
_ALLOWED_TOPIC_IDS_CACHE: Dict[int, Set[int]] = {}
_batch_week_limit_cache: Dict[int, int] = {}


# ----------------
# Time utils
# ----------------
def week_bounds(d: date) -> Tuple[date, date]:
    start = d - timedelta(days=d.weekday())  # Monday
    end = start + timedelta(days=6)
    return start, end


def weeks_between(d1: date, d2: date) -> int:
    s1, _ = week_bounds(d1)
    _, e2 = week_bounds(d2)
    days = (e2 - s1).days + 1
    return max(1, (days + 6) // 7)


def should_skip_for_gap(batch, current_date: date, preview: List[dict]) -> bool:
    """
    Simple spacing rule: for this batch, do NOT schedule lectures
    on two consecutive calendar days.

    If the last lecture (DB or preview) for this batch was yesterday,
    skip scheduling on current_date.
    """
    # Find last lecture for this batch before current_date (DB)
    db_last = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__lt=current_date
    ).order_by('-lacture_date').first()

    last_lec_date = db_last.lacture_date if db_last else None

    # Also check preview schedule (in-memory)
    prev_last = None
    for lec in reversed(preview):
        if batch.id not in lec.get("batch_ids", []):
            continue
        try:
            d = datetime.strptime(lec["date"], "%Y-%m-%d").date()
        except Exception:
            continue
        if d < current_date:
            prev_last = d
            break

    if prev_last and (not last_lec_date or prev_last > last_lec_date):
        last_lec_date = prev_last

    # If there is a previous lecture and it was yesterday → skip today
    if last_lec_date:
        days_gap = (current_date - last_lec_date).days
        if days_gap == 1:   # consecutive days
            return True

    return False

def get_batch_available_dates_in_week(batch, week_start: date, week_end: date) -> List[date]:
    """
    Return all dates in this week where the batch has at least one slot.
    """
    dates = []
    d = week_start

    while d <= week_end:
        day_name = d.strftime("%A")

        if BatchTimeSlot.objects.filter(batch=batch, day=day_name).exists():
            dates.append(d)

        d += timedelta(days=1)

    return dates


def get_batch_lecture_dates_in_week(batch_id: int, week_start: date, week_end: date, preview: List[dict]) -> Set[date]:
    """
    Return dates in this week where batch already has DB/preview lecture.
    """
    dates = set()

    db_dates = Live_Lecture.objects.filter(
        batches__id=batch_id,
        is_cancel=False,
        lacture_date__range=[week_start, week_end]
    ).values_list("lacture_date", flat=True).distinct()

    for d in db_dates:
        dates.add(d)

    for lec in preview:
        if batch_id not in lec.get("batch_ids", []):
            continue

        try:
            lec_date = datetime.strptime(lec.get("date"), "%Y-%m-%d").date()
        except Exception:
            continue

        if week_start <= lec_date <= week_end:
            dates.add(lec_date)

    return dates


def planned_weekly_lecture_dates(batch, current_date: date, preview: List[dict]) -> Set[date]:
    """
    Plan lecture dates for the week.

    Rule:
    - Do not generate lectures back-to-back if enough dates are available.
    - Prefer spreading lectures across 4–5 days.
    - Allow 1 lecture per day.
    - Allow 2 lectures per day only when required by weekly limit.
    """

    week_start, week_end = week_bounds(current_date)

    weekly_limit = get_batch_week_limit(batch.id)

    available_dates = get_batch_available_dates_in_week(batch, week_start, week_end)

    if not available_dates:
        return set()

    # If only few available days, use all available days
    if len(available_dates) <= 3:
        return set(available_dates)

    # Decide preferred number of lecture days
    # Example:
    # weekly_limit 6 => prefer 4 or 5 days
    # weekly_limit 7 => prefer 5 days
    preferred_days = min(len(available_dates), max(4, min(5, weekly_limit)))

    # If available dates are 4 or 5, use all
    if len(available_dates) <= preferred_days:
        return set(available_dates)

    planned = []

    # First pass: take alternate days
    index = 0
    while index < len(available_dates) and len(planned) < preferred_days:
        planned.append(available_dates[index])
        index += 2

    # If alternate selection does not give enough days, fill remaining from unused dates
    if len(planned) < preferred_days:
        for d in available_dates:
            if d not in planned:
                planned.append(d)

            if len(planned) >= preferred_days:
                break

    return set(planned)

def legacy_should_allow_second_lecture_today(batch, current_date: date, preview: List[dict]) -> bool:
    """
    Allow second lecture only if needed to reach weekly lecture target.
    """

    week_start, week_end = week_bounds(current_date)
    week_start_str = week_start.strftime("%Y-%m-%d")
    week_end_str = week_end.strftime("%Y-%m-%d")

    weekly_limit = get_batch_week_limit(batch.id)

    planned_dates = planned_weekly_lecture_dates(batch, current_date, preview)

    db_week_count = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__range=[week_start, week_end]
    ).distinct().count()

    preview_week_count = sum(
        1 for lec in preview
        if batch.id in lec.get("batch_ids", [])
        and week_start_str <= lec.get("date", "") <= week_end_str
    )

    total_week_count = db_week_count + preview_week_count

    remaining_week_lectures = weekly_limit - total_week_count

    remaining_planned_dates = [
        d for d in planned_dates
        if d >= current_date
    ]

    # If remaining lectures are more than remaining dates,
    # second lecture is needed on some days.
    return remaining_week_lectures > len(remaining_planned_dates)

def should_skip_for_week_distribution(batch, current_date: date, preview: List[dict]) -> bool:
    """
    Skip dates that are not part of the planned weekly distribution.

    Allows second lecture on the same planned date.
    Does not affect weeks where batch has limited available days.
    """
    week_start, week_end = week_bounds(current_date)

    available_dates = get_batch_available_dates_in_week(batch, week_start, week_end)

    if current_date not in available_dates:
        return True

    planned_dates = planned_weekly_lecture_dates(batch, current_date, preview)

    if current_date not in planned_dates:
        return True

    return False

# ----------------
# Lecture Counts
# ----------------
def get_faculty_week_limit(faculty_id: int) -> int:
    if faculty_id in _fac_week_limit_cache:
        return _fac_week_limit_cache[faculty_id]
    try:
        lim = Faculty.objects.only('id', 'lecture_per_week').get(id=faculty_id).lecture_per_week
    except Faculty.DoesNotExist:
        lim = None
    if not isinstance(lim, int) or lim <= 0:
        lim = DEFAULT_FACULTY_WEEK_LIMIT
    _fac_week_limit_cache[faculty_id] = lim
    return lim


def count_db_lectures_for(batch, topic) -> int:
    lec_ids = Live_Lecture.objects.filter(topic_id=topic).values_list('id', flat=True)
    b_lec_ids = Lecture_batches.objects.filter(batches_id=batch).values_list('lecture_id', flat=True)
    valid_ids = set(lec_ids).intersection(set(b_lec_ids))
    return Live_Lecture.objects.filter(id__in=valid_ids, is_cancel=False).count()


def count_preview_for(batch, topic, preview: List[dict]) -> int:
    return sum(
        1 for lec in preview
        if batch.id in lec["batch_ids"]
        and topic.id in {lec.get("topic_id"), lec.get("extra_topic_id")}
    )


def remaining_for(batch, topic, preview: List[dict]) -> int:
    tt = BatchTopicTiming.objects.filter(batch=batch, topic=topic).first()
    need = (tt.no_of_lectures if tt else 0)
    done = count_db_lectures_for(batch, topic) + count_preview_for(batch, topic, preview)
    return max(0, need - done)


def total_remaining_for_batch(batch, preview: List[dict]) -> int:
    total = 0
    allowed_ids = allowed_topic_ids_for_batch(batch)
    
    if not allowed_ids:
        return 0

    for tt in BatchTopicTiming.objects.filter(
        batch=batch,
        topic_id__in=allowed_ids
    ).select_related('topic'):
        total += remaining_for(batch, tt.topic, preview)

    return total


def batch_lectures_on_day(batch_id: int, day_str: str, preview: List[dict]) -> int:
    db_cnt = Live_Lecture.objects.filter(
        batches__id=batch_id,
        lacture_date=day_str,
        is_cancel=False
    ).count()
    prev_cnt = sum(1 for lec in preview if day_str == lec["date"] and batch_id in lec["batch_ids"])
    return db_cnt + prev_cnt


def faculty_lectures_on_day(faculty_id: int, batch_id: int, day_str: str, preview: List[dict]) -> int:
    db_cnt = Live_Lecture.objects.filter(
        faculty_id=faculty_id,
        batches__id=batch_id,
        lacture_date=day_str,
        is_cancel=False
    ).distinct().count()

    prev_cnt = sum(
        1 for lec in preview
        if lec.get("faculty_id") == faculty_id
        and batch_id in lec.get("batch_ids", [])
        and lec.get("date") == day_str
    )

    return db_cnt + prev_cnt

def faculty_lectures_in_week(faculty_id: int, batch_id: int, week_start: date, week_end: date, preview: List[dict]) -> int:
    db_cnt = Live_Lecture.objects.filter(
        faculty_id=faculty_id,
        batches__id=batch_id,
        is_cancel=False,
        lacture_date__range=[week_start, week_end],
    ).distinct().count()

    wstart = week_start.strftime("%Y-%m-%d")
    wend = week_end.strftime("%Y-%m-%d")

    prev_cnt = sum(
        1 for lec in preview
        if lec.get('faculty_id') == faculty_id
        and batch_id in lec.get("batch_ids", [])
        and wstart <= lec.get('date', '') <= wend
    )

    return db_cnt + prev_cnt

def faculty_week_quota_ok(faculty_id: int, batch_id: int, day: date, preview: List[dict]) -> bool:
    """
    Ensure faculty has not exceeded weekly lecture limit for this specific batch.
    """

    limit = get_faculty_week_limit(faculty_id)
    week_start, week_end = week_bounds(day)

    total = faculty_lectures_in_week(
        faculty_id=faculty_id,
        batch_id=batch_id,
        week_start=week_start,
        week_end=week_end,
        preview=preview
    )

    return total < limit


def monthly_batch_quota_ok(batch, day: date, preview: List[dict]) -> bool:
    month_number = day.month
    month_key = day.strftime("%Y-%m")

    row = MonthlyLectureCount.objects.filter(
        batch=batch,
        month=month_number
    ).first()

    # If monthly record not added, allow scheduling
    if not row:
        return True

    monthly_limit = int(row.no_of_lectures or 0)

    # If month count is 0, do not schedule anything in that month
    if monthly_limit <= 0:
        return False

    first = day.replace(day=1)
    last = day.replace(day=monthrange(day.year, day.month)[1])

    db_cnt = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__range=[first, last]
    ).distinct().count()

    prev_cnt = sum(
        1 for lec in preview
        if batch.id in lec.get("batch_ids", [])
        and lec.get("date", "")[:7] == month_key
    )

    return (db_cnt + prev_cnt) < monthly_limit

def month_bounds(d: date) -> Tuple[date, date]:
    first = d.replace(day=1)
    last = d.replace(day=monthrange(d.year, d.month)[1])
    return first, last


def count_batch_lectures_in_month_till_date(batch, day: date, preview: List[dict]) -> int:
    first, _ = month_bounds(day)

    db_cnt = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__range=[first, day]
    ).distinct().count()

    first_str = first.strftime("%Y-%m-%d")
    day_str = day.strftime("%Y-%m-%d")

    prev_cnt = sum(
        1 for lec in preview
        if batch.id in lec.get("batch_ids", [])
        and first_str <= lec.get("date", "") <= day_str
    )

    return db_cnt + prev_cnt


def get_monthly_batch_limit(batch, day: date) -> Optional[int]:
    row = MonthlyLectureCount.objects.filter(
        batch=batch,
        month=day.month
    ).first()

    if not row:
        return None

    monthly_limit = int(row.no_of_lectures or 0)

    if monthly_limit <= 0:
        return 0

    return monthly_limit


def monthly_distribution_ok(batch, day: date, preview: List[dict]) -> bool:
    """
    Keep the monthly target spread over dates on which the batch can
    actually attend. Calendar days without a batch slot must not suppress
    lecture generation.
    """

    monthly_limit = get_monthly_batch_limit(batch, day)

    # If no monthly limit added, do not apply distribution rule
    if monthly_limit is None:
        return True

    if monthly_limit <= 0:
        return False

    first, last = month_bounds(day)

    available_dates = []
    cursor = first
    while cursor <= last:
        if BatchTimeSlot.objects.filter(
            batch=batch,
            day=cursor.strftime("%A")
        ).exists():
            available_dates.append(cursor)
        cursor += timedelta(days=1)

    if not available_dates:
        return False

    elapsed_available_dates = sum(1 for d in available_dates if d <= day)

    # Base progress on usable batch capacity, not all calendar days.
    total_capacity = len(available_dates) * MAX_LECTURES_PER_DAY_PER_BATCH
    elapsed_capacity = elapsed_available_dates * MAX_LECTURES_PER_DAY_PER_BATCH
    expected_allowed_till_today = (
        (monthly_limit * elapsed_capacity + total_capacity - 1)
        // total_capacity
    )
    expected_allowed_till_today = max(1, expected_allowed_till_today)

    current_count_till_today = count_batch_lectures_in_month_till_date(
        batch,
        day,
        preview
    )

    return current_count_till_today < expected_allowed_till_today
    
def global_time_block_all(the_date: date,
                          start_t: time,
                          end_t: time,
                          preview: List[dict]) -> bool:
    """
    Return True if *any* lecture (DB or preview) overlaps [start_t, end_t) on the_date.
    No exceptions; blocks all batches, merged or not.
    """
    if Live_Lecture.objects.filter(
        lacture_date=the_date,
        start_time__lt=end_t,
        end_time__gt=start_t,
        is_cancel=False,
    ).exists():
        return True

    day_str = the_date.strftime("%Y-%m-%d")
    win_s = start_t.strftime("%H:%M")
    win_e = end_t.strftime("%H:%M")
    for lec in preview:
        if lec.get("date") != day_str:
            continue
        if lec["start_time"] < win_e and lec["end_time"] > win_s:
            return True

    return False

def get_topic_started_faculty_id(batch, topic, preview: List[dict]) -> Optional[int]:
    """
    If this topic has already started for this batch,
    return the faculty who started/taught it first.

    Priority:
    1. Existing saved DB lectures
    2. Preview lectures generated in current run

    Once found, same topic should continue with same faculty only.
    """

    # 1. Check already saved DB lectures
    db_lecture = (
        Live_Lecture.objects
        .filter(
            batches=batch,
            topic=topic,
            is_cancel=False
        )
        .exclude(faculty_id__isnull=True)
        .order_by('lacture_date', 'start_time')
        .first()
    )

    if db_lecture:
        return db_lecture.faculty_id

    # 2. Check preview schedule
    matching_preview = []

    for lec in preview:
        if batch.id not in lec.get("batch_ids", []):
            continue

        if topic.id not in {
            lec.get("topic_id"),
            lec.get("extra_topic_id")
        }:
            continue

        if not lec.get("faculty_id"):
            continue

        matching_preview.append(lec)

    if matching_preview:
        matching_preview.sort(
            key=lambda x: (
                x.get("date", ""),
                x.get("start_time", "")
            )
        )
        return matching_preview[0].get("faculty_id")

    return None
# ----------------
# Syllabus & dependencies
# ----------------
def allowed_topic_ids_for_batch(batch) -> Set[int]:
    """
    Return only topics linked to syllabus for this batch.

    Rules:
    - Topic must exist in ExamSyllabus for batch.exam_category.
    - If batch has section_id, syllabus must match that section.
    - If batch has sub_section_id, syllabus must match that subsection.
    - Topic must also exist in BatchTopicTiming for this batch.
    """

    if batch.id in _ALLOWED_TOPIC_IDS_CACHE:
        return _ALLOWED_TOPIC_IDS_CACHE[batch.id]

    syllabus_qs = ExamSyllabus.objects.filter(
        exam_category=batch.exam_category
    )

    if getattr(batch, "section_id", None):
        syllabus_qs = syllabus_qs.filter(section_id=batch.section_id)

    if getattr(batch, "sub_section_id", None):
        syllabus_qs = syllabus_qs.filter(sub_section_id=batch.sub_section_id)

    syllabus_topic_ids = set(
        syllabus_qs.values_list("topic_id", flat=True)
    )

    batch_topic_ids = set(
        BatchTopicTiming.objects.filter(batch=batch)
        .values_list("topic_id", flat=True)
    )

    allowed = syllabus_topic_ids & batch_topic_ids

    _ALLOWED_TOPIC_IDS_CACHE[batch.id] = allowed

    return allowed


def parents_in_plan(batch, topic: 'Topics') -> List['Topics']:
    """
    Return dependency topics only if they are:
    - linked in ExamSyllabus
    - planned in BatchTopicTiming
    - allowed for this batch
    """

    allowed_ids = allowed_topic_ids_for_batch(batch)

    planned_ids = set(
        BatchTopicTiming.objects.filter(
            batch=batch,
            topic_id__in=allowed_ids
        ).values_list('topic_id', flat=True)
    )

    qs = ExamSyllabus.objects.filter(
        exam_category=batch.exam_category,
        topic=topic
    )

    if getattr(batch, "section_id", None):
        qs = qs.filter(section_id=batch.section_id)

    if getattr(batch, "sub_section_id", None):
        qs = qs.filter(sub_section_id=batch.sub_section_id)

    parent_ids = set()

    for es in qs.only('id'):
        parent_ids.update(
            es.dependent_topics.values_list('id', flat=True)
        )

    parent_ids = parent_ids & planned_ids & allowed_ids

    return list(Topics.objects.filter(id__in=parent_ids))

def pending_topic_ids_for_batch(batch, preview: List[dict]) -> Set[int]:
    """
    Return only topics that are:
    - allowed by syllabus
    - added in BatchTopicTiming
    - still pending based on DB + preview count
    """

    allowed_ids = allowed_topic_ids_for_batch(batch)

    if not allowed_ids:
        return set()

    topic_ids = BatchTopicTiming.objects.filter(
        batch=batch,
        topic_id__in=allowed_ids
    ).values_list('topic_id', flat=True)

    pending_ids = set()

    for topic in Topics.objects.filter(id__in=topic_ids):
        if remaining_for(batch, topic, preview) > 0:
            pending_ids.add(topic.id)

    return pending_ids
    
def build_dependency_layers(batch, preview: List[dict]) -> List[List['Topics']]:
    """
    Build dependency layers only for pending topics.

    Completed topics will not enter the generation loop.
    """

    pending_ids = pending_topic_ids_for_batch(batch, preview)

    if not pending_ids:
        return []

    planned = Topics.objects.filter(id__in=pending_ids)

    id_map = {t.id: t for t in planned}
    indeg = {t.id: 0 for t in planned}
    children = {t.id: [] for t in planned}

    for t in planned:
        for p in parents_in_plan(batch, t):
            # If parent is completed, it will not be in pending/id_map.
            # That is okay. parents_completed_for_topic() will validate completion separately.
            if p.id not in id_map:
                continue

            indeg[t.id] += 1
            children[p.id].append(t.id)

    layer, frontier = [], [t.id for t in planned if indeg[t.id] == 0]

    while frontier:
        layer.append([id_map[i] for i in frontier])

        next_frontier = []

        for u in frontier:
            for v in children[u]:
                indeg[v] -= 1

                if indeg[v] == 0:
                    next_frontier.append(v)

        frontier = next_frontier

    remaining = [id_map[i] for i, d in indeg.items() if d > 0]

    if remaining:
        layer.append(remaining)

    return layer

def parents_completed_for_topic(batch, topic: 'Topics', preview: List[dict]) -> bool:
    """
    STRICT: A topic becomes eligible ONLY after *all* its dependency topics
    are fully completed for this batch.

    'Fully completed' = DB lectures + preview lectures >= required no_of_lectures.
    """
    for p in parents_in_plan(batch, topic):
        tt = BatchTopicTiming.objects.filter(batch=batch, topic=p).first()
        req = int(tt.no_of_lectures or 0) if tt else 0
        if req <= 0:
            continue

        done = count_db_lectures_for(batch, p) + count_preview_for(batch, p, preview)
        if done < req:
            return False

    return True


# ----------------
# Time windows
# ----------------
def build_windows(slot_start: time, slot_end: time) -> List[Tuple[time, time]]:
    windows = []
    dt = datetime.combine(date.today(), slot_start)
    end_dt = datetime.combine(date.today(), slot_end)
    step = timedelta(minutes=OVERLAP_STEP_MINUTES)
    dur = timedelta(minutes=ONE_LECTURE_MINUTES)
    while dt + dur <= end_dt:
        windows.append((dt.time(), (dt + dur).time()))
        dt += step
    return windows


def overlap_windows(batch_slot, fac_slot) -> List[Tuple[time, time]]:
    fs = datetime.combine(date.today(), fac_slot.start_time)
    fe = datetime.combine(date.today(), fac_slot.end_time)
    bs = datetime.combine(date.today(), batch_slot.start_time)
    be = datetime.combine(date.today(), batch_slot.end_time)
    s = max(fs, bs)
    e = min(fe, be)
    if e - s < timedelta(minutes=ONE_LECTURE_MINUTES):
        return []
    return build_windows(s.time(), e.time())


def any_batch_time_conflict(batch_ids: List[int], day_date: date, start_t: time, end_t: time, preview: List[dict]) -> bool:
    if Live_Lecture.objects.filter(
        batches__id__in=batch_ids,
        lacture_date=day_date,
        start_time__lt=end_t,
        end_time__gt=start_t,
        is_cancel=False
    ).exists():
        return True
    for lec in preview:
        if set(batch_ids) & set(lec["batch_ids"]):
            if lec["date"] == day_date.strftime("%Y-%m-%d"):
                if lec["start_time"] < end_t.strftime("%H:%M") and lec["end_time"] > start_t.strftime("%H:%M"):
                    return True
    return False


# ----------------
# Faculty availability check
# ----------------
def faculty_has_conflict(faculty_id: int, the_date: date, start_t: time, end_t: time, preview: List[dict]) -> bool:
    if Live_Lecture.objects.filter(
        faculty_id=faculty_id,
        lacture_date=the_date,
        start_time__lt=end_t,
        end_time__gt=start_t,
        is_cancel=False
    ).exists():
        return True
    for lec in preview:
        if lec.get("faculty_id") == faculty_id and lec["date"] == the_date.strftime("%Y-%m-%d"):
            if lec["start_time"] < end_t.strftime("%H:%M") and lec["end_time"] > start_t.strftime("%H:%M"):
                return True
    return False


# ----------------
# Batch merge helpers (DB lectures only)
# ----------------
def allowed_merge_batch_ids(main_batch) -> Set[int]:
    if hasattr(main_batch, 'merged_batches'):
        return set(main_batch.merged_batches.values_list('id', flat=True))
    return set()


def batch_slot_covers_window(batch, day_name: str, win_s: time, win_e: time) -> bool:
    for bs in BatchTimeSlot.objects.filter(batch=batch, day=day_name):
        if bs.start_time <= win_s and bs.end_time >= win_e:
            return True
    return False


def find_and_merge_into_existing_db_lecture(
    schedule: List[dict],
    main_batch,
    topic,
    from_date: date,
    end_date: date,
) -> bool:
    """
    Try to attach main_batch to an existing DB lecture for this topic
    for any mergeable batch, on ANY date between from_date and end_date
    (not just the current loop date).

    If found and all constraints pass, append a schedule row for that lecture date.
    """
    if remaining_for(main_batch, topic, schedule) <= 0:
        return False

    if not parents_completed_for_topic(main_batch, topic, schedule):
        return False

    mergeable_ids = allowed_merge_batch_ids(main_batch)
    if not mergeable_ids:
        return False

    # Look for any future (and current) DB lectures for this topic
    existing_qs = (
        Live_Lecture.objects
        .filter(
            lacture_date__range=[from_date, end_date],
            is_cancel=False,
            topic_id=topic,
            batches__id__in=mergeable_ids,
        )
        .select_related('faculty')
        .prefetch_related('batches', 'topics')
        .distinct()
        .order_by('lacture_date', 'start_time')
    )

    for lec in existing_qs:
        lec_date = lec.lacture_date
        day_name = lec_date.strftime("%A")
        day_str = lec_date.strftime("%Y-%m-%d")
        win_s, win_e = lec.start_time, lec.end_time

        # If we already merged this batch into this lecture in preview, skip
        already_merged = any(
            (row.get("lecture_id") == lec.id and main_batch.id in row.get("batch_ids", []))
            for row in schedule
        )
        if already_merged:
            continue

        # Batch must actually be free & have a slot for this lecture's time
        if not batch_slot_covers_window(main_batch, day_name, win_s, win_e):
            continue

        if any_batch_time_conflict([main_batch.id], lec_date, win_s, win_e, schedule):
            continue

        if not monthly_batch_quota_ok(main_batch, lec_date, schedule):
            continue

        if not batch_week_quota_ok(main_batch, lec_date, schedule):
            continue
        
        if not weekly_distribution_ok(main_batch, lec_date, schedule):
            continue

        # Per-day batch limit on that lecture date
        today_count = batch_lectures_on_day(main_batch.id, day_str, schedule)

        if today_count >= MAX_LECTURES_PER_DAY_PER_BATCH:
            continue

        # Prefer only 1 lecture per day unless weekly target needs 2
        if today_count >= 1 and not should_allow_second_lecture_today(main_batch, lec_date, schedule):
            continue

        # NOTE: we DO NOT check global_time_block_all or faculty_week_quota_ok here,
        # because we are not creating a new lecture, just adding another batch
        # to an already existing DB lecture. Faculty & global time are already taken.

        other_batch_ids = list(lec.batches.values_list('id', flat=True))
        other_batch_names = list(lec.batches.values_list('batch_name', flat=True))

        schedule.append({
            "lecture_id": lec.id,
            "topic_id": topic.id,
            "topic_name": topic.topic_name,
            "section_id": topic.section_id,
            "section_name": getattr(topic.section, "section_name", ""),
            "faculty_id": lec.faculty_id,
            "faculty_name": str(getattr(lec.faculty, "faculty_name", "")),
            "batch_ids": [main_batch.id] + other_batch_ids,
            "batch_names": [main_batch.batch_name] + other_batch_names,
            "date": day_str,
            "day": day_name,
            "start_time": win_s.strftime("%H:%M"),
            "end_time": win_e.strftime("%H:%M"),
            "note": "Merged into existing DB lecture (future)",
            "status_tag": "merged_existing",
        })
        return True

    return False


def merge_batch_into_future_topic_lectures(
    main_batch,
    topic,
    from_date: date,
    end_date: date,
    schedule: List[dict],
) -> bool:
    """
    For dependent topics:
    If any *mergeable* batch already has this topic scheduled in the preview
    between from_date and end_date, add main_batch to that lecture instead
    of creating a new lecture.
    """
    mergeable_ids = allowed_merge_batch_ids(main_batch)
    if not mergeable_ids:
        return False

    for lec in schedule:
        if lec.get("topic_id") != topic.id:
            continue

        try:
            lec_date = datetime.strptime(lec["date"], "%Y-%m-%d").date()
        except Exception:
            continue

        if lec_date < from_date or lec_date > end_date:
            continue

        if not (set(lec["batch_ids"]) & mergeable_ids):
            continue

        day_name = lec_date.strftime("%A")
        try:
            win_s = datetime.strptime(lec["start_time"], "%H:%M").time()
            win_e = datetime.strptime(lec["end_time"], "%H:%M").time()
        except Exception:
            continue

        if not batch_slot_covers_window(main_batch, day_name, win_s, win_e):
            continue

        if any_batch_time_conflict(
            [main_batch.id], lec_date, win_s, win_e, schedule
        ):
            continue

        if not monthly_batch_quota_ok(main_batch, lec_date, schedule):
            continue
        if not batch_week_quota_ok(main_batch, lec_date, schedule):
            continue
        day_str = lec_date.strftime("%Y-%m-%d")
        today_count = batch_lectures_on_day(main_batch.id, day_str, schedule)

        if today_count >= MAX_LECTURES_PER_DAY_PER_BATCH:
            continue

        if today_count >= 1 and not should_allow_second_lecture_today(main_batch, lec_date, schedule):
            continue

        if main_batch.id not in lec["batch_ids"]:
            lec["batch_ids"].append(main_batch.id)
        if main_batch.batch_name not in lec["batch_names"]:
            lec["batch_names"].append(main_batch.batch_name)

        # keep section info consistent with current topic
        lec["section_id"] = topic.section_id
        lec["section_name"] = getattr(topic.section, "section_name", "")

        old_note = lec.get("note") or ""
        if old_note:
            lec["note"] = f"{old_note} + merged future ({main_batch.batch_name})"
        else:
            lec["note"] = f"Merged future ({main_batch.batch_name})"

        if lec.get("status_tag") not in ("merged_existing", "merged_new"):
            lec["status_tag"] = "merged_future"

        return True

    return False


# ----------------
# Batch eligibility for merge/new
# ----------------
def get_batch_week_limit(batch_id: int) -> int:
    DEFAULT_BATCH_WEEK_LIMIT = 6

    if batch_id in _batch_week_limit_cache:
        return _batch_week_limit_cache[batch_id]

    try:
        lim = Batch_Management.objects.only('id', 'lecture_per_week').get(id=batch_id).lecture_per_week
    except Batch_Management.DoesNotExist:
        lim = None

    if not isinstance(lim, int) or lim <= 0:
        lim = DEFAULT_BATCH_WEEK_LIMIT

    _batch_week_limit_cache[batch_id] = lim
    return lim


def batch_week_quota_ok(batch, day: date, preview: List[dict]) -> bool:
    limit = get_batch_week_limit(batch.id)
    week_start, week_end = week_bounds(day)

    db_cnt = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__range=[week_start, week_end]
    ).count()

    prev_cnt = sum(
        1 for lec in preview
        if batch.id in lec["batch_ids"]
        and week_start.strftime("%Y-%m-%d") <= lec["date"] <= week_end.strftime("%Y-%m-%d")
    )

    total = db_cnt + prev_cnt
    return total < limit


def batch_can_attend_topic_on_day(batch, topic, day_name: str, the_date: date, schedule: List[dict]) -> bool:
    if topic.id not in allowed_topic_ids_for_batch(batch):
        return False
    if remaining_for(batch, topic, schedule) <= 0:
        return False
    if not parents_completed_for_topic(batch, topic, schedule):
        return False
    if not monthly_batch_quota_ok(batch, the_date, schedule):
        return False
    if not batch_week_quota_ok(batch, the_date, schedule):
        return False
    day_str = the_date.strftime("%Y-%m-%d")
    if batch_lectures_on_day(batch.id, day_str, schedule) >= MAX_LECTURES_PER_DAY_PER_BATCH:
        return False
    if not BatchTimeSlot.objects.filter(batch=batch, day=day_name).exists():
        return False
    return True


# ----------------
# Last topic helper
# ----------------
def last_topic_for_batch(batch, preview: List[dict]) -> Optional[int]:
    for lec in reversed(preview):
        if batch.id in lec.get("batch_ids", []):
            return lec.get("topic_id")
    return None

def faculty_on_leave(faculty_id, lecture_date):
    return FacultyLeaves.objects.filter(
        faculty_id=faculty_id,
        from_date__lte=lecture_date,
        to_date__gte=lecture_date
    ).exists()
    
def add_not_generated_reason(not_generated_topics, topic, batch, reason, date_obj=None, repeat_by_date=False):
    """
    Store topic skip / not-generated reason without changing scheduling logic.
    Prevents duplicate repeated messages for same topic + reason + date.
    """

    item = {
        "topic_id": topic.id if topic else None,
        "topic_name": getattr(topic, "topic_name", ""),
        "batch_id": batch.id if batch else None,
        "batch_name": getattr(batch, "batch_name", ""),
        "date": date_obj.strftime("%Y-%m-%d") if date_obj else None,
        "reason": reason,
    }

    for existing in not_generated_topics:
        same_topic = existing.get("topic_id") == item["topic_id"]
        same_batch = existing.get("batch_id") == item["batch_id"]
        same_reason = existing.get("reason") == item["reason"]

        if same_topic and same_batch and same_reason:
            if not repeat_by_date:
                return

            if existing.get("date") == item["date"]:
                return

    not_generated_topics.append(item)


def add_skipped_date_reason(not_generated_topics, batch, date_obj, reason):
    """Record a date-level skip in the same visible report as topic skips."""
    item = {
        "topic_id": None,
        "topic_name": "All topics (date skipped)",
        "batch_id": batch.id,
        "batch_name": batch.batch_name,
        "date": date_obj.strftime("%Y-%m-%d"),
        "reason": reason,
    }
    if not any(
        row.get("topic_id") is None
        and row.get("batch_id") == item["batch_id"]
        and row.get("date") == item["date"]
        and row.get("reason") == item["reason"]
        for row in not_generated_topics
    ):
        not_generated_topics.append(item)

def same_topic_non_continuous_conflict(batch_id: int, topic_id: int, day_date: date, start_t: time, end_t: time, preview: List[dict]) -> bool:
    """
    Prevent same topic for same batch on same date in separated time slots.

    Allowed:
    - Same topic back-to-back continuation
      Example: 07:00-08:00 and 08:00-09:00

    Not allowed:
    - Same topic morning and evening
      Example: 07:00-08:00 and 21:00-22:00
    """

    existing_slots = []

    # Check DB lectures through Live_Lecture.topics M2M
    try:
        db_lectures = Live_Lecture.objects.filter(
            batches__id=batch_id,
            topics__id=topic_id,
            lacture_date=day_date,
            is_cancel=False
        ).distinct()

        for lec in db_lectures:
            existing_slots.append((lec.start_time, lec.end_time))
    except Exception:
        pass

    # Check DB lectures through Lecture_Topics table
    try:
        lecture_ids = Lecture_Topics.objects.filter(
            topics_id=topic_id
        ).values_list('lecture_id', flat=True)

        db_lectures = Live_Lecture.objects.filter(
            id__in=lecture_ids,
            batches__id=batch_id,
            lacture_date=day_date,
            is_cancel=False
        ).distinct()

        for lec in db_lectures:
            existing_slots.append((lec.start_time, lec.end_time))
    except Exception:
        pass

    # Check preview lectures
    day_str = day_date.strftime("%Y-%m-%d")

    for lec in preview:
        if lec.get("date") != day_str:
            continue

        if batch_id not in lec.get("batch_ids", []):
            continue

        if topic_id not in {
            lec.get("topic_id"),
            lec.get("extra_topic_id")
        }:
            continue

        try:
            prev_start = datetime.strptime(lec["start_time"], "%H:%M").time()
            prev_end = datetime.strptime(lec["end_time"], "%H:%M").time()
            existing_slots.append((prev_start, prev_end))
        except Exception:
            continue

    # No same topic already scheduled today, so allowed
    if not existing_slots:
        return False

    # If same topic exists today, allow only if this new slot is continuous
    for existing_start, existing_end in existing_slots:
        if existing_end == start_t or existing_start == end_t:
            return False

    # Same topic exists today, but not continuous
    return True
    
def batch_has_lecture_on_date(batch_id: int, day_date: date, preview: List[dict]) -> bool:
    day_str = day_date.strftime("%Y-%m-%d")

    db_exists = Live_Lecture.objects.filter(
        batches__id=batch_id,
        lacture_date=day_date,
        is_cancel=False
    ).exists()

    if db_exists:
        return True

    return any(
        lec.get("date") == day_str and batch_id in lec.get("batch_ids", [])
        for lec in preview
    )


def batch_has_slot_on_date(batch, day_date: date) -> bool:
    day_name = day_date.strftime("%A")

    return BatchTimeSlot.objects.filter(
        batch=batch,
        day=day_name
    ).exists()


def previous_batch_timetable_date(batch, current_date: date) -> Optional[date]:
    """Return the previous configured timetable date for this batch."""
    cursor = current_date - timedelta(days=1)

    # A weekly timetable repeats every seven days. Fourteen days also safely
    # covers unusually sparse configurations without an unbounded DB loop.
    for _ in range(14):
        if batch_has_slot_on_date(batch, cursor):
            return cursor
        cursor -= timedelta(days=1)

    return None


def must_schedule_to_avoid_two_missed_days(
    batch,
    current_date: date,
    preview: List[dict],
) -> bool:
    """
    A distribution/spacing rule must not skip the current timetable date when
    the immediately previous timetable date was also left without a lecture.

    Hard batch/faculty quotas, leave, dependency and conflict checks are still
    enforced by the normal scheduling pipeline.
    """
    previous_date = previous_batch_timetable_date(batch, current_date)
    if previous_date is None:
        return False

    return not batch_has_lecture_on_date(batch.id, previous_date, preview)


def should_skip_for_smart_gap(batch, current_date: date, preview: List[dict]) -> bool:
    """
    Skip consecutive lecture days only when the batch has enough available days
    in the week to maintain a 1-day gap.

    Example:
    Weekly limit = 6, max/day = 2
    Required lecture days = 3

    If available days are 6 or 7, schedule alternate days.
    If available days are only 3, do not enforce gap.
    """

    if MAX_LECTURES_PER_DAY_PER_BATCH is None or MAX_LECTURES_PER_DAY_PER_BATCH <= 0:
        return False

    week_start, week_end = week_bounds(current_date)

    weekly_limit = get_batch_week_limit(batch.id)

    required_days = (weekly_limit + MAX_LECTURES_PER_DAY_PER_BATCH - 1) // MAX_LECTURES_PER_DAY_PER_BATCH

    available_dates = []

    d = week_start
    while d <= week_end:
        if batch_has_slot_on_date(batch, d):
            available_dates.append(d)
        d += timedelta(days=1)

    # To maintain 1-day gap, required days need this many calendar dates:
    # 3 lecture days need at least 5 available calendar spread: Mon/Wed/Fri
    min_days_needed_for_gap = (required_days * 2) - 1

    # If not enough available days, do not enforce gap.
    # Example: only Mon/Tue/Wed available, then use all 3.
    if len(available_dates) < min_days_needed_for_gap:
        return False

    previous_date = current_date - timedelta(days=1)

    if batch_has_lecture_on_date(batch.id, previous_date, preview):
        return True

    return False

def last_faculty_for_batch(batch, preview: List[dict]) -> Optional[int]:
    for lec in reversed(preview):
        if batch.id in lec.get("batch_ids", []):
            return lec.get("faculty_id")
    return None


def min_faculty_priority_for_topic(topic, batch, day_name: str, day_str: str, current_date: date, preview: List[dict], last_faculty_id=None):
    """
    Lower score = better candidate.

    Preference:
    1. Faculty priority slot (priority 1 first)
    2. Faculty with fewer lectures today for this batch
    3. Different faculty than last scheduled lecture
    4. Faculty with fewer lectures this week
    """

    fac_details = FacultyDetails.objects.filter(
        topic_expertise=topic,
        selected_exams=batch.exam_category_id,
        faculty__status='Active'
    ).select_related('faculty')

    best_score = (10**9, 10**9, 10**9, 10**9, topic.id)

    for fd in fac_details:
        fac = fd.faculty

        if faculty_on_leave(fac.id, current_date):
            continue

        fslots = FacultyTimeSlot.objects.filter(
            faculty=fac,
            day=day_name
        ).annotate(
            priority_order=Case(
                When(priority=0, then=Value(999999)),
                default='priority',
                output_field=IntegerField()
            )
        ).order_by(
            'priority_order',
            'start_time'
        )

        if not fslots.exists():
            continue

        today_count = faculty_lectures_on_day(
            fac.id,
            batch.id,
            day_str,
            preview
        )

        week_start, week_end = week_bounds(current_date)

        week_count = faculty_lectures_in_week(
            faculty_id=fac.id,
            batch_id=batch.id,
            week_start=week_start,
            week_end=week_end,
            preview=preview
        )

        same_as_last = 1 if last_faculty_id and fac.id == last_faculty_id else 0

        min_priority = fslots.first().priority_order

        # An available priority-1 slot must be evaluated before lower-priority
        # faculty slots. Counts remain tie-breakers within the same priority.
        score = (
            min_priority,
            today_count,
            same_as_last,
            week_count,
            topic.id
        )

        if score < best_score:
            best_score = score

    return best_score

def weekly_distribution_ok(batch, day: date, preview: List[dict]) -> bool:
    """
    Spread weekly lectures across the week.
    Prevent finishing weekly quota too early.
    """

    weekly_limit = get_batch_week_limit(batch.id)

    if weekly_limit <= 0:
        return False

    week_start, week_end = week_bounds(day)

    total_week_days = 7
    elapsed_days = (day - week_start).days + 1

    expected_allowed_till_today = int((weekly_limit * elapsed_days) / total_week_days)
    expected_allowed_till_today = max(1, expected_allowed_till_today)

    db_cnt = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__range=[week_start, day]
    ).distinct().count()

    week_start_str = week_start.strftime("%Y-%m-%d")
    day_str = day.strftime("%Y-%m-%d")

    prev_cnt = sum(
        1 for lec in preview
        if batch.id in lec.get("batch_ids", [])
        and week_start_str <= lec.get("date", "") <= day_str
    )

    current_count_till_today = db_cnt + prev_cnt

    return current_count_till_today < expected_allowed_till_today

def slot_part(start_t: time) -> str:
    """
    Categorize slot as morning / evening / other.
    """
    if start_t < time(12, 0):
        return "morning"

    if start_t >= time(17, 0):
        return "evening"

    return "other"
    
def batch_slot_part_count_in_week(
    batch_id: int,
    week_start: date,
    week_end: date,
    part: str,
    preview: List[dict]
) -> int:
    """
    Count how many lectures this batch has in morning/evening in a week.
    Includes DB + preview.
    """

    db_count = 0

    db_lectures = Live_Lecture.objects.filter(
        batches__id=batch_id,
        is_cancel=False,
        lacture_date__range=[week_start, week_end]
    ).distinct()

    for lec in db_lectures:
        if slot_part(lec.start_time) == part:
            db_count += 1

    week_start_str = week_start.strftime("%Y-%m-%d")
    week_end_str = week_end.strftime("%Y-%m-%d")

    preview_count = 0

    for lec in preview:
        if batch_id not in lec.get("batch_ids", []):
            continue

        if not (week_start_str <= lec.get("date", "") <= week_end_str):
            continue

        try:
            start_t = datetime.strptime(lec["start_time"], "%H:%M").time()
        except Exception:
            continue

        if slot_part(start_t) == part:
            preview_count += 1

    return db_count + preview_count
    
def slot_distribution_score(
    batch_id: int,
    day_date: date,
    start_t: time,
    preview: List[dict]
) -> int:
    """
    Lower score is better.

    Goal:
    - Avoid always selecting morning.
    - Prefer evening if morning count is already higher.
    """

    week_start, week_end = week_bounds(day_date)

    morning_count = batch_slot_part_count_in_week(
        batch_id,
        week_start,
        week_end,
        "morning",
        preview
    )

    evening_count = batch_slot_part_count_in_week(
        batch_id,
        week_start,
        week_end,
        "evening",
        preview
    )

    current_part = slot_part(start_t)

    if current_part == "morning":
        # Penalize morning if already more morning lectures exist
        return morning_count - evening_count

    if current_part == "evening":
        # Prefer evening when morning is ahead
        return evening_count - morning_count - 1

    return 0

def batch_double_lecture_days_in_week(
    batch_id: int,
    week_start: date,
    week_end: date,
    preview: List[dict]
) -> int:
    """
    Count how many days in this week already have 2 or more lectures.
    """

    day_counts = {}

    db_lectures = Live_Lecture.objects.filter(
        batches__id=batch_id,
        is_cancel=False,
        lacture_date__range=[week_start, week_end]
    ).distinct()

    for lec in db_lectures:
        d = lec.lacture_date.strftime("%Y-%m-%d")
        day_counts[d] = day_counts.get(d, 0) + 1

    week_start_str = week_start.strftime("%Y-%m-%d")
    week_end_str = week_end.strftime("%Y-%m-%d")

    for lec in preview:
        if batch_id not in lec.get("batch_ids", []):
            continue

        lec_date = lec.get("date", "")

        if week_start_str <= lec_date <= week_end_str:
            day_counts[lec_date] = day_counts.get(lec_date, 0) + 1

    return sum(1 for cnt in day_counts.values() if cnt >= 2)

def should_allow_second_lecture_today(batch, current_date: date, preview: List[dict]) -> bool:
    """
    Allow 2 lectures in a day only occasionally.

    Rule:
    - Max one double-lecture day per week.
    - If weekly target cannot be achieved with remaining days, allow second lecture.
    """

    week_start, week_end = week_bounds(current_date)

    weekly_limit = get_batch_week_limit(batch.id)

    week_start_str = week_start.strftime("%Y-%m-%d")
    week_end_str = week_end.strftime("%Y-%m-%d")

    current_week_count = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__range=[week_start, week_end]
    ).distinct().count()

    current_week_count += sum(
        1 for lec in preview
        if batch.id in lec.get("batch_ids", [])
        and week_start_str <= lec.get("date", "") <= week_end_str
    )

    remaining_week_lectures = weekly_limit - current_week_count

    if remaining_week_lectures <= 0:
        return False

    double_days = batch_double_lecture_days_in_week(
        batch.id,
        week_start,
        week_end,
        preview
    )

    # Allow only one double lecture day per week
    if double_days >= 1:
        return False

    # Allow second lecture if needed to complete weekly target
    remaining_dates = []

    d = current_date
    while d <= week_end:
        if BatchTimeSlot.objects.filter(batch=batch, day=d.strftime("%A")).exists():
            remaining_dates.append(d)
        d += timedelta(days=1)

    return remaining_week_lectures > len(remaining_dates)

def get_last_topic_lecture_date_for_batch(batch, topic, preview: List[dict]) -> Optional[date]:
    """
    Get latest lecture date for this batch + topic from DB and preview.
    Used to maintain gap between multiple lectures of same topic.
    """

    last_date = None

    # DB lectures through topics M2M
    try:
        db_lecture = (
            Live_Lecture.objects
            .filter(
                batches=batch,
                topics=topic,
                is_cancel=False
            )
            .order_by('-lacture_date', '-start_time')
            .first()
        )

        if db_lecture:
            last_date = db_lecture.lacture_date
    except Exception:
        pass

    # DB lectures through Lecture_Topics table
    try:
        lecture_ids = Lecture_Topics.objects.filter(
            topics=topic
        ).values_list('lecture_id', flat=True)

        db_lecture = (
            Live_Lecture.objects
            .filter(
                id__in=lecture_ids,
                batches=batch,
                is_cancel=False
            )
            .order_by('-lacture_date', '-start_time')
            .first()
        )

        if db_lecture and (last_date is None or db_lecture.lacture_date > last_date):
            last_date = db_lecture.lacture_date
    except Exception:
        pass

    # Preview lectures
    for lec in preview:
        if batch.id not in lec.get("batch_ids", []):
            continue

        if topic.id not in {
            lec.get("topic_id"),
            lec.get("extra_topic_id")
        }:
            continue

        try:
            lec_date = datetime.strptime(lec["date"], "%Y-%m-%d").date()
        except Exception:
            continue

        if last_date is None or lec_date > last_date:
            last_date = lec_date

    return last_date
def batch_available_lecture_capacity_left(batch, from_date: date, end_date: date, preview: List[dict]) -> int:
    """
    Estimate how many lectures can still be scheduled for this batch
    from from_date to end_date based on batch slots, weekly quota, monthly quota,
    and max lectures per day.
    """

    capacity = 0
    d = from_date

    while d <= end_date:
        day_name = d.strftime("%A")

        if not BatchTimeSlot.objects.filter(batch=batch, day=day_name).exists():
            d += timedelta(days=1)
            continue

        if not monthly_batch_quota_ok(batch, d, preview):
            d += timedelta(days=1)
            continue

        if not batch_week_quota_ok(batch, d, preview):
            d += timedelta(days=1)
            continue

        current_day_count = batch_lectures_on_day(
            batch.id,
            d.strftime("%Y-%m-%d"),
            preview
        )

        allowed_today = max(
            0,
            MAX_LECTURES_PER_DAY_PER_BATCH - current_day_count
        )

        capacity += allowed_today

        d += timedelta(days=1)

    return capacity
def same_topic_gap_ok(
    batch,
    topic,
    current_date: date,
    end_date: date,
    preview: List[dict]
) -> bool:
    """
    Maintain 1-2 day gap between multiple lectures of same topic.

    Preferred:
    - At least 1 day gap after previous lecture.
    - Better if 2 days gap.

    But if remaining syllabus capacity is tight, allow earlier scheduling.
    """

    last_date = get_last_topic_lecture_date_for_batch(batch, topic, preview)

    if not last_date:
        return True

    gap_days = (current_date - last_date).days

    # Same day duplicate is already handled by same_topic_non_continuous_conflict
    if gap_days <= 0:
        return True

    # Ideal gap: 2 days
    if gap_days >= 2:
        return True

    # Accept 1 day gap if needed
    if gap_days == 1:
        total_remaining = total_remaining_for_batch(batch, preview)
        capacity_left = batch_available_lecture_capacity_left(
            batch,
            current_date,
            end_date,
            preview
        )

        # If we have enough capacity, wait for a better gap
        if capacity_left > total_remaining:
            return False

        # If capacity is tight, allow it
        return True

    return True
def batch_has_any_lecture_in_preview_or_db(batch, start_date: date, current_date: date, preview: List[dict]) -> bool:
    db_exists = Live_Lecture.objects.filter(
        batches=batch,
        is_cancel=False,
        lacture_date__range=[start_date, current_date]
    ).exists()

    if db_exists:
        return True

    start_str = start_date.strftime("%Y-%m-%d")
    current_str = current_date.strftime("%Y-%m-%d")

    return any(
        batch.id in lec.get("batch_ids", [])
        and start_str <= lec.get("date", "") <= current_str
        for lec in preview
    )
# ----------------
# Main generator
# ----------------
def generate_lectures(request):
    _ALLOWED_TOPIC_IDS_CACHE.clear()
    _fac_week_limit_cache.clear()
    _batch_week_limit_cache.clear()
    debug = request.GET.get('debug') in ('1', 'true', 'yes')
    debug_logs: List[str] = []
    not_generated_topics: List[dict] = []

    try:
        batches = Batch_Management.objects.filter(status='Active')
        start_date_str = request.GET.get('start_date')
        end_date_str = request.GET.get('end_date')

        batch_ids = request.GET.getlist('batch_ids')

        # fallback for old single batch dropdown
        if not batch_ids and request.GET.get('batch_id'):
            batch_ids = [request.GET.get('batch_id')]

        batch_ids = [int(bid) for bid in batch_ids if str(bid).isdigit()]

        if not (start_date_str and end_date_str and batch_ids):
            return render(request, "preview-lecture.html", {
                'batches': batches,
                'schedule': [],
                'schedule_json': json.dumps([], default=str),
                'debug_logs': debug_logs,
                'selected_batch_ids': [str(x) for x in batch_ids],
            })

        start_date = datetime.strptime(start_date_str, "%Y-%m-%d").date()
        end_date = datetime.strptime(end_date_str, "%Y-%m-%d").date()
        selected_batches = list(
            Batch_Management.objects.filter(
                id__in=batch_ids,
                status='Active'
            ).order_by('id')
        )

        if not selected_batches:
            return JsonResponse({
                "status": "empty",
                "message": "No active selected batches found."
            })

        schedule: List[dict] = []

        for main_batch in selected_batches:
            try:
                _mb_rel = getattr(main_batch, "merged_batches", None)
                if hasattr(_mb_rel, "all"):
                    merged_batches = list(_mb_rel.all().filter(status='Active').exclude(id=main_batch.id))
                else:
                    merged_batches = []
            except Exception:
                merged_batches = []

            allowed_ids = allowed_topic_ids_for_batch(main_batch)
            if not allowed_ids:
                return JsonResponse({"status": "empty", "message": "No allowed topics."})

            current = start_date

            try:
                _mb_rel = getattr(main_batch, "merged_batches", None)
                if hasattr(_mb_rel, "all"):
                    merged_batches = list(
                        _mb_rel.all()
                        .filter(status='Active')
                        .exclude(id=main_batch.id)
                    )
                else:
                    merged_batches = []
            except Exception:
                merged_batches = []

            allowed_ids = allowed_topic_ids_for_batch(main_batch)

            if not allowed_ids:
                add_not_generated_reason(
                    not_generated_topics,
                    None,
                    main_batch,
                    "No allowed syllabus topics found for this batch.",
                    None
                )
                continue

#            while current <= end_date:
            while current <= end_date:
                if total_remaining_for_batch(main_batch, schedule) == 0:
                    break

                day_str = current.strftime("%Y-%m-%d")
                day_name = current.strftime("%A")
                if debug:
                    debug_logs.append(
                        f"[{current}] Checking {main_batch.batch_name} | "
                        f"Day={day_name} | "
                        f"WeekLimit={get_batch_week_limit(main_batch.id)} | "
                        f"TodayCount={batch_lectures_on_day(main_batch.id, day_str, schedule)}"
                    )

                if not monthly_batch_quota_ok(main_batch, current, schedule):
                    if batch_has_slot_on_date(main_batch, current):
                        add_skipped_date_reason(
                            not_generated_topics,
                            main_batch,
                            current,
                            "Monthly lecture quota is already full."
                        )
                    if debug:
                        debug_logs.append(
                            f"[{current}] Skipped because monthly lecture quota is full."
                        )
                    current += timedelta(days=1)
                    continue

                has_started_in_selected_range = batch_has_any_lecture_in_preview_or_db(
                    main_batch,
                    start_date,
                    current,
                    schedule
                )

                if not batch_week_quota_ok(main_batch, current, schedule):
                    if batch_has_slot_on_date(main_batch, current):
                        add_skipped_date_reason(
                            not_generated_topics,
                            main_batch,
                            current,
                            "Weekly lecture quota is already full."
                        )
                    if debug:
                        debug_logs.append(
                            f"[{current}] Skipped because weekly lecture quota is full."
                        )
                    current += timedelta(days=1)
                    continue
                distribution_allows_date = monthly_distribution_ok(
                    main_batch,
                    current,
                    schedule,
                )
                force_after_missed_date = must_schedule_to_avoid_two_missed_days(
                    main_batch,
                    current,
                    schedule,
                )

                if (
                    has_started_in_selected_range
                    and not distribution_allows_date
                    and not force_after_missed_date
                ):
                    if batch_has_slot_on_date(main_batch, current):
                        add_skipped_date_reason(
                            not_generated_topics,
                            main_batch,
                            current,
                            "Skipped to keep lectures distributed across the batch's available dates this month."
                        )
                    if debug:
                        debug_logs.append(
                            f"[{current}] Skipped because monthly distribution limit reached for this date."
                        )
                    current += timedelta(days=1)
                    continue

                if (
                    debug
                    and has_started_in_selected_range
                    and force_after_missed_date
                    and not distribution_allows_date
                ):
                    debug_logs.append(
                        f"[{current}] Monthly distribution spacing bypassed to avoid "
                        "two consecutive missed timetable dates."
                    )


    #            if should_skip_for_smart_gap(main_batch, current, schedule):
    #                if debug:
    #                    debug_logs.append(f"[{current}] Skipped for gap enforcement (monthly spacing rule)")
    #                current += timedelta(days=1)
    #                continue

                batch_slots = list(
                    BatchTimeSlot.objects
                    .filter(batch=main_batch, day=day_name)
                    .order_by('start_time')
                )
                if not batch_slots:
                    if debug:
                        debug_logs.append(
                            f"[{current}] No batch slot found for {main_batch.batch_name} on {day_name}."
                        )
                    current += timedelta(days=1)
                    continue
                
    #            if should_skip_for_week_distribution(main_batch, current, schedule):
    #                if debug:
    #                    debug_logs.append(
    #                        f"[{current}] Skipped because date is not part of planned weekly distribution."
    #                    )
    #                current += timedelta(days=1)
    #                continue

                layers = build_dependency_layers(main_batch, schedule)

                if not layers:
                    add_skipped_date_reason(
                        not_generated_topics,
                        main_batch,
                        current,
                        "No pending eligible topic was available; topics may be completed or blocked by dependencies."
                    )
                    if debug:
                        debug_logs.append(
                            f"[{current}] No layers."
                        )
                    current += timedelta(days=1)
                    continue
                last_tid = last_topic_for_batch(main_batch, schedule)

                candidates: List['Topics'] = []
                added_topic_ids = set()

                # 1. Prefer direct dependents of last scheduled topic
                if last_tid is not None:
                    for lvl in layers:
                        for t in lvl:
                            if remaining_for(main_batch, t, schedule) <= 0:
                                if debug:
                                    debug_logs.append(
                                        f"[{current}] remaining for condition."
                                    )
                                continue

                            parents = parents_in_plan(main_batch, t)
                            parent_ids = {p.id for p in parents}

                            if last_tid in parent_ids and parents_completed_for_topic(main_batch, t, schedule):
                                candidates.append(t)
                                added_topic_ids.add(t.id)

                # 2. Add all other eligible pending topics as fallback
                # This keeps dependency logic intact because parents_completed_for_topic is still checked.
                for lvl in layers:
                    for t in lvl:
                        if t.id in added_topic_ids:
                            continue

                        if remaining_for(main_batch, t, schedule) <= 0:
                            if debug:
                                debug_logs.append(
                                    f"[{current}] remaining for condition inside layers."
                                )
                            continue

                        if not parents_completed_for_topic(main_batch, t, schedule):
                            add_not_generated_reason(
                                not_generated_topics,
                                t,
                                main_batch,
                                "Dependency topic is not completed yet.",
                                current,
                                repeat_by_date=False
                            )
                            continue

                        candidates.append(t)
                        added_topic_ids.add(t.id)

                if not candidates:
                    add_skipped_date_reason(
                        not_generated_topics,
                        main_batch,
                        current,
                        "No pending topic was eligible because its dependencies were not completed."
                    )
                    if debug:
                        debug_logs.append(
                            f"No topics."
                        )
                    current += timedelta(days=1)
                    continue
                
                last_faculty_id = last_faculty_for_batch(main_batch, schedule)

                candidates = sorted(
                    candidates,
                    key=lambda t: min_faculty_priority_for_topic(
                        topic=t,
                        batch=main_batch,
                        day_name=day_name,
                        day_str=day_str,
                        current_date=current,
                        preview=schedule,
                        last_faculty_id=last_faculty_id
                    )
                )

                for topic in candidates:
                    topic_reason_count_before = len(not_generated_topics)
                    # All topics, including independent/root topics, are eligible
                    # for a compatible preview merge.
                    merged_future = merge_batch_into_future_topic_lectures(
                        main_batch=main_batch,
                        topic=topic,
                        from_date=current,
                        end_date=end_date,
                        schedule=schedule,
                    )
                    if merged_future:
                        if debug:
                            debug_logs.append(
                                f"[{current}] {topic.topic_name}: merged into future lecture instead of new slot."
                            )
                        continue

                    if find_and_merge_into_existing_db_lecture(schedule, main_batch, topic, current, end_date):
                        if debug:
                            debug_logs.append(
                                f"[{current}] {topic.topic_name}: merged into existing DB lecture."
                            )
                        break

                    fac_details = FacultyDetails.objects.filter(
                        topic_expertise=topic,
                        selected_exams=main_batch.exam_category_id,
                        faculty__status='Active'
                    ).select_related('faculty')
                    if not fac_details.exists():
                        add_not_generated_reason(
                            not_generated_topics,
                            topic,
                            main_batch,
                            "No active faculty mapped for this topic and exam category.",
                            current
                        )
                        if debug:
                            debug_logs.append(
                                f"No active faculty mapped for this topic and exam category."
                            )
                        continue
                    started_faculty_id = get_topic_started_faculty_id(
                        main_batch,
                        topic,
                        schedule
                    )

                    if started_faculty_id:
                        started_faculty = Faculty.objects.filter(id=started_faculty_id).first()

                        # If the topic's original faculty is on leave today,
                        # skip this topic completely and try next topic.
                        if faculty_on_leave(started_faculty_id, current):
                            add_not_generated_reason(
                                not_generated_topics,
                                topic,
                                main_batch,
                                f"Topic already started with {getattr(started_faculty, 'faculty_name', started_faculty_id)}, but that faculty is on leave.",
                                current
                            )

                            if debug:
                                debug_logs.append(
                                    f"[{current}] {topic.topic_name}: skipped because it is already started "
                                    f"by {getattr(started_faculty, 'faculty_name', started_faculty_id)} "
                                    f"and that faculty is on leave."
                                )
                            continue
                    topic_scheduled = False
                    valid_options = []
                    for fd in fac_details:
                        fac = fd.faculty
                        
                        if faculty_on_leave(fac.id, current):
                            add_not_generated_reason(
                                not_generated_topics,
                                topic,
                                main_batch,
                                f"Faculty {fac.faculty_name} is on leave.",
                                current
                            )

                            if debug:
                                debug_logs.append(
                                    f"[{current}] {topic.topic_name}: skipped {fac.faculty_name}, faculty on leave."
                                )
                            continue
                        fslots = FacultyTimeSlot.objects.filter(
                            faculty=fac,
                            day=day_name
                        ).annotate(
                            priority_order=Case(
                                When(priority=0, then=Value(999999)),
                                default='priority',
                                output_field=IntegerField()
                            )
                        ).order_by(
                            'priority_order',
                            'start_time'
                        )
                        for fslot in fslots:
                            for bslot in batch_slots:
                                windows = overlap_windows(bslot, fslot)
                                for win_s, win_e in windows:
                                    if MAX_LECTURES_PER_DAY_PER_BATCH is not None and \
                                       batch_lectures_on_day(main_batch.id, day_str, schedule) >= MAX_LECTURES_PER_DAY_PER_BATCH:
                                        break
                                    if any_batch_time_conflict([main_batch.id], current, win_s, win_e, schedule):
                                        add_not_generated_reason(
                                            not_generated_topics,
                                            topic,
                                            main_batch,
                                            f"Batch has time conflict at {win_s.strftime('%H:%M')}-{win_e.strftime('%H:%M')}.",
                                            current
                                        )
                                        continue

                                    if faculty_has_conflict(fac.id, current, win_s, win_e, schedule):
                                        add_not_generated_reason(
                                            not_generated_topics,
                                            topic,
                                            main_batch,
                                            f"Faculty {fac.faculty_name} has time conflict at {win_s.strftime('%H:%M')}-{win_e.strftime('%H:%M')}.",
                                            current
                                        )
                                        continue

                                    if MAX_LECTURES_PER_DAY_PER_FACULTY is not None and \
                                       faculty_lectures_on_day(fac.id, main_batch.id, day_str, schedule) >= MAX_LECTURES_PER_DAY_PER_FACULTY:
                                        add_not_generated_reason(
                                            not_generated_topics,
                                            topic,
                                            main_batch,
                                            f"Faculty {fac.faculty_name} daily lecture limit reached.",
                                            current,
                                            repeat_by_date=True
                                        )
                                        continue

                                    if not faculty_week_quota_ok(fac.id, main_batch.id, current, schedule):
                                        add_not_generated_reason(
                                            not_generated_topics,
                                            topic,
                                            main_batch,
                                            f"Faculty {fac.faculty_name} weekly lecture limit reached.",
                                            current,
                                            repeat_by_date=True
                                        )
                                        continue

                                    # Different batches may run simultaneously when both the
                                    # batch and faculty are free. Their conflicts were already
                                    # checked above, so no institution-wide time block is needed.
                                                                            
                                    if same_topic_non_continuous_conflict(main_batch.id, topic.id, current, win_s, win_e, schedule):
                                        add_not_generated_reason(
                                            not_generated_topics,
                                            topic,
                                            main_batch,
                                            f"Same topic already scheduled on this date in a non-continuous time slot. Proposed slot {win_s.strftime('%H:%M')}-{win_e.strftime('%H:%M')} skipped.",
                                            current,
                                            repeat_by_date=True
                                        )
                                        continue
                                    if not same_topic_gap_ok(main_batch, topic, current, end_date, schedule):
                                        add_not_generated_reason(
                                            not_generated_topics,
                                            topic,
                                            main_batch,
                                            "Same topic lecture skipped to maintain 1-2 day gap.",
                                            current,
                                            repeat_by_date=True
                                        )
                                        continue
    #                                if global_time_block_all(current, win_s, win_e, schedule):
    #                                    continue

                                    matched_batches = [main_batch]
                                    for mb in merged_batches:
                                        if not batch_can_attend_topic_on_day(mb, topic, day_name, current, schedule):
                                            continue

                                        if any_batch_time_conflict([mb.id], current, win_s, win_e, schedule):
                                            continue

                                        if same_topic_non_continuous_conflict(mb.id, topic.id, current, win_s, win_e, schedule):
                                            continue

                                        if not same_topic_gap_ok(mb, topic, current, end_date, schedule):
                                            continue

                                        matched_batches.append(mb)
                                    valid_options.append({
                                        "topic": topic,
                                        "faculty": fac,
                                        "matched_batches": matched_batches,
                                        "start_time": win_s,
                                        "end_time": win_e,
                                        "faculty_priority": getattr(fslot, "priority", 999999) or 999999,
                                        "slot_score": slot_distribution_score(main_batch.id, current, win_s, schedule),
                                    })
    #                                schedule.append({
    #                                    "lecture_id": None,
    #                                    "topic_id": topic.id,
    #                                    "topic_name": topic.topic_name,
    #                                    "section_id": topic.section_id,
    #                                    "section_name": getattr(topic.section, "section_name", ""),
    #                                    "faculty_id": fac.id,
    #                                    "faculty_name": str(fac.faculty_name),
    #                                    "batch_ids": [b.id for b in matched_batches],
    #                                    "batch_names": [b.batch_name for b in matched_batches],
    #                                    "date": day_str,
    #                                    "day": day_name,
    #                                    "start_time": win_s.strftime("%H:%M"),
    #                                    "end_time": win_e.strftime("%H:%M"),
    #                                    "note": "Merged" if len(matched_batches) > 1 else "Single Batch (new)",
    #                                    "status_tag": "merged_new" if len(matched_batches) > 1 else "new",
    #                                })
    #                                topic_scheduled = True
    #                                if debug:
    #                                    debug_logs.append(
    #                                        f"[{current}] {topic.topic_name}: scheduled "
    #                                        f"{win_s.strftime('%H:%M')}-{win_e.strftime('%H:%M')} "
    #                                        f"for batches {[b.batch_name for b in matched_batches]}"
    #                                    )
    #                                break
    #                        else:
    #                            continue
    #                        break
                        else:
                            continue
                        break
                    if valid_options:
                        valid_options = sorted(
                            valid_options,
                            key=lambda x: (
                                x["faculty_priority"],
                                x["slot_score"],
                                x["start_time"].strftime("%H:%M")
                            )
                        )

                        selected = valid_options[0]

                        fac = selected["faculty"]
                        matched_batches = selected["matched_batches"]
                        win_s = selected["start_time"]
                        win_e = selected["end_time"]

                        schedule.append({
                            "lecture_id": None,
                            "topic_id": topic.id,
                            "topic_name": topic.topic_name,
                            "section_id": topic.section_id,
                            "section_name": getattr(topic.section, "section_name", ""),
                            "faculty_id": fac.id,
                            "faculty_name": str(fac.faculty_name),
                            "batch_ids": [b.id for b in matched_batches],
                            "batch_names": [b.batch_name for b in matched_batches],
                            "date": day_str,
                            "day": day_name,
                            "start_time": win_s.strftime("%H:%M"),
                            "end_time": win_e.strftime("%H:%M"),
                            "note": "Merged" if len(matched_batches) > 1 else "Single Batch (new)",
                            "status_tag": "merged_new" if len(matched_batches) > 1 else "new",
                        })

                        topic_scheduled = True

                        if debug:
                            debug_logs.append(
                                f"[{current}] {topic.topic_name}: scheduled "
                                f"{win_s.strftime('%H:%M')}-{win_e.strftime('%H:%M')} "
                                f"for batches {[b.batch_name for b in matched_batches]}"
                            )
                    if (
                        not topic_scheduled
                        and len(not_generated_topics) == topic_reason_count_before
                    ):
                        add_not_generated_reason(
                            not_generated_topics,
                            topic,
                            main_batch,
                            "No valid faculty/batch slot combination found.",
                            current,
                            repeat_by_date=True
                        )
                current += timedelta(days=1)

        schedule.sort(
            key=lambda row: (
                row.get("date", ""),
                row.get("start_time", ""),
                ",".join(str(x) for x in row.get("batch_ids", [])),
            )
        )

        return render(request, "preview-lecture.html", {
            'batches': batches,
            'start_date': start_date_str,
            'end_date': end_date_str,
            'selected_batch_ids': [str(x) for x in batch_ids],
            'schedule': schedule,
            'schedule_json': json.dumps(schedule, default=str),
            'debug_logs': debug_logs if debug else [],
            'not_generated_topics': not_generated_topics,
            'not_generated_topics_json': json.dumps(not_generated_topics, default=str),
        })

    except Exception as e:
        tb = traceback.extract_tb(e.__traceback__)
        filename, lineno, func, text = tb[-1]
        return JsonResponse({"error": f"{e} (File: {filename}, Line: {lineno})"}, status=500)

def get_next_topic_lecture_number(batch_ids, topic_id):
    """
    Get next lecture number for this topic across selected batch/batches.

    Example:
    If Averages already has 2 lectures for this batch,
    next title should be Averages 3.
    """

    existing_count = Live_Lecture.objects.filter(
        batches__id__in=batch_ids,
        topic_id=topic_id,
        is_cancel=False
    ).distinct().count()

    return existing_count + 1
# ---------------------------------------------------------------------
# Persist preview rows
# ---------------------------------------------------------------------
@csrf_exempt
def save_previewed_lectures(request):
    if request.method != 'POST':
        return JsonResponse({"error": "Invalid request"}, status=400)

    data = json.loads(request.POST.get('payload', '[]'))
    for item in data:
        lecture = None

        lecture_id = item.get('lecture_id')
        if lecture_id:
            try:
                lecture = Live_Lecture.objects.get(id=lecture_id)
            except Live_Lecture.DoesNotExist:
                lecture = None

        if lecture is None:
            batch_ids = item.get('batch_ids', [])
            topic_id = item.get('topic_id')
            topic_id=item.get('topic_id')

            lecture_number = get_next_topic_lecture_number(batch_ids, topic_id)

            lecture = Live_Lecture.objects.create(
                title=f"{item['topic_name']} {lecture_number}",
                faculty_id=item['faculty_id'],
                lacture_date=item['date'],
                start_time=item['start_time'],
                end_time=item['end_time'],
                section_id=item.get('section_id'),
                lecture_type="selected_batches",
                topic_id=item.get('topic_id')
            )

        if item.get('topic_id'):
#            lecture.topics.add(item['topic_id'])
            Lecture_Topics.objects.get_or_create(
                lecture_id=lecture.id,
                topics_id=item['topic_id']
            )

        if 'extra_topic_id' in item and item['extra_topic_id']:
#            lecture.topics.add(item['extra_topic_id'])
            Lecture_Topics.objects.get_or_create(
                lecture_id=lecture.id,
                topics_id=item['extra_topic_id']
            )

        for bid in item['batch_ids']:
            lecture.batches.add(bid)
            Lecture_batches.objects.get_or_create(
                lecture_id=lecture.id,
                batches_id=bid
            )

    return JsonResponse({"message": "Lectures saved successfully!"})


def add_batch_time_slot(request):
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            batch_id = data.get("batch_id")
            day = data.get("day")
            time_slots = data.get("time_slots", [])

            if not day:
                return JsonResponse({"error": "Day is missing from the request"}, status=400)

            batch = get_object_or_404(Batch_Management, id=batch_id)

            for slot in time_slots:
                BatchTimeSlot.objects.create(
                    batch=batch,
                    day=day,
                    start_time=slot["start_time"],
                    end_time=slot["end_time"],
                )

            return JsonResponse({"message": "Time slots added successfully!"}, status=200)
        except Exception as e:
            return JsonResponse({"error": str(e)}, status=400)

    return JsonResponse({"error": "Invalid request method"}, status=405)



def delete_batch_slot(request):
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            slot_id = data.get("slot_id")

            # Fetch and delete the slot
            slot = get_object_or_404(BatchTimeSlot, id=slot_id)
            slot.delete()

            return JsonResponse({"message": "Time slot deleted successfully!"}, status=200)
        except Exception as e:
            return JsonResponse({"error": str(e)}, status=400)

    return JsonResponse({"error": "Invalid request method"}, status=405)

def get_month_range(start_date, end_date):
    """Return list of month numbers between two dates (inclusive)."""
    months = []
    current = date(start_date.year, start_date.month, 1)
    end = date(end_date.year, end_date.month, 1)
    
    while current <= end:
        month_num = current.month  # e.g., 7 for July
        months.append(month_num)
        current += relativedelta(months=1)
    return months


def save_monthly_lectures(request, batch_id):
    if request.method == "POST":
        try:
            batch = Batch_Management.objects.get(id=batch_id)
        except Batch_Management.DoesNotExist:
            return JsonResponse({"error": "Batch not found"}, status=404)

        # Generate month map for lookup
        from dateutil.relativedelta import relativedelta
        from datetime import date

        def get_month_range(start_date, end_date):
            months = []
            current = date(start_date.year, start_date.month, 1)
            end = date(end_date.year, end_date.month, 1)
            while current <= end:
                month_num = current.month  # e.g., 7 for July
                months.append(month_num)
                current += relativedelta(months=1)
            return months

        # Map: slugified_month => original_month
       
        batch_months = get_month_range(
            batch.start_date,
            batch.end_date
        )

        for key, value in request.POST.items():

            if key.startswith("monthly_lectures_"):

                month = key.replace(
                    "monthly_lectures_",
                    ""
                )

                try:

                    month = int(month)

                except ValueError:

                    continue

                if month not in batch_months:

                    continue

                try:

                    no_of_lectures = int(value)

                except ValueError:

                    no_of_lectures = 0

                MonthlyLectureCount.objects.update_or_create(

                    batch=batch,

                    month=month,

                    defaults={

                        "no_of_lectures": no_of_lectures

                    }

                )


        return JsonResponse({"message": "Saved successfully!"})

    return JsonResponse({"error": "Invalid request"}, status=400)



@login_required(login_url=ADMIN_LOGIN_URL)
def faculty_details(request, id):
    faculty = get_object_or_404(Faculty, id=id)
    faculty_detail_entries = FacultyDetails.objects.filter(faculty=faculty)

    exam_topic_map = {}

    for entry in faculty_detail_entries:
        if entry.selected_exams and entry.topic_expertise:
            exam_name = entry.selected_exams.exam_category_name
            topic_name = entry.topic_expertise.topic_name

            if exam_name in exam_topic_map:
                exam_topic_map[exam_name].append(topic_name)
            else:
                exam_topic_map[exam_name] = [topic_name]

    all_days = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]

    time_slots_list = [
        (day, FacultyTimeSlot.objects.filter(faculty=faculty, day=day).order_by("priority", "start_time"))
        for day in all_days
    ]

    faculty_leaves = FacultyLeaves.objects.filter(faculty=faculty).order_by("-from_date")

    exams = ExamCategory.objects.all()
    topics = Topics.objects.all()
    faculty_details = (
        FacultyDetails.objects
        .filter(faculty=faculty)
        .select_related(
            "selected_exams",
            "topic_expertise",
        )
        .order_by(
            "selected_exams__exam_category_name",
            "topic_expertise__topic_name",
        )
    )


    return render(request, "faculty-details.html", {
        "faculty": faculty,
        "exams": exams,
        "topics": topics,
        "time_slots_list": time_slots_list,
        "exam_topic_map": exam_topic_map,
        "faculty_leaves": faculty_leaves,
        "faculty_details": faculty_details
    })


@login_required(login_url=ADMIN_LOGIN_URL)
def add_faculty_leave(request, faculty_id):
    faculty = get_object_or_404(Faculty, id=faculty_id)

    if request.method == "POST":
        from_date = request.POST.get("from_date")
        to_date = request.POST.get("to_date")

        if not from_date or not to_date:
            messages.error(request, "From date and To date are required.")
            return redirect("faculty_details", id=faculty.id)

        if from_date > to_date:
            messages.error(request, "From date cannot be greater than To date.")
            return redirect("faculty_details", id=faculty.id)

        FacultyLeaves.objects.create(
            faculty=faculty,
            from_date=from_date,
            to_date=to_date
        )

        messages.success(request, "Faculty leave added successfully.")

    return redirect("faculty_details", id=faculty.id)


@login_required(login_url=ADMIN_LOGIN_URL)
def edit_faculty_leave(request, leave_id):
    leave = get_object_or_404(FacultyLeaves, id=leave_id)
    faculty = leave.faculty

    if request.method == "POST":
        from_date = request.POST.get("from_date")
        to_date = request.POST.get("to_date")

        if not from_date or not to_date:
            messages.error(request, "From date and To date are required.")
            return redirect("faculty_details", id=faculty.id)

        if from_date > to_date:
            messages.error(request, "From date cannot be greater than To date.")
            return redirect("faculty_details", id=faculty.id)

        leave.from_date = from_date
        leave.to_date = to_date
        leave.save()

        messages.success(request, "Faculty leave updated successfully.")

    return redirect("faculty_details", id=faculty.id)


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_faculty_leave(request, leave_id):
    leave = get_object_or_404(FacultyLeaves, id=leave_id)
    faculty_id = leave.faculty.id

    leave.delete()

    messages.success(request, "Faculty leave deleted successfully.")
    return redirect("faculty_details", id=faculty_id)
# def add_faculty_details(request):
#     return render(request, "faculty-details.html")


def add_faculty_details(request):
    try:
        data = json.loads(request.body)

        faculty_id = data.get("faculty_id")
        selected_exams = data.get("selected_exams") or []
        selected_topics = data.get("selected_topics") or []

        if not faculty_id:
            return JsonResponse(
                {
                    "status": "error",
                    "message": "Faculty ID is missing",
                },
                status=400,
            )

        if not selected_exams:
            return JsonResponse(
                {
                    "status": "error",
                    "message": "Please select at least one exam",
                },
                status=400,
            )

        if not selected_topics:
            return JsonResponse(
                {
                    "status": "error",
                    "message": "Please select at least one topic",
                },
                status=400,
            )

        faculty = Faculty.objects.get(id=faculty_id)

        created_count = 0

        for exam_id in selected_exams:
            for topic_id in selected_topics:
                _, created = FacultyDetails.objects.get_or_create(
                    faculty=faculty,
                    selected_exams_id=exam_id,
                    topic_expertise_id=topic_id,
                )

                if created:
                    created_count += 1

        return JsonResponse(
            {
                "status": "success",
                "message": f"{created_count} topic expertise record(s) added successfully.",
            }
        )

    except Faculty.DoesNotExist:
        return JsonResponse(
            {
                "status": "error",
                "message": "Faculty not found",
            },
            status=404,
        )

    except json.JSONDecodeError:
        return JsonResponse(
            {
                "status": "error",
                "message": "Invalid JSON data",
            },
            status=400,
        )

    except Exception as e:
        return JsonResponse(
            {
                "status": "error",
                "message": str(e),
            },
            status=500,
        )

def edit_faculty_details(request, detail_id):
    try:
        data = json.loads(request.body)

        exam_id = data.get("exam_id")
        topic_id = data.get("topic_id")

        if not exam_id or not topic_id:
            return JsonResponse(
                {
                    "status": "error",
                    "message": "Exam and topic are required",
                },
                status=400,
            )

        faculty_detail = FacultyDetails.objects.get(id=detail_id)

        duplicate_exists = FacultyDetails.objects.filter(
            faculty=faculty_detail.faculty,
            selected_exams_id=exam_id,
            topic_expertise_id=topic_id,
        ).exclude(id=detail_id).exists()

        if duplicate_exists:
            return JsonResponse(
                {
                    "status": "error",
                    "message": "This exam and topic expertise already exists",
                },
                status=400,
            )

        faculty_detail.selected_exams_id = exam_id
        faculty_detail.topic_expertise_id = topic_id
        faculty_detail.save(
            update_fields=[
                "selected_exams",
                "topic_expertise",
            ]
        )

        return JsonResponse(
            {
                "status": "success",
                "message": "Topic expertise updated successfully",
            }
        )

    except FacultyDetails.DoesNotExist:
        return JsonResponse(
            {
                "status": "error",
                "message": "Topic expertise record not found",
            },
            status=404,
        )

    except Exception as e:
        return JsonResponse(
            {
                "status": "error",
                "message": str(e),
            },
            status=500,
        )
        
def delete_faculty_details(request, detail_id):
    try:
        faculty_detail = FacultyDetails.objects.get(id=detail_id)
        faculty_detail.delete()

        return JsonResponse(
            {
                "status": "success",
                "message": "Topic expertise deleted successfully",
            }
        )

    except FacultyDetails.DoesNotExist:
        return JsonResponse(
            {
                "status": "error",
                "message": "Topic expertise record not found",
            },
            status=404,
        )

    except Exception as e:
        return JsonResponse(
            {
                "status": "error",
                "message": str(e),
            },
            status=500,
        )
def add_time_slot(request):
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            faculty_id = data.get("faculty_id")
            day = data.get("day")
            time_slots = data.get("time_slots", [])

            if not day:
                return JsonResponse({"error": "Day is missing from the request"}, status=400)

            faculty = get_object_or_404(Faculty, id=faculty_id)

            for slot in time_slots:
                FacultyTimeSlot.objects.create(
                    faculty=faculty,
                    day=day,
                    start_time=slot["start_time"],
                    end_time=slot["end_time"],
                )

            return JsonResponse({"message": "Time slots added successfully!"}, status=200)
        except Exception as e:
            return JsonResponse({"error": str(e)}, status=400)

    return JsonResponse({"error": "Invalid request method"}, status=405)



def delete_slot(request):
    if request.method == "POST":
        try:
            data = json.loads(request.body)
            slot_id = data.get("slot_id")

            # Fetch and delete the slot
            slot = get_object_or_404(FacultyTimeSlot, id=slot_id)
            slot.delete()

            return JsonResponse({"message": "Time slot deleted successfully!"}, status=200)
        except Exception as e:
            return JsonResponse({"error": str(e)}, status=400)

    return JsonResponse({"error": "Invalid request method"}, status=405)


@login_required(login_url=ADMIN_LOGIN_URL)
def exam_topics(request):
    if request.method == "GET":
        exam_ids = request.GET.getlist("exam_ids[]")

        if not exam_ids:
            return JsonResponse({"topics": []}, status=200)

        main_topics = Topics.objects.filter(
            id__in=ExamSyllabus.objects.filter(exam_category_id__in=exam_ids)
            .values_list("topic_id", flat=True)
        ).distinct()

        all_topics = (main_topics).distinct()

        if all_topics.exists():
            topic_list = [
                {"id": topic.id, "topic_name": topic.topic_name}
                for topic in all_topics
            ]
            return JsonResponse({"topics": topic_list}, status=200)
        else:
            return JsonResponse({"topics": []}, status=200)  # Return empty if no topics found

    return JsonResponse({"error": "Invalid request"}, status=400)
  

# -----------------------------
# Slot config (as per screenshot)
# -----------------------------
WEEKDAY_SLOTS = [
    {"slot": 1, "label": "Slot 1", "start": time(7, 0),  "end": time(8, 30)},   # 7:00 - 8:30 am
    {"slot": 2, "label": "Slot 2", "start": time(9, 0),  "end": time(10, 30)},  # 9:00 - 10:30 am
    {"slot": 3, "label": "Slot 3", "start": time(19, 0), "end": time(20, 30)},  # 7:00 - 8:30 pm
    {"slot": 4, "label": "Slot 4", "start": time(21, 0), "end": time(22, 30)},  # 9:00 - 10:30 pm
]

WEEKEND_SLOTS = [
    {"slot": 1, "label": "Slot 1", "start": time(7, 0),  "end": time(8, 30)},   # 7:00 - 8:30 am
    {"slot": 2, "label": "Slot 2", "start": time(8, 30), "end": time(10, 0)},   # 8:30 - 10:00 am
    {"slot": 3, "label": "Slot 3", "start": time(10, 0), "end": time(11, 30)},  # 10:00 - 11:30 am
    {"slot": 4, "label": "Slot 4", "start": time(11, 30),"end": time(13, 0)},   # 11:30 - 1:00 pm
]


def is_weekend(d: date) -> bool:
    return d.weekday() >= 5  # Sat/Sun


def get_slots_for_date(d: date):
    return WEEKEND_SLOTS if is_weekend(d) else WEEKDAY_SLOTS


def find_slot_for_lecture(lec_date: date, lec_start_time: time):
    slots = get_slots_for_date(lec_date)
    for s in slots:
        if s["start"] <= lec_start_time < s["end"]:
            return s["slot"]
    return None


def fmt_time_range(s: time, e: time) -> str:
    return f"{s.strftime('%I:%M %p')} - {e.strftime('%I:%M %p')}"


def color_for_section(section_name: str) -> str:
    """
    Basic mapping to resemble your sheet colors.
    Change these as per your section names.
    """
    if not section_name:
        return "bg-gray"
    name = section_name.lower()
    if "cat" in name:
        return "bg-orange"
    if "english" in name or "verbal" in name:
        return "bg-pink"
    if "lr" in name or "di" in name:
        return "bg-green"
    return "bg-blue"


@login_required(login_url=ADMIN_LOGIN_URL)
@require_GET
def lectures_timetable_html(request):
    """
    Renders HTML timetable view like spreadsheet.

    Query:
      /timetable?year=2026&month=5&batch_id=123
      OR
      /timetable?start=2026-05-01&end=2026-05-31&batch_id=123
    """

    # 1) Parse range
    start_str = request.GET.get("start")
    end_str = request.GET.get("end")
    batch_id = request.GET.get("batch_id")

    if start_str and end_str:
        start_day = datetime.strptime(start_str, "%Y-%m-%d").date()
        end_day = datetime.strptime(end_str, "%Y-%m-%d").date()
    else:
        today = timezone.localdate()
        year = int(request.GET.get("year", today.year))
        month = int(request.GET.get("month", today.month))
        start_day = date(year, month, 1)
        end_day = date(year, month, monthrange(year, month)[1])

    if end_day < start_day:
        start_day, end_day = end_day, start_day

    # 2) Fetch lectures
    qs = (
        Live_Lecture.objects
        .filter(lacture_date__range=(start_day, end_day))
        .select_related("faculty", "section", "topic")
        .prefetch_related("batches")
        .order_by("lacture_date", "start_time")
    )

    if batch_id:
        qs = qs.filter(batches__id=batch_id).distinct()

    # 3) Prepare date rows
    # timetable_map: { "YYYY-MM-DD": {1:[...],2:[...],3:[...],4:[...]} }
    timetable_map = {}
    cur = start_day
    while cur <= end_day:
        timetable_map[cur.isoformat()] = {1: [], 2: [], 3: [], 4: []}
        cur += timedelta(days=1)

    # 4) Fill slots
    # Existing loop – keep everything else same
    for lec in qs:
        d = lec.lacture_date
        key = d.isoformat()

        slot_no = find_slot_for_lecture(d, lec.start_time)
        if not slot_no:
            continue

        section_name = getattr(lec.section, "name", "") or getattr(lec.section, "title", "") or ""
        # ----- title -----
        topic_name = getattr(lec.topic, "topic_name", "") or getattr(lec.topic, "name", "") or ""
        title = topic_name or (lec.title if lec.title and lec.title != "Null" else "Lecture")

        # ----- batches -----
        batches = list(lec.batches.all())

        exam_name = ""
        batch_names = ""

        if batches:
            # Exam name (same for all batches usually)
            exam_name = getattr(batches[0].exam_category, "exam_category_name", "")

            # Join batch names
            batch_names = ", ".join(
                b.batch_name for b in batches
                if b.batch_name and b.batch_name != "Null"
            )

        # ✅ Final display string
        parts = []
        if exam_name:
            parts.append(exam_name)
        parts.append(title)
        if batch_names:
            parts.append(batch_names)

        display = " | ".join(parts)

        timetable_map[key][slot_no].append({
            "id": lec.id,
            "display": display,
            "start": lec.start_time.strftime("%I:%M %p"),
            "end": lec.end_time.strftime("%I:%M %p"),
            "section": section_name,
            "color_class": color_for_section(section_name),
        })

    # 5) Build rows for template
    rows = []
    cur = start_day
    while cur <= end_day:
        key = cur.isoformat()
        rows.append({
            "date_label": cur.strftime("%d/%m/%Y") if hasattr(cur, "strftime") else key,
            "date_iso": key,
            "day_name": cur.strftime("%A"),
            "is_weekend": is_weekend(cur),
            "slots": timetable_map[key],  # dict {1:[],2:[],3:[],4:[]}
        })
        cur += timedelta(days=1)

    context = {
        "start_day": start_day,
        "end_day": end_day,
        "batch_id": batch_id,
        "weekday_slots": [
            {"slot": s["slot"], "label": s["label"], "time": fmt_time_range(s["start"], s["end"])}
            for s in WEEKDAY_SLOTS
        ],
        "weekend_slots": [
            {"slot": s["slot"], "label": s["label"], "time": fmt_time_range(s["start"], s["end"])}
            for s in WEEKEND_SLOTS
        ],
        "rows": rows,
    }

    return render(request, "lecture-timetable.html", context)

def student_endTest(request):
    results = Test_Result.objects.filter(not_accessible=0,is_end=1,is_deleted=0)
    for r in results:
        test_id = r.mock_test_id
        student_id = r.student_id
        result_id = r.id
        time_left = r.time_left
        test=Mock_Test.objects.filter(id=test_id).first()
        if test:
            time_taken=int(test.mock_duration)*60-int(time_left)
            bookmarked = Student_Answers.objects.filter(student_id=student_id, result_id=result_id, bookmarked=1,answer_id__isnull=True, answer_id__exact='').count()
            answer_bookmarked = Student_Answers.objects.filter(student_id=student_id, result_id=result_id, bookmarked=1).exclude(answer_id__isnull=True, answer_id__exact='').count()
            right_answered = Student_Answers.objects.filter(student_id=student_id, result_id=result_id, is_right=1).count()
            answered = Student_Answers.objects.filter(student_id=student_id, result_id=result_id,attempted=1,answer_id__isnull=True).count()
            not_attempted = Student_Answers.objects.filter(student_id=student_id, result_id=result_id,answer_id__isnull=True).count()
            wrong_answered = Student_Answers.objects.filter(student_id=student_id, result_id=result_id, is_right=0,attempted=1,answer_id__isnull=False).exclude(answer_id__exact='').count()
            marks=Student_Answers.objects.filter(student_id=student_id,result_id=result_id).aggregate(TOTAL = Sum('marks'))['TOTAL']
        #        time_taken=Student_Answers.objects.filter(student_id=student_id,result_id=result_id).aggregate(time_taken = Sum('time_taken'))['time_taken']
            Test_Result.objects.filter(student_id=student_id,id=result_id).update(
                time_left = time_left,
                right_answered = right_answered,
                wrong_answered = wrong_answered,
                bookmarked = bookmarked,
                attempted = answered,
                not_attempted=not_attempted,
                is_end = 1,
                marks=marks,
                time_taken=time_taken,
                answer_bookmarked=answer_bookmarked
            )
            section_statistics = (
                Student_Answers.objects
                .filter(student_id=student_id,result_id=result_id)
                .values('question__section_id', 'question__section__section_name')  # Group by section_id and select section_name
                .annotate(
                not_attempted=Sum(Case(When(attempted=0, then=1), default=0, output_field=IntegerField())),
                time_taken=Sum('time_taken'),
                total_question=Count('question_id'),
                attempted=Sum('attempted'),
                # answer_id=F('answer_id'),
                # bookmark=F('bookmarked'),
                mark_review=Sum(
                    Case(
                        When(Q(answer_id__isnull=True) & Q(answer_id__exact='') & Q(bookmarked=1), then=Value(1)),
                        default=0,
                        output_field=IntegerField()
                    )
                ),
                answer_bookmarked=ExpressionWrapper(
                    Sum(
                        Case(
                            When(Q(answer_id__isnull=False) & ~Q(answer_id='') & Q(bookmarked=1), then=Value(1)),
                            default=Value(0),
                            output_field=IntegerField()
                        )
                    ),
                    output_field=fields.FloatField()
                ),
                right=ExpressionWrapper(
                    Sum(
                        Case(
                            When(Q(answer_id__isnull=False) & ~Q(answer_id='') & Q(is_right=1), then=Value(1)),
                            default=Value(0),
                            output_field=IntegerField()
                        )
                    ),
                    output_field=fields.FloatField()
                ),
                wrong=ExpressionWrapper(
                    Sum(
                        Case(
                            When(Q(answer_id__isnull=False) & ~Q(answer_id='') & Q(is_right=0), then=Value(1)),
                            default=Value(0),
                            output_field=IntegerField()
                        )
                    ),
                    output_field=fields.FloatField()
                ),
                marks=Sum('marks')
                    )
            )
            for section in section_statistics:
                sresult = Section_Test_Result.objects.filter(
                    mock_test_id = test_id,
                    student_id = student_id,
                    section_id = section['question__section_id'],
                    result_id = result_id)
                if sresult.count():
                    Section_Test_Result.objects.filter(
                    mock_test_id = test_id,
                    student_id = student_id,
                    section_id = section['question__section_id'],
                    result_id = result_id).update(
                        time_taken = section['time_taken'],
                        total_questions=section['total_question'],
                        attempted=section['attempted'],
                        not_attempted=section['not_attempted'],
                        right_answered=section['right'],
                        wrong_answered=section['wrong'],
                        bookmarked=section['mark_review'],
                        marks=section['marks'],
                        answer_bookmarked=section['answer_bookmarked']
                    )
                else:
                    sresult=Section_Test_Result(
                        mock_test_id = test_id,
                        student_id = student_id,
                        result_id = result_id,
                        section_id = section['question__section_id'],
                        time_taken = section['time_taken'],
                        time_left = 0,
                        total_questions=section['total_question'],
                        attempted=section['attempted'],
                        not_attempted=section['not_attempted'],
                        right_answered=section['right'],
                        wrong_answered=section['wrong'],
                        bookmarked=section['mark_review'],
                        marks=section['marks'],
                        answer_bookmarked=section['answer_bookmarked']
                    )
                    sresult.save()
    return JsonResponse({'success': True,'message': 'Test end successfully.'}, safe=False)

from collections import defaultdict
from django.db import transaction
from django.db.models import F, Q

@transaction.atomic
def regrade_correct_answer_delta_answered_only():
    # lock all edited questions
    edited_questions = (
        Question_Bank.objects
        .select_for_update()
        .filter(is_edited=1)
    )

    if not edited_questions.exists():
        return JsonResponse({
            "success": False,
            "reason": "No edited questions found"
        })

    total_updated_answers = 0
    total_result_delta = defaultdict(lambda: {"marks": 0.0, "right": 0, "wrong": 0})
    total_section_delta = defaultdict(lambda: {"marks": 0.0, "right": 0, "wrong": 0})

    for qb in edited_questions:
        correct = (qb.correct_answer or "").strip()
        pos_marks = float(qb.marks or 0)
        neg_marks = float(qb.negative_marks or 0)

        # IMPORTANT:
        # assumes Student_Answers.question_id == Question_Bank.id
        # if not, change to: question__question_bank_id=qb.id
        answers = (
            Student_Answers.objects
            .select_for_update()
            .select_related("question")
            .filter(
                question_id=qb.id,
                result__is_end=1,
                result__not_accessible=0,
                answer_id__isnull=False   # ✅ answered only
            )
        )

        for sa in answers:
            old_is_right = int(sa.is_right or 0)
            old_marks = float(sa.marks or 0)

            selected = (sa.answer_id or "").strip()
            new_is_right = 1 if selected == correct else 0
            new_marks = pos_marks if new_is_right else -neg_marks

            if new_is_right == old_is_right and new_marks == old_marks:
                continue

            # deltas
            d_marks = new_marks - old_marks
            d_right = (1 if new_is_right else 0) - (1 if old_is_right else 0)
            d_wrong = (1 if not new_is_right else 0) - (1 if not old_is_right else 0)

            # update student answer
            sa.is_right = new_is_right
            sa.marks = new_marks
            sa.save(update_fields=["is_right", "marks"])
            total_updated_answers += 1

            rid = sa.result_id
            sid = sa.question.section_id

            total_result_delta[rid]["marks"] += d_marks
            total_result_delta[rid]["right"] += d_right
            total_result_delta[rid]["wrong"] += d_wrong

            key = (rid, sid, sa.student_id)
            total_section_delta[key]["marks"] += d_marks
            total_section_delta[key]["right"] += d_right
            total_section_delta[key]["wrong"] += d_wrong

        # mark this question as processed
        qb.is_edited = 0
        qb.save(update_fields=["is_edited"])

    # -------------------------------
    # Apply deltas to Test_Result
    # -------------------------------
    for result_id, d in total_result_delta.items():
        Test_Result.objects.filter(id=result_id).update(
            marks=F("marks") + d["marks"],
            right_answered=F("right_answered") + d["right"],
            wrong_answered=F("wrong_answered") + d["wrong"],
        )

    # -------------------------------
    # Apply deltas to Section_Test_Result
    # -------------------------------
    for (result_id, section_id, student_id), d in total_section_delta.items():
        tr = (
            Test_Result.objects
            .filter(id=result_id, student_id=student_id)
            .only("mock_test_id")
            .first()
        )
        if not tr:
            continue

        updated = Section_Test_Result.objects.filter(
            result_id=result_id,
            student_id=student_id,
            section_id=section_id,
            mock_test_id=tr.mock_test_id,
        ).update(
            marks=F("marks") + d["marks"],
            right_answered=F("right_answered") + d["right"],
            wrong_answered=F("wrong_answered") + d["wrong"],
        )

        if updated == 0:
            Section_Test_Result.objects.create(
                mock_test_id=tr.mock_test_id,
                student_id=student_id,
                result_id=result_id,
                section_id=section_id,
                marks=d["marks"],
                right_answered=d["right"],
                wrong_answered=d["wrong"],
                attempted=0,
                not_attempted=0,
                total_questions=0,
                bookmarked=0,
                answer_bookmarked=0,
                time_taken=0,
                time_left=0,
            )

    return JsonResponse({
        "success": True,
        "edited_questions": edited_questions.count(),
        "updated_answers": total_updated_answers,
        "affected_results": len(total_result_delta),
        "affected_sections": len(total_section_delta),
    })

def regrade_edited_questions(request):
    info = regrade_correct_answer_delta_answered_only()
    return info

@login_required(login_url=ADMIN_LOGIN_URL)
def verify_student(request):
    if request.method == "POST":
        student_id = request.POST.get('student_id')
        id_type = request.POST.get('id_type')
        aadhar_name = request.POST.get('aadhar_name')
        id_number = request.POST.get('id_number')
        is_verify = int(request.POST.get('is_verify'))  # 0 or 1

        student = get_object_or_404(Students, id=student_id)

        if id_type == 'aadhar':
            student.aadhar_number = id_number
        elif id_type == 'voter':
            student.voter_number = id_number
        elif id_type == 'dl':
            student.dl_number = id_number

        student.aadhar_name = aadhar_name
        student.is_verified = is_verify
        student.save()

    return redirect(request.META.get('HTTP_REFERER', '/'))

#Course Discount


@login_required(login_url=ADMIN_LOGIN_URL)
def course_discount(request):

    discounts = CourseDiscount.objects.select_related('exam_category').all().order_by('-id')
    paginator = Paginator(discounts, 10)

    page = request.GET.get('page')
    all_discount = paginator.get_page(page)

    exam_categories = ExamCategory.objects.filter(status='Active').order_by('display_order', 'exam_category_name')

    return render(request, "course_discount.html", {
        "all_discount": all_discount,
        "exam_categories": exam_categories,
        "course_types": (
            ('selfprep', 'Self Prep'),
            ('classroom', 'Classroom'),
        ),
    })



@login_required(login_url=ADMIN_LOGIN_URL)
def add_course_discount(request):

    if request.method != "POST":
        return JsonResponse({"status": "error", "msg": "Invalid request"})

    try:
        exam_category_id = request.POST.get('exam_category')
        course_type = (request.POST.get('course_type') or '').strip().lower()
        min_scores = request.POST.getlist('min_score[]')
        max_scores = request.POST.getlist('max_score[]')
        discounts = request.POST.getlist('discount[]')
        status = request.POST.get('status')

        if not exam_category_id:
            return JsonResponse({"status": "error", "msg": "Exam category required"})
        if course_type not in ('selfprep', 'classroom'):
            return JsonResponse({"status": "error", "msg": "Valid course type required"})

        for min_s, max_s, disc in zip(min_scores, max_scores, discounts):

            if not min_s or not max_s or not disc:
                continue

            min_s = int(min_s)
            max_s = int(max_s)
            disc = float(disc)

            # Validation
            if min_s > max_s:
                return JsonResponse({
                    "status": "error",
                    "msg": f"Min score cannot be greater than max score ({min_s}-{max_s})"
                })

            # Overlap check (DB)
#            exists = CourseDiscount.objects.filter(
#                course_id=course_id
#            ).filter(
#                Q(min_score__lte=max_s) &
#                Q(max_score__gte=min_s)
#            ).exists()
#
#            if exists:
#                return JsonResponse({
#                    "status": "error",
#                    "msg": f"Range {min_s}-{max_s} overlaps with existing data"
#                })

            # Save
            CourseDiscount.objects.create(
                exam_category_id=exam_category_id,
                course_type=course_type,
                min_score=min_s,
                max_score=max_s,
                discount_percentage=disc,
                status=status
            )

        return JsonResponse({"status": "success", "msg": "All discounts added successfully"})

    except Exception as e:
        return JsonResponse({"status": "error", "msg": str(e)})

# =========================
# EDIT
# =========================
@login_required(login_url=ADMIN_LOGIN_URL)
def edit_course_discount(request):

    if request.method != "POST":
        return JsonResponse({"status": "error", "msg": "Invalid request"})

    try:
        obj = get_object_or_404(CourseDiscount, id=request.POST.get('id'))

        exam_category_id = request.POST.get('exam_category')
        course_type = (request.POST.get('course_type') or '').strip().lower()
        min_score = int(request.POST.get('min_score'))
        max_score = int(request.POST.get('max_score'))
        discount = float(request.POST.get('discount'))
        status = request.POST.get('status')

        if min_score > max_score:
            return JsonResponse({"status": "error", "msg": "Min score must be less than Max score"})

        if not exam_category_id:
            return JsonResponse({"status": "error", "msg": "Exam category required"})
        if course_type not in ('selfprep', 'classroom'):
            return JsonResponse({"status": "error", "msg": "Valid course type required"})

   
        exists = CourseDiscount.objects.filter(
            exam_category_id=exam_category_id,
            course_type__iexact=course_type,
        ).exclude(id=obj.id).filter(
            Q(min_score__lte=max_score) &
            Q(max_score__gte=min_score)
        ).exists()

        if exists:
            return JsonResponse({
                "status": "error",
                "msg": "Score range overlaps with existing discount"
            })

        obj.exam_category_id = exam_category_id
        obj.course_type = course_type
        obj.min_score = min_score
        obj.max_score = max_score
        obj.discount_percentage = discount
        obj.status = status
        obj.save()

        return JsonResponse({"status": "success", "msg": "Updated successfully"})

    except Exception as e:
        return JsonResponse({"status": "error", "msg": str(e)})


@login_required(login_url=ADMIN_LOGIN_URL)
def delete_course_discount(request):

    if request.method != "POST":
        return JsonResponse({"status": "error", "msg": "Invalid request"})

    try:
        obj = get_object_or_404(CourseDiscount, id=request.POST.get('id'))
        obj.delete()

        return JsonResponse({"status": "success", "msg": "Deleted successfully"})

    except Exception as e:
        return JsonResponse({"status": "error", "msg": str(e)})


#Reports
def sales_reports(request):
    exam_category_id = request.GET.get('exam_category')
    course_id = request.GET.get('course')
    sales_executive_id = request.GET.get('sales_executive')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    search = request.GET.get('search')
    valid_orders = Orders.objects.filter(
        is_order=1
    ).exclude(
        order_action__in=['refunded', 'canceled']
    ).select_related(
        'student', 'course'
    ).prefetch_related(
        'course__exams'
    )
    if exam_category_id:
        valid_orders = valid_orders.filter(
            course__exams__id=exam_category_id
        )

    if course_id:
        valid_orders = valid_orders.filter(
            course__id=course_id
        )

    if sales_executive_id:
        valid_orders = valid_orders.filter(
            student__sales_executive_id=sales_executive_id
        )

    if from_date:
        valid_orders = valid_orders.filter(
            created_at__date__gte=from_date
        )

    if to_date:
        valid_orders = valid_orders.filter(
            created_at__date__lte=to_date
        )

    if search:
        valid_orders = valid_orders.filter(
            Q(student__first_name__icontains=search) |
            Q(student__last_name__icontains=search) |
            Q(student__email__icontains=search) |
            Q(student__mobile__icontains=search)
        )

   

    students_list = Students.objects.prefetch_related(
        Prefetch('order_courses', queryset=valid_orders)
    ).filter(order_courses__in=valid_orders).distinct().order_by('id')

    

    paginator = Paginator(students_list, 10)
    page_number = request.GET.get('page')
    students = paginator.get_page(page_number)

   

    exam_categories = ExamCategory.objects.all()
    courses = Course.objects.all()
    # sales_executives = SalesExecutive.objects.all()

    context = {
        'students': students,
        'exam_categories': exam_categories,
        'courses': courses,
        # 'sales_executives': sales_executives,
        'request': request
    }

    return render(request, 'sales_reports.html', context)

def lead_conversion_report(request):

    exam_category_id = request.GET.get('exam_category')
    course_id = request.GET.get('course')
    sales_executive_id = request.GET.get('sales_executive')
    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')
    search = request.GET.get('search')

    # 🔥 STEP 1: Valid Orders
    valid_orders = Orders.objects.filter(
        is_order=1
    ).exclude(
        order_action__in=['refunded', 'canceled']
    ).select_related('course').prefetch_related('course__exams')

    if exam_category_id:
        valid_orders = valid_orders.filter(course__exams__id=exam_category_id)

    if course_id:
        valid_orders = valid_orders.filter(course__id=course_id)

    if from_date:
        valid_orders = valid_orders.filter(created_at__date__gte=from_date)

    if to_date:
        valid_orders = valid_orders.filter(created_at__date__lte=to_date)

    valid_orders = valid_orders.distinct()

    # 🔥 STEP 2: Students Base Query
    students = Students.objects.all()

    if sales_executive_id:
        students = students.filter(sales_executive_id=sales_executive_id)

    if search:
        students = students.filter(
            Q(first_name__icontains=search) |
            Q(last_name__icontains=search) |
            Q(email__icontains=search) |
            Q(mobile__icontains=search)
        )

    # 🔥 VERY IMPORTANT
    # If exam/category/date filter applied → show only students having matching orders

    if exam_category_id or course_id or from_date or to_date:
        students = students.filter(
            id__in=valid_orders.values_list('student_id', flat=True)
        )

    # 🔥 Prefetch filtered orders
    students = students.prefetch_related(
        Prefetch(
            'order_courses',
            queryset=valid_orders,
            to_attr='filtered_orders'
        )
    )

    # 🔥 Conversion status
    students = students.annotate(
        is_converted=Exists(
            valid_orders.filter(student_id=OuterRef('id'))
        )
    ).order_by('-id')

    # 🔥 Filter course dropdown
    courses = Course.objects.all()
    if exam_category_id:
        courses = courses.filter(exams__id=exam_category_id).distinct()

    paginator = Paginator(students, 10)
    page_number = request.GET.get('page')
    students_page = paginator.get_page(page_number)

    context = {
        'students': students_page,
        'exam_categories': ExamCategory.objects.all(),
        'courses': courses,
    }

    return render(request, 'lead_conversion_report.html', context)
def get_courses_by_exam(request):
    exam_id = request.GET.get('exam_id')

    courses = Course.objects.filter(
        exams__id=exam_id
    ).values('id', 'course_name').distinct()

    return JsonResponse(list(courses), safe=False)
def potential_sales_report(request):

    order_subquery = OrderCourses.objects.filter(
        student=OuterRef('pk')
    )
    selfprep_subquery = OrderCourses.objects.filter(
        student=OuterRef('pk'),
        course__course_type='selfprep'
    )
    activity_subquery = StudentActivity.objects.filter(
        student=OuterRef('pk')
    )

    latest_activity = activity_subquery.order_by('-created_at')

    students = Students.objects.annotate(
        has_order=Exists(order_subquery),
        has_selfprep=Exists(selfprep_subquery),
        has_activity=Exists(activity_subquery),
        latest_activity_date=Subquery(
            latest_activity.values('created_at')[:1]
        ),
        latest_activity_type=Subquery(
            latest_activity.values('activity_type')[:1]
        )
    ).filter(
        has_activity=True
    ).filter(
        Q(has_selfprep=True) | Q(has_order=False)
    )

    exam_category = request.GET.get('exam_category')

    if exam_category:
        students = students.filter(
            ordercourses__course__course_type='selfprep',
            ordercourses__course__exams__id=exam_category
        )

    search = request.GET.get('search')
    if search:
        students = students.filter(
            Q(first_name__icontains=search) |
            Q(last_name__icontains=search) |
            Q(email__icontains=search) |
            Q(mobile__icontains=search)
        )

    activity_type = request.GET.get('activity_type')
    if activity_type:
        students = students.filter(
            studentactivity__activity_type=activity_type
        )

    from_date = request.GET.get('from_date')
    to_date = request.GET.get('to_date')

    if from_date and to_date:
        from_date = datetime.strptime(from_date, "%Y-%m-%d")
        to_date = datetime.strptime(to_date, "%Y-%m-%d")

        students = students.filter(
            latest_activity_date__date__range=[from_date, to_date]
        )

    students = students.distinct().order_by('-latest_activity_date')


    paginator = Paginator(students, 10)
    page_number = request.GET.get('page')
    students = paginator.get_page(page_number)

    activity_type_dict = dict(StudentActivity.ACTIVITY_TYPES)

    for student in students:
        student.latest_activity_label = activity_type_dict.get(
            student.latest_activity_type, "-"
        )

    context = {
        'students': students,
        'exam_categories': ExamCategory.objects.all(),
        'courses': Course.objects.all(),
        'activity_types': StudentActivity.ACTIVITY_TYPES,
    }

    return render(request, 'potential_sales_report.html', context)


from collections import defaultdict
from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import render

from collections import defaultdict
from datetime import timedelta

from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import render
from django.utils import timezone


MOCK_TEST_TYPES = [
    'area_mock',
    'sectional_mock',
    'mini_mock',
    'full_mock',
    'advance_mock',
    'pyq_mock',
    'basic_concept_test',
    'advance_concept_test',
]

VIDEO_TYPES = [
    'video',
    'lecture_video',
    'recorded_video',
    'concept_video',
]

DOWNLOAD_TYPES = [
    'notes',
    'downloads',
]


def calculate_activity_score(act, score_value=None):
    """
    Score rules:
    +10 = mock test attempted
    +8  = video watched
    +5  = good assessment score
    +5  = free resource downloaded
    +1  = per 10 minutes spent
    """

    points = 0

    # Mock test attempted
    if act.activity_type in MOCK_TEST_TYPES:
        points += 10

    # Video watched
    elif act.activity_type in VIDEO_TYPES:
        points += 8

    # Resource downloaded
    elif act.activity_type in DOWNLOAD_TYPES:
        points += 5

    # Good score in assessment
    # Here good score means 70% or above
    if score_value:
        try:
            obtained_marks, total_marks = score_value

            if total_marks and float(obtained_marks) >= float(total_marks) * 0.7:
                points += 5

        except Exception:
            pass

    # Progressive time scoring
    # Assumption: StudentActivity has time_spent_seconds field
    # Example: 35 minutes = +3 points
    time_spent_seconds = getattr(act, "time_spent_seconds", 0)

    if time_spent_seconds:
        minutes_spent = int(time_spent_seconds) // 60
        points += minutes_spent // 10

    return points


def not_enrolled_students_report(request):

    student = request.GET.get("student")
    activity_type = request.GET.get("activity_type")
    from_date = request.GET.get("from_date")
    to_date = request.GET.get("to_date")
    exam = request.GET.get("exam")
    exam_categories = ExamCategory.objects.filter(status='Active')

    # Enrolled students
    enrolled_students = Orders.objects.filter(
        is_order=1
    ).values_list("student_id", flat=True)

    # NOT enrolled students activities
    activities = StudentActivity.objects.exclude(
        student_id__in=enrolled_students
    ).select_related("student").order_by("-created_at")

    # -------- Filters --------
    if student:
        activities = activities.filter(
            Q(student__first_name__icontains=student) |
            Q(student__last_name__icontains=student) |
            Q(student__mobile__icontains=student) |
            Q(student__email__icontains=student)
        )

    if activity_type:
        activities = activities.filter(activity_type=activity_type)

    if from_date:
        activities = activities.filter(created_at__date__gte=from_date)

    if to_date:
        activities = activities.filter(created_at__date__lte=to_date)

    if exam:
        activities = activities.filter(exam_id=exam)
    # -------- Exam Map --------
    exam_ids = set(
        activities.exclude(exam_id__isnull=True)
        .values_list("exam_id", flat=True)
    )

    exam_map = {
        e.id: e.exam_category_name
        for e in ExamCategory.objects.filter(id__in=exam_ids)
    }

    # -------- Last Activity Map --------
    # Used for inactive score -10
    student_ids = set(activities.values_list("student_id", flat=True))

    last_activity_map = {}

    last_activities = StudentActivity.objects.filter(
        student_id__in=student_ids
    ).order_by("student_id", "-created_at")

    for activity in last_activities:
        if activity.student_id not in last_activity_map:
            last_activity_map[activity.student_id] = activity.created_at

    # -------- Grouping --------
    grouped = defaultdict(lambda: {
        "student_id": None,
        "name": "",
        "contact": "",
        "email": "",
        "exam": set(),
        "activity_logs": [],
        "score_points": 0,
        "new_lead_score_added": False,
        "inactive_score_added": False,
    })

    now = timezone.now()

    for act in activities:

        key = (act.student.id, act.created_at.date())

        resource_name = ""
        score = ""
        score_value_for_points = None

        exam_name = exam_map.get(act.exam_id, "")

        # -------- Notes / Downloads --------
        if act.activity_type in ["notes", "downloads"]:

            res = Resources.objects.filter(id=act.resource_id).first()

            if res:
                resource_name = res.title

        # -------- Mock Tests --------
        elif act.activity_type in MOCK_TEST_TYPES:

            mock = Mock_Test.objects.filter(id=act.resource_id).first()

            result = Test_Result.objects.filter(
                student=act.student,
                mock_test_id=act.resource_id
            ).order_by("-created_at").first()

            if result:
            
                if mock:
                    resource_name = mock.mock_title

                obtained_marks = getattr(result, "marks", None)
                total_marks = getattr(result, "total_marks", None)

                if obtained_marks is not None and total_marks:
                    score = f"{obtained_marks}/{total_marks}"
                    score_value_for_points = (obtained_marks, total_marks)

                elif obtained_marks is not None:
                    score = str(obtained_marks)

        # -------- Speed Math --------
        elif act.activity_type == "daily_speedMath":

            resource_name = "Daily Speed Math"

            sm = Daily_speedMath_Score.objects.filter(
                student=act.student
            ).order_by("-date").first()

            if sm:
                score = str(sm.score)

        # -------- Vocabulary --------
        elif act.activity_type == "daily_vocab":

            resource_name = "Daily Vocabulary"

            vocab = Daily_Vocab_Score.objects.filter(
                student=act.student
            ).order_by("-date").first()

            if vocab:
                score = str(vocab.score)

        # -------- Videos --------
        elif act.activity_type in VIDEO_TYPES:

            resource_name = "Video Watched"

            # If you have a Video model, use it here
            # Example:
            # video = Video.objects.filter(id=act.resource_id).first()
            # if video:
            #     resource_name = video.title
        # -------- Skip activity if no resource found --------
        if not resource_name:
            continue

        # -------- Activity Display Text --------
        if resource_name and score:
            activity_text = f"{resource_name} ({score})"

        elif resource_name:
            activity_text = f"{act.get_activity_type_display()}: {resource_name}"

        else:
            activity_text = act.get_activity_type_display()

        time_str = act.created_at.strftime("%I:%M %p")

        # Calculate points for this activity
        activity_points = calculate_activity_score(
            act,
            score_value=score_value_for_points
        )

        if activity_points > 0:
            formatted_block = f"{activity_text}\n🕒 {time_str}\nScore: +{activity_points}"
        elif activity_points < 0:
            formatted_block = f"{activity_text}\n🕒 {time_str}\nScore: {activity_points}"
        else:
            formatted_block = f"{activity_text}\n🕒 {time_str}\nScore: 0"

        # -------- Store Student Info --------
        grouped[key]["student_id"] = act.student.id
        grouped[key]["name"] = f"{act.student.first_name} {act.student.last_name}".strip()
        grouped[key]["contact"] = act.student.mobile
        grouped[key]["email"] = act.student.email

        if exam_name:
            grouped[key]["exam"].add(exam_name)

        grouped[key]["activity_logs"].append({
            "text": formatted_block,
            "time": act.created_at,
        })

        grouped[key]["score_points"] += activity_points

        # -------- New Lead Score --------
        # +10 if lead generated within last 5 days
        # Assumption: Student model has created_at field
        student_created_at = getattr(act.student, "created_at", None)

        if student_created_at and not grouped[key]["new_lead_score_added"]:
            if student_created_at >= now - timedelta(days=5):
                grouped[key]["score_points"] += 10
                grouped[key]["new_lead_score_added"] = True

        # -------- Inactive Lead Score --------
        # -10 if lead has been inactive for more than 30 days
        last_activity_date = last_activity_map.get(act.student.id)

        if last_activity_date and not grouped[key]["inactive_score_added"]:
            if last_activity_date <= now - timedelta(days=30):
                grouped[key]["score_points"] -= 10
                grouped[key]["inactive_score_added"] = True

    # -------- Final Rows --------
    rows = []

    for (student_id, date), data in grouped.items():

        logs = sorted(data["activity_logs"], key=lambda x: x["time"])

        rows.append({
            "student_id": student_id,
            "name": data["name"],
            "contact": data["contact"],
            "email": data["email"],
            "date": date,
            "exam": ", ".join(data["exam"]),
            "activity": "\n\n".join([l["text"] for l in logs]),
            "score_points": data["score_points"],
        })

    # Optional: sort by score high to low
    rows = sorted(rows, key=lambda x: x["score_points"], reverse=True)

    paginator = Paginator(rows, 50)
    page = request.GET.get("page")
    rows = paginator.get_page(page)

    return render(request, "admin/not_enrolled_report.html", {
        "rows": rows,
        "student": student,
        "activity_type": activity_type,
        "from_date": from_date,
        "to_date": to_date,
        "exam_categories":exam_categories
    })
from collections import defaultdict
from django.core.paginator import Paginator
from django.db.models import Q
from django.shortcuts import render

def enrolled_students_report(request):

    student = request.GET.get("student")
    activity_type = request.GET.get("activity_type")
    from_date = request.GET.get("from_date")
    to_date = request.GET.get("to_date")

    # Enrolled students
    enrolled_students = Orders.objects.filter(
        is_order=1
    ).values_list("student_id", flat=True)

    activities = StudentActivity.objects.filter(
        student_id__in=enrolled_students
    ).select_related("student").order_by("-created_at")

    # -------- Filters --------
    if student:
        activities = activities.filter(
            Q(student__first_name__icontains=student) |
            Q(student__last_name__icontains=student) |
            Q(student__mobile__icontains=student)
        )

    if activity_type:
        activities = activities.filter(activity_type=activity_type)

    if from_date:
        activities = activities.filter(created_at__date__gte=from_date)

    if to_date:
        activities = activities.filter(created_at__date__lte=to_date)

    # -------- Exam Map --------
    exam_ids = set(activities.values_list("exam_id", flat=True))

    exam_map = {
        e.id: e.exam_category_name
        for e in ExamCategory.objects.filter(id__in=exam_ids)
    }

    # -------- GROUPING --------
    grouped = defaultdict(lambda: {
        "name": "",
        "contact": "",
        "email": "",
        "exam": set(),
        "activity_logs": []
    })

    for act in activities:

        key = (act.student.id, act.created_at.date())

        resource_name = ""
        score = ""

        exam_name = exam_map.get(act.exam_id, "")

        # Notes / Downloads
        if act.activity_type in ["notes", "downloads"]:
            res = Resources.objects.filter(id=act.resource_id).first()
            if res:
                resource_name = res.title

        # Mock Tests
        elif act.activity_type in [
            'area_mock','sectional_mock','mini_mock',
            'full_mock','advance_mock','pyq_mock',
            'basic_concept_test','advance_concept_test'
        ]:
            mock = Mock_Test.objects.filter(id=act.resource_id).first()
            if mock:
                resource_name = mock.mock_title

            result = Test_Result.objects.filter(
                student=act.student,
                mock_test_id=act.resource_id
            ).order_by('-created_at').first()

            if result:
                if hasattr(result, "total_marks"):
                    score = f"{result.marks}/{result.total_marks}"
                else:
                    score = str(result.marks)

        # Speed Math
        elif act.activity_type == "daily_speedMath":
            resource_name = "Daily Speed Math"
            sm = Daily_speedMath_Score.objects.filter(
                student=act.student
            ).order_by('-date').first()
            if sm:
                score = str(sm.score)

        # Vocabulary
        elif act.activity_type == "daily_vocab":
            resource_name = "Daily Vocabulary"
            vocab = Daily_Vocab_Score.objects.filter(
                student=act.student
            ).order_by('-date').first()
            if vocab:
                score = str(vocab.score)

        # -------- FORMAT --------
        if not resource_name:
            continue

        if resource_name and score:
            activity_text = f"{resource_name} ({score})"
        elif resource_name:
            activity_text = f"{act.get_activity_type_display()}: {resource_name}"
        else:
            activity_text = act.get_activity_type_display()

        time_str = act.created_at.strftime("%I:%M %p")

        formatted_block = f"{activity_text}\n🕒 {time_str}"

        # Store student info
        grouped[key]["name"] = f"{act.student.first_name} {act.student.last_name}"
        grouped[key]["contact"] = act.student.mobile
        grouped[key]["email"] = act.student.email

        if exam_name:
            grouped[key]["exam"].add(exam_name)

        grouped[key]["activity_logs"].append({
            "text": formatted_block,
            "time": act.created_at
        })

    # -------- FINAL ROWS --------
    rows = []

    for (student_id, date), data in grouped.items():

        logs = sorted(data["activity_logs"], key=lambda x: x["time"])

        rows.append({
            "name": data["name"],
            "contact": data["contact"],
            "email": data["email"],
            "date": date,
            "exam": ", ".join(data["exam"]),
            "activity": "\n\n".join([l["text"] for l in logs])
        })

    paginator = Paginator(rows, 50)
    page = request.GET.get("page")
    rows = paginator.get_page(page)

    return render(request, "admin/enrolled_report.html", {
        "rows": rows,
        "student": student,
        "activity_type": activity_type,
        "from_date": from_date,
        "to_date": to_date
    })
    
def selfprep_students_report(request):

    student = request.GET.get("student")
    activity_type = request.GET.get("activity_type")
    from_date = request.GET.get("from_date")
    to_date = request.GET.get("to_date")

    enrolled_students = Orders.objects.filter(
        is_order=1,
        course__course_type='selfprep'
    ).values_list("student_id", flat=True)

    activities = StudentActivity.objects.filter(
        student_id__in=enrolled_students
    ).select_related("student").order_by("-created_at")

    # -------- Filters --------
    if student:
        activities = activities.filter(
            Q(student__first_name__icontains=student) |
            Q(student__last_name__icontains=student) |
            Q(student__mobile__icontains=student)
        )

    if activity_type:
        activities = activities.filter(activity_type=activity_type)

    if from_date:
        activities = activities.filter(created_at__date__gte=from_date)

    if to_date:
        activities = activities.filter(created_at__date__lte=to_date)

    # -------- Exam Map --------
    exam_ids = set(activities.values_list("exam_id", flat=True))

    exam_map = {
        e.id: e.exam_category_name
        for e in ExamCategory.objects.filter(id__in=exam_ids)
    }

    grouped = defaultdict(lambda: {
        "name": "",
        "contact": "",
        "email": "",
        "exam": set(),
        "activity_logs": []
    })

    for act in activities:

        key = (act.student.id, act.created_at.date())

        resource_name = ""
        score = ""

        exam_name = exam_map.get(act.exam_id, "")

        # SAME LOGIC AS ABOVE (keep identical)

        if act.activity_type in ["notes", "downloads"]:
            res = Resources.objects.filter(id=act.resource_id).first()
            if res:
                resource_name = res.title

        elif act.activity_type in [
            'area_mock','sectional_mock','mini_mock',
            'full_mock','advance_mock','pyq_mock',
            'basic_concept_test','advance_concept_test'
        ]:
            mock = Mock_Test.objects.filter(id=act.resource_id).first()
            if mock:
                resource_name = mock.mock_title

            result = Test_Result.objects.filter(
                student=act.student,
                mock_test_id=act.resource_id
            ).order_by('-created_at').first()

            if result:
                score = f"{result.marks}/{getattr(result, 'total_marks', '')}".strip('/')

        elif act.activity_type == "daily_speedMath":
            resource_name = "Daily Speed Math"
            sm = Daily_speedMath_Score.objects.filter(
                student=act.student
            ).order_by('-date').first()
            if sm:
                score = str(sm.score)

        elif act.activity_type == "daily_vocab":
            resource_name = "Daily Vocabulary"
            vocab = Daily_Vocab_Score.objects.filter(
                student=act.student
            ).order_by('-date').first()
            if vocab:
                score = str(vocab.score)

        if resource_name and score:
            activity_text = f"{resource_name} ({score})"
        elif resource_name:
            activity_text = f"{act.get_activity_type_display()}: {resource_name}"
        else:
            activity_text = act.get_activity_type_display()

        time_str = act.created_at.strftime("%I:%M %p")

        grouped[key]["name"] = f"{act.student.first_name} {act.student.last_name}"
        grouped[key]["contact"] = act.student.mobile
        grouped[key]["email"] = act.student.email

        if exam_name:
            grouped[key]["exam"].add(exam_name)

        grouped[key]["activity_logs"].append({
            "text": f"{activity_text}\n🕒 {time_str}",
            "time": act.created_at
        })

    rows = []

    for (student_id, date), data in grouped.items():

        logs = sorted(data["activity_logs"], key=lambda x: x["time"])

        rows.append({
            "name": data["name"],
            "contact": data["contact"],
            "email": data["email"],
            "date": date,
            "exam": ", ".join(data["exam"]),
            "activity": "\n\n".join([l["text"] for l in logs])
        })

    paginator = Paginator(rows, 50)
    page = request.GET.get("page")
    rows = paginator.get_page(page)

    return render(request, "admin/selfprep_report.html", {
        "rows": rows,
        "student": student,
        "activity_type": activity_type,
        "from_date": from_date,
        "to_date": to_date
    })


def user_role_management(request):

    roles_list = UserRole.objects.all().order_by('-id')

    paginator = Paginator(roles_list,10)
    page_number = request.GET.get('page')
    roles = paginator.get_page(page_number)

    context = {
        "roles": roles
    }

    return render(request,"admin/user-roles.html",context)



def add_user_role(request, id=None):

    features = Feature.objects.all()
    role = None
    permissions = []

    if id:
        role = UserRole.objects.get(id=id)
        permissions = RolePermission.objects.filter(role=role)

    if request.method == "POST":

        role_name = request.POST.get("role_name")
        status = request.POST.get("status")

        if role:
            role.role_name = role_name
            role.status = status
            role.save()

            RolePermission.objects.filter(role=role).delete()

        else:
            role = UserRole.objects.create(
                role_name=role_name,
                status=status
            )

        for feature in features:

            RolePermission.objects.create(
                role=role,
                feature=feature,
                can_view=True if request.POST.get(f"view_{feature.id}") else False,
                can_add=True if request.POST.get(f"add_{feature.id}") else False,
                can_edit=True if request.POST.get(f"edit_{feature.id}") else False,
                can_delete=True if request.POST.get(f"delete_{feature.id}") else False,
            )

        return redirect('user_role_management')

    context = {
        'features': features,
        'role': role,
        'permissions': permissions
    }

    return render(request,'add-user-role.html',context)

def edit_user_role(request):

    if request.method == "POST":

        id = request.POST.get('id')
        role_name = request.POST.get('role_name')
        status = request.POST.get('status')

        role = UserRole.objects.get(id=id)

        role.role_name = role_name
        role.status = status
        role.save()

        return JsonResponse({"status":"success"})



def delete_user_role(request):

    if request.method == "POST":

        id = request.POST.get('id')

        UserRole.objects.filter(id=id).delete()

        return JsonResponse({"status":"success"})
    

def user_profile_list(request):
    user_list = User.objects.select_related('userprofile').all().order_by('-id')
    paginator = Paginator(user_list, 10)
    page_number = request.GET.get('page')
    users = paginator.get_page(page_number)
    context = {
        "users": users
    }
    return render(request, "admin/user-profile-list.html", context)




def add_user(request, id=None):

    roles = UserRole.objects.filter(status="Active")
    user = None
    profile = None

    # Edit Mode
    if id:
        user = get_object_or_404(User, id=id)
        profile = UserProfile.objects.filter(user=user).first()

    if request.method == "POST":

        first_name = request.POST.get("first_name")
        last_name = request.POST.get("last_name")
        email = request.POST.get("email")
        password = request.POST.get("password")
        contact = request.POST.get("contact")
        dob = request.POST.get("dob")
        role_id = request.POST.get("role")
        status = request.POST.get("status")

        if id:  # UPDATE
            user.first_name = first_name
            user.last_name = last_name
            user.email = email
            user.username = email

            if password:
                user.set_password(password)

            user.save()

        else:  # CREATE
            user = User.objects.create_user(
                username=email,
                email=email,
                password=password,
                first_name=first_name,
                last_name=last_name
            )

        profile, created = UserProfile.objects.get_or_create(user=user)

        profile.contact_number = contact
        profile.date_of_birth = dob
        profile.role_id = role_id
        profile.status = status
        profile.save()

        return redirect("user_profile_list")

    context = {
        "roles": roles,
        "user_obj": user,
        "profile": profile
    }

    return render(request, "admin/add-user.html", context)

def delete_user(request):
    if request.method == "POST":
        user_id = request.POST.get("id")
        User.objects.filter(id=user_id).delete()
        return JsonResponse({
            "status":"success"
        })


from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.core.paginator import Paginator
from django.views.decorators.http import require_http_methods
from django.db import transaction

def academic_year_master(request):
    years = AcademicYear.objects.all().order_by('-created_at')

    name = request.GET.get('name')
    status = request.GET.get('status')

    if name:
        years = years.filter(name__icontains=name)

    if status:
        years = years.filter(status=status)

    page = request.GET.get('page', 1)
    paginator = Paginator(years, 10)
    all_years = paginator.get_page(page)

    filter_query = ""
    if name:
        filter_query += f"&name={name}"
    if status:
        filter_query += f"&status={status}"

    context = {
        'all_years': all_years,
        'name': name or '',
        'status': status or '',
        'filter': filter_query,
        'page': page,
    }
    return render(request, 'admin/academic_year_master.html', context)


@require_http_methods(["POST"])
def add_academic_year(request):
    try:
        name = (request.POST.get('name') or '').strip()
        status = (request.POST.get('status') or 'Active').strip()

        if not name:
            return JsonResponse({
                'status': 'error',
                'msg': 'Academic year name is required.'
            })

        if AcademicYear.objects.filter(name=name).exists():
            return JsonResponse({
                'status': 'error',
                'msg': 'Academic year already exists.'
            })

        if status == 'Active':
            AcademicYear.objects.filter(status='Active').update(status='Inactive')

        AcademicYear.objects.create(
            name=name,
            status=status
        )

        return JsonResponse({
            'status': 'success',
            'msg': 'Academic year added successfully.'
        })

    except Exception as e:
        return JsonResponse({
            'status': 'error',
            'msg': str(e)
        })


@require_http_methods(["POST"])
def edit_academic_year(request):
    try:
        year_id = request.POST.get('id')
        name = (request.POST.get('name') or '').strip()
        status = (request.POST.get('status') or 'Active').strip()

        if not year_id:
            return JsonResponse({
                'status': 'error',
                'msg': 'Academic year id is required.'
            })

        if not name:
            return JsonResponse({
                'status': 'error',
                'msg': 'Academic year name is required.'
            })

        year = get_object_or_404(AcademicYear, id=year_id)

        if AcademicYear.objects.exclude(id=year_id).filter(name=name).exists():
            return JsonResponse({
                'status': 'error',
                'msg': 'Academic year already exists.'
            })

        with transaction.atomic():
            if status == 'Active':
                AcademicYear.objects.exclude(id=year_id).filter(status='Active').update(status='Inactive')

            year.name = name
            year.status = status
            year.save()

        return JsonResponse({
            'status': 'success',
            'msg': 'Academic year updated successfully.'
        })

    except Exception as e:
        return JsonResponse({
            'status': 'error',
            'msg': str(e)
        })


@require_http_methods(["POST"])
def delete_academic_year(request):
    try:
        year_id = request.POST.get('id')

        if not year_id:
            return JsonResponse({
                'status': 'error',
                'msg': 'Academic year id is required.'
            })

        year = get_object_or_404(AcademicYear, id=year_id)
        year.delete()

        return JsonResponse({
            'status': 'success',
            'msg': 'Academic year deleted successfully.'
        })

    except Exception as e:
        return JsonResponse({
            'status': 'error',
            'msg': str(e)
        })

def exam_validities(request):
    exams = ExamCategory.objects.filter(is_deleted=False).order_by('exam_category_name')
    years = AcademicYear.objects.filter(is_deleted=False).order_by('name')

    mappings = ExamAcademic.objects.all()

    # 🔥 convert to list
    data = []

    for year in years:
        row = {
            'year': year,
            'values': []
        }

        for exam in exams:
            obj = mappings.filter(
                academic_year=year,
                exam_category=exam
            ).first()

            row['values'].append({
                'exam': exam,
                'expiry': obj.expiry_date.strftime('%Y-%m-%d') if obj and obj.expiry_date else ''
            })

        data.append(row)

    context = {
        'data': data,
        'exams': exams
    }

    return render(request, 'admin/exam_validities.html', context)

@require_http_methods(["POST"])
def save_exam_validities(request):
    try:
        years = AcademicYear.objects.filter(is_deleted=False)
        exams = ExamCategory.objects.filter(is_deleted=False)

        for year in years:
            for exam in exams:
                field_name = f"expiry_{year.id}_{exam.id}"
                expiry_date = request.POST.get(field_name)

                obj, created = ExamAcademic.objects.get_or_create(
                    academic_year=year,
                    exam_category=exam
                )

                if expiry_date:
                    obj.expiry_date = expiry_date
                    obj.save()
                else:
                    # optional: clear date if blank
                    obj.expiry_date = None
                    obj.save()

        return JsonResponse({
            'status': 'success',
            'msg': 'Exam validities updated successfully.'
        })

    except Exception as e:
        return JsonResponse({
            'status': 'error',
            'msg': str(e)
        })

@login_required(login_url=ADMIN_LOGIN_URL)
def update_question_tags_view(request, test_id):
    """
    Auto-tag questions that share a paragraph inside the same mock and section.

    The tag uses the first question number in the group, for example:
    M-42-S-3-Q-26. Questions 26-30 with the same paragraph receive that tag.
    """
    if request.method == 'GET':
        return redirect('/admin/question_bank?test_id={0}'.format(test_id))
    if request.method != 'POST':
        return JsonResponse({
            "status": "error",
            "msg": "Only POST requests can generate question tags."
        }, status=405)

    mock_test = get_object_or_404(Mock_Test, id=test_id)
    questions = list(
        Question_Bank.objects.filter(test_id=test_id).order_by('section_id', 'id')
    )
    if not questions:
        return JsonResponse({
            "status": "error",
            "msg": "This mock test has no questions."
        }, status=400)

    def normalize_paragraph(value):
        value = str(value or "").strip()
        if not value or value.lower() in ("null", "none"):
            return ""

        # Preserve image identity before removing HTML markup.
        image_sources = re.findall(
            r'<img[^>]+src=["\']([^"\']+)["\']',
            value,
            flags=re.IGNORECASE,
        )
        value = re.sub(r'<(?:br|hr)\s*/?>', ' ', value, flags=re.IGNORECASE)
        value = re.sub(r'</(?:p|div|li|tr|td|th|h[1-6])\s*>', ' ', value, flags=re.IGNORECASE)
        value = re.sub(r'<[^>]+>', '', value)
        value = html.unescape(value)
        value = unicodedata.normalize('NFKC', value).casefold()
        value = ' '.join(value.split())
        return json.dumps(
            {"text": value, "images": image_sources},
            sort_keys=True,
            ensure_ascii=False,
        )

    first_question_by_group = {}
    group_counts = defaultdict(int)
    updated_objects = []

    for question_number, question in enumerate(questions, start=1):
        normalized = normalize_paragraph(question.paragraph)
        # Empty paragraphs must remain individual; otherwise all standalone
        # questions would incorrectly share one explanation video.
        group_key = (
            question.section_id,
            normalized if normalized else "question-{0}".format(question.id),
        )
        first_question_number = first_question_by_group.setdefault(
            group_key, question_number
        )
        group_counts[group_key] += 1
        new_tag = "M-{0}-S-{1}-Q-{2}".format(
            mock_test.id,
            question.section_id,
            first_question_number,
        )

        if question.tag != new_tag:
            question.tag = new_tag
            updated_objects.append(question)

    if updated_objects:
        Question_Bank.objects.bulk_update(updated_objects, ['tag'])

    return JsonResponse({
        "status": "success",
        "msg": "Question tags generated successfully.",
        "updated_count": len(updated_objects),
        "total_questions": len(questions),
        "shared_paragraph_groups": sum(
            1 for count in group_counts.values() if count > 1
        ),
    })

def update_slot_priority(request):
    priorities = json.loads(request.POST.get('priorities', '[]'))

    for item in priorities:
        slot_id = item.get('id')
        priority = item.get('priority') or 0

        FacultyTimeSlot.objects.filter(id=slot_id).update(
            priority=priority
        )

    return JsonResponse({
        'status': True,
        'message': 'Priority updated successfully'
    })


@login_required(login_url=ADMIN_LOGIN_URL)
def update_strength_bulk(request):

    if request.method == "POST":
        category_id = request.POST.get('category_id')
        sections = json.loads(request.POST.get('sections'))
        subsections = json.loads(request.POST.get('subsections'))
        # Sections
        for item in sections:

            ExamSyllabus.objects.filter(
                section_id=item['id'],
                exam_category_id=category_id
            ).update(
                s_strong=item.get('strong') or None,
                s_weak=item.get('weak') or None
            )


        # Subsections
        for item in subsections:

            ExamSyllabus.objects.filter(
                sub_section_id=item['id'],
                exam_category_id=category_id
            ).update(
                ss_strong=item.get('strong') or None,
                ss_weak=item.get('weak') or None
            )

        return JsonResponse({
            'status': 1
        })

    return JsonResponse({
        'status': 0
    })
