from django.shortcuts import render, get_object_or_404, redirect
from django.urls import reverse
from django.http import HttpResponse, JsonResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.utils import timezone
from django.db import models
from django.db.models import Q, Count, Sum
from django.contrib import messages
from .models import School, Student, Device, AttendanceLog, UserProfile, StudentClass, FeeType, StudentFee, DetectionFee, ClassSchedule, StudentLeave, Holiday, FeeTransaction, AdmitPass, FeeTransactionItem, Exam, Subject, ResultRecord, SubjectGroup, GradeConfig, VirtualClass, Announcement, PromotionRecord, GlobalSMSSettings, SMSLog, SMSGateway
from django.contrib.auth.models import User
from django.contrib.auth import authenticate, login, logout, update_session_auth_hash
from django.contrib.auth.decorators import login_required, user_passes_test
import logging
from datetime import datetime, date, time, timedelta
import decimal
import requests
import threading
import re
import zoneinfo

from .utils import InstitutionExporter, InstitutionImporter
logger = logging.getLogger(__name__)

# --- Public Landing ---
def public_home(request):
    redirect_url = None
    if request.user.is_authenticated:
        redirect_url = reverse('school_dashboard')
    elif 'p_student_id' in request.session:
        redirect_url = reverse('parent_dashboard')
        
    return render(request, 'public/stdylab.html', {'redirect_url': redirect_url})

# --- Authentication & Entry ---

def user_login(request):
    if request.user.is_authenticated:
        profile = getattr(request.user, 'profile', None)
        if request.user.is_superuser or (profile and profile.role == 'SUPERUSER'):
            return redirect('super_dashboard')
        if profile and profile.role == 'TEACHER':
            return redirect('teacher_dashboard')
        return redirect('school_dashboard')
        
    if request.method == 'POST':
        code = request.POST.get('code', '').strip()
        username = request.POST.get('username', '').strip()
        password = request.POST.get('password', '').strip()
        
        user = None
        # If code is provided, try school admin or teacher login
        if code:
            # 1. Check School Admin (code_username)
            targeted_username = f"{code}_{username}"
            user = authenticate(request, username=targeted_username, password=password)
            
            if not user:
                # 2. Check Teacher (Phone Number)
                try:
                    profile = UserProfile.objects.filter(phone_number=username, school__code=code).first()
                    if profile:
                        user = authenticate(request, username=profile.user.username, password=password)
                except Exception: pass
        else:
            # Probable Superuser login
            user = authenticate(request, username=username, password=password)
            
        if user:
            login(request, user)
            return redirect('login') 
        return render(request, 'login.html', {'error': 'Invalid Credentials'})
    return render(request, 'login.html')

def user_logout(request):
    logout(request)
    return redirect('login')

# --- ADMS / ZKTeco Receiver ---
@csrf_exempt
def iclock_receiver(request):
    sn = request.GET.get('SN')
    if not sn:
        return HttpResponse("NOT FOUND", status=404)
    
    device, created = Device.objects.get_or_create(serial_number=sn)
    if not device.is_authorized or not device.school or device.is_terminated:
        return HttpResponse("OK") # Ignored if not authorized, unassigned, or terminated

    # Activate School Timezone for processing
    timezone.activate(zoneinfo.ZoneInfo(device.school.timezone_name))

    if request.path.endswith('/getrequest'):
        return HttpResponse("OK")
    
    if request.path.endswith('/cdata'):
        raw_data = request.body.decode('utf-8', errors='ignore')
        processed_count = process_attendance_data(raw_data, device.school, device, request=request)
        return HttpResponse(f"OK: {processed_count}")
    
    return HttpResponse("OK")

def _get_active_sms_config():
    """Returns (url, api_key, sender_id). Priority: SMSGateway -> GlobalSMSSettings -> None."""
    gw = SMSGateway.objects.filter(is_active=True).first()
    if gw: return gw.api_url, gw.api_key, gw.sender_id
    
    gs = GlobalSMSSettings.objects.filter(is_active=True).first()
    if gs: return "https://bulksmsbd.net/api/smsapi", gs.api_key, gs.sender_id
    
    return None, None, None

def get_school_now(school):
    """Returns the current aware datetime, respecting Time Test mode if active."""
    now = timezone.now()
    if school.is_time_test_active and school.test_timestamp and school.test_set_at:
        delta = now - school.test_set_at
        return school.test_timestamp + delta
    return now

def _send_sms_api(number, message, api_url, api_key, sender_id):
    if not api_url or not api_key: return False
    payload = {"api_key": api_key, "senderid": sender_id, "number": number, "message": message}
    try:
        resp = requests.post(api_url, data=payload, timeout=12)
        return resp.status_code == 200
    except: return False

def send_generic_school_sms(number, message, school):
    from .models import GlobalSMSSettings, SMSLog
    if not school.is_sms_enabled: return False
    
    sms_settings = GlobalSMSSettings.get_settings()
    if not sms_settings.is_active or not sms_settings.api_key: return False
    
    # Clean phone
    phone_cleaned = re.sub(r'\D', '', str(number))
    if phone_cleaned.startswith('01') and len(phone_cleaned) == 11:
        phone_cleaned = '88' + phone_cleaned
    elif phone_cleaned.startswith('1') and len(phone_cleaned) == 10:
        phone_cleaned = '880' + phone_cleaned

    # 4. Dispatch
    api_url, api_key_dyn, sender_id_dyn = _get_active_sms_config()
    result = _send_sms_api(phone_cleaned, message, api_url, api_key_dyn, sender_id_dyn)
    
    # Log
    status = 'SENT'
    if result is None: status = 'FAILED'
    elif isinstance(result, dict):
        rc = result.get('response_code') or result.get('code') or result.get('status_code')
        if str(rc) not in ('202', '200'): status = 'FAILED'
        
    SMSLog.objects.create(school=school, recipient=phone_cleaned, message=message, status=status)
    return status == 'SENT'


def send_attendance_sms(log, student, school, request=None):
    from .models import GlobalSMSSettings
    # 1. School SMS toggle
    if not school.is_sms_enabled:
        return

    # 1.0 Specific Type Control
    if log.status == 'LUNCH_BREAK':
        if not school.is_lunch_break_sms_enabled:
            return
    elif log.check_type == 'IN':
        if not school.is_punch_in_sms_enabled:
            return
    elif log.check_type == 'OUT':
        if not school.is_punch_out_sms_enabled:
            return

    sms_settings = GlobalSMSSettings.get_settings()
    # 2. Global gateway check
    if not sms_settings.is_active or not sms_settings.api_key:
        return

    if not student.contact_number:
        return

    # 1.1 Individual SMS check
    if school.is_individual_sms_enabled and not student.is_sms_enabled:
        return

    # Prefer the globally configured domain, fallback to request's domain only if missing
    domain = (sms_settings.site_domain or '').rstrip('/')
    if not domain and request:
        # ZKTeco device requests often contain raw IP, which triggers SMS spam filters.
        # Fallback will use the request's host, but settings should ideally be populated.
        domain = request.build_absolute_uri('/').rstrip('/')
    
    phone_for_url = re.sub(r'\D', '', str(student.contact_number))
    
    if domain:
        history_url = f"{domain}/history/?phone={phone_for_url}&id={student.id}&code={school.code}"
    else:
        history_url = ""

    # --- Prepare substitution variables ---
    local_dt = timezone.localtime(log.timestamp)
    time_str = local_dt.strftime('%I:%M %p')           # 09:15 AM
    student_name_short = (student.name or 'Student')[:10].strip().upper()

    # Status-Direction label (PRESENT-IN, LATE-IN, EARLY-OUT, etc.)
    status_map = {
        'EARLY_IN':          'EARLY-IN',
        'PRESENT_IN':        'CHECK-IN',
        'PRESENT_OUT':       'CHECK-OUT',
        'LATE':              'LATE-IN',
        'LATE_OUT':          'LATE-OUT',
        'LATE_OUT_APPROVED': 'LATE-OUT',
        'EARLY_OUT':         'EARLY-OUT',
        'LUNCH_BREAK':       'LUNCH-BREAK',
        'NORMAL':            'PRESENT',
    }
    status_label = status_map.get(log.status, (log.status or 'PRESENT').replace('_', '-'))

    # --- Pick the right template from DB ---
    if log.check_type == 'IN':
        template = (sms_settings.msg_check_in or '').strip()
    else:
        template = (sms_settings.msg_check_out or '').strip()

    # --- Substitute variables ---
    if template:
        # Format the message using template variables
        message = template.format(
            student_name=student_name_short,
            status=status_label,
            time=time_str,
            history_url=history_url,
            school_name=school.name,
            school_code=school.code.upper(),
        )
        
        # If history_url is empty, cleanly remove "Details:" and any trailing spaces/punctuation
        if not history_url:
            # Matches "Details: ", "Details:" or "Details :"
            message = re.sub(r'Details\s*:\s*', '', message).strip()
            # Also clean up trailing punctuation if any
            message = message.rstrip('. ,')
    else:
        # Fallback if template is blank
        body = f"Dear, Your Child {student_name_short} {status_label} at {time_str}."
        message = f"{body} Details: {history_url}" if history_url else body

    message = message.strip()




    # --- Clean phone number ---
    phone_cleaned = re.sub(r'\D', '', str(student.contact_number))
    if phone_cleaned.startswith('880'):
        pass  # Already correctly prefixed
    elif phone_cleaned.startswith('01') and len(phone_cleaned) == 11:
        phone_cleaned = '88' + phone_cleaned
    elif phone_cleaned.startswith('1') and len(phone_cleaned) == 10:
        phone_cleaned = '880' + phone_cleaned

    # --- Pre-fetch ALL primitives — NO model instances in thread (MySQL thread-safety) ---
    api_url, api_key_dyn, sender_id_dyn = _get_active_sms_config()
    school_id = school.id  # Pass ID, not the ORM object

    # --- Background thread ---
    def background_sms():
        import time as _time_mod
        from django.db import close_old_connections

        # MUST call before any DB access in a new thread
        close_old_connections()

        # 1. Send SMS via API (no DB involved)
        result = _send_sms_api(phone_cleaned, message, api_url, api_key_dyn, sender_id_dyn)

        # 2. Write log to DB — retry up to 3 times for connection reliability
        sms_status = 'SENT'
        if result is None:
            sms_status = 'FAILED'
        elif isinstance(result, dict):
            rc = result.get('response_code') or result.get('code') or result.get('status_code')
            if str(rc) not in ('202', '200'):
                sms_status = 'FAILED'

        for attempt in range(3):
            try:
                close_old_connections()  # Fresh connection each attempt
                from .models import SMSLog, School as SchoolModel
                # Use school_id (primitive) — safe across threads
                school_obj = SchoolModel.objects.get(id=school_id)
                SMSLog.objects.create(
                    school=school_obj,
                    recipient=phone_cleaned,
                    message=message,
                    status=sms_status
                )
                break  # Success — exit retry loop
            except Exception as e:
                logger.error(f"SMSLog DB write attempt {attempt+1} failed: {e}")
                if attempt < 2:
                    _time_mod.sleep(0.5)  # Brief pause before retry
                else:
                    import traceback
                    logger.error(traceback.format_exc())

    t = threading.Thread(target=background_sms, daemon=True)
    t.start()
    return t



def send_fee_payment_sms(transaction, school):
    if not school.is_fee_sms_enabled:
        return
    
    # 0. Global School Broadcast Limit Check
    if school.monthly_fee_limit_per_student > 0 and school.fees_sent_this_month >= school.monthly_fee_limit_per_student:
        return

    student = transaction.student
    if not student.contact_number:
        return
    
    # 1. Monthly Limit Check
    if not check_and_increment_sms_limit(student, 'FEE'):
        return

    # 2. Individual Mode check
    if school.is_individual_sms_enabled and not student.is_sms_enabled:
        return
        
    # Get fee purposes
    items = transaction.items.all()
    purposes = []
    for item in items:
        purposes.append(f"{item.fee.fee_type.name}(৳{int(item.amount_paid)})")
    
    if transaction.fine_amount > 0:
        purposes.append(f"Fines(৳{int(transaction.fine_amount)})")
        
    purposes_str = ", ".join(purposes)
    msg = f"Fee Received: {student.name[:15]}, Total: ৳{int(transaction.total_amount)}. Purpose: {purposes_str}. - {school.name}"
    
    # Clean threading like others
    import threading
    threading.Thread(target=send_generic_school_sms, args=(student.contact_number, msg, school), daemon=True).start()
    
    # 3. Increment School Counter
    school.fees_sent_this_month += 1
    school.save()


def send_admission_sms(student, school):
    """
    Sends an admission confirmation SMS to the student's guardian.
    Includes: Name, Class, Roll, PIN, Secret Code.
    """
    from .models import GlobalSMSSettings, SMSLog
    # 1. School SMS toggle check
    if not school.is_sms_enabled:
        return

    settings = GlobalSMSSettings.get_settings()
    # 2. Global gateway active check
    if not settings.is_active or not settings.api_key:
        return

    if not student.contact_number:
        return

    # 1.2 Individual SMS check
    if school.is_individual_sms_enabled and not student.is_sms_enabled:
        return

    # --- Build message with all required fields ---
    class_name = student.student_class.name if student.student_class else 'N/A'
    roll       = str(student.roll) if student.roll else 'N/A'
    pin        = student.pin
    secret     = student.secret_code or 'N/A'

    message = (
        f"Admission Successful!\n"
        f"We are delighted to welcome {student.name} to {school.name}. We look forward to a successful academic journey together.\n\n"
        f"Academic Profile:\n"
        f"Name: {student.name}\n"
        f"Class: {class_name}\n"
        f"Roll: {roll}\n"
        f"System PIN: {pin}\n"
        f"Secret Code: {secret}"
    )

    # --- Normalise phone number ---
    phone_cleaned = re.sub(r'\D', '', str(student.contact_number))
    if phone_cleaned.startswith('01') and len(phone_cleaned) == 11:
        phone_cleaned = '88' + phone_cleaned
    elif phone_cleaned.startswith('1') and len(phone_cleaned) == 10:
        phone_cleaned = '880' + phone_cleaned

    # --- Snapshot primitives for thread safety ---
    api_key   = settings.api_key
    sender_id = settings.sender_id
    school_id = school.id

    # --- Send in background thread ---
    def post_sms():
        from django.db import close_old_connections
        from .models import SMSLog, School as SchoolModel
        close_old_connections()

        api_url, api_key, sender_id = _get_active_sms_config()
        if not api_url: return
        
        sms_status = 'FAILED'
        try:
            payload = {
                "api_key":  api_key,
                "senderid": sender_id,
                "number":   phone_cleaned,
                "message":  message
            }
            resp   = requests.post(api_url, data=payload, timeout=12)
            result = resp.json() if resp.content else {}
            rc     = result.get('response_code') or result.get('code') or result.get('status_code')
            sms_status = 'SENT' if str(rc) in ('202', '200') else 'FAILED'
        except Exception as e:
            logger.error(f"Admission SMS error: {e}")

        # Write audit log
        try:
            close_old_connections()
            school_obj = SchoolModel.objects.get(id=school_id)
            SMSLog.objects.create(
                school=school_obj,
                recipient=phone_cleaned,
                message=message,
                status=sms_status
            )
        except Exception as e:
            logger.error(f"Admission SMSLog write failed: {e}")

    threading.Thread(target=post_sms, daemon=True).start()


def check_absent_students(school):
    """
    Checks all students in a school who haven't punched in today.
    Triggered when lunch break start time is reached/passed.
    """
    if not school.is_absent_sms_enabled:
        return
        
    now = timezone.localtime()
    today = now.date()
    
    # Critical imports inside function to avoid circular/thread issues
    from .models import Holiday, StudentClass, AttendanceLog, SMSLog, Student
    
    # 1. Global Holiday check
    if Holiday.objects.filter(school=school, date=today, day_type='HOLIDAY').exists():
        return
    if today.weekday() == 4: # Friday is default holiday in BD
        return

    # 2. Iterate through classes to check specific schedules
    classes = StudentClass.objects.filter(school=school)
    for cls in classes:
        schedule = getattr(cls, 'schedule', None)
        if not schedule: continue
        
        # Only proceed if current time is past lunch start (Absent threshold)
        if now.time() < schedule.lunch_start:
            continue
            
        # 3. Check each student in the class
        students = Student.objects.filter(student_class=cls, school=school)
        for student in students:
            # Check for ANY valid 'IN' punch today
            has_in = AttendanceLog.objects.filter(
                student=student, 
                timestamp__date=today, 
                check_type='IN'
            ).exclude(status__in=['FALSE_PUNCH', 'HOLD']).exists()
            
            if not has_in:
                # 4. Anti-Spam: Check if we already sent an absent notice today
                already = SMSLog.objects.filter(
                    school=school, 
                    recipient=student.contact_number, 
                    message__icontains="not detected", 
                    timestamp__date=today
                ).exists()
                
                if not already and student.contact_number:
                    # Individual Mode preference
                    if school.is_individual_sms_enabled and not student.is_sms_enabled:
                        continue
                        
                    msg = f"Absence Notice: Student {student.name[:15]} was not detected in school today as of {now.strftime('%I:%M %p')}. - {school.name}"
                    # Use existing threading logic via send_generic_school_sms
                    send_generic_school_sms(student.contact_number, msg, school)


def check_and_increment_sms_limit(student, msg_type):
    """
    Checks if student has reached monthly limit for a specific SMS type.
    Increments counter if allowed. Returns True/False.
    Types: 'ANNOUNCEMENT', 'EXAM', 'MEETING', 'FEE'
    """
    school = student.school
    today = timezone.now().date()
    
    # Monthly Reset Logic
    if not student.last_limit_reset or student.last_limit_reset.month != today.month or student.last_limit_reset.year != today.year:
        student.announcements_this_month = 0
        student.exams_this_month = 0
        student.meetings_this_month = 0
        student.fees_this_month = 0
        student.last_limit_reset = today
        
    if msg_type == 'ANNOUNCEMENT':
        if student.announcements_this_month >= school.monthly_announcement_limit_per_student:
            return False
        student.announcements_this_month += 1
    elif msg_type == 'EXAM':
        if student.exams_this_month >= school.monthly_exam_limit_per_student:
            return False
        student.exams_this_month += 1
    elif msg_type == 'MEETING':
        if student.meetings_this_month >= school.monthly_meeting_limit_per_student:
            return False
        student.meetings_this_month += 1
    elif msg_type == 'FEE':
        if student.fees_this_month >= school.monthly_fee_limit_per_student:
            return False
        student.fees_this_month += 1
        
    student.save()
    return True


def process_attendance_data(raw_data, school, device, request=None):
    count = 0
    lines = raw_data.strip().split('\n')
    students = {str(s.pin): s for s in school.students.all()}
    sms_threads = []
    
    for line in lines:
        if not line or line.startswith('#'): continue
        parts = line.split('\t')
        if len(parts) >= 2:
            pin = parts[0]
            ts_str = parts[1]
            try:
                # We extract the PIN from device data but ignore its timestamp
                # prioritized by the requirement to sync with system location-time.
                ts = get_school_now(school)
            except: continue
            
            student = students.get(pin)
            local_dt = timezone.localtime(ts)
            p_date = local_dt.date()
            p_time = local_dt.time()
            status = 'UNIDENTIFIED'
            
            if student:
                from datetime import datetime as _dt, time as _t
                range_start = timezone.make_aware(_dt.combine(p_date, _t.min))
                range_end = timezone.make_aware(_dt.combine(p_date, _t.max))

                schedule = getattr(student.student_class, 'schedule', None)
                l_start = (schedule.lunch_start if (schedule and schedule.lunch_start) else time(13, 0))
                l_end = (schedule.lunch_end if (schedule and schedule.lunch_end) else time(14, 0))
                
                # Only count authorized IN logs for identifying the session start
                has_earlier_log = AttendanceLog.objects.filter(student=student, timestamp__gte=range_start, timestamp__lte=range_end, check_type='IN').exclude(status__in=['FALSE_PUNCH', 'HOLD']).exists()
                
                # Check if it's a holiday
                holiday = Holiday.objects.filter(school=school, date=p_date).first()
                is_holiday = False
                if holiday:
                    is_holiday = (holiday.day_type == 'HOLIDAY')
                else:
                    is_holiday = (p_date.weekday() == 4) # Friday is default holiday in BD

                if is_holiday:
                    status = 'FALSE_PUNCH'
                elif schedule:
                    if not has_earlier_log:
                        # Logic for first punch (IN)
                        if p_time < schedule.punch_in_start:
                            status = 'HOLD' if school.is_hold_enabled else 'EARLY_IN'
                        elif p_time <= schedule.punch_in_end:
                            status = 'PRESENT_IN'
                        elif p_time < l_start:
                            status = 'LATE'
                        else:
                            # Arrival after lunch start is LATE (authorized) or HOLD (restricted)
                            status = 'HOLD' if school.is_hold_enabled else 'LATE'
                    else:
                        # Logic for subsequent punches (OUT)
                        
                        # Find the first punch of today to determine arrival time relative to break
                        first_log = AttendanceLog.objects.filter(student=student, timestamp__gte=range_start, timestamp__lte=range_end, check_type='IN').first()
                        arrival_time = timezone.localtime(first_log.timestamp).time() if first_log else p_time # Fallback to current if no IN

                        # Requirement: Arrival before break -> MUST have a break punch.
                        # If they missed the break window, the FIRST punch after the window is HELD for review.
                        has_lunch_log = AttendanceLog.objects.filter(student=student, timestamp__gte=range_start, timestamp__lte=range_end, status='LUNCH_BREAK').exists()
                        has_final_checkout = AttendanceLog.objects.filter(
                            student=student, 
                            timestamp__gte=range_start, 
                            timestamp__lte=range_end, 
                            status__in=['PRESENT_OUT', 'EARLY_OUT', 'LATE_OUT', 'LATE_OUT_APPROVED']
                        ).exists()

                        if has_final_checkout:
                            status = 'FALSE_PUNCH'
                        elif arrival_time < l_start and p_time < l_start:
                            status = 'FALSE_PUNCH'
                        elif school.is_lunch_break_required and arrival_time < l_start and p_time > l_end and not has_lunch_log:
                            # LUNCH BREAK REQUIRED MODE:
                            # Student arrived before lunch but never punched during break.
                            # Force the first post-lunch punch to be LUNCH_BREAK.
                            # The NEXT punch will then be their real checkout action.
                            status = 'LUNCH_BREAK'
                        else:
                            # Standard OUT logic
                            # If lunch break NOT required, skip the lunch window entirely
                            if schedule.checkout_start <= p_time <= schedule.checkout_end:
                                status = 'PRESENT_OUT'
                            elif p_time > schedule.checkout_end:
                                status = 'LATE_OUT'
                            elif school.is_lunch_break_required and l_start <= p_time <= l_end:
                                # Only assign LUNCH_BREAK status if the feature is ON
                                status = 'LUNCH_BREAK'
                            else:
                                status = 'EARLY_OUT'
                else:
                    # BUG FIX: schedule নেই কিন্তু আগে NORMAL IN লগ আছে → FALSE_PUNCH
                    if has_earlier_log:
                        status = 'FALSE_PUNCH'
                    else:
                        status = 'NORMAL'
            
            # One status per day rule: If this status already exists for this student today, mark as FALSE_PUNCH
            # NOTE: NORMAL is now included — without schedule, only one NORMAL per day allowed
            if student and status not in ('UNIDENTIFIED', 'FALSE_PUNCH'):
                if AttendanceLog.objects.filter(student=student, timestamp__gte=range_start, timestamp__lte=range_end, status=status).exists():
                    status = 'FALSE_PUNCH'

            # Check for absolute duplicate (exact same timestamp)
            if AttendanceLog.objects.filter(school=school, timestamp=ts, unidentified_pin=pin if not student else None, student=student).exists():
                continue
            
            log = AttendanceLog.objects.create(
                student=student,
                school=school,
                device=device,
                timestamp=ts,
                check_type='IN' if not has_earlier_log else 'OUT', 
                status=status,
                unidentified_pin=None if student else pin,
                student_class=student.student_class if student else None
            )
            
            # Trigger SMS if student exists and not a false punch or on hold
            if student and status not in ('FALSE_PUNCH', 'HOLD'):
                t = send_attendance_sms(log, student, school, request=request)
                if t: sms_threads.append(t)
                
            count += 1
            
    # CRITICAL FIX: SMS Delay inside WSGI Workers.
    # Background threads get suspended by uWSGI/Gunicorn when the HTTP response completes.
    # We must wait for SMS threads to finish their API calls before responding to the device.
    import time
    deadline = time.time() + 6.0 # Max 6 sec total wait so device connection doesn't drop
    for t in sms_threads:
        rem_time = deadline - time.time()
        if rem_time > 0:
            t.join(timeout=rem_time)

    if count > 0:
        # Check for absent students in a background thread
        import threading
        t_absent = threading.Thread(target=check_absent_students, args=(school,))
        t_absent.daemon = True
        t_absent.start()

    return count

# --- Superuser Views ---
@login_required(login_url='/login/')
def super_dashboard(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
    
    schools = School.objects.all()
    devices = Device.objects.all()
    
    context = {
        'total_schools': schools.count(),
        'total_devices': devices.count(),
        'authorized_devices': devices.filter(is_authorized=True).count(),
        'recent_schools': schools.order_by('-created_at')[:5],
        'total_records': AttendanceLog.objects.count()
    }
    return render(request, 'superuser/dashboard.html', context)

def super_backup(request):
    import tempfile
    import io
    from django.core.management import call_command
    from django.http import HttpResponse

    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')

    if request.method == 'POST':
        action = request.POST.get('action')
        if action == 'generate':
            response = HttpResponse(content_type='application/json')
            response['Content-Disposition'] = 'attachment; filename="school_lab_full_backup.json"'
            try:
                out = io.StringIO()
                # Exclude types that break upon loading
                call_command('dumpdata', exclude=['auth.permission', 'contenttypes', 'sessions.session', 'admin.logentry'], format='json', indent=2, stdout=out)
                response.write(out.getvalue())
                return response
            except Exception as e:
                messages.error(request, f"Backup failed: {e}")
                return redirect('super_backup')
                
        elif action == 'restore':
            file = request.FILES.get('backup_file')
            if not file:
                messages.error(request, "Please provide a valid backup json file.")
                return redirect('super_backup')
            
            try:
                with tempfile.NamedTemporaryFile(delete=False, suffix=".json") as temp:
                    for chunk in file.chunks():
                        temp.write(chunk)
                    temp_path = temp.name
                
                call_command('loaddata', temp_path)
                import os
                os.unlink(temp_path)
                messages.success(request, "System Data restored successfully!")
            except Exception as e:
                messages.error(request, f"Restore corrupted or failed: {e}")
            return redirect('super_backup')

    return render(request, 'superuser/backup.html')

def super_monitoring(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')

    query = request.GET.get('q')
    view_filter = request.GET.get('filter', 'active') # active, terminated
    
    devices = Device.objects.all().select_related('school')

    if view_filter == 'terminated':
        devices = devices.filter(is_terminated=True)
    else:
        devices = devices.filter(is_terminated=False)

    if query:
        devices = devices.filter(
            models.Q(serial_number__icontains=query) |
            models.Q(school__name__icontains=query) |
            models.Q(name__icontains=query)
        )

    device_stats = []
    for d in devices:
        stats = {
            'device': d,
            'logs_count': AttendanceLog.objects.filter(device=d).count(),
            'students_count': Student.objects.filter(school=d.school).count() if d.school else 0,
            'school_name': d.school.name if d.school else 'UNASSIGNED',
        }
        device_stats.append(stats)

    return render(request, 'superuser/monitoring.html', {
        'device_stats': device_stats, 
        'query': query,
        'view_filter': view_filter,
        'total_devices': Device.objects.count(),
        'active_count': Device.objects.filter(is_terminated=False).count(),
        'terminated_count': Device.objects.filter(is_terminated=True).count(),
        'unassigned_count': Device.objects.filter(school__isnull=True, is_terminated=False).count()
    })

def super_device_edit(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return redirect('school_dashboard')
        
    device = get_object_or_404(Device, pk=pk)
    schools = School.objects.all()
    
    if request.method == 'POST':
        device.name = request.POST.get('name')
        device.serial_number = request.POST.get('serial_number')
        school_id = request.POST.get('school')
        if school_id:
            device.school = get_object_or_404(School, pk=school_id)
            device.is_authorized = True
        else:
            device.school = None
            device.is_authorized = False
            
        device.save()
        messages.success(request, f"Device {device.serial_number} updated.")
        return redirect('super_monitoring')
        
    return render(request, 'superuser/device_form.html', {'device': device, 'schools': schools})

def super_device_terminate(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return redirect('school_dashboard')
        
    if request.method == 'POST':
        device = get_object_or_404(Device, pk=pk)
        device.is_terminated = not device.is_terminated # Toggle
        device.save()
        
        status = "terminated" if device.is_terminated else "reactived"
        messages.info(request, f"Device {device.serial_number} has been {status}.")
        
    return redirect('super_monitoring')

def super_device_delete(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return redirect('school_dashboard')
        
    if request.method == 'POST':
        device = get_object_or_404(Device, pk=pk)
        sn = device.serial_number
        device.delete()
        messages.error(request, f"Device {sn} permanently deleted.")
        
    return redirect('super_monitoring')

def super_device_unbind(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
    
    device = get_object_or_404(Device, pk=pk)
    device.school = None
    device.assigned_class = None
    device.is_authorized = False
    device.save()
    messages.warning(request, f"Terminal link severed for {device.serial_number}")
    return redirect('super_monitoring')

def super_sms_settings(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
    
    from .models import GlobalSMSSettings
    settings = GlobalSMSSettings.get_settings()
    
    if request.method == 'POST':
        settings.api_key = request.POST.get('api_key')
        settings.sender_id = request.POST.get('sender_id')
        settings.is_active = 'is_active' in request.POST
        settings.msg_check_in = request.POST.get('msg_check_in')
        settings.msg_check_out = request.POST.get('msg_check_out')
        settings.save()
        messages.success(request, "Global SMS settings updated successfully.")
        return redirect('super_sms_settings')
        
    return render(request, 'superuser/sms_settings.html', {'settings': settings})

def super_institution_settings(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
    
    institution = get_object_or_404(School, pk=pk)
    
    # Monthly Reset Check for School
    today = timezone.now().date()
    if not institution.last_limit_reset or institution.last_limit_reset.month != today.month or institution.last_limit_reset.year != today.year:
        institution.announcements_sent_this_month = 0
        institution.exams_sent_this_month = 0
        institution.meetings_sent_this_month = 0
        institution.fees_sent_this_month = 0
        institution.last_limit_reset = today
        institution.save()

    if request.method == 'POST':
        institution.is_sms_enabled = 'is_sms_enabled' in request.POST
        institution.is_individual_sms_enabled = 'is_individual_sms_enabled' in request.POST
        institution.is_announcement_sms_enabled = 'is_announcement_sms_enabled' in request.POST
        institution.is_exam_sms_enabled = 'is_exam_sms_enabled' in request.POST
        institution.is_online_meeting_sms_enabled = 'is_online_meeting_sms_enabled' in request.POST
        institution.is_lunch_break_sms_enabled = 'is_lunch_break_sms_enabled' in request.POST
        institution.is_punch_in_sms_enabled = 'is_punch_in_sms_enabled' in request.POST
        institution.is_punch_out_sms_enabled = 'is_punch_out_sms_enabled' in request.POST
        institution.is_fee_sms_enabled = 'is_fee_sms_enabled' in request.POST
        institution.is_absent_sms_enabled = 'is_absent_sms_enabled' in request.POST
        institution.monthly_announcement_limit_per_student = int(request.POST.get('monthly_announcement_limit_per_student', 5))
        institution.monthly_exam_limit_per_student = int(request.POST.get('monthly_exam_limit_per_student', 5))
        institution.monthly_meeting_limit_per_student = int(request.POST.get('monthly_meeting_limit_per_student', 5))
        institution.monthly_fee_limit_per_student = int(request.POST.get('monthly_fee_limit_per_student', 5))
        institution.is_lunch_break_required = 'is_lunch_break_required' in request.POST
        institution.save()
        messages.success(request, f"Configuration for {institution.name} updated successfully.")
        return redirect('super_school_list')
        
    # Calculate Remaining
    rem_announcement = max(0, institution.monthly_announcement_limit_per_student - institution.announcements_sent_this_month)
    rem_exam = max(0, institution.monthly_exam_limit_per_student - institution.exams_sent_this_month)
    rem_meeting = max(0, institution.monthly_meeting_limit_per_student - institution.meetings_sent_this_month)
    rem_fee = max(0, institution.monthly_fee_limit_per_student - institution.fees_sent_this_month)

    context = {
        'institution': institution,
        'rem_announcement': rem_announcement,
        'rem_exam': rem_exam,
        'rem_meeting': rem_meeting,
        'rem_fee': rem_fee,
    }
    return render(request, 'superuser/institution_settings.html', context)

@csrf_exempt
def api_toggle_school_sms(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return JsonResponse({'error': 'Unauthorized'}, status=403)
        
    institution = get_object_or_404(School, pk=pk)
    institution.is_sms_enabled = not institution.is_sms_enabled
    institution.save()
    return JsonResponse({'status': 'ok', 'is_sms_enabled': institution.is_sms_enabled})

@csrf_exempt
def api_toggle_school_time_test(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return JsonResponse({'error': 'Unauthorized'}, status=403)
        
    institution = get_object_or_404(School, pk=pk)
    institution.is_time_test_enabled = not institution.is_time_test_enabled
    institution.save()
    return JsonResponse({'status': 'ok', 'is_time_test_enabled': institution.is_time_test_enabled})

@csrf_exempt
def api_toggle_school_hold(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return JsonResponse({'error': 'Unauthorized'}, status=403)
        
    institution = get_object_or_404(School, pk=pk)
    institution.is_hold_enabled = not institution.is_hold_enabled
    institution.save()
    return JsonResponse({'status': 'ok', 'is_hold_enabled': institution.is_hold_enabled})

@csrf_exempt
def api_toggle_school_individual_sms(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return JsonResponse({'error': 'Unauthorized'}, status=403)
        
    institution = get_object_or_404(School, pk=pk)
    institution.is_individual_sms_enabled = not institution.is_individual_sms_enabled
    institution.save()
    return JsonResponse({'status': 'ok', 'is_individual_sms_enabled': institution.is_individual_sms_enabled})

@csrf_exempt
def api_toggle_lunch_break_required(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return JsonResponse({'error': 'Unauthorized'}, status=403)
        
    institution = get_object_or_404(School, pk=pk)
    institution.is_lunch_break_required = not institution.is_lunch_break_required
    institution.save()
    return JsonResponse({'status': 'ok', 'is_lunch_break_required': institution.is_lunch_break_required})

def super_school_list(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
        
    schools = School.objects.all().order_by('-created_at')
    return render(request, 'superuser/school_list.html', {'schools': schools})

def super_school_add(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
        
    if request.method == 'POST':
        name = request.POST.get('name')
        code = request.POST.get('code')
        admin_username = request.POST.get('admin_username')
        password = request.POST.get('password')
        address = request.POST.get('address')
        contact_number = request.POST.get('contact_number')
        
        if School.objects.filter(code=code).exists():
            return render(request, 'superuser/school_form.html', {'error': f'Code {code} already exists.'})
        
        school = School.objects.create(name=name, code=code, admin_username=admin_username, password=password, address=address, contact_number=contact_number)
        
        # Create a Django User for the School Admin
        # Username format: {school_code}_{admin_username}
        django_username = f"{code}_{admin_username}"
        user = User.objects.create_user(username=django_username, password=password)
        UserProfile.objects.create(user=user, role='SCHOOL_ADMIN', school=school)
        
        return redirect('super_school_list')
    return render(request, 'superuser/school_form.html')

def super_school_edit(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
        
    school = get_object_or_404(School, pk=pk)
    if request.method == 'POST':
        school.name = request.POST.get('name')
        school.address = request.POST.get('address')
        school.contact_number = request.POST.get('contact_number')
        limit = request.POST.get('announcement_limit')
        if limit: school.monthly_announcement_limit = int(limit)
        school.save()
        return redirect('super_school_list')
    return render(request, 'superuser/school_form.html', {'school': school, 'is_edit': True})

@csrf_exempt
def api_send_school_credentials(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER': is_super = True
    if not is_super: return JsonResponse({'error': 'Unauthorized'}, status=403)
    
    school = get_object_or_404(School, pk=pk)
    if not school.contact_number:
        return JsonResponse({'error': 'No contact number defined for this school.'}, status=400)
    
    sms_settings = GlobalSMSSettings.objects.filter(is_active=True).first()
    if not sms_settings or not sms_settings.api_key:
        return JsonResponse({'error': 'Global SMS Settings not configured or inactive.'}, status=400)
        
    login_url = sms_settings.site_domain if sms_settings.site_domain else request.build_absolute_uri('/')
    message = f"Dear {school.name}, login credentials: URL: {login_url}, Code: {school.code}, User: {school.admin_username}, Pass: {school.password}"
    
    if not school.contact_number:
        return JsonResponse({'error': 'Institution contact number is missing.'}, status=400)
        
    phone = re.sub(r'\D', '', str(school.contact_number))
    if phone.startswith('01') and len(phone) == 11: phone = '88' + phone
    
    try:
        api_url, api_key, senderid = _get_active_sms_config()
        if not api_url:
            return JsonResponse({'error': 'No active SMS Gateway found.'}, status=400)

        resp = requests.post(api_url, 
                 data={'api_key': api_key, 'senderid': senderid, 'number': phone, 'message': message},
                 timeout=12)
        
        SMSLog.objects.create(
            school=school,
            recipient=phone,
            message=message,
            status='SENT' if resp.status_code == 200 else 'FAILED'
        )
        return JsonResponse({'status': 'ok'})
    except Exception as e:
        return JsonResponse({'error': str(e)}, status=500)

def super_school_delete(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
        
    school = get_object_or_404(School, pk=pk)
    school.delete()
    return redirect('super_school_list')

def super_school_export(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER': is_super = True
    if not is_super: return redirect('school_dashboard')
    
    school = get_object_or_404(School, pk=pk)
    exporter = InstitutionExporter(school.id)
    zip_buffer = exporter.export()
    
    response = HttpResponse(zip_buffer.getvalue(), content_type='application/zip')
    filename = f"{school.code}_export_{timezone.now().strftime('%Y%m%d')}.zip"
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    return response

@csrf_exempt
def api_update_school_password(request):
    if not request.user.is_authenticated: return JsonResponse({'error': 'Unauthorized'}, status=403)
    profile = getattr(request.user, 'profile', None)
    if not profile or profile.role != 'SCHOOL_ADMIN': return JsonResponse({'error': 'Unauthorized'}, status=403)
    
    school = profile.school
    new_password = request.POST.get('password')
    if not new_password or len(new_password) < 4:
        return JsonResponse({'error': 'Too short'}, status=400)
    
    school.password = new_password
    school.save()
    
    request.user.set_password(new_password)
    request.user.save()
    update_session_auth_hash(request, request.user)
    return JsonResponse({'status': 'ok'})

@csrf_exempt
def api_toggle_sms_gateway(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER': is_super = True
    if not is_super: return JsonResponse({'error': 'Unauthorized'}, status=403)
    
    gw = get_object_or_404(SMSGateway, pk=pk)
    gw.is_active = not gw.is_active
    gw.save()
    return JsonResponse({'status': 'ok', 'is_active': gw.is_active})

def super_sms_gateway_list(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER': is_super = True
    if not is_super: return redirect('school_dashboard')
    
    gateways = SMSGateway.objects.all().order_by('-created_at')
    return render(request, 'superuser/sms_gateway_list.html', {'gateways': gateways})

def super_sms_gateway_add(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER': is_super = True
    if not is_super: return redirect('school_dashboard')
    
    if request.method == 'POST':
        name = request.POST.get('name')
        api_url = request.POST.get('api_url')
        api_key = request.POST.get('api_key')
        sender_id = request.POST.get('sender_id')
        SMSGateway.objects.create(name=name, api_url=api_url, api_key=api_key, sender_id=sender_id)
        return redirect('super_sms_gateway_list')
    return render(request, 'superuser/sms_gateway_form.html')

def super_sms_gateway_delete(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER': is_super = True
    if not is_super: return redirect('school_dashboard')
    
    get_object_or_404(SMSGateway, pk=pk).delete()
    return redirect('super_sms_gateway_list')

def super_school_import(request):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER': is_super = True
    if not is_super: return redirect('school_dashboard')
    
    if request.method == 'POST' and request.FILES.get('export_file'):
        file = request.FILES['export_file']
        file_content = file.read()
        
        try:
            importer = InstitutionImporter(file_content)
            
            if request.POST.get('action') == 'preview':
                info = importer.get_preview_info()
                return JsonResponse(info)
                
            success, result = importer.import_data()
            if success:
                messages.success(request, f"Institution {result.name} imported successfully!")
                return redirect('super_school_list')
            else:
                messages.error(request, f"Import failed: {result}")
                return redirect('super_school_list')
        except Exception as e:
            if request.POST.get('action') == 'preview':
                return JsonResponse({'error': str(e)}, status=400)
            messages.error(request, f"Invalid file: {e}")
            return redirect('super_school_list')
            
    return render(request, 'superuser/school_import.html')

def super_device_assign(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
        
    school = get_object_or_404(School, pk=pk)
    if request.method == 'POST':
        sn = request.POST.get('serial_number')
        name = request.POST.get('name', 'New Device')
        device, _ = Device.objects.get_or_create(serial_number=sn)
        device.school = school
        device.name = name
        device.is_authorized = True
        device.save()
        messages.success(request, f"Device {sn} assigned to {school.name}")
        return redirect('super_school_list')
    
    devices = Device.objects.filter(school=school)
    return render(request, 'superuser/device_assign.html', {'school': school, 'devices': devices})

def super_device_remove(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
        
    if not is_super:
        return redirect('school_dashboard')
        
    device = get_object_or_404(Device, pk=pk)
    school_name = device.school.name if device.school else "Unknown"
    device.school = None
    device.assigned_class = None
    device.is_authorized = False
    device.save()
    messages.info(request, f"Device {device.serial_number} removed from {school_name}")
    return redirect('super_school_list')

# --- School Admin Dashboards ---
@login_required(login_url='/login/')
def school_dashboard(request):
    profile = getattr(request.user, 'profile', None)
    if not profile or profile.role != 'SCHOOL_ADMIN':
        return redirect('home')
    
    school = profile.school
    logs = AttendanceLog.objects.filter(school=school).select_related('student', 'student__student_class').exclude(device__is_terminated=True).order_by('-timestamp')[:100]
    
    # Announcements logic
    pending_announcements = Announcement.objects.filter(
        Q(is_bulk=True) | Q(schools=school)
    ).exclude(seen_by=school).order_by('created_at')
    
    # Respect Time Test mode
    current_now = get_school_now(school)
    
    is_expired = False
    if school.subscription_end_date and school.subscription_end_date < current_now.date():
        is_expired = True

    context = {
        'school': school,
        'logs': logs,
        'student_count': school.students.count() if school else 0,
        'device_count': school.devices.count() if school else 0,
        'is_time_test_active': school.is_time_test_active,
        'current_system_time': current_now,
        'pending_announcements': pending_announcements,
        'is_subscription_expired': is_expired,
    }
    return render(request, 'school/dashboard.html', context)

def school_toggle_test_mode(request):
    profile = request.user.profile
    school = profile.school
    if not school.is_time_test_enabled:
        return HttpResponse("Feature not enabled", status=403)
    
    school.is_time_test_active = not school.is_time_test_active
    if school.is_time_test_active and not school.test_timestamp:
        school.test_timestamp = timezone.now()
        school.test_set_at = timezone.now()
    school.save()
    return redirect('school_dashboard')

@csrf_exempt
def school_update_test_time(request):
    profile = request.user.profile
    school = profile.school
    if not school.is_time_test_active:
         return JsonResponse({'status': 'error', 'msg': 'Test mode inactive'}, status=400)
    
    if request.method == 'POST':
        ts_str = request.POST.get('timestamp')
        ss = request.POST.get('seconds', '0')
        if ts_str:
            try:
                # Format: YYYY-MM-DDTHH:MM
                ts = datetime.strptime(ts_str, '%Y-%m-%dT%H:%M')
                ts = ts.replace(second=int(ss))
                ts = timezone.make_aware(ts)
                school.test_timestamp = ts
                school.test_set_at = timezone.now()
                school.save()
                return JsonResponse({
                    'status': 'ok', 
                    'time': ts.strftime('%Y-%m-%d %H:%M:%S'),
                    'full_iso': ts.isoformat()
                })
            except Exception as e: return JsonResponse({'status': 'error', 'msg': str(e)})
    return JsonResponse({'status': 'error', 'msg': 'Invalid request'})

@csrf_exempt
def api_sync_location(request):
    profile = request.user.profile
    school = profile.school
    
    if request.method == 'POST':
        loc_name = request.POST.get('location')
        tz_name = request.POST.get('timezone')
        
        if loc_name:
            school.location_name = loc_name
        if tz_name:
            school.timezone_name = tz_name
        school.save()
        return JsonResponse({
            'status': 'ok', 
            'location': school.location_name, 
            'timezone': school.timezone_name
        })
    return JsonResponse({'status': 'error', 'msg': 'Invalid request'})

def school_announcement(request):
    profile = request.user.profile
    school = profile.school
    
    # Check/Reset Quota
    today = date.today()
    if not school.last_limit_reset or school.last_limit_reset.month != today.month or school.last_limit_reset.year != today.year:
        school.announcements_sent_this_month = 0
        school.exams_sent_this_month = 0
        school.meetings_sent_this_month = 0
        school.fees_sent_this_month = 0
        school.last_limit_reset = today
        school.save()
    
    can_send = school.announcements_sent_this_month < school.monthly_announcement_limit
    
    if not school.is_sms_enabled or not school.is_announcement_sms_enabled:
        can_send = False
    
    if request.method == 'POST':
        if not can_send:
            if not school.is_sms_enabled:
                messages.error(request, "SMS function is currently DISABLED for this institution. Enable it in settings first.")
            else:
                messages.error(request, "Monthly announcement limit reached.")
            return redirect('school_announcement')
            
        message = request.POST.get('message', '').strip()
        class_ids = request.POST.getlist('classes')
        
        # Word count check
        words = message.split()
        if len(words) > 30:
            messages.error(request, "Message cannot exceed 30 words.")
            return redirect('school_announcement')
            
        if not class_ids:
            messages.error(request, "Select at least one class.")
            return redirect('school_announcement')
            
        # Send SMS
        import re, threading, requests
        from .models import GlobalSMSSettings, SMSLog
        
        settings = GlobalSMSSettings.get_settings()
        students = Student.objects.filter(student_class_id__in=class_ids, school=school)
        
        sent_count = 0
        if settings.is_active and settings.api_key and students.exists():
            # Broadcast Limit Check
            if school.monthly_announcement_limit_per_student > 0 and school.announcements_sent_this_month >= school.monthly_announcement_limit_per_student:
                messages.error(request, f"Monthly broadcast limit reached for Announcements ({school.monthly_announcement_limit_per_student}).")
                return redirect('school_announcement')

            api_url, api_key, sender_id = _get_active_sms_config()
            
            def _send_bulk(phone_list, msg, url, ak, sid, sch):
                from django.db import close_old_connections
                close_old_connections()
                
                # We need student objects to check per-student limits
                # phone_list is a list of raw phone numbers from students queryset
                # To be accurate, we'll re-fetch students to check limits
                
                for s in students:
                    if not s.contact_number: continue
                    
                    # Check Limit
                    if not check_and_increment_sms_limit(s, 'ANNOUNCEMENT'):
                        continue
                        
                    p = re.sub(r'\D', '', str(s.contact_number))
                    if p.startswith('01') and len(p) == 11: p = '88' + p
                    elif p.startswith('1') and len(p) == 10: p = '880' + p
                    try:
                        resp = requests.post(url,
                                 data={'api_key': ak, 'senderid': sid, 'number': p, 'message': msg},
                                 timeout=12)
                        
                        SMSLog.objects.create(
                            school=sch,
                            recipient=p,
                            message=msg,
                            status='SENT' if resp.status_code == 200 else 'FAILED'
                        )
                    except Exception as e:
                        print(f"SMS error for {p}: {e}")
            
            threading.Thread(target=_send_bulk, args=(None, message, api_url, api_key, sender_id, school), daemon=True).start()
            # Note: _send_bulk now uses 'students' from outer scope
            sent_count = students.count() 
        
        school.announcements_sent_this_month += 1
        school.save()
        
        msg_text = f"Announcement queued for {sent_count} contacts." if sent_count else "Announcement saved (no SMS API key set)."
        messages.success(request, msg_text)
        return redirect('school_announcement')
        
    classes = StudentClass.objects.filter(school=school).order_by('name')
    return render(request, 'school/announcement.html', {
        'school': school,
        'classes': classes,
        'remaining': school.monthly_announcement_limit - school.announcements_sent_this_month
    })

def school_device_list(request):
    profile = request.user.profile
    school = profile.school
    devices = Device.objects.filter(school=school).order_by('name')
    classes = StudentClass.objects.filter(school=school).order_by('name')
    
    if request.method == 'POST':
        did = request.POST.get('device_id')
        cid = request.POST.get('class_id')
        name = request.POST.get('name')
        
        device = get_object_or_404(Device, pk=did, school=school)
        device.name = name
        if cid:
            device.assigned_class = get_object_or_404(StudentClass, pk=cid, school=school)
        else:
            device.assigned_class = None
        device.save()
        messages.success(request, f"Device {device.serial_number} settings updated.")
        return redirect('school_device_list')

    return render(request, 'school/device_list.html', {'devices': devices, 'classes': classes})

def school_level_up(request):
    profile = request.user.profile
    school = profile.school
    
    # Handle AJAX Requests
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        # 1. Level Up Action
        if request.method == 'POST':
            sid = request.POST.get('student_id')
            cid = request.POST.get('new_class_id')
            new_roll = request.POST.get('new_roll')
            
            student = get_object_or_404(Student, pk=sid, school=school)
            new_class = get_object_or_404(StudentClass, pk=cid, school=school)
            
            # Check for roll conflict in destination class
            if Student.objects.filter(school=school, student_class=new_class, roll=new_roll).exclude(pk=sid).exists():
                return JsonResponse({'status': 'error', 'msg': 'This roll number already exists.'}, status=400)
                
            old_class = student.student_class
            
            if old_class and old_class != new_class:
                from .models import PromotionRecord
                # Ensure PromotionRecord exists for the OLD class
                PromotionRecord.objects.get_or_create(
                    student=student,
                    student_class=old_class,
                    defaults={'roll': student.roll}
                )
                # Stamp orphaned fees/results/logs to old class before switching
                StudentFee.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)
                ResultRecord.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)
                AttendanceLog.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)
                FeeTransaction.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)
                
                # Create fresh fee records for the new class based on FeeType configuration
                new_class_fees = FeeType.objects.filter(school=school, applicable_classes=new_class)
                for ft in new_class_fees:
                    StudentFee.objects.get_or_create(
                        student=student,
                        fee_type=ft,
                        student_class=new_class,
                        defaults={'amount': ft.default_amount, 'paid_amount': 0, 'is_paid': False}
                    )
            
            student.student_class = new_class
            student.roll = new_roll

            student.save()
            return JsonResponse({'status': 'ok', 'msg': f'{student.name} promoted to {new_class.name}', 'last_roll': new_roll})
            
        # 2. Fetch Students for a Class
        class_id = request.GET.get('class_id')
        if class_id:
            students = Student.objects.filter(student_class_id=class_id, school=school).order_by('name')
            data = []
            for s in students:
                data.append({
                    'id': s.id,
                    'name': s.name,
                    'roll': s.roll,
                    'current_class': s.student_class.name if s.student_class else 'N/A',
                    'photo': s.photo.url if s.photo else None
                })
            return JsonResponse({'students': data})

    # Use a subquery or aggregation to get max roll for each class
    from django.db.models import Max
    classes = StudentClass.objects.filter(school=school).order_by('name')
    for c in classes:
        # roll is an IntegerField in Student model
        res = Student.objects.filter(student_class=c, school=school).aggregate(Max('roll'))
        c.max_roll = res['roll__max'] or 0
        
    return render(request, 'school/level_up.html', {'classes': classes})

@csrf_exempt
def api_check_roll_availability(request):
    profile = request.user.profile
    school = profile.school
    
    class_id = request.GET.get('class_id')
    roll = request.GET.get('roll')
    student_id = request.GET.get('student_id') # To exclude self

    if not class_id or not roll:
        return JsonResponse({'available': True})
    
    exists = Student.objects.filter(school=school, student_class_id=class_id, roll=roll).exclude(pk=student_id).exists()
    return JsonResponse({'available': not exists})

def school_time_test(request):
    profile = request.user.profile
    if not profile.school.is_time_test_enabled:
        return HttpResponse("Feature not enabled for this institution.", status=403)
        
    classes = StudentClass.objects.filter(school=profile.school).order_by('name')
    result = None
    
    if request.method == 'POST':
        class_id = request.POST.get('class_id')
        p_time_str = request.POST.get('punch_time')
        has_earlier = 'has_earlier' in request.POST
        
        target_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
        schedule = getattr(target_class, 'schedule', None)
        
        if not schedule:
            result = {'error': f"No attendance schedule defined for {target_class.name}. Please set timings first."}
        else:
            try:
                p_time = datetime.strptime(p_time_str, '%H:%M').time()
                status = 'NORMAL'
                check_type = 'IN' if not has_earlier else 'OUT'
                
                l_start = schedule.lunch_start or time(13, 0)
                l_end = schedule.lunch_end or time(14, 0)
                
                if not has_earlier:
                    if p_time < schedule.punch_in_start:
                        status = 'EARLY_IN'
                    elif schedule.punch_in_start <= p_time <= schedule.punch_in_end:
                        status = 'PRESENT_IN'
                    else:
                        status = 'LATE'
                else:
                    # Simulation: assume they arrived well before lunch
                    if p_time < l_start: 
                         status = 'FALSE_PUNCH'
                    elif schedule.checkout_start <= p_time <= schedule.checkout_end:
                         status = 'PRESENT_OUT'
                    elif p_time > schedule.checkout_end:
                         status = 'HOLD'
                    elif l_start <= p_time <= l_end:
                         status = 'LUNCH_BREAK'
                    else:
                         status = 'EARLY_OUT'
                         
                result = {
                    'class': target_class.name,
                    'time': p_time.strftime('%I:%M %p'),
                    'type': check_type,
                    'status': status,
                    'schedule_info': {
                        'in': f"{schedule.punch_in_start.strftime('%I:%M %p')} - {schedule.punch_in_end.strftime('%I:%M %p')}",
                        'out': f"{schedule.checkout_start.strftime('%I:%M %p')} - {schedule.checkout_end.strftime('%I:%M %p')}",
                        'lunch': f"{l_start.strftime('%I:%M %p')} - {l_end.strftime('%I:%M %p')}"
                    }
                }
            except Exception as e:
                result = {'error': f"Invalid time format or error: {str(e)}"}
            
    return render(request, 'school/time_test.html', {'classes': classes, 'result': result})

def attendance_log(request):
    profile = request.user.profile
    school = profile.school
    
    # Calculate System Now (Real or Emulated)
    current_now = timezone.now()
    if school.is_time_test_active and school.test_timestamp and school.test_set_at:
        delta = current_now - school.test_set_at
        current_now = school.test_timestamp + delta
    
    # Filters
    date_str = request.GET.get('date') or current_now.date().strftime('%Y-%m-%d')
    class_id = request.GET.get('class_id')
    if not class_id or class_id == 'None' or class_id == 'undefined':
        class_id = None
    
    view_type = request.GET.get('type', 'authorized') 
    
    try:
        filter_date = datetime.strptime(date_str, '%Y-%m-%d').date()
    except:
        filter_date = timezone.localtime().date()

    # Define Local Day Range to avoid UTC shifting
    from datetime import time as d_time
    day_start = timezone.make_aware(datetime.combine(filter_date, d_time.min))
    day_end = timezone.make_aware(datetime.combine(filter_date, d_time.max))

    logs = AttendanceLog.objects.filter(
        school=school,
        timestamp__range=(day_start, day_end)
    ).select_related('student', 'student__student_class').exclude(device__is_terminated=True).order_by('-timestamp')
    
    last_id = request.GET.get('last_id')
    if last_id and last_id != '0' and last_id != 'undefined':
        # Return newest first even for background polling
        logs = logs.filter(id__gt=last_id)

    # Cache schedules early for badge and log processing
    schedules = {s.student_class_id: s for s in school.schedules.all()}
    processed_logs = []

    if view_type == 'false':
        logs = logs.filter(models.Q(student__isnull=True) | models.Q(status='FALSE_PUNCH'))
    elif view_type == 'hold':
        logs = logs.filter(status__in=['HOLD', 'LATE_OUT'])
    elif view_type == 'lunch_break':
        logs = logs.filter(status='LUNCH_BREAK')
    else:
        logs = logs.filter(student__isnull=False).exclude(status__in=['FALSE_PUNCH', 'HOLD', 'LATE_OUT'])
        if class_id:
            logs = logs.filter(student__student_class_id=class_id)
    
    # Process remaining logs
    for l in logs:
        status = l.status or 'N/A'
        local_ts = timezone.localtime(l.timestamp)
        if l.student:
            processed_logs.append({
                'id': l.id,
                'pin': l.student.pin,
                'roll': l.student.roll,
                'name': l.student.name,
                'photo_url': l.student.photo.url if l.student.photo else None,
                'class_name': l.student.student_class.name if l.student.student_class else 'N/A',
                'time': local_ts.strftime('%I:%M:%S %p'),
                'status': status,
                'device_name': l.device.name if l.device else 'Unknown',
                'is_false_punch': (status == 'FALSE_PUNCH')
            })
        else:
            processed_logs.append({
                'id': l.id,
                'pin': l.unidentified_pin,
                'roll': '-',
                'name': 'Unidentified Punch',
                'photo_url': None,
                'class_name': 'N/A',
                'time': local_ts.strftime('%I:%M:%S %p'),
                'status': 'UNIDENTIFIED',
                'device_name': l.device.name if l.device else 'Unknown',
                'is_false_punch': True
            })

    # Calculate badge counts (Filtered by class if selected)
    badge_base = AttendanceLog.objects.filter(school=school, timestamp__range=(day_start, day_end))
    if class_id:
        badge_base = badge_base.filter(student__student_class_id=class_id)

    hold_cnt = badge_base.filter(status__in=['HOLD', 'LATE_OUT']).count()
    auth_cnt = badge_base.filter(student__isnull=False).exclude(status__in=['FALSE_PUNCH', 'HOLD', 'LATE_OUT']).count()
    false_cnt = badge_base.filter(models.Q(student__isnull=True) | models.Q(status='FALSE_PUNCH')).count()

    lb_count = badge_base.filter(status='LUNCH_BREAK').count()

    if request.GET.get('format') == 'json':
        return JsonResponse({
            'logs': processed_logs, 
            'hold_count': hold_cnt,
            'auth_count': auth_cnt,
            'false_count': false_cnt,
            'lunch_break_count': lb_count
        })

    classes = school.classes.all().order_by('name')
    return render(request, 'school/attendance_log.html', {
        'logs': processed_logs,
        'classes': classes,
        'selected_date': date_str,
        'selected_class_id': class_id,
        'view_type': view_type,
        'hold_count': hold_cnt,
        'auth_count': auth_cnt,
        'false_count': false_cnt,
        'lunch_break_count': lb_count,
    })


# --- Student Management ---
def student_list(request):
    profile = request.user.profile
    classes = StudentClass.objects.filter(school=profile.school).order_by('name')
    
    class_id = request.GET.get('class_id')
    student_id = request.GET.get('student_id')
    query = request.GET.get('q')
    
    selected_class = None
    selected_student = None
    search_results = None
    students = []
    
    if query and not class_id and not student_id:
        search_results = Student.objects.filter(school=profile.school).filter(
            models.Q(name__icontains=query) | models.Q(pin__icontains=query)
        ).select_related('student_class').order_by('name')

    if class_id:
        selected_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
        students = Student.objects.filter(student_class=selected_class, school=profile.school)
        
        if query:
            students = students.filter(
                models.Q(name__icontains=query) | 
                models.Q(pin__icontains=query) | 
                models.Q(contact_number__icontains=query)
            )
        students = students.order_by('roll', 'name')
        
    attendance_calendar = []
    prev_month_params = None
    next_month_params = None
    current_month_name = ""

    if student_id:
        selected_student = get_object_or_404(Student, pk=student_id, school=profile.school)
        # Fetch related data for detailed view
        selected_student.assigned_fees = StudentFee.objects.filter(student=selected_student).select_related('fee_type')
        results = ResultRecord.objects.filter(student=selected_student).select_related('exam', 'subject').order_by('-exam__year', 'exam__name')
        
        # Group results by exam for display
        results_grouped = {}
        for r in results:
            exam_key = f"{r.exam.name} ({r.exam.year})"
            if exam_key not in results_grouped:
                results_grouped[exam_key] = []
            results_grouped[exam_key].append(r)
        selected_student.results_grouped = results_grouped

        now = timezone.now()
        month = int(request.GET.get('month', now.month))
        year = int(request.GET.get('year', now.year))
        
        cal_data = get_attendance_calendar_data(selected_student, month, year)
        attendance_calendar = cal_data
        current_month_name = cal_data['month_name']
        prev_month_params = cal_data['prev_params']
        next_month_params = cal_data['next_params']

    return render(request, 'school/student_list.html', {
        'classes': classes,
        'selected_class': selected_class,
        'students': students,
        'selected_student': selected_student,
        'attendance_calendar': attendance_calendar,
        'prev_month_params': prev_month_params,
        'next_month_params': next_month_params,
        'current_month_name': current_month_name,
        'query': query,
        'search_results': search_results,
        'today': timezone.now().date(),
        'school': profile.school
    })

def get_attendance_calendar_data(student, month, year):
    import calendar
    from datetime import date, timedelta
    
    start_of_month = date(year, month, 1)
    _, num_days = calendar.monthrange(year, month)
    end_of_month = date(year, month, num_days)
    month_name = start_of_month.strftime('%B %Y')

    if month == 1: pm, py = 12, year - 1
    else: pm, py = month - 1, year
        
    if month == 12: nm, ny = 1, year + 1
    else: nm, ny = month + 1, year

    # Build local-date range for DB query (avoids UTC shifting at DST boundaries)
    from datetime import datetime as _dt, time as _time
    local_day_start = timezone.make_aware(_dt.combine(start_of_month, _time.min))
    local_day_end   = timezone.make_aware(_dt.combine(end_of_month,   _time.max))

    logs = AttendanceLog.objects.filter(
        student=student,
        timestamp__gte=local_day_start,
        timestamp__lte=local_day_end,
    ).order_by('timestamp')

    schedule = getattr(student.student_class, 'schedule', None)
    attendance_data = {}
    for log in logs:
        # CRITICAL FIX: Convert UTC timestamp to LOCAL time before extracting day
        local_ts = timezone.localtime(log.timestamp)
        day = local_ts.date().day
        
        # Determine if this log is "Valid Presence" (Authorized/Approved)
        # HOLD, LATE_OUT, FALSE_PUNCH, UNIDENTIFIED do not count toward daily attendance until approved
        authorized_statuses = [
            'EARLY_IN', 'PRESENT_IN', 'LATE', 
            'EARLY_OUT', 'PRESENT_OUT', 'LATE_OUT_APPROVED', 
            'LUNCH_BREAK', 'LATE_OUT_APPROVED'
        ]
        is_primary = log.status in authorized_statuses

        if day not in attendance_data:
            if not is_primary:
                continue # Skip initialization for pending/rejected logs
                
            status = 'PRESENT'
            if log.status == 'LATE': status = 'LATE'
            
            attendance_data[day] = {
                'status': status,
                'time': local_ts.strftime('%I:%M %p'),
                'punch_in': local_ts.strftime('%I:%M %p'),
                'lunch_break': None,
                'punch_out': None,
                'logs': []
            }
        else:
            # If day already created by a primary log, update properties if needed
            if log.status == 'LATE':
                attendance_data[day]['status'] = 'LATE'
            
            if log.status == 'LUNCH_BREAK':
                attendance_data[day]['lunch_break'] = local_ts.strftime('%I:%M %p')
            
            # Only set punch_out for authorized checkout statuses
            elif log.status in ['PRESENT_OUT', 'EARLY_OUT', 'LATE_OUT_APPROVED']:
                attendance_data[day]['punch_out'] = local_ts.strftime('%I:%M %p')
        
        # Always add the log to the detail list IF the day exists (was authorized at some point)
        if day in attendance_data:
            attendance_data[day]['logs'].append({
                'time': local_ts.strftime('%I:%M %p'),
                'type': log.check_type,
                'status': log.status
            })

    leaves = StudentLeave.objects.filter(student=student, start_date__lte=end_of_month, end_date__gte=start_of_month, is_deleted=False)
    for leave in leaves:
        curr = max(leave.start_date, start_of_month)
        limit = min(leave.end_date, end_of_month)
        while curr <= limit:
            d = curr.day
            if d not in attendance_data:
                attendance_data[d] = {'status': 'LEAVE', 'leave_type': leave.leave_type, 'reason': leave.reason}
            curr += timedelta(days=1)
    
    cal = []
    summary = {'present': 0, 'late': 0, 'leave': 0, 'absent': 0}
    today_dt = timezone.now().date()
    
    # Fetch holidays for this period
    holidays = {h.date: h for h in Holiday.objects.filter(school=student.school, date__gte=start_of_month, date__lte=end_of_month)}
    
    # ADD PADDING FOR THE FIRST WEEK
    # Python weekday: Mon=0, Sun=6. For Sun-start grid: Sun=0, Mon=1...
    # Grid offset = (weekday + 1) % 7
    first_weekday = (start_of_month.weekday() + 1) % 7
    for _ in range(first_weekday):
        cal.append({'day': '', 'data': {'status': 'EMPTY'}})

    for d in range(1, num_days + 1):
        curr_date = date(year, month, d)
        data = attendance_data.get(d)
        
        # Determine holiday status
        day_holiday = holidays.get(curr_date)
        is_holiday = False
        holiday_note = ""
        if day_holiday:
            is_holiday = (day_holiday.day_type == 'HOLIDAY')
            holiday_note = day_holiday.note
        else:
            is_holiday = (curr_date.weekday() == 4) # Friday
            if is_holiday: holiday_note = "Weekly Holiday"

        # PRIORITY 1: LEAVE / PRESENT / LATE (Student Action)
        if data:
            st = data.get('status')
            if st == 'PRESENT': summary['present'] += 1
            elif st == 'LATE':
                summary['late'] += 1
                summary['present'] += 1
            elif st == 'LEAVE': summary['leave'] += 1
            
            # If it's a holiday but they have an action, we still show the holiday note if available
            if is_holiday:
                data['is_holiday'] = True
                data['holiday_note'] = holiday_note
        else:
            # PRIORITY 2: SYSTEM HOLIDAYS (Only if no log/leave)
            adm_date = student.admission_date if isinstance(student.admission_date, date) else student.admission_date.date()
            if curr_date < adm_date:
                data = {'status': 'NOT_ENROLLED'}
            elif is_holiday:
                data = {'status': 'HOLIDAY', 'note': holiday_note}
            elif curr_date < today_dt:
                data = {'status': 'ABSENT'}
                summary['absent'] += 1
            elif curr_date == today_dt:
                data = {'status': 'PENDING'}
            else:
                data = {'status': 'FUTURE'}

        cal.append({'day': d, 'data': data})

        
    return {
        'calendar': cal,
        'month_name': month_name,
        'prev_params': {'month': pm, 'year': py},
        'next_params': {'month': nm, 'year': ny},
        'summary': summary
    }

def student_session_record(request, pk):
    profile = request.user.profile
    student = get_object_or_404(Student, pk=pk, school=profile.school)
    
    # 0. Lazy Backfill logic
    if student.student_class:
        PromotionRecord.objects.get_or_create(
            student=student, 
            student_class=student.student_class,
            defaults={'roll': student.roll}
        )
        # Only backfill orphaned fees/results if no prior promotion exists.
        # If the student has been promoted before, orphaned records belong to
        # the FIRST class, not the current one.  Level-up already stamps them.
        promo_count = PromotionRecord.objects.filter(student=student).count()
        if promo_count <= 1:
            StudentFee.objects.filter(student=student, student_class__isnull=True).update(student_class=student.student_class)
            ResultRecord.objects.filter(student=student, student_class__isnull=True).update(student_class=student.student_class)
            AttendanceLog.objects.filter(student=student, student_class__isnull=True).update(student_class=student.student_class)
            FeeTransaction.objects.filter(student=student, student_class__isnull=True).update(student_class=student.student_class)
        else:
            # For promoted students, assign orphans to the earliest class
            first_promo = PromotionRecord.objects.filter(student=student).order_by('promoted_at').first()
            if first_promo:
                StudentFee.objects.filter(student=student, student_class__isnull=True).update(student_class=first_promo.student_class)
                ResultRecord.objects.filter(student=student, student_class__isnull=True).update(student_class=first_promo.student_class)
                AttendanceLog.objects.filter(student=student, student_class__isnull=True).update(student_class=first_promo.student_class)
                FeeTransaction.objects.filter(student=student, student_class__isnull=True).update(student_class=first_promo.student_class)

    # 1. Identify all enrollment cards via PromotionRecord
    # We also keep the legacy logic for results/fees just in case there are orphaned ones
    res_classes = ResultRecord.objects.filter(student=student).values_list('student_class', flat=True).distinct()
    fee_classes = StudentFee.objects.filter(student=student).values_list('student_class', flat=True).distinct()
    prom_classes = PromotionRecord.objects.filter(student=student).values_list('student_class', flat=True).distinct()
    attn_classes = AttendanceLog.objects.filter(student=student).values_list('student_class', flat=True).distinct()
    txn_classes = FeeTransaction.objects.filter(student=student).values_list('student_class', flat=True).distinct()
    
    all_class_ids = set(cid for cid in list(res_classes) + list(fee_classes) + list(prom_classes) + list(attn_classes) + list(txn_classes) if cid is not None)
    
    historical_classes = StudentClass.objects.filter(id__in=all_class_ids).order_by('-created_at')
    
    # 2. Filtering Logic
    selected_class_id = request.GET.get('class_id')
    if not selected_class_id and student.student_class:
        selected_class_id = str(student.student_class.id)
    
    selected_class = None
    results_grouped = {}
    avg_gpa = 0
    total_fee = 0
    total_paid = 0
    total_due = 0
    
    if selected_class_id:
        selected_class = get_object_or_404(StudentClass, pk=selected_class_id, school=profile.school)
        
        # Academic results for THIS class
        results = ResultRecord.objects.filter(student=student, student_class=selected_class).select_related('exam', 'subject').order_by('-exam__year', 'exam__name')
        total_gpa_sum = 0
        record_count = 0
        for r in results:
            exam_key = f"{r.exam.name} ({r.exam.year})"
            if exam_key not in results_grouped: results_grouped[exam_key] = []
            results_grouped[exam_key].append(r)
            total_gpa_sum += float(r.gpa)
            record_count += 1
        avg_gpa = round(total_gpa_sum / record_count, 2) if record_count > 0 else 0

        # Financials for THIS class
        fees = StudentFee.objects.filter(student=student, student_class=selected_class).select_related('fee_type')
        total_fee = sum(f.amount for f in fees)
        total_paid = sum(f.paid_amount for f in fees)
        total_due = total_fee - total_paid

    # 3. Attendance Summary
    q_args = {'student': student}
    
    promotions = list(PromotionRecord.objects.filter(student=student).order_by('promoted_at'))
    prom_class_ids = set(p.student_class_id for p in promotions)
    historical_class_ids = set(c.id for c in historical_classes)
    has_legacy = len(historical_class_ids - prom_class_ids) > 0
    
    promo_index = -1
    if selected_class_id:
        for i in range(len(promotions) - 1, -1, -1):
            if str(promotions[i].student_class_id) == str(selected_class_id):
                promo_index = i
                break

    if promo_index != -1:
        if promo_index > 0 or has_legacy:
            q_args['timestamp__gte'] = promotions[promo_index].promoted_at
        if promo_index + 1 < len(promotions):
            q_args['timestamp__lt'] = promotions[promo_index+1].promoted_at
    else:
        if promotions:
            q_args['timestamp__lt'] = promotions[0].promoted_at

    logs = AttendanceLog.objects.filter(**q_args).order_by('timestamp')
    present_days = logs.filter(status__in=['PRESENT_IN', 'EARLY_IN', 'LATE', 'PRESENT_OUT']).values('timestamp__date').distinct().count()
    late_days = logs.filter(status='LATE').values('timestamp__date').distinct().count()
    total_logs = logs.count()

    return render(request, 'school/session_record.html', {
        'student': student,
        'historical_classes': historical_classes,
        'selected_class': selected_class,
        'results_grouped': results_grouped,
        'avg_gpa': avg_gpa,
        'total_fee': total_fee,
        'total_paid': total_paid,
        'total_due': total_due,
        'present_days': present_days,
        'late_days': late_days,
        'total_logs': total_logs
    })

def attendance_api(request, pk):
    profile = request.user.profile
    student = get_object_or_404(Student, pk=pk, school=profile.school)
    month = int(request.GET.get('month'))
    year = int(request.GET.get('year'))
    return JsonResponse(get_attendance_calendar_data(student, month, year))

def attendance_manual_update(request, pk):
    if request.method != 'POST':
        return JsonResponse({'error': 'Invalid request'}, status=400)
    
    profile = request.user.profile
    student = get_object_or_404(Student, pk=pk, school=profile.school)
    
    date_str = request.POST.get('date')
    status = request.POST.get('status')
    punch_in = request.POST.get('punch_in')
    punch_out = request.POST.get('punch_out')
    
    from datetime import datetime
    target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
    
    # 1. Clear existing biometric & manual logs for this specific date
    AttendanceLog.objects.filter(student=student, timestamp__date=target_date).delete()

    if status == 'ABSENT':
        return JsonResponse({'success': True, 'msg': f'Attendance cleared for {target_date}'})

    # 2. Record CHECK-IN
    t_in = None
    if punch_in:
        try: t_in = datetime.strptime(punch_in, '%H:%M').time()
        except: pass
    
    if not t_in:
        # Default to 9:00 if no time provided for Present/Late
        t_in = datetime.strptime("09:00", "%H:%M").time()

    AttendanceLog.objects.create(
        student=student,
        school=student.school,
        timestamp=timezone.make_aware(datetime.combine(target_date, t_in)),
        check_type='IN',
        status=status, # e.g. PRESENT, LATE
        student_class=student.student_class
    )

    # 3. Record CHECK-OUT (Optional)
    if punch_out:
        try:
            t_out = datetime.strptime(punch_out, '%H:%M').time()
            AttendanceLog.objects.create(
                student=student,
                school=student.school,
                timestamp=timezone.make_aware(datetime.combine(target_date, t_out)),
                check_type='OUT',
                status='PRESENT',
                student_class=student.student_class
            )
        except: pass
    
    return JsonResponse({'success': True, 'msg': f'Audit history updated for {target_date}'})

def student_add(request):
    profile = request.user.profile
    classes = StudentClass.objects.filter(school=profile.school)
    fee_types = FeeType.objects.filter(school=profile.school)
    
    if request.method == 'POST':
        pin = request.POST.get('pin')
        name = request.POST.get('name')
        contact = request.POST.get('contact')
        address = request.POST.get('address')
        father_name = request.POST.get('father_name')
        mother_name = request.POST.get('mother_name')
        dob = request.POST.get('dob') or None
        birth_cert = request.POST.get('birth_certificate')
        class_id = request.POST.get('student_class')
        photo = request.FILES.get('photo')
        admission_date = request.POST.get('admission_date') or None
        
        assigned_fee_types = request.POST.getlist('fee_types')
        
        if not class_id:
            return render(request, 'school/student_form.html', {
                'error': 'Please select a Class for the student.',
                'classes': classes,
                'fee_types': fee_types,
                'current_fee_types': []
            })
        
        # Validation
        if Student.objects.filter(school=profile.school, pin=pin).exists():
            return render(request, 'school/student_form.html', {
                'error': f'System PIN {pin} is already assigned to a student in this school.',
                'classes': classes,
                'fee_types': fee_types,
                'current_fee_types': assigned_fee_types
            })
            
        roll_val = request.POST.get('roll')
        if roll_val and Student.objects.filter(school=profile.school, student_class_id=class_id, roll=roll_val).exists():
            return render(request, 'school/student_form.html', {
                'error': f'Roll number {roll_val} is already assigned to another student in the selected class.',
                'classes': classes,
                'fee_types': fee_types,
                'current_fee_types': assigned_fee_types
            })
        
        student_class = None
        if class_id:
            student_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)

        # --- Auto-generate a unique 6-char alphanumeric Secret Code ---
        import random
        import string
        def _gen_secret():
            chars = string.digits
            while True:
                code = ''.join(random.choices(chars, k=5))
                # Ensure uniqueness across the school
                if not Student.objects.filter(school=profile.school, secret_code=code).exists():
                    return code
        secret_code = _gen_secret()

        student = Student.objects.create(
            school=profile.school,
            student_class=student_class,
            pin=pin,
            roll=request.POST.get('roll'),
            name=name,
            contact_number=contact,
            address=address,
            father_name=father_name,
            mother_name=mother_name,
            dob=dob,
            birth_certificate_number=birth_cert,
            photo=photo,
            admission_date=admission_date or timezone.now().date(),
            secret_code=secret_code,
            is_sms_enabled='is_sms_enabled' in request.POST if profile.school.is_individual_sms_enabled else True,
        )
        
        # Assign Subjects
        assigned_subjects = request.POST.getlist('subject_ids')
        if assigned_subjects:
            student.subjects.set(Subject.objects.filter(school=profile.school, pk__in=assigned_subjects))
        elif student_class:
            # Fallback: Auto-assign all class subjects if none specified
            student.subjects.set(student_class.subjects.all())

        # Assign Fees
        today = timezone.localtime().date()
        selected_ft_objs = FeeType.objects.filter(id__in=assigned_fee_types, school=profile.school)
        student.assigned_fee_types.set(selected_ft_objs)
        
        for ft in selected_ft_objs:
            if ft.frequency == 'ONCE':
                StudentFee.objects.create(
                    student=student,
                    fee_type=ft,
                    amount=ft.default_amount,
                    student_class=student_class
                )
            elif ft.frequency == 'MONTHLY':
                StudentFee.objects.get_or_create(
                    student=student, fee_type=ft, month=today.month, year=today.year,
                    defaults={'amount': ft.default_amount, 'student_class': student_class}
                )
            elif ft.frequency == 'YEARLY':
                StudentFee.objects.get_or_create(
                    student=student, fee_type=ft, year=today.year,
                    defaults={'amount': ft.default_amount, 'student_class': student_class}
                )
            
        # Trigger Admission SMS (includes class, roll, PIN, secret code)
        send_admission_sms(student, profile.school)
            
        messages.success(request, f"Student {student.name} added successfully!")
        return redirect('student_list')
    
    return render(request, 'school/student_form.html', {
        'classes': classes, 
        'fee_types': fee_types,
        'current_fee_types': []
    })

def student_edit(request, pk):
    profile = request.user.profile
    student = get_object_or_404(Student, pk=pk, school=profile.school)
    classes = StudentClass.objects.filter(school=profile.school)
    fee_types = FeeType.objects.filter(school=profile.school)
    current_fee_types = student.assigned_fee_types.values_list('id', flat=True)
    
    if request.method == 'POST':
        student.name = request.POST.get('name')
        student.roll = request.POST.get('roll')
        student.contact_number = request.POST.get('contact')
        student.address = request.POST.get('address')
        student.father_name = request.POST.get('father_name')
        student.mother_name = request.POST.get('mother_name')
        student.dob = request.POST.get('dob') or None
        student.birth_certificate_number = request.POST.get('birth_certificate')
        adm_date = request.POST.get('admission_date')
        if adm_date:
            student.admission_date = adm_date
        
        old_class = student.student_class
        class_id = request.POST.get('student_class')
        if class_id:
            student.student_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
        else:
            student.student_class = None
            
        if old_class and old_class != student.student_class:
            PromotionRecord.objects.get_or_create(
                student=student,
                student_class=old_class,
                defaults={'roll': student.roll}
            )
            # Stamp orphaned fees/results to old class
            StudentFee.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)
            ResultRecord.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)
            # Create fresh fee records for new class based on its DEFAULT fees
            if student.student_class:
                new_class_ft = student.student_class.fee_types.all()
                student.assigned_fee_types.set(new_class_ft)
                
                today = timezone.localtime().date()
                for ft in new_class_ft:
                    if ft.frequency == 'ONCE':
                        StudentFee.objects.get_or_create(student=student, fee_type=ft, student_class=student.student_class, defaults={'amount': ft.default_amount})
                    elif ft.frequency == 'MONTHLY':
                        StudentFee.objects.get_or_create(student=student, fee_type=ft, month=today.month, year=today.year, student_class=student.student_class, defaults={'amount': ft.default_amount})
                    elif ft.frequency == 'YEARLY':
                        StudentFee.objects.get_or_create(student=student, fee_type=ft, year=today.year, student_class=student.student_class, defaults={'amount': ft.default_amount})
            
        if request.FILES.get('photo'):
            student.photo = request.FILES.get('photo')
            
        if profile.school.is_individual_sms_enabled:
            student.is_sms_enabled = 'is_sms_enabled' in request.POST

        student.save()
        
        # Update Subject Assignments
        assigned_subjects = [int(i) for i in request.POST.getlist('subject_ids')]
        student.subjects.set(Subject.objects.filter(school=profile.school, pk__in=assigned_subjects))
        
        # Update Fees (Carefully)
        today = timezone.localtime().date()
        assigned_ft_ids = [int(i) for i in request.POST.getlist('fee_types')]
        selected_ft_objs = FeeType.objects.filter(id__in=assigned_ft_ids, school=profile.school)
        student.assigned_fee_types.set(selected_ft_objs)

        for ft in selected_ft_objs:
            if ft.id not in current_fee_types:
                if ft.frequency == 'ONCE':
                    StudentFee.objects.create(
                        student=student,
                        fee_type=ft,
                        amount=ft.default_amount,
                        student_class=student.student_class
                    )
                elif ft.frequency == 'MONTHLY':
                    StudentFee.objects.get_or_create(
                        student=student, fee_type=ft, month=today.month, year=today.year,
                        defaults={'amount': ft.default_amount, 'student_class': student.student_class}
                    )
                elif ft.frequency == 'YEARLY':
                    StudentFee.objects.get_or_create(
                        student=student, fee_type=ft, year=today.year,
                        defaults={'amount': ft.default_amount, 'student_class': student.student_class}
                    )
        
        # REMOVAL LOGIC: If a fee type is UNCHECKED, remove any unpaid records for the CURRENT class only
        if student.student_class:
            student.fees.filter(student_class=student.student_class, is_paid=False).exclude(fee_type_id__in=assigned_ft_ids).delete()
        
        messages.success(request, f"Student {student.name} updated.")
        return redirect('student_list')
    return render(request, 'school/student_form.html', {
        'student': student, 
        'is_edit': True,
        'classes': classes,
        'fee_types': fee_types,
        'current_fee_types': current_fee_types,
        'all_subjects': student.student_class.subjects.all() if student.student_class else [],
        'current_subjects': student.subjects.values_list('id', flat=True)
    })

def api_get_class_subjects(request):
    profile = request.user.profile
    class_id = request.GET.get('class_id')
    if not class_id: return JsonResponse({'subjects': []})
    
    cls = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
    subjects = list(cls.subjects.values('id', 'name'))
    return JsonResponse({'subjects': subjects})

def api_get_class_fees(request):
    profile = request.user.profile
    class_id = request.GET.get('class_id')
    if not class_id: return JsonResponse({'fees': []})
    
    # Get fees that are globally assigned to this class
    fees = FeeType.objects.filter(school=profile.school, applicable_classes=class_id).values('id', 'name', 'default_amount')
    return JsonResponse({'fees': list(fees)})

def student_delete(request, pk):
    profile = request.user.profile
    student = get_object_or_404(Student, pk=pk, school=profile.school)
    student.delete()
    messages.success(request, f"Student {student.name} deleted.")
    return redirect('student_list')

def reset_student_attendance(request, pk):
    profile = request.user.profile
    student = get_object_or_404(Student, pk=pk, school=profile.school)
    
    if not profile.school.is_time_test_active:
        messages.error(request, "Reset is only allowed in Time Test Mode.")
        return redirect('student_list')

    # Define Local Day Range to avoid UTC shifting
    filter_date = timezone.localtime().date()
    from datetime import time as d_time
    day_start = timezone.make_aware(datetime.combine(filter_date, d_time.min))
    day_end = timezone.make_aware(datetime.combine(filter_date, d_time.max))
    
    # Delete logs
    deleted_count, _ = AttendanceLog.objects.filter(
        student=student, 
        timestamp__range=(day_start, day_end)
    ).delete()
    
    messages.warning(request, f"Reset {deleted_count} logs for {student.name} today.")
    return redirect(request.META.get('HTTP_REFERER', 'student_list'))

# --- Fee Management ---
def fee_type_list(request):
    profile = request.user.profile
    fee_types = FeeType.objects.filter(school=profile.school).prefetch_related('applicable_classes').order_by('name')
    classes = StudentClass.objects.filter(school=profile.school).order_by('name')
    return render(request, 'school/fee_type_list.html', {'fee_types': fee_types, 'classes': classes})

def fee_type_add(request):
    profile = request.user.profile
    if request.method == 'POST':
        name = request.POST.get('name')
        amount = request.POST.get('amount') or 0
        freq = request.POST.get('frequency', 'ONCE')
        class_ids = request.POST.getlist('applicable_classes')
        
        fee_type = FeeType.objects.create(school=profile.school, name=name, default_amount=amount, frequency=freq)
        if class_ids:
            fee_type.applicable_classes.set(class_ids)
            
        messages.success(request, f"Fee Model {name} created!")
        return redirect('fee_type_list')
    classes = StudentClass.objects.filter(school=profile.school).order_by('name')
    return render(request, 'school/fee_type_form.html', {'classes': classes})

def fee_type_edit(request, pk):
    profile = request.user.profile
    fee_type = get_object_or_404(FeeType, pk=pk, school=profile.school)
    if request.method == 'POST':
        fee_type.name = request.POST.get('name')
        new_amount = request.POST.get('amount') or 0
        fee_type.default_amount = new_amount
        fee_type.frequency = request.POST.get('frequency', 'ONCE')
        class_ids = request.POST.getlist('applicable_classes')
        
        fee_type.save()
        fee_type.applicable_classes.set(class_ids)
        
        # Auto-update all UNPAID student fees for this type
        StudentFee.objects.filter(fee_type=fee_type, is_paid=False).update(amount=new_amount)
        
        messages.success(request, f"Fee Model {fee_type.name} updated and synced with unpaid records!")
        return redirect('fee_type_list')
    classes = StudentClass.objects.filter(school=profile.school).order_by('name')
    return render(request, 'school/fee_type_form.html', {'fee_type': fee_type, 'is_edit': True, 'classes': classes})

def api_apply_fee(request, pk):
    profile = request.user.profile
    fee_type = get_object_or_404(FeeType, pk=pk, school=profile.school)
    
    if request.method == 'POST':
        class_ids = request.POST.getlist('class_ids')
        
        # 1. Update the FeeType's applicable_classes mapping (for Level Up)
        fee_type.applicable_classes.set(class_ids)
        
        # 2. Apply to current students (Bulk Creation)
        if class_ids:
            students = Student.objects.filter(school=profile.school, student_class_id__in=class_ids, is_active=True)
            count = 0
            for std in students:
                # Avoid duplicate fees for the same class-period if needed, 
                # but basically standard Apply logic:
                StudentFee.objects.get_or_create(
                    student=std,
                    fee_type=fee_type,
                    student_class=std.student_class,
                    defaults={'amount': fee_type.default_amount}
                )
                count += 1
            messages.success(request, f"Fee configuration updated and applied to {count} students!")
        else:
            messages.info(request, f"Fee configuration cleared for all classes.")
            
        return redirect('fee_type_list')
    return redirect('fee_type_list')

def fee_type_delete(request, pk):
    profile = request.user.profile
    fee_type = get_object_or_404(FeeType, pk=pk, school=profile.school)
    name = fee_type.name
    fee_type.delete()
    messages.success(request, f"Fee Model {name} deleted.")
    return redirect('fee_type_list')

# --- Fine Logic Management ---
def fine_logic_list(request):
    profile = request.user.profile
    FineRule = DetectionFee # Alias for clarify
    FineRule.objects.get_or_create(school=profile.school, reason='Absent')
    FineRule.objects.get_or_create(school=profile.school, reason='Late')
    
    fines = FineRule.objects.filter(school=profile.school).order_by('reason')
    return render(request, 'school/fine_logic_list.html', {'fines': fines})

def fine_logic_add(request):
    profile = request.user.profile
    if request.method == 'POST':
        reason = request.POST.get('reason')
        amount = request.POST.get('amount') or 0
        grace = request.POST.get('grace_minutes') or 0
        if reason == 'Late':
            grace = 1
        DetectionFee.objects.create(school=profile.school, reason=reason, amount=amount, grace_minutes=grace)
        messages.success(request, f"New fine rule for {reason} added!")
        return redirect('fine_logic_list')
    return render(request, 'school/fine_logic_form.html')

def fine_logic_edit(request, pk):
    profile = request.user.profile
    fine = get_object_or_404(DetectionFee, pk=pk, school=profile.school)
    if request.method == 'POST':
        fine.amount = request.POST.get('amount') or 0
        if fine.reason == 'Late':
            fine.grace_minutes = 1
        else:
            fine.grace_minutes = request.POST.get('grace_minutes') or 0
        fine.save()
        messages.success(request, f"Fine rule for {fine.reason} updated!")
        return redirect('fine_logic_list')
    return render(request, 'school/fine_logic_form.html', {'fine': fine, 'is_edit': True})

def fine_logic_delete(request, pk):
    profile = request.user.profile
    fine = get_object_or_404(DetectionFee, pk=pk, school=profile.school)
    reason = fine.reason
    fine.delete()
    messages.success(request, f"Fine rule for {reason} removed.")
    return redirect('fine_logic_list')

def _ensure_automated_fees(student, school, today):
    """Ensures Monthly/Yearly fees exist for the current period, only if assigned to student class."""
    if not student.student_class: return

    active_types = student.assigned_fee_types.filter(
        frequency__in=['MONTHLY', 'YEARLY']
    )
    for ft in active_types:
        if ft.frequency == 'MONTHLY':
            if not StudentFee.objects.filter(student=student, fee_type=ft, month=today.month, year=today.year).exists():
                StudentFee.objects.create(
                    student=student, fee_type=ft, amount=ft.default_amount,
                    student_class=student.student_class, month=today.month, year=today.year
                )
        elif ft.frequency == 'YEARLY':
            if not StudentFee.objects.filter(student=student, fee_type=ft, year=today.year).exists():
                StudentFee.objects.create(
                    student=student, fee_type=ft, amount=ft.default_amount,
                    student_class=student.student_class, year=today.year
                )

# --- Fee Collection ---
def fee_collection(request):
    profile = request.user.profile

    selected_class = None
    selected_student = None
    students_with_stats = []
    fees = []
    fines = []
    summary = {}

    classes = StudentClass.objects.filter(school=profile.school).order_by('name')
    search_query = request.GET.get('q')
    class_id = request.GET.get('class_id')
    student_id = request.GET.get('student_id')
    status_filter = request.GET.get('status')

    from datetime import date as date_cls, timedelta as td, time as time_cls
    import calendar
    from django.db.models import Sum
    
    # Respect Time Test mode for automated fees
    now_aware = get_school_now(profile.school)
    now_local = timezone.localtime(now_aware)
    today = now_local.date()


    # 1. Global ID Search - Redirect directly to student's fee page
    if search_query and not class_id and not student_id:
        student_match = Student.objects.filter(school=profile.school, pin=search_query).first()
        if student_match:
            return redirect(f"{request.path}?student_id={student_match.id}")

    if class_id:
        selected_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
        students_query = Student.objects.filter(student_class=selected_class, school=profile.school)
        if search_query:
            students_query = students_query.filter(
                models.Q(name__icontains=search_query) | 
                models.Q(pin__icontains=search_query)
            )
        
        for student in students_query:
            _ensure_automated_fees(student, profile.school, today)
            all_fees = student.fees.filter(student_class=selected_class)
            total_amount = sum(f.amount for f in all_fees)
            total_paid = sum(f.paid_amount for f in all_fees)
            due = total_amount - total_paid
            status = 'PAID' if due <= 0 and total_amount > 0 else 'PARTIAL' if total_paid > 0 else 'UNPAID'
            if status_filter and status != status_filter:
                continue
            students_with_stats.append({'student': student, 'total': total_amount, 'paid': total_paid, 'due': due, 'status': status})
    
    if student_id:
        selected_student = get_object_or_404(Student, pk=student_id, school=profile.school)
        _ensure_automated_fees(selected_student, profile.school, today)
        if class_id:
            raw_fees = StudentFee.objects.filter(student=selected_student, student_class_id=class_id).select_related('fee_type').order_by('is_paid', '-year', '-month', '-created_at')
        else:
            raw_fees = StudentFee.objects.filter(student=selected_student, student_class=selected_student.student_class).select_related('fee_type').order_by('is_paid', '-year', '-month', '-created_at')

        # Group fees by type for breakdown UI
        fee_groups = {}
        for f in raw_fees:
            tid = f.fee_type_id
            if tid not in fee_groups:
                fee_groups[tid] = {
                    'type': f.fee_type,
                    'total_amount': 0,
                    'total_paid': 0,
                    'total_due': 0,
                    'is_recurring': f.fee_type.frequency in ['MONTHLY', 'YEARLY'],
                    'items': []
                }
            fee_groups[tid]['items'].append(f)
            fee_groups[tid]['total_amount'] += f.amount
            fee_groups[tid]['total_paid'] += f.paid_amount
            fee_groups[tid]['total_due'] += f.due

        fees = raw_fees # Keep original for bulk settlement if needed
        # fee_groups will be used for the breakdown display

        late_logic = profile.school.detection_fees.filter(reason='Late').first()
        absent_logic = profile.school.detection_fees.filter(reason='Absent').first()
        schedule = getattr(selected_student.student_class, 'schedule', None)

        current_year = today.year
        current_month = today.month
        start_of_month = date_cls(current_year, current_month, 1)


        from django.utils.timezone import make_aware
        import datetime as dt_mod
        from datetime import time as time_cls

        # --- Dynamic Fine Search Range ---
        # Requirement: Bill must reflect ALL uncleared fines, not just the current month's.
        # But we still want to know current month stats for the dashboard cards.
        
        # 1. Determine the effective start dates for fine detection (cleared date + 1 day)
        late_start_date = (selected_student.late_cleared_until + td(days=1)) if selected_student.late_cleared_until else (selected_student.admission_date or date_cls(2000, 1, 1))
        absent_start_date = (selected_student.absent_cleared_until + td(days=1)) if selected_student.absent_cleared_until else (selected_student.admission_date or date_cls(2000, 1, 1))

        # 2. Get logs from the earliest uncleared date until now
        # Standard filter by year/month is replaced with range for production reliability
        fine_search_start = make_aware(dt_mod.datetime.combine(min(late_start_date, absent_start_date), time_cls.min))
        
        logs = AttendanceLog.objects.filter(
            student=selected_student,
            timestamp__gte=fine_search_start
        ).order_by('timestamp')

        fines = []
        summary = {
            'late_count': 0, 
            'absent_count': 0, 
            'leave_count': 0, 
            'total_late_min': 0, 
            'total_fine': decimal.Decimal('0.00'),
            'current_month_late': 0,
            'current_month_absent': 0
        }

        # --- Late Fine Calculation ---
        for log in logs.filter(status='LATE'):
            log_dt_local = timezone.localtime(log.timestamp)
            log_date = log_dt_local.date()
            
            # Count for "Current Month" card if applicable
            if log_date.month == today.month and log_date.year == today.year:
                summary['current_month_late'] += 1

            if log_date < late_start_date: continue
            
            summary['late_count'] += 1
            if schedule:
                # Calculate late minutes relative to punch_in_end
                target_in_end = make_aware(dt_mod.datetime.combine(log_date, schedule.punch_in_end))
                diff_sec = (log.timestamp - target_in_end).total_seconds()
                diff_min = diff_sec / 60
                
                if diff_min > 0:
                    summary['total_late_min'] += int(diff_min)
                    if late_logic and diff_min > late_logic.grace_minutes:
                        fine_amt = late_logic.amount * decimal.Decimal(int(diff_min))
                        summary['total_fine'] += fine_amt
                        fines.append({
                            'type': 'Late', 
                            'date': log_date, 
                            'detail': f"{int(diff_min)} min late", 
                            'amount': fine_amt
                        })

        # --- Absent Fine Calculation ---
        logged_days = set(
            timezone.localtime(l.timestamp).date() for l in logs
            if l.status not in ('FALSE_PUNCH', 'EARLY_IN')
        )
        
        # Determine holiday and leave ranges
        leave_days = set()
        for leave in StudentLeave.objects.filter(student=selected_student, is_deleted=False, end_date__gte=absent_start_date):
            d_check = max(leave.start_date, absent_start_date)
            while d_check <= min(leave.end_date, today):
                leave_days.add(d_check)
                d_check += td(days=1)
        
        summary['leave_count'] = len(leave_days)
        
        # Determine holidays more robustly
        h_overrides = {h.date: h.day_type for h in Holiday.objects.filter(
            school=profile.school, date__gte=absent_start_date, date__lte=today
        )}
        
        # Iterate through all days from start until yesterday (today is still pending)
        check = absent_start_date
        while check < today:
            # Check for holiday (Explicit OR Default Friday)
            h_type = h_overrides.get(check)
            is_holiday = (h_type == 'HOLIDAY') or (h_type is None and check.weekday() == 4)
            
            if check not in logged_days and check not in leave_days and not is_holiday:
                summary['absent_count'] += 1
                if check.month == today.month and check.year == today.year:
                    summary['current_month_absent'] += 1
                
                if absent_logic:
                    fine_amt = absent_logic.amount
                    summary['total_fine'] += fine_amt
                    fines.append({
                        'type': 'Absent', 
                        'date': check, 
                        'detail': 'No entry recorded', 
                        'amount': fine_amt
                    })
            check += td(days=1)

        # Finalize fine summary with carry-forward balance
        summary['fine_balance'] = selected_student.carry_forward_fine
        summary['total_fine'] += summary['fine_balance']

        if summary['fine_balance'] > 0:
            fines.append({
                'type': 'Balance', 
                'date': (selected_student.late_cleared_until or today), 
                'detail': 'Previous Uncleared Balance', 
                'amount': summary['fine_balance']
            })

        fines.sort(key=lambda x: x['date'], reverse=True)
        # Academic Fees handles unpaid balances from any time
        summary['total_due'] = sum(f.amount - f.paid_amount for f in StudentFee.objects.filter(student=selected_student, is_paid=False))
        summary['grand_total'] = summary['total_due'] + summary['total_fine']
        
    # --- ATTENDANCE FINES DATA (MONTHLY) ---
    fine_month_offset = int(request.GET.get('fine_month_offset', 0))
    total_fine_months = today.year * 12 + today.month - 1 + fine_month_offset
    fine_year = total_fine_months // 12
    fine_month = (total_fine_months % 12) + 1
    start_of_fine = date_cls(fine_year, fine_month, 1)
    _, last_fine_day = calendar.monthrange(fine_year, fine_month)
    end_of_fine = date_cls(fine_year, fine_month, last_fine_day)

    # DetectionFee is a RULE model (no timestamp/student).
    # Calculate fines from AttendanceLog + DetectionFee rules.
    late_rule = profile.school.detection_fees.filter(reason='Late').first()
    absent_rule = profile.school.detection_fees.filter(reason='Absent').first()

    fine_summary_by_student = []
    all_students = Student.objects.filter(school=profile.school, is_active=True).select_related('student_class')

    for stu in all_students:
        student_fine = decimal.Decimal('0.00')
        # Robust range filter for production compatibility
        m_start = timezone.make_aware(datetime.combine(start_of_fine, time_cls.min))
        m_end = timezone.make_aware(datetime.combine(end_of_fine, time_cls.max))

        month_logs = AttendanceLog.objects.filter(
            student=stu,
            timestamp__gte=m_start,
            timestamp__lte=m_end
        ).exclude(status__in=['FALSE_PUNCH', 'UNIDENTIFIED'])

        # Late fines
        if late_rule:
            sch = getattr(stu.student_class, 'schedule', None)
            for log in month_logs.filter(status='LATE'):
                if sch:
                    log_date = timezone.localtime(log.timestamp).date()
                    start_dt = timezone.make_aware(datetime.combine(log_date, sch.punch_in_end))
                    diff_min = (log.timestamp - start_dt).total_seconds() / 60
                    if diff_min > late_rule.grace_minutes:
                        student_fine += late_rule.amount * decimal.Decimal(int(diff_min))
                else:
                    student_fine += late_rule.amount

        # Absent fines
        if absent_rule:
            logged_days = set(timezone.localtime(l.timestamp).date() for l in month_logs)
            # Identify holidays (Explicit records + Default Fridays)
            holiday_days = set()
            h_overrides = {h.date: h.day_type for h in Holiday.objects.filter(
                school=profile.school, date__gte=start_of_fine,
                date__lte=min(end_of_fine, today)
            )}
            h_check = start_of_fine
            while h_check <= min(end_of_fine, today):
                h_type = h_overrides.get(h_check)
                if h_type == 'HOLIDAY' or (h_type is None and h_check.weekday() == 4):
                    holiday_days.add(h_check)
                h_check += td(days=1)
            leave_days = set()
            for leave in StudentLeave.objects.filter(student=stu, is_deleted=False,
                                                      start_date__lte=end_of_fine,
                                                      end_date__gte=start_of_fine):
                d = max(leave.start_date, start_of_fine)
                from datetime import timedelta as _td
                while d <= min(leave.end_date, min(end_of_fine, today)):
                    leave_days.add(d)
                    d += _td(days=1)

            check = start_of_fine
            while check < min(end_of_fine, today):
                if check not in logged_days and check not in leave_days and check not in holiday_days:
                    student_fine += absent_rule.amount
                check += td(days=1)

        if student_fine > 0:
            fine_summary_by_student.append({
                'student_name': stu.name,
                'pin': stu.pin,
                'total_fine': student_fine,
                'date': start_of_fine,
            })

    fine_summary_by_student.sort(key=lambda x: x['total_fine'], reverse=True)
    if not selected_student:
        fines = fine_summary_by_student  # used in context below



    if selected_student:
        # Use the class-scoped fees (already set at lines 2184-2187), NOT all unpaid.
        # 'fees' is already filtered by student_class so session isolation is preserved.
        scoped_unpaid = [f for f in fees if not f.is_paid]
        summary['total_due'] = sum(f.amount for f in scoped_unpaid)
        summary['total_paid'] = sum(f.paid_amount for f in scoped_unpaid)
        summary['remaining_due'] = summary['total_due'] - summary['total_paid']
        
        # summary['total_fine'] is already accurately populated from the primary logs iteration.
        summary['grand_total'] = summary['remaining_due'] + summary['total_fine']
        
    # --- GLOBAL COLLECTION STATS ---
    month_offset = int(request.GET.get('month_offset', 0))
    filter_method = request.GET.get('method')

    
    # Calculate target month based on offset
    total_months = today.year * 12 + today.month - 1 + month_offset
    target_year = total_months // 12
    target_month = (total_months % 12) + 1
    start_of_target = date_cls(target_year, target_month, 1)
    _, last_day = calendar.monthrange(target_year, target_month)
    end_of_target = date_cls(target_year, target_month, last_day)
    
    # Production-safe: use timezone-aware range instead of timestamp__date
    import datetime as _dt_mod
    target_range_start = timezone.make_aware(_dt_mod.datetime.combine(start_of_target, time_cls.min))
    target_range_end = timezone.make_aware(_dt_mod.datetime.combine(end_of_target, time_cls.max))
    
    # Base Query for stats (only date/school filtered, NOT method filtered)
    stats_raw_query = FeeTransaction.objects.filter(
        school=profile.school,
        timestamp__gte=target_range_start,
        timestamp__lte=target_range_end
    )
    
    # But for the listing/total month, we apply the method filter if present
    base_stats_query = stats_raw_query
    if filter_method:
        base_stats_query = base_stats_query.filter(payment_method=filter_method)
        
    month_total = base_stats_query.aggregate(s=Sum('total_amount'))['s'] or 0
    # Always calculate mobile/cash stats from the full month's raw query
    mobile_total = stats_raw_query.filter(payment_method__in=['BKASH', 'NOGOD', 'ROCKET']).aggregate(s=Sum('total_amount'))['s'] or 0
    cash_bank_total = stats_raw_query.filter(payment_method__in=['CASH', 'BANK']).aggregate(s=Sum('total_amount'))['s'] or 0
    
    coll_stats = {
        'month': month_total,
        'mobile': mobile_total,
        'cash_bank': cash_bank_total
    }
    
    context_data = {
        'coll_stats': coll_stats,
        'current_month_name': start_of_target.strftime('%B %Y'),
        'month_offset': month_offset,
        'current_method': filter_method,
        'prev_offset': month_offset - 1,
        'next_offset': month_offset + 1,
        'fine_month_name': start_of_fine.strftime('%B %Y'),
        'prev_fine_offset': fine_month_offset - 1,
        'next_fine_offset': fine_month_offset + 1,
    }

    if request.method == 'POST':
        if 'collect_all' in request.POST:
            # Enhanced Bulk Collection with Discount & Distribution
            total_collected = decimal.Decimal(request.POST.get('total_collected') or 0)
            discount_type = request.POST.get('discount_type') # FIXED or PERCENT
            discount_value = decimal.Decimal(request.POST.get('discount_value') or 0)
            
            # Fetch all unpaid/partial fees ordered by date (Oldest First)
            if class_id:
                outstanding_fees = StudentFee.objects.filter(student=selected_student, student_class_id=class_id, is_paid=False).order_by('created_at')
            else:
                outstanding_fees = StudentFee.objects.filter(student=selected_student, student_class=selected_student.student_class, is_paid=False).order_by('created_at')
            total_due = sum(f.amount - f.paid_amount for f in outstanding_fees)
            
            # 1. Calculate Discount
            final_discount = decimal.Decimal('0.00')
            if discount_type == 'PERCENT':
                final_discount = (total_due * discount_value) / 100
            else:
                final_discount = discount_value

            # 2. Total Credit to distribute (Paid + Discount)
            # We treat discount as 'paid' by the system
            distribution_pool = total_collected + final_discount
            original_pool = distribution_pool
            
            items_to_record = []
            for f in outstanding_fees:
                if distribution_pool <= 0: break
                
                due_on_fee = f.amount - f.paid_amount
                amt_contributed = min(distribution_pool, due_on_fee)
                
                f.paid_amount += amt_contributed
                if f.paid_amount >= f.amount:
                    f.is_paid = True
                    f.paid_at = timezone.now()
                f.save()
                
                items_to_record.append((f, amt_contributed))
                distribution_pool -= amt_contributed

            # 3. RECORD TRANSACTION
            if total_collected > 0:
                txn = FeeTransaction.objects.create(
                    school=profile.school,
                    student=selected_student,
                    total_amount=total_collected,
                    discount_amount=final_discount,
                    payment_method=request.POST.get('payment_method', 'CASH'),
                    reference_number=request.POST.get('reference_number'),
                    student_class=selected_student.student_class
                )
                for f, amt in items_to_record:
                    if amt > 0:
                        FeeTransactionItem.objects.create(transaction=txn, fee=f, amount_paid=amt)
                
                messages.success(request, f"Settlement applied! Collected: ৳{total_collected}")
                
                # Send Fee SMS
                send_fee_payment_sms(txn, profile.school)
                
                return redirect(f"{request.path}?student_id={selected_student.id}&success=1&txn_id={txn.id}")
            
            messages.success(request, f"Settlement applied! Collected: ৳{total_collected}, Discounted: ৳{final_discount}")
            return redirect(f"{request.path}?student_id={selected_student.id}")

        elif 'collect_fines' in request.POST:
            # Settle Biometric Fines (Support Partial Payment)
            amount_paid = decimal.Decimal(request.POST.get('total_collected') or 0)
            
            # 1. Calculate what is actually due at this moment
            # (Matches the logic above in fee_collection)
            current_fine_total = summary['total_fine'] # summary is already calculated in the view
            
            remaining_fine = current_fine_total - amount_paid
            if remaining_fine < 0: remaining_fine = decimal.Decimal('0.00')
            
            # 2. Update student record
            today_local = timezone.localtime().date()
            selected_student.late_cleared_until = today_local
            selected_student.absent_cleared_until = today_local
            selected_student.carry_forward_fine = remaining_fine
            selected_student.save()
            
            txn = FeeTransaction.objects.create(
                school=profile.school,
                student=selected_student,
                total_amount=amount_paid,
                fine_amount=amount_paid,
                payment_method=request.POST.get('payment_method', 'CASH'),
                reference_number=request.POST.get('reference_number') or "BIOMETRIC_FINE_SETTLEMENT",
                student_class=selected_student.student_class
            )
            
            messages.success(request, f"Biometric fines settled! Collection of ৳{amount_paid} recorded.")
            
            # Send Fee SMS (Fine)
            send_fee_payment_sms(txn, profile.school)
            
            return redirect(f"{request.path}?student_id={selected_student.id}&success=1&txn_id={txn.id}")

        elif 'reset_fine' in request.POST:
            reset_type = request.POST.get('reset_fine')
            # Reset up to yesterday to allow today's new fines to still show up
            reset_date = today - td(days=1)
            if reset_type == 'Late':
                selected_student.late_cleared_until = reset_date
                # Carry forward is global for fines, so we only clear it if both are reset or just reduce it?
                # User wants a full reset, so let's clear carry forward too.
                selected_student.carry_forward_fine = 0
                selected_student.save()
                messages.success(request, f"Late fines for {selected_student.name} have been reset.")
            elif reset_type == 'Absent':
                selected_student.absent_cleared_until = reset_date
                selected_student.carry_forward_fine = 0
                selected_student.save()
                messages.success(request, f"Absent fines for {selected_student.name} have been reset.")
            return redirect(f"{request.path}?student_id={selected_student.id}")

        elif 'fee_id' in request.POST:
            # Individual Payment
            fee_id = request.POST.get('fee_id')
            payment_amount = decimal.Decimal(request.POST.get('amount_paid', 0))
            fee = get_object_or_404(StudentFee, pk=fee_id, student__school=profile.school)
            
            fee.paid_amount += payment_amount
            status_msg = f"Collected ৳{payment_amount} for {fee.fee_type.name}"
            
            if fee.paid_amount >= fee.amount:
                fee.paid_amount = fee.amount # Cap at total
                fee.is_paid = True
                fee.paid_at = timezone.now()
                status_msg = f"Full payment of ৳{fee.amount} received for {fee.fee_type.name}"
            
            fee.save()
            
            txn = FeeTransaction.objects.create(
                school=profile.school,
                student=fee.student,
                total_amount=payment_amount,
                payment_method=request.POST.get('payment_method', 'CASH'),
                reference_number=request.POST.get('reference_number'),
                student_class=fee.student.student_class
            )
            FeeTransactionItem.objects.create(transaction=txn, fee=fee, amount_paid=payment_amount)

            messages.success(request, status_msg)
            
            # Send Fee SMS
            send_fee_payment_sms(txn, profile.school)
            
            redirect_url = f"{request.path}?student_id={fee.student.id}&success=1&txn_id={txn.id}"
            if selected_class:
                redirect_url = f"{request.path}?class_id={selected_class.id}&student_id={fee.student.id}&success=1&txn_id={txn.id}"
            return redirect(redirect_url)

    student_transactions = []
    if student_id:
        student_transactions = FeeTransaction.objects.filter(
            student=selected_student, 
            student_class=selected_student.student_class
        ).order_by('-timestamp')

    recent_transactions = FeeTransaction.objects.filter(school=profile.school).select_related('student', 'student__student_class').order_by('-timestamp')[:5]

    return render(request, 'school/fee_collection.html', {
        'classes': classes,
        'selected_class': selected_class,
        'selected_student': selected_student,
        'students_with_stats': students_with_stats,
        'fees': fees,
        'fee_groups': fee_groups.values() if student_id else [],
        'student_transactions': student_transactions,
        'fines': fines,
        'summary': summary,
        'search_query': search_query,
        'recent_transactions': recent_transactions,
        **context_data
    })
def class_list(request):
    profile = request.user.profile
    classes = StudentClass.objects.filter(school=profile.school).select_related('teacher', 'teacher__user').prefetch_related('subjects').order_by('name')
    return render(request, 'school/class_list.html', {'classes': classes})

def class_add(request):
    profile = request.user.profile
    teachers = UserProfile.objects.filter(school=profile.school, role='TEACHER', user__is_active=True).select_related('user')
    subjects_all = Subject.objects.filter(school=profile.school)
    if request.method == 'POST':
        name = request.POST.get('name')
        teacher_id = request.POST.get('teacher_id')
        subject_ids = request.POST.getlist('subject_ids')
        
        teacher = None
        if teacher_id:
            teacher = UserProfile.objects.filter(school=profile.school, role='TEACHER', pk=teacher_id).first()
        
        cls = StudentClass.objects.create(school=profile.school, name=name, teacher=teacher)
        if subject_ids:
            cls.subjects.set(Subject.objects.filter(school=profile.school, pk__in=subject_ids))
        return redirect('class_list')
    return render(request, 'school/class_form.html', {'teachers': teachers, 'subjects_all': subjects_all})

def class_edit(request, pk):
    profile = request.user.profile
    student_class = get_object_or_404(StudentClass, pk=pk, school=profile.school)
    teachers = UserProfile.objects.filter(school=profile.school, role='TEACHER', user__is_active=True).select_related('user')
    subjects_all = Subject.objects.filter(school=profile.school)
    if request.method == 'POST':
        student_class.name = request.POST.get('name')
        teacher_id = request.POST.get('teacher_id')
        subject_ids = request.POST.getlist('subject_ids')
        
        if teacher_id:
            student_class.teacher = UserProfile.objects.filter(school=profile.school, role='TEACHER', pk=teacher_id).first()
        else:
            student_class.teacher = None
        
        student_class.save()
        student_class.subjects.set(Subject.objects.filter(school=profile.school, pk__in=subject_ids))
        return redirect('class_list')
    return render(request, 'school/class_form.html', {
        'class': student_class, 
        'is_edit': True, 
        'teachers': teachers,
        'subjects_all': subjects_all
    })

def class_delete(request, pk):
    profile = request.user.profile
    student_class = get_object_or_404(StudentClass, pk=pk, school=profile.school)
    student_class.delete()
    return redirect('class_list')

# --- Attendance Scheduling ---
def schedule_list(request):
    profile = request.user.profile
    classes = StudentClass.objects.filter(school=profile.school).order_by('name')
    return render(request, 'school/schedule_list.html', {'classes': classes})

def schedule_update(request, class_id):
    profile = request.user.profile
    student_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
    
    # Simple defaults if no record exists
    schedule, created = ClassSchedule.objects.get_or_create(
        school=profile.school, 
        student_class=student_class,
        defaults={
            'punch_in_start': '08:00:00',
            'punch_in_end': '09:00:00',
            'checkout_start': '14:00:00',
            'checkout_end': '15:00:00'
        }
    )
    
    if request.method == 'POST':
        # Safely get values from POST
        p_start = request.POST.get('punch_in_start')
        p_end = request.POST.get('punch_in_end')
        c_start = request.POST.get('checkout_start')
        c_end = request.POST.get('checkout_end')
        l_start = request.POST.get('lunch_start') or None
        l_end = request.POST.get('lunch_end') or None
        
        # Update schedule
        schedule.punch_in_start = p_start
        schedule.punch_in_end = p_end
        schedule.checkout_start = c_start
        schedule.checkout_end = c_end
        schedule.lunch_start = l_start
        schedule.lunch_end = l_end
        schedule.save()
        
        messages.success(request, f"Timing rules for {student_class.name} updated!")
        return redirect('schedule_list')
        
    return render(request, 'school/schedule_form.html', {
        'student_class': student_class,
        'schedule': schedule
    })

# --- Leave Management ---
def leave_management(request):
    profile = request.user.profile
    search_id = request.GET.get('student_id')
    selected_student = None
    leave_history = []
    
    if search_id:
        selected_student = Student.objects.filter(pin=search_id, school=profile.school).first()
        if selected_student:
            active_leaves = StudentLeave.objects.filter(student=selected_student, is_deleted=False).order_by('-start_date')
            deleted_leaves = StudentLeave.objects.filter(student=selected_student, is_deleted=True).order_by('-deleted_at')
        else:
            messages.warning(request, f"No student found with ID: {search_id}")

    if request.method == 'POST':
        student_id_val = request.POST.get('target_student_id')
        student = get_object_or_404(Student, pk=student_id_val, school=profile.school)
        
        start_date = request.POST.get('start_date')
        end_date = request.POST.get('end_date')
        l_type = request.POST.get('leave_type')
        reason = request.POST.get('reason')
        
        StudentLeave.objects.create(
            school=profile.school,
            student=student,
            start_date=start_date,
            end_date=end_date,
            leave_type=l_type,
            reason=reason
        )
        messages.success(request, f"Leave recorded for {student.name} ({l_type})")
        return redirect(f"{request.path}?student_id={student.pin}")

    return render(request, 'school/leave_management.html', {
        'selected_student': selected_student,
        'active_leaves': active_leaves if selected_student else [],
        'deleted_leaves': deleted_leaves if selected_student else [],
        'search_id': search_id,
        'today': timezone.now().date()
    })

def leave_delete(request, pk):
    profile = request.user.profile
    leave = get_object_or_404(StudentLeave, pk=pk, student__school=profile.school)
    
    # Can delete if end date is today or in the future
    if leave.end_date >= timezone.now().date():
        student_pin = leave.student.pin
        leave.is_deleted = True
        leave.deleted_at = timezone.now()
        leave.save()
        messages.success(request, "Leave record dismissed successfully.")
        return redirect(f"{reverse('leave_management')}?student_id={student_pin}")
    else:
        messages.error(request, "Cannot dismiss leave that has already passed.")
        return redirect('leave_management')

# --- Holiday/Event Calendar ---
def holiday_list(request):
    profile = request.user.profile
    
    # Get month and year from request or default to current
    month = int(request.GET.get('month', timezone.now().month))
    year = int(request.GET.get('year', timezone.now().year))
    
    import calendar
    from datetime import date, timedelta
    
    # Calculate next/prev month
    first_day = date(year, month, 1)
    prev_month_date = first_day - timedelta(days=1)
    next_month_date = (first_day + timedelta(days=32)).replace(day=1)
    
    prev_month = prev_month_date.month
    prev_year = prev_month_date.year
    next_month = next_month_date.month
    next_year = next_month_date.year
    
    # Calendar data
    cal = calendar.Calendar(firstweekday=6) # Sunday start
    month_days = cal.monthdayscalendar(year, month)
    month_name = calendar.month_name[month]
    
    # Get holidays for this month
    holidays_in_month = Holiday.objects.filter(
        school=profile.school,
        date__year=year,
        date__month=month
    )
    holiday_days = {h.date.day: h for h in holidays_in_month}
    
    all_holidays = Holiday.objects.filter(school=profile.school).order_by('-date')
    
    context = {
        'holidays': all_holidays,
        'month_days': month_days,
        'month_name': month_name,
        'current_month': month,
        'current_year': year,
        'prev_month': prev_month,
        'prev_year': prev_year,
        'next_month': next_month,
        'next_year': next_year,
        'holiday_days': holiday_days,
        'today': timezone.now().date(),
    }
    return render(request, 'school/holiday_list.html', context)

def holiday_add(request):
    profile = request.user.profile
    prefill_date = request.GET.get('date')
    
    if request.method == 'POST':
        date = request.POST.get('date')
        note = request.POST.get('note')
        day_type = request.POST.get('day_type', 'HOLIDAY')
        Holiday.objects.create(school=profile.school, date=date, note=note, day_type=day_type)
        messages.success(request, f"Event on {date} recorded!")
        return redirect('holiday_list')
    return render(request, 'school/holiday_form.html', {'prefill_date': prefill_date})

def holiday_delete(request, pk):
    profile = request.user.profile
    holiday = get_object_or_404(Holiday, pk=pk, school=profile.school)
    holiday.delete()
    messages.success(request, "Event record removed.")
    return redirect('holiday_list')

def fee_receipt(request, txn_id):
    profile = request.user.profile
    txn = get_object_or_404(FeeTransaction.objects.select_related('student', 'student__student_class'), pk=txn_id, school=profile.school)
    
    # Financial Review - Prefetch everything needed
    items = txn.items.select_related('fee', 'fee__fee_type', 'fee__student_class')
    
    # Determine which classes are relevant to this transaction
    involved_class_ids = set(item.fee.student_class_id for item in items if item.fee.student_class_id)
    
    # Scoping logic for balance calculation (requested by user)
    if involved_class_ids:
        relevant_fees_query = txn.student.fees.filter(student_class_id__in=involved_class_ids)
        # For header display, prioritize the class from the fees. If multiple, fallback to txn record.
        if len(involved_class_ids) == 1:
            display_class = StudentClass.objects.filter(id=list(involved_class_ids)[0]).first()
        else:
            display_class = txn.student_class or txn.student.student_class
    else:
        # Fallback if no items (e.g. generic penalty record)
        display_class = txn.student_class or txn.student.student_class
        relevant_fees_query = txn.student.fees.filter(student_class=display_class)

    # Current Scoped Outstanding
    remaining_balance = sum(f.amount - f.paid_amount for f in relevant_fees_query.filter(is_paid=False))
    
    # This transaction's total impact (Real Pay + Discount)
    this_impact = txn.total_amount + txn.discount_amount
    
    # Previous Due for the relevant session(s)
    previous_due = remaining_balance + this_impact
    
    # Historical logic: calculate total paid by student BEFORE this transaction (Global)
    previous_total_paid = FeeTransaction.objects.filter(
        student=txn.student, 
        timestamp__lt=txn.timestamp
    ).aggregate(s=Sum('total_amount'))['s'] or 0
    
    return render(request, 'school/fee_receipt.html', {
        'txn': txn, 
        'items': items,
        'remaining_balance': remaining_balance,
        'previous_due': previous_due,
        'previous_total_paid': previous_total_paid,
        'display_class': display_class
    })

from django.urls import reverse
from django.utils import timezone

def collect_fee(request, pk):
    return redirect(f"{reverse('fee_collection')}?student_id={pk}")

def admit_pass_management(request):
    profile = request.user.profile
    search_query = request.GET.get('q', '')
    selected_student = None
    students_found = []
    
    if search_query:
        from django.db.models import Q
        students_found = Student.objects.filter(school=profile.school).filter(
            Q(name__icontains=search_query) | Q(pin__icontains=search_query)
        )
    
    student_id = request.GET.get('student_id')
    student_fee_summary = None
    if student_id:
        selected_student = get_object_or_404(Student, pk=student_id, school=profile.school)
        
        # Calculate Fee Status for Review
        total_due = sum(f.amount for f in selected_student.fees.all())
        total_paid = sum(f.paid_amount for f in selected_student.fees.all())
        student_fee_summary = {
            'total_due': total_due,
            'total_paid': total_paid,
            'outstanding': total_due - total_paid
        }
    
    # Recent Passes History (Admin Audit)
    recent_passes = AdmitPass.objects.filter(school=profile.school).order_by('-issued_date', '-id')[:10]
    
    if request.method == 'POST' and selected_student:
        title = request.POST.get('title')
        start_date = request.POST.get('start_date')
        end_date = request.POST.get('end_date')
        note = request.POST.get('note')
        
        ap = AdmitPass.objects.create(
            school=profile.school,
            student=selected_student,
            title=title,
            start_date=start_date,
            end_date=end_date,
            note=note
        )
        return redirect(f"{request.path}?student_id={selected_student.id}&success_ap=1&ap_id={ap.id}")

    return render(request, 'school/admit_pass_management.html', {
        'search_query': search_query,
        'students_found': students_found,
        'selected_student': selected_student,
        'student_fee_summary': student_fee_summary,
        'recent_passes': recent_passes,
        'today': timezone.now().date()
    })

def admit_pass_print(request, ap_id):
    profile = request.user.profile
    ap = get_object_or_404(AdmitPass, pk=ap_id, school=profile.school)
    return render(request, 'school/admit_pass_print.html', {'ap': ap})

def attendance_report(request):
    profile = request.user.profile
    classes = StudentClass.objects.filter(school=profile.school)
    selected_class = None
    class_id = request.GET.get('class_id')
    
    import calendar
    today = timezone.now().date()
    month = int(request.GET.get('month', today.month))
    year = int(request.GET.get('year', today.year))
    
    _, last_day = calendar.monthrange(year, month)
    days = range(1, last_day + 1)
    
    student_data = []
    if class_id:
        selected_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
        students = Student.objects.filter(student_class=selected_class, school=profile.school).order_by('roll')
        
        # Prefetch logs for the month
        from datetime import date as d_cls, datetime as dt_cls, time as t_cls
        start_date = d_cls(year, month, 1)
        end_date = d_cls(year, month, last_day)
        
        start_dt = timezone.make_aware(dt_cls.combine(start_date, t_cls.min))
        end_dt = timezone.make_aware(dt_cls.combine(end_date, t_cls.max))
        
        logs = AttendanceLog.objects.filter(
            student__in=students, 
            timestamp__range=(start_dt, end_dt),
            student_class=selected_class
        )
        
        leaves = StudentLeave.objects.filter(
            student__in=students,
            is_approved=True,
            start_date__lte=end_date,
            end_date__gte=start_date
        )
        
        holidays = Holiday.objects.filter(school=profile.school)
        
        for std in students:
            row = {'student': std, 'attendance': []}
            for day in days:
                cur_date = d_cls(year, month, day)
                status = 'A' # Default Absent
                
                # Holiday check (Fixed: Holiday model has 'date', not 'start_date/end_date')
                holiday_obj = next((h for h in holidays if h.date == cur_date), None)
                if holiday_obj:
                    if holiday_obj.day_type == 'HOLIDAY':
                        status = 'H'
                elif cur_date.weekday() == 4: # Friday
                    status = 'H'
                
                # Log check
                day_log = next((l for l in logs if l.student_id == std.id and timezone.localtime(l.timestamp).date() == cur_date), None)
                if day_log:
                    status = day_log.status[0] if day_log.status else 'P'
                else:
                    # Leave check if no log
                    if any(lv.student_id == std.id and lv.start_date <= cur_date <= lv.end_date for lv in leaves):
                        status = 'L'
                        
                row['attendance'].append(status)
            student_data.append(row)

    return render(request, 'school/attendance_report.html', {
        'classes': classes,
        'selected_class': selected_class,
        'days': days,
        'student_data': student_data,
        'current_month': month,
        'current_year': year,
        'month_name': calendar.month_name[month]
    })

def attendance_report_pdf(request):
    # PDF generation logic using ReportLab
    from io import BytesIO
    from reportlab.pdfgen import canvas
    from reportlab.lib.pagesizes import A4, landscape
    from reportlab.lib import colors
    from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph
    from reportlab.lib.styles import getSampleStyleSheet
    
    profile = request.user.profile
    class_id = request.GET.get('class_id')
    selected_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
    
    import calendar
    today = timezone.now().date()
    month = int(request.GET.get('month', today.month))
    year = int(request.GET.get('year', today.year))
    
    _, last_day = calendar.monthrange(year, month)
    days = range(1, last_day + 1)
    
    # Same logic as view to get data
    students = Student.objects.filter(student_class=selected_class, school=profile.school).order_by('roll')
    # ... (skipping repetition for brevity, using same logic below)
    
    from datetime import date as d_cls, datetime as dt_cls, time as t_cls
    start_date = d_cls(year, month, 1)
    end_date = d_cls(year, month, last_day)
    
    start_dt = timezone.make_aware(dt_cls.combine(start_date, t_cls.min))
    end_dt = timezone.make_aware(dt_cls.combine(end_date, t_cls.max))
    
    logs = AttendanceLog.objects.filter(student__in=students, timestamp__range=(start_dt, end_dt))
    leaves = StudentLeave.objects.filter(student__in=students, is_approved=True, start_date__lte=end_date, end_date__gte=start_date)
    holidays = Holiday.objects.filter(school=profile.school)

    buffer = BytesIO()
    doc = SimpleDocTemplate(buffer, pagesize=landscape(A4), topMargin=30)
    elements = []
    styles = getSampleStyleSheet()
    
    elements.append(Paragraph(f"<b>{profile.school.name}</b>", styles['Title']))
    elements.append(Paragraph(f"Attendance Report: {selected_class.name} | {calendar.month_name[month]} {year}", styles['Heading2']))
    
    # Table Matrix
    header = ['Roll', 'ID'] + [str(d) for d in days]
    data = [header]
    
    for std in students:
        row = [str(std.roll), std.pin]
        for day in days:
            cur_date = d_cls(year, month, day)
            status = '.'
            # Holiday check (Fixed: Holiday model has 'date', not 'start_date/end_date')
            holiday_obj = next((h for h in holidays if h.date == cur_date), None)
            if holiday_obj:
                if holiday_obj.day_type == 'HOLIDAY':
                    status = 'H'
            elif cur_date.weekday() == 4: # Friday
                status = 'H'
            
            # Use local time for log date check
            day_log = next((l for l in logs if l.student_id == std.id and timezone.localtime(l.timestamp).date() == cur_date), None)
            if day_log: status = day_log.status[0] if day_log.status else 'P'
            else:
                if any(lv.student_id == std.id and lv.start_date <= cur_date <= lv.end_date for lv in leaves):
                    status = 'L'
            row.append(status)
        data.append(row)

    # Creating table
    col_widths = [30, 60] + [16 for _ in days]
    table = Table(data, colWidths=col_widths)
    table.setStyle(TableStyle([
        ('BACKGROUND', (0,0), (-1,0), colors.HexColor('#0f172a')),
        ('TEXTCOLOR', (0,0), (-1,0), colors.whitesmoke),
        ('ALIGN', (0,0), (-1,-1), 'CENTER'),
        ('FONTNAME', (0,0), (-1,0), 'Helvetica-Bold'),
        ('FONTSIZE', (0,0), (-1,-1), 7),
        ('BOTTOMPADDING', (0,0), (-1,0), 8),
        ('GRID', (0,0), (-1,-1), 0.5, colors.grey),
        ('ROWBACKGROUNDS', (0, 1), (-1, -1), [colors.white, colors.HexColor('#f8fafc')]),
    ]))
    
    elements.append(table)
    doc.build(elements)
    
    
    pdf = buffer.getvalue()
    buffer.close()
    
    response = HttpResponse(content_type='application/pdf')
    response['Content-Disposition'] = f'attachment; filename="attendance_{selected_class.name}_{calendar.month_name[month]}.pdf"'
    response.write(pdf)
    return response

# --- SUPERADMIN SMS DASHBOARD ---
@user_passes_test(lambda u: u.is_superuser)
def super_sms_dashboard(request):
    from datetime import date as d_cls
    import calendar
    
    today = timezone.now().date()
    month_offset = int(request.GET.get('month_offset', 0))
    
    # Calculate target month and year
    total_months = today.year * 12 + today.month - 1 + month_offset
    year = total_months // 12
    month = (total_months % 12) + 1
    
    start_date = d_cls(year, month, 1)
    _, last_day = calendar.monthrange(year, month)
    end_date = d_cls(year, month, last_day)
    
    # Get all schools and their SMS totals
    schools = School.objects.all()
    school_report = []
    
    for sch in schools:
        # BUG FIX: Use datetime range (aware) instead of date__range to avoid UTC shifting
        from datetime import datetime as _dt, time as _t
        range_start = timezone.make_aware(_dt.combine(start_date, _t.min))
        range_end   = timezone.make_aware(_dt.combine(end_date,   _t.max))
        logs_count = sch.sms_logs.filter(timestamp__gte=range_start, timestamp__lte=range_end).count()
        school_report.append({
            'school': sch,
            'count': logs_count
        })
    
    context = {
        'school_report': school_report,
        'current_month_name': start_date.strftime('%B %Y'),
        'prev_offset': month_offset - 1,
        'next_offset': month_offset + 1,
    }
    return render(request, 'superuser/sms_dashboard.html', context)

@user_passes_test(lambda u: u.is_superuser)
def super_school_sms_details(request, school_id):
    from datetime import date as d_cls
    import calendar
    
    sch = get_object_or_404(School, id=school_id)
    today = timezone.now().date()
    month_offset = int(request.GET.get('month_offset', 0))
    
    total_months = today.year * 12 + today.month - 1 + month_offset
    year = total_months // 12
    month = (total_months % 12) + 1
    
    start_date = d_cls(year, month, 1)
    _, last_day = calendar.monthrange(year, month)
    end_date = d_cls(year, month, last_day)
    
    # BUG FIX: Aware datetime range for correct timezone handling
    from datetime import datetime as _dt, time as _t
    _range_start = timezone.make_aware(_dt.combine(start_date, _t.min))
    _range_end   = timezone.make_aware(_dt.combine(end_date,   _t.max))
    logs = sch.sms_logs.filter(timestamp__gte=_range_start, timestamp__lte=_range_end).order_by('-timestamp')
    
    context = {
        'school': sch,
        'logs': logs,
        'current_month_name': start_date.strftime('%B %Y'),
        'prev_offset': month_offset - 1,
        'next_offset': month_offset + 1,
        'total_count': logs.count()
    }
    return render(request, 'superuser/school_sms_details.html', context)

def approve_hold_punch(request, pk):
    profile = request.user.profile
    log = get_object_or_404(AttendanceLog, pk=pk, school=profile.school)
    
    if log.status not in ['HOLD', 'LATE_OUT']:
        messages.error(request, "This log is not on hold/late-out.")
        return redirect('attendance_log')

    action = request.GET.get('action')
    if action == 'accept':
        # Re-calc status based on timing
        p_time = timezone.localtime(log.timestamp).time()
        schedule = getattr(log.student.student_class, 'schedule', None)

        if log.check_type == 'IN':
            if schedule:
                if p_time < schedule.punch_in_start:
                    log.status = 'EARLY_IN'
                elif p_time <= schedule.punch_in_end:
                    log.status = 'PRESENT_IN'
                else:
                    log.status = 'LATE'
            else:
                log.status = 'PRESENT_IN'
        else: # OUT
            # Check if this student arrived before lunch and missed lunch punch
            first_in = AttendanceLog.objects.filter(student=log.student, timestamp__date=log.timestamp.date(), check_type='IN').first()
            arrival = timezone.localtime(first_in.timestamp).time() if first_in else p_time
            has_lunch = AttendanceLog.objects.filter(student=log.student, timestamp__date=log.timestamp.date(), status='LUNCH_BREAK').exists()
            
            # If Admin accepts, we override HOLD and LATE_OUT restrictions
            if schedule and arrival < schedule.lunch_start and p_time > schedule.lunch_end and not has_lunch:
                log.status = 'LUNCH_BREAK'
            else:
                # Keep it as Late Out but authorized
                log.status = 'LATE_OUT_APPROVED'
        
        log.save()
        send_attendance_sms(log, log.student, log.school, request=request)
        messages.success(request, f"Attendance approved for {log.student.name}.")

    elif action == 'reject':
        log.delete()
        messages.warning(request, f"Punch rejected and discarded.")
    
    return redirect(reverse('attendance_log') + '?type=hold')


# --- Public Attendance History (No Login Required) ---
from functools import wraps

def parent_login_required(view_func):
    @wraps(view_func)
    def _wrapped_view(request, *args, **kwargs):
        if 'p_student_id' not in request.session:
            # Check if this is an SMS link and try auto-login if possible (requires PIN/Secret logic)
            # For now, just redirect to login
            return redirect('parent_login')
        return view_func(request, *args, **kwargs)
    return _wrapped_view

def parent_login(request):
    """
    Parent entry point. Institution Code + PIN + Secret Code.
    Supports legacy SMS links: /history/?phone=...&id=...&code=...
    """
    error = None
    # Track inputs to repopulate on error
    school_code_val = request.POST.get('school_code', '').strip()
    pin_val = request.POST.get('pin', '').strip()

    if request.method == 'POST':
        secret_code = request.POST.get('secret_code', '').strip()
        
        try:
            student = Student.objects.get(
                pin=pin_val, 
                secret_code=secret_code, 
                school__code__iexact=school_code_val
            )
            request.session['p_student_id'] = student.id
            return redirect('parent_dashboard')
        except Student.DoesNotExist:
            error = "Invalid Details. Please check Institution Code, PIN and Secret Code."
    
    # Pre-fill logic for SMS links (if they are not already set by a POST attempt)
    if not school_code_val:
        school_code_val = request.GET.get('code', '').strip()
    
    prefill_pin = ""
    if not pin_val:
        sid_param = request.GET.get('id', '').strip()
        if sid_param:
            try:
                student = Student.objects.select_related('school').get(id=sid_param)
                prefill_pin = student.pin
                if not school_code_val:
                    school_code_val = student.school.code
            except: pass
    else:
        prefill_pin = pin_val

    return render(request, 'public/parent_login.html', {
        'error': error, 
        'prefill_pin': prefill_pin,
        'prefill_code': school_code_val
    })

def parent_logout(request):
    if 'p_student_id' in request.session:
        del request.session['p_student_id']
    return redirect('parent_login')

@parent_login_required
def parent_dashboard(request):
    sid = request.session['p_student_id']
    student = Student.objects.select_related('school', 'student_class').get(id=sid)
    
    # Overview Data
    # 1. Last Result
    last_res = ResultRecord.objects.filter(student=student).order_by('-created_at').first()
    
    # 2. Today's Attendance
    today = timezone.localtime(timezone.now()).date()
    today_log = AttendanceLog.objects.filter(student=student, timestamp__date=today).first()
    
    # 3. Pending Fee
    all_fees = StudentFee.objects.filter(student=student)
    total_fee = sum(f.amount for f in all_fees)
    total_paid = sum(f.paid_amount for f in all_fees)
    total_due = total_fee - total_paid

    context = {
        'student': student,
        'school': student.school,
        'last_res': last_res,
        'today_log': today_log,
        'total_due': total_due,
    }
    return render(request, 'public/parent_dashboard.html', context)

@parent_login_required
def parent_attendance(request):
    sid = request.session['p_student_id']
    student = Student.objects.get(id=sid)
    
    thirty_days_ago = timezone.now() - timedelta(days=60) # Show 2 months for parents
    logs = list(AttendanceLog.objects.filter(
        student=student,
        student_class=student.student_class,
        timestamp__gte=thirty_days_ago
    ).exclude(status__in=['FALSE_PUNCH', 'UNIDENTIFIED']).order_by('-timestamp')[:100])
    
    total_late = sum(1 for l in logs if l.status == 'LATE')
    total_present = sum(1 for l in logs if l.check_type == 'IN')

    return render(request, 'public/parent_attendance.html', {
        'student': student,
        'logs': logs,
        'total_late': total_late,
        'total_present': total_present
    })

@parent_login_required
def parent_results(request):
    sid = request.session['p_student_id']
    student = Student.objects.get(id=sid)
    
    results = ResultRecord.objects.filter(student=student).select_related('exam', 'subject').order_by('-exam__year', 'exam__name')
    results_grouped = {}
    for r in results:
        exam_key = f"{r.exam.name} ({r.exam.year})"
        if exam_key not in results_grouped: results_grouped[exam_key] = []
        results_grouped[exam_key].append(r)

    return render(request, 'public/parent_results.html', {
        'student': student,
        'results_grouped': results_grouped
    })

@parent_login_required
def parent_fees(request):
    sid = request.session['p_student_id']
    student = Student.objects.get(id=sid)
    
    transactions = FeeTransaction.objects.filter(student=student).prefetch_related('items__fee__fee_type').order_by('-timestamp')
    
    all_fees = StudentFee.objects.filter(student=student).select_related('fee_type')
    total_due = sum(f.amount - f.paid_amount for f in all_fees)

    return render(request, 'public/parent_fees.html', {
        'student': student,
        'transactions': transactions,
        'total_due': total_due
    })


def student_transactions(request):
    """
    Public view — shows payment transaction history for a student.
    URL: /transactions/?phone=<phone>&id=<student_id>&code=<school_code>
    Uses the same 3-param security validation as attendance_history.
    """
    phone = request.GET.get('phone', '').strip()
    student_id = request.GET.get('id', '').strip()
    school_code = request.GET.get('code', '').strip()

    error = None
    student = None
    school = None
    transactions = []
    total_paid = 0
    total_transactions = 0

    if not (phone and student_id and school_code):
        error = "Invalid or incomplete link. Please check the URL parameters."
    else:
        try:
            student = Student.objects.select_related('school', 'student_class').get(
                id=student_id,
                school__code__iexact=school_code,
            )
            school = student.school

            # Phone verification (normalise to last 11 digits for comparison)
            stored_phone = re.sub(r'\D', '', str(student.contact_number or ''))
            req_phone = re.sub(r'\D', '', phone)
            if stored_phone[-11:] != req_phone[-11:]:
                error = "Phone number does not match our records."
                student = None
            else:
                transactions = FeeTransaction.objects.filter(
                    student=student
                ).prefetch_related('items__fee__fee_type').order_by('-timestamp')

                total_transactions = transactions.count()
                total_paid = sum(t.total_amount for t in transactions)

        except Student.DoesNotExist:
            error = "Student not found. Please use the original link from the SMS."

    context = {
        'student': student,
        'school': school,
        'transactions': transactions,
        'error': error,
        'total_transactions': total_transactions,
        'total_paid': total_paid,
    }
    return render(request, 'public/student_transactions.html', context)


@require_POST
def verify_txn_secret(request):
    """
    API endpoint for the transaction modal on the history page.
    Verifies the student's secret code and returns their transactions.
    """
    import json
    
    student_id = request.POST.get('student_id', '').strip()
    phone = request.POST.get('phone', '').strip()
    school_code = request.POST.get('code', '').strip()
    secret_code = request.POST.get('secret_code', '').strip().upper()

    if not all([student_id, phone, school_code, secret_code]):
        return JsonResponse({'success': False, 'error': 'Missing parameters.'})

    try:
        student = Student.objects.select_related('school').get(
            id=student_id,
            school__code__iexact=school_code,
        )
        
        # Verify phone number
        stored_phone = re.sub(r'\D', '', str(student.contact_number or ''))
        req_phone = re.sub(r'\D', '', phone)
        if stored_phone[-11:] != req_phone[-11:]:
            return JsonResponse({'success': False, 'error': 'Invalid request.'})

        # Verify secret code (case-insensitive)
        if not student.secret_code or student.secret_code.upper() != secret_code:
            return JsonResponse({'success': False, 'error': 'Invalid secret code.'})

        # Fetch transactions
        transactions = FeeTransaction.objects.filter(student=student).prefetch_related('items__fee__fee_type').order_by('-timestamp')
        
        total_paid = sum(t.total_amount for t in transactions)
        
        txn_data = []
        for txn in transactions:
            items_data = [{'name': item.fee.fee_type.name, 'amount': float(item.amount_paid)} for item in txn.items.all()]
            txn_data.append({
                'id': txn.id,
                'method': txn.get_payment_method_display(),
                'total': float(txn.total_amount),
                'date': txn.timestamp.strftime('%d %b %Y, %I:%M %p'),
                'items': items_data
            })

        return JsonResponse({
            'success': True,
            'total_transactions': transactions.count(),
            'total_paid': float(total_paid),
            'transactions': txn_data
        })

    except Student.DoesNotExist:
        return JsonResponse({'success': False, 'error': 'Student not found.'})
    except Exception as e:
        return JsonResponse({'success': False, 'error': 'An error occurred.'})

@login_required
@require_POST
def regenerate_secret_code(request, pk):
    import random, string
    profile = request.user.profile
    try:
        student = Student.objects.get(pk=pk, school=profile.school)
        chars = string.digits
        while True:
            code = ''.join(random.choices(chars, k=5))
            if not Student.objects.filter(school=profile.school, secret_code=code).exists():
                break
        student.secret_code = code
        student.save()
        return JsonResponse({'success': True, 'new_code': code})
    except Student.DoesNotExist:
        return JsonResponse({'success': False, 'error': 'Student not found.'})

@login_required
def api_check_duplicate_student(request):
    profile = request.user.profile
    pin = request.GET.get('pin', '').strip()
    roll = request.GET.get('roll', '').strip()
    class_id = request.GET.get('class_id', '').strip()
    exclude_id = request.GET.get('exclude_id', '').strip()
    
    pin_exists = False
    roll_exists = False
    
    # Querysets
    qs_pin = Student.objects.filter(school=profile.school, pin=pin)
    qs_roll = Student.objects.filter(school=profile.school, student_class_id=class_id, roll=roll)
    
    if exclude_id and exclude_id.isdigit():
        qs_pin = qs_pin.exclude(id=exclude_id)
        qs_roll = qs_roll.exclude(id=exclude_id)
    
    if pin:
        pin_exists = qs_pin.exists()
        
    if roll and class_id:
        roll_exists = qs_roll.exists()
        
    return JsonResponse({
        'pin_exists': pin_exists,
        'roll_exists': roll_exists
    })

# --- Teacher Management ---
def teacher_list(request):
    profile = request.user.profile
    school = profile.school
    query = request.GET.get('q')
    
    teachers = UserProfile.objects.filter(school=school, role='TEACHER').select_related('user')
    
    if query:
        teachers = teachers.filter(
            models.Q(user__first_name__icontains=query) |
            models.Q(phone_number__icontains=query)
        )
        
    return render(request, 'school/teacher_list.html', {
        'teachers': teachers,
        'query': query,
        'school': school
    })

def teacher_add(request):
    profile = request.user.profile
    school = profile.school
    
    if request.method == 'POST':
        name = request.POST.get('name')
        contact = request.POST.get('contact')
        password = request.POST.get('password')
        nid = request.POST.get('nid')
        address = request.POST.get('address')
        dob = request.POST.get('dob') or None
        qualification = request.POST.get('qualification')
        
        if not name or not contact or not password:
            messages.error(request, "Name, Contact Number, and Password are required.")
            return redirect('teacher_add')
            
        username = contact
        if User.objects.filter(username=username).exists():
            import string
            import random
            random_str = ''.join(random.choices(string.digits, k=4))
            username = f"{contact}_{random_str}"
            
        try:
            user = User.objects.create_user(username=username, password=password, first_name=name)
            UserProfile.objects.create(
                user=user,
                school=school,
                role='TEACHER',
                phone_number=contact,
                nid=nid,
                address=address,
                dob=dob,
                qualification=qualification
            )
            messages.success(request, f"Teacher {name} added successfully!")
            return redirect('teacher_list')
        except Exception as e:
            messages.error(request, f"Error adding teacher: {str(e)}")
            return redirect('teacher_add')

    return render(request, 'school/teacher_form.html')

def teacher_edit(request, pk):
    profile = request.user.profile
    school = profile.school
    teacher_profile = get_object_or_404(UserProfile, pk=pk, school=school, role='TEACHER')
    
    if request.method == 'POST':
        name = request.POST.get('name')
        contact = request.POST.get('contact')
        password = request.POST.get('password')
        
        teacher_profile.user.first_name = name
        if password:
            teacher_profile.user.set_password(password)
        teacher_profile.user.save()
        
        teacher_profile.phone_number = contact
        teacher_profile.nid = request.POST.get('nid')
        teacher_profile.address = request.POST.get('address')
        teacher_profile.dob = request.POST.get('dob') or None
        teacher_profile.qualification = request.POST.get('qualification')
        teacher_profile.save()
        
        messages.success(request, "Teacher details updated successfully.")
        return redirect('teacher_list')
        
    return render(request, 'school/teacher_form.html', {'teacher': teacher_profile, 'is_edit': True})

def teacher_terminate(request, pk):
    profile = request.user.profile
    school = profile.school
    teacher_profile = get_object_or_404(UserProfile, pk=pk, school=school, role='TEACHER')
    
    # Toggle active status
    user = teacher_profile.user
    user.is_active = not user.is_active
    user.save()
    
    status_str = "activated" if user.is_active else "terminated"
    messages.success(request, f"Teacher account has been {status_str}.")
    return redirect('teacher_list')

def teacher_delete(request, pk):
    profile = request.user.profile
    school = profile.school
    teacher_profile = get_object_or_404(UserProfile, pk=pk, school=school, role='TEACHER')
    
    teacher_profile.user.delete() # this cascades and deletes the profile as well
    messages.success(request, "Teacher deleted successfully.")
    return redirect('teacher_list')


def teacher_dashboard(request):
    profile = request.user.profile
    if profile.role != 'TEACHER':
        return redirect('school_dashboard')
    
    assigned_classes = profile.assigned_classes.all()
    all_students = Student.objects.filter(student_class__in=assigned_classes)
    
    today = timezone.now().date()
    # Today's logs
    day_start = timezone.make_aware(datetime.combine(today, time.min))
    day_end = timezone.make_aware(datetime.combine(today, time.max))
    
    present_count = AttendanceLog.objects.filter(
        student__student_class__in=assigned_classes,
        timestamp__range=(day_start, day_end)
    ).values('student').distinct().count()
    
    absent_count = all_students.count() - present_count

    return render(request, 'teacher/dashboard.html', {
        'profile': profile,
        'present_count': present_count,
        'absent_count': absent_count,
        'total_students': all_students.count()
    })

def teacher_attendance(request):
    profile = request.user.profile
    if profile.role != 'TEACHER':
        return redirect('school_dashboard')
    
    assigned_classes = profile.assigned_classes.all()
    all_students = Student.objects.filter(student_class__in=assigned_classes)

    # Date filter
    date_str = request.GET.get('date')
    if date_str:
        try: target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
        except: target_date = timezone.now().date()
    else:
        target_date = timezone.now().date()
        
    day_start = timezone.make_aware(datetime.combine(target_date, time.min))
    day_end = timezone.make_aware(datetime.combine(target_date, time.max))

    # Logs for the date
    logs = AttendanceLog.objects.filter(
        student__student_class__in=assigned_classes,
        timestamp__range=(day_start, day_end)
    )
    
    present_student_ids = logs.values_list('student_id', flat=True).distinct()
    
    # Enrichment for Present Students (getting first/last punch)
    present_list = []
    found_present = all_students.filter(id__in=present_student_ids)
    
    # Search
    query = request.GET.get('q', '').strip()
    if query:
        search_filter = Q(name__icontains=query) | Q(pin__icontains=query)
        found_present = found_present.filter(search_filter)
        absent_query_students = all_students.exclude(id__in=present_student_ids).filter(search_filter)
    else:
        absent_query_students = all_students.exclude(id__in=present_student_ids)

    for student in found_present:
        s_logs = logs.filter(student=student).order_by('timestamp')
        punch_in = s_logs.first().timestamp if s_logs.exists() else None
        punch_out = s_logs.last().timestamp if s_logs.count() > 1 else None
        
        is_edited = s_logs.filter(is_manually_edited=True).exists()
        
        present_list.append({
            'student': student,
            'punch_in': punch_in,
            'punch_out': punch_out,
            'is_edited': is_edited
        })

    return render(request, 'teacher/attendance.html', {
        'present_list': present_list,
        'absent_students': absent_query_students,
        'target_date': target_date,
        'query': query
    })

@require_POST
def teacher_edit_attendance(request):
    profile = request.user.profile
    if profile.role != 'TEACHER':
        return JsonResponse({'status': 'error', 'message': 'Unauthorized'}, status=403)
    
    student_id = request.POST.get('student_id')
    date_str = request.POST.get('date') # YYYY-MM-DD
    punch_in_str = request.POST.get('punch_in') # HH:MM
    punch_out_str = request.POST.get('punch_out') # HH:MM
    
    student = get_object_or_404(Student, id=student_id, student_class__teacher=profile)
    
    try: target_date = datetime.strptime(date_str, '%Y-%m-%d').date()
    except: return JsonResponse({'status': 'error', 'message': 'Invalid date'})

    day_start = timezone.make_aware(datetime.combine(target_date, time.min))
    day_end = timezone.make_aware(datetime.combine(target_date, time.max))
    
    # Check if already edited
    already_edited = AttendanceLog.objects.filter(
        student=student, 
        timestamp__range=(day_start, day_end),
        is_manually_edited=True
    ).exists()
    
    if already_edited:
        return JsonResponse({'status': 'error', 'message': 'Attendance already edited. Only one edit allowed.'})

    # Clear existing non-edited logs for this day? Or just mark ours?
    # Usually we want to replace them if we are manually setting it.
    # But user said "edit".
    
    # Process Punch In
    if punch_in_str:
        p_in_time = datetime.strptime(punch_in_str, '%H:%M').time()
        p_in_dt = timezone.make_aware(datetime.combine(target_date, p_in_time))
        AttendanceLog.objects.create(
            student=student, school=student.school, 
            timestamp=p_in_dt, check_type='IN', status='PRESENT_IN',
            is_manually_edited=True,
            student_class=student.student_class
        )
        
    # Process Punch Out
    if punch_out_str:
        p_out_time = datetime.strptime(punch_out_str, '%H:%M').time()
        p_out_dt = timezone.make_aware(datetime.combine(target_date, p_out_time))
        AttendanceLog.objects.create(
            student=student, school=student.school, 
            timestamp=p_out_dt, check_type='OUT', status='PRESENT_OUT',
            is_manually_edited=True,
            student_class=student.student_class
        )
        
    return JsonResponse({'status': 'ok'})

# --- Result Management ---
def exam_list(request):
    profile = request.user.profile
    exams = Exam.objects.filter(school=profile.school).order_by('-year', 'name')
    classes = StudentClass.objects.filter(school=profile.school)
    return render(request, 'school/exam_list.html', {'exams': exams, 'classes': classes})

def exam_add(request):
    profile = request.user.profile
    if request.method == 'POST':
        name = request.POST.get('name')
        year = request.POST.get('year', 2026)
        Exam.objects.create(school=profile.school, name=name, year=year)
        messages.success(request, f"Exam {name} created.")
        return redirect('exam_list')
    return render(request, 'school/exam_form.html')

def subject_list(request):
    profile = request.user.profile
    groups = SubjectGroup.objects.filter(school=profile.school)
    subjects = Subject.objects.filter(school=profile.school).select_related('group').prefetch_related('classes').order_by('name')
    
    group_filter = request.GET.get('group_id')
    if group_filter:
        subjects = subjects.filter(group_id=group_filter)
        
    for sub in subjects:
        # Sum of students in all classes assigned to this subject
        sub.student_count = Student.objects.filter(student_class__in=sub.classes.all()).count()
        
    return render(request, 'school/subject_list.html', {
        'subjects': subjects,
        'groups': groups,
        'selected_group': group_filter
    })

def subject_edit(request, pk):
    profile = request.user.profile
    subject = get_object_or_404(Subject, pk=pk, school=profile.school)
    groups = SubjectGroup.objects.filter(school=profile.school)
    
    if request.method == 'POST':
        subject.name = request.POST.get('name')
        subject.code = request.POST.get('code')
        group_id = request.POST.get('group_id')
        
        group = None
        if group_id:
            group = SubjectGroup.objects.filter(school=profile.school, pk=group_id).first()
        
        subject.group = group
        subject.save()
        messages.success(request, f"Subject {subject.name} updated.")
        return redirect('subject_list')
    return render(request, 'school/subject_form.html', {'subject': subject, 'is_edit': True, 'groups': groups})

def subject_delete(request, pk):
    profile = request.user.profile
    subject = get_object_or_404(Subject, pk=pk, school=profile.school)
    name = subject.name
    subject.delete()
    messages.success(request, f"Subject {name} deleted.")
    return redirect('subject_list')

def subject_group_add(request):
    profile = request.user.profile
    if request.method == 'POST':
        name = request.POST.get('name')
        SubjectGroup.objects.create(school=profile.school, name=name)
        messages.success(request, f"Subject Group {name} created.")
        return redirect('subject_list')
    return render(request, 'school/subject_group_form.html')

def subject_add(request):
    profile = request.user.profile
    groups = SubjectGroup.objects.filter(school=profile.school)
    
    if request.method == 'POST':
        name = request.POST.get('name')
        code = request.POST.get('code')
        group_id = request.POST.get('group_id')
        
        group = None
        if group_id:
            group = SubjectGroup.objects.filter(school=profile.school, pk=group_id).first()
            
        Subject.objects.create(school=profile.school, name=name, code=code, group=group)
        messages.success(request, f"Subject {name} created.")
        return redirect('subject_list')
    return render(request, 'school/subject_form.html', {'groups': groups})

def mark_entry(request):
    profile = request.user.profile
    classes = StudentClass.objects.filter(school=profile.school)
    exams = Exam.objects.filter(school=profile.school)
    subjects = Subject.objects.filter(school=profile.school)
    
    selected_class = request.GET.get('class_id')
    selected_exam = request.GET.get('exam_id')
    selected_subject = request.GET.get('subject_id')
    
    students = []
    if selected_class and selected_exam and selected_subject:
        cls = get_object_or_404(StudentClass, pk=selected_class, school=profile.school)
        exm = get_object_or_404(Exam, pk=selected_exam, school=profile.school)
        sub = get_object_or_404(Subject, pk=selected_subject, school=profile.school)
        
        students_qs = Student.objects.filter(student_class=cls, school=profile.school, subjects=sub).order_by('roll')
        existing_records = {r.student_id: r for r in ResultRecord.objects.filter(exam=exm, subject=sub, student__in=students_qs)}
        
        for std in students_qs:
            students.append({
                'student': std,
                'record': existing_records.get(std.id)
            })

    if request.method == 'POST':
        exam_id = request.POST.get('exam_id')
        subject_id = request.POST.get('subject_id')
        exm = get_object_or_404(Exam, pk=exam_id, school=profile.school)
        sub = get_object_or_404(Subject, pk=subject_id, school=profile.school)
        
        for key, value in request.POST.items():
            if key.startswith('marks_'):
                student_id = key.split('_')[1]
                if value.strip():
                    marks = decimal.Decimal(value)
                    ResultRecord.objects.update_or_create(
                        student_id=student_id, exam=exm, subject=sub,
                        defaults={'marks': marks}
                    )
        messages.success(request, "Marks updated successfully.")
        return redirect(f"{request.path}?class_id={request.POST.get('class_id')}&exam_id={exam_id}&subject_id={subject_id}")

    return render(request, 'school/mark_entry.html', {
        'classes': classes,
        'exams': exams,
        'subjects': subjects,
        'students': students,
        'selected_class': selected_class,
        'selected_exam': selected_exam,
        'selected_subject': selected_subject
    })

def api_send_result_sms(request):
    profile = request.user.profile
    record_id = request.GET.get('record_id')
    record = get_object_or_404(ResultRecord, pk=record_id, student__school=profile.school)
    
    if not profile.school.is_exam_sms_enabled:
        return JsonResponse({'success': False, 'error': 'Exam SMS is disabled for this institution.'})

    if record.student.contact_number:
        if not check_and_increment_sms_limit(record.student, 'EXAM'):
            return JsonResponse({'success': False, 'error': 'Monthly exam SMS limit reached for this student.'})
            
        # Standard logic for result message
        # Format: Result: [Name], [Exam], [Subject]: Marks [M], Grade [G] - [School]
        msg = f"Result: {record.student.name[:15]}, {record.exam.name}, {record.subject.name}: Marks {record.marks}, Grade {record.grade}. - {profile.school.name}"
        
        sent = send_generic_school_sms(record.student.contact_number, msg, profile.school)
        if sent:
            record.is_sms_sent = True
            record.save()
            return JsonResponse({'success': True})
    return JsonResponse({'success': False, 'error': 'Failed to send SMS or missing phone'})

def grade_config_list(request):
    profile = request.user.profile
    configs = GradeConfig.objects.filter(school=profile.school).order_by('-min_marks')
    return render(request, 'school/grade_config_list.html', {'configs': configs})

def grade_config_add(request):
    profile = request.user.profile
    if request.method == 'POST':
        name = request.POST.get('grade_name')
        min_m = request.POST.get('min_marks')
        max_m = request.POST.get('max_marks')
        gpa = request.POST.get('gpa_value')
        GradeConfig.objects.create(school=profile.school, grade_name=name, min_marks=min_m, max_marks=max_m, gpa_value=gpa)
        messages.success(request, f"Grade {name} added!")
        return redirect('grade_config_list')
    return render(request, 'school/grade_config_form.html')

def api_check_exam_sms_status(request):
    profile = request.user.profile
    exam_id = request.GET.get('exam_id')
    class_id = request.GET.get('class_id')
    
    if not exam_id or not class_id:
        return JsonResponse({'sent': False})
        
    # We consider SMS "sent" if all students with records in this class/exam have is_sms_sent=True
    sent_all = ResultRecord.objects.filter(
        exam_id=exam_id, 
        student__student_class_id=class_id, 
        student__school=profile.school
    ).exclude(is_sms_sent=True).exists()
    
    return JsonResponse({'sent': not sent_all})

def api_exam_bulk_sms(request):
    profile = request.user.profile
    
    if not profile.school.is_exam_sms_enabled:
        return JsonResponse({'success': False, 'error': 'Exam SMS is disabled for this institution.'})

    # Global Broadcast Limit Check
    if profile.school.monthly_exam_limit_per_student > 0 and profile.school.exams_sent_this_month >= profile.school.monthly_exam_limit_per_student:
        return JsonResponse({'success': False, 'error': f'Monthly result broadcast limit reached ({profile.school.monthly_exam_limit_per_student}).'})

    exam_id = request.GET.get('exam_id')
    class_id = request.GET.get('class_id')
    
    exam = get_object_or_404(Exam, pk=exam_id, school=profile.school)
    cls = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
    
    students = Student.objects.filter(student_class=cls, school=profile.school, is_active=True).order_by('roll')
    count = 0
    for std in students:
        records = ResultRecord.objects.filter(student=std, exam=exam).select_related('subject')
        # Skip if already sent or no records
        if not records.exists() or records.filter(is_sms_sent=True).exists(): continue
        
        sub_summaries = []
        total_gpa = 0
        total_marks = 0
        for r in records:
            sub_summaries.append(f"{r.subject.name[:10]}: {int(r.marks)}({r.grade})")
            total_gpa += float(r.gpa)
            total_marks += float(r.marks)
        
        count_records = records.count()
        avg_gpa = round(total_gpa / count_records, 2)
        summary_str = ", ".join(sub_summaries)
        
        header = f"{exam.name} Result: {std.name[:15]}, Roll:{std.roll or 'NA'}"
        footer = f"Total:{int(total_marks)}, GPA:{avg_gpa}. - {profile.school.name}"
        
        msg = f"{header}\n{summary_str}\n{footer}"

        if std.contact_number:
            if send_generic_school_sms(std.contact_number, msg, profile.school):
                count += 1
                records.update(is_sms_sent=True)
                
    if count > 0:
        profile.school.exams_sent_this_month += 1
        profile.school.save()
        
    return JsonResponse({'success': True, 'count': count})

def exam_result_print(request):
    profile = request.user.profile
    exam_id = request.GET.get('exam_id')
    class_id = request.GET.get('class_id')
    
    if not exam_id or not class_id:
        exams = Exam.objects.filter(school=profile.school).order_by('-year', 'id')
        classes = StudentClass.objects.filter(school=profile.school).order_by('name')
        return render(request, 'school/exam_result_select.html', {'exams': exams, 'classes': classes})
    
    exam = get_object_or_404(Exam, pk=exam_id, school=profile.school)
    student_class = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
    
    # 1. Fetch subjects present in this exam for this class
    subjects_ids = ResultRecord.objects.filter(
        exam=exam, student__student_class=student_class
    ).values_list('subject_id', flat=True).distinct()
    subjects = Subject.objects.filter(id__in=subjects_ids).order_by('id')
    
    # 2. Fetch students and their marks
    students = Student.objects.filter(student_class=student_class, is_active=True).order_by('roll')
    result_data = []
    
    for std in students:
        marks_dict = {}
        total = 0
        records = ResultRecord.objects.filter(student=std, exam=exam)
        if not records.exists(): continue
        
        for r in records:
            marks_dict[r.subject_id] = r.marks
            total += r.marks
            
        row = {
            'roll': std.roll,
            'name': std.name,
            'marks': [marks_dict.get(sub.id, '-') for sub in subjects],
            'total': int(total)
        }
        result_data.append(row)
        
    return render(request, 'school/exam_result_print.html', {
        'exam': exam,
        'student_class': student_class,
        'subjects': subjects,
        'results': result_data,
        'school': profile.school
    })

def virtual_class_list(request):
    profile = request.user.profile
    v_classes = VirtualClass.objects.filter(school=profile.school).select_related('student_class', 'teacher')
    return render(request, 'school/virtual_class_list.html', {'v_classes': v_classes})

def virtual_class_add(request):
    profile = request.user.profile
    teachers = UserProfile.objects.filter(school=profile.school, role='TEACHER', user__is_active=True).select_related('user')
    student_classes = StudentClass.objects.filter(school=profile.school)
    
    if request.method == 'POST':
        title = request.POST.get('title')
        class_id = request.POST.get('class_id')
        teacher_id = request.POST.get('teacher_id')
        join_link = request.POST.get('join_link')
        meeting_code = request.POST.get('meeting_code')
        schedule_time = request.POST.get('schedule_time')
        subject_ids = request.POST.getlist('subject_ids')
        student_ids = request.POST.getlist('student_ids')
        send_sms = request.POST.get('send_sms') == 'on'
        
        cls_obj = get_object_or_404(StudentClass, pk=class_id, school=profile.school)
        teacher = UserProfile.objects.filter(school=profile.school, pk=teacher_id).first()
        
        v_class = VirtualClass.objects.create(
            school=profile.school,
            student_class=cls_obj,
            teacher=teacher,
            title=title,
            join_link=join_link,
            meeting_code=meeting_code,
            schedule_time=schedule_time
        )
        
        if subject_ids:
            v_class.subjects.set(Subject.objects.filter(school=profile.school, pk__in=subject_ids))
        if student_ids:
            v_class.students.set(Student.objects.filter(school=profile.school, pk__in=student_ids))
            
        if send_sms:
            if not profile.school.is_online_meeting_sms_enabled:
                messages.warning(request, "Class scheduled, but Online Meeting SMS is disabled for this institution.")
                return redirect('virtual_class_list')
            
            # Global Broadcast Limit Check
            if profile.school.monthly_meeting_limit_per_student > 0 and profile.school.meetings_sent_this_month >= profile.school.monthly_meeting_limit_per_student:
                messages.error(request, f"Monthly virtual class SMS limit reached ({profile.school.monthly_meeting_limit_per_student}).")
                return redirect('virtual_class_list')

            # Send SMS to parents
            count = 0
            # Convert schedule_time string to pretty date
            try:
                dt_obj = datetime.strptime(schedule_time, "%Y-%m-%dT%H:%M")
                time_str = dt_obj.strftime("%d %b %I:%M %p")
            except:
                time_str = schedule_time
                
            msg = f"{profile.school.name}: Virtual Class on '{title}' at {time_str}. Join: {join_link}"
            if meeting_code:
                msg += f" Code: {meeting_code}"
            
            invited_students = v_class.students.all()
            for std in invited_students:
                if std.contact_number:
                    if not check_and_increment_sms_limit(std, 'MEETING'):
                        continue
                    if send_generic_school_sms(std.contact_number, msg, profile.school):
                        count += 1
            v_class.is_sms_sent = True
            v_class.save()
            
            # Increment School Counter
            profile.school.meetings_sent_this_month += 1
            profile.school.save()
            
            messages.success(request, f"Class scheduled and SMS sent to {count} parents.")
        else:
            messages.success(request, f"Class scheduled successfully.")
            
        return redirect('virtual_class_list')
        
    return render(request, 'school/virtual_class_form.html', {
        'teachers': teachers,
        'student_classes': student_classes
    })

def virtual_class_delete(request, pk):
    profile = request.user.profile
    v_class = get_object_or_404(VirtualClass, pk=pk, school=profile.school)
    v_class.delete()
    messages.success(request, "Virtual class cancelled.")
    return redirect('virtual_class_list')

def api_get_students_for_subjects(request):
    profile = request.user.profile
    class_id = request.GET.get('class_id')
    subject_ids = request.GET.getlist('subject_ids[]')
    
    if not class_id: return JsonResponse({'students': []})
    
    students_qs = Student.objects.filter(student_class_id=class_id, school=profile.school, is_active=True)
    if subject_ids:
        students_qs = students_qs.filter(subjects__id__in=subject_ids).distinct()
    
    students = list(students_qs.values('id', 'name', 'roll'))
    return JsonResponse({'students': students})

# --- Superuser Announcements & Subscriptions ---
@user_passes_test(lambda u: u.is_superuser or (hasattr(u, 'profile') and u.profile.role == 'SUPERUSER'))
def super_announcement_list(request):
    announcements = Announcement.objects.all().order_by('-created_at')
    return render(request, 'superuser/announcement_list.html', {'announcements': announcements})

@user_passes_test(lambda u: u.is_superuser or (hasattr(u, 'profile') and u.profile.role == 'SUPERUSER'))
def super_announcement_add(request):
    if request.method == 'POST':
        message = request.POST.get('message')
        is_bulk = request.POST.get('is_bulk') == 'on'
        school_ids = request.POST.getlist('schools')
        
        announcement = Announcement.objects.create(message=message, is_bulk=is_bulk)
        if not is_bulk and school_ids:
            announcement.schools.set(school_ids)
            
        messages.success(request, "Announcement broadcasted successfully!")
        return redirect('super_announcement_list')
        
    schools = School.objects.all().order_by('name')
    return render(request, 'superuser/announcement_form.html', {'schools': schools})

@user_passes_test(lambda u: u.is_superuser or (hasattr(u, 'profile') and u.profile.role == 'SUPERUSER'))
def super_announcement_delete(request, pk):
    announcement = get_object_or_404(Announcement, pk=pk)
    announcement.delete()
    messages.success(request, "Announcement deleted.")
    return redirect('super_announcement_list')

@csrf_exempt
def api_set_subscription_limit(request, pk):
    is_super = request.user.is_superuser
    profile = getattr(request.user, 'profile', None)
    if profile and profile.role == 'SUPERUSER':
        is_super = True
    if not is_super:
        return JsonResponse({'error': 'Unauthorized'}, status=403)
        
    school = get_object_or_404(School, pk=pk)
    if request.method == 'POST':
        import json
        try:
            data = json.loads(request.body)
            end_date = data.get('end_date')
            if end_date:
                school.subscription_end_date = datetime.strptime(end_date, '%Y-%m-%d').date()
            else:
                school.subscription_end_date = None
            school.save()
            return JsonResponse({'status': 'ok'})
        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)}, status=400)
    return JsonResponse({'status': 'error'}, status=400)

@csrf_exempt
def api_ack_announcement(request, pk):
    profile = getattr(request.user, 'profile', None)
    if not profile or profile.role != 'SCHOOL_ADMIN':
        return JsonResponse({'status': 'error'}, status=403)
        
    announcement = get_object_or_404(Announcement, pk=pk)
    announcement.seen_by.add(profile.school)
    return JsonResponse({'status': 'ok'})


# --- School Level Up (Student Promotion) ---
@login_required(login_url='/login/')
def school_level_up(request):
    profile = request.user.profile
    school = profile.school

    classes_qs = StudentClass.objects.filter(school=school)

    # Annotate each class with the max roll of its current students
    class_list = []
    for cls in classes_qs:
        max_roll = Student.objects.filter(student_class=cls, school=school, is_active=True).aggregate(
            max_roll=models.Max('roll')
        )['max_roll'] or 0
        cls.max_roll = max_roll
        class_list.append(cls)

    # --- AJAX: Load students for a class ---
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest' and request.method == 'GET':
        class_id = request.GET.get('class_id')
        students_qs = Student.objects.filter(student_class_id=class_id, school=school, is_active=True).order_by('roll')
        student_data = []
        for s in students_qs:
            student_data.append({
                'id': s.id,
                'name': s.name,
                'roll': s.roll or '',
                'photo': s.photo.url if s.photo else None,
            })
        return JsonResponse({'students': student_data})

    # --- POST: Promote student to new class ---
    if request.method == 'POST' and request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        student_id = request.POST.get('student_id')
        new_class_id = request.POST.get('new_class_id')
        new_roll = request.POST.get('new_roll')

        try:
            student = get_object_or_404(Student, pk=student_id, school=school)
            new_class = get_object_or_404(StudentClass, pk=new_class_id, school=school)
            old_class = student.student_class

            # --- Session Isolation: Freeze old class data ---
            if old_class and old_class != new_class:
                # 1. Ensure PromotionRecord exists for the OLD class
                PromotionRecord.objects.get_or_create(
                    student=student,
                    student_class=old_class,
                    defaults={'roll': student.roll}
                )
                # 2. Stamp any orphaned fees/results with old class
                StudentFee.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)
                ResultRecord.objects.filter(student=student, student_class__isnull=True).update(student_class=old_class)

                # 3. Update subscriptions to new class defaults
                new_class_ft = new_class.fee_types.all()
                student.assigned_fee_types.set(new_class_ft)

                # 4. Create current period records for the new class
                today = timezone.localtime().date()
                for ft in new_class_ft:
                    if ft.frequency == 'ONCE':
                        StudentFee.objects.get_or_create(student=student, fee_type=ft, student_class=new_class, defaults={'amount': ft.default_amount})
                    elif ft.frequency == 'MONTHLY':
                        StudentFee.objects.get_or_create(student=student, fee_type=ft, month=today.month, year=today.year, student_class=new_class, defaults={'amount': ft.default_amount})
                    elif ft.frequency == 'YEARLY':
                        StudentFee.objects.get_or_create(student=student, fee_type=ft, year=today.year, student_class=new_class, defaults={'amount': ft.default_amount})

            student.student_class = new_class
            if new_roll:
                student.roll = int(new_roll)
            student.save()

            # Return last assigned roll in that class
            last_roll = Student.objects.filter(student_class=new_class, school=school, is_active=True).aggregate(
                max_roll=models.Max('roll')
            )['max_roll'] or 0

            return JsonResponse({
                'status': 'ok',
                'msg': f'Student "{student.name}" has been successfully promoted to {new_class.name}.',
                'last_roll': last_roll
            })
        except Exception as e:
            return JsonResponse({'status': 'error', 'msg': str(e)}, status=400)

    return render(request, 'school/level_up.html', {'classes': class_list})
