# exams/views.py
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth import authenticate, login, get_user_model
from django.http import JsonResponse, HttpResponse
from django.views.decorators.http import require_GET
from django.db.models import Avg, Count, Sum, Q, FloatField, F
from django.contrib import messages
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import PasswordChangeForm
   
from django.db import transaction
 
from django.http import HttpResponseForbidden

from django.template.loader import render_to_string

from django.db.models.functions import Coalesce
from django.core.paginator import Paginator


from django.views.decorators.http import require_POST
from django.db import connection

from exams.forms import ExamTypeForm

# from slugify import slugify
# from django.utils.text import slugify
import pprint

from io import BytesIO
import csv, io
import logging
import math

# from xhtml2pdf import pisa

# Excel (openpyxl)
import openpyxl
from openpyxl import Workbook
from openpyxl.utils import get_column_letter
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
# PDF (ReportLab)
from reportlab.platypus import (
    SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, Flowable
)
from reportlab.lib.pagesizes import A4, A3, landscape
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import mm, inch
# Excel

# Forms
from .forms import UserUpdateForm, ProfileUpdateForm



from exams.views_helpers import (build_region_summary_context,
build_school_rank_data,
compute_district_ranking,build_best_worst_tables,
build_ward_rank_data,get_region_subject_summary,
get_district_subject_ranking,
get_regional_subject_school_ranking,
build_district_ward_ranking_context,
build_district_school_ranking_context,
build_district_top_bottom_context,
build_district_best_worst_tables,
build_district_subject_summary,
get_district_subject_school_ranking,
build_district_trend,
build_ward_trend, 
build_school_trend,
build_ward_summary_context,
build_school_summary_context,
build_school_subject_performance,
build_school_trend,
compute_school_trend,
compute_region_trend,
build_school_subject_summary_context,
compute_region_school_rank,
compute_region_school_rank_ext,
compute_subject_district_rank,
compute_subject_school_rank,
compute_subject_school_rank_ext,
build_district_summary_context,
build_district_trend,
compute_district_school_rank,
compute_district_school_rank_ext,
compute_subject_dis_school_rank,
compute_subject_dis_school_rank_ext,
compute_school_subject_trend,
compute_district_subject_trend,
compute_region_subject_trend,
build_school_trend_context
) # your existing helper


from exams.app_utils.pdf.pdf_tables import (
    pdf_header_block_simple, pdf_students_block, pdf_grades_block,
    pdf_ownership_block, pdf_district_summary_block, pdf_footer_block,
    pdf_school_ranking_block,pdf_ward_ranking_table,
    simple_rank_table
)
from exams.app_utils.pdf.autofit import PAGE_USABLE_WIDTH
# Models

import os, tempfile
from decimal import Decimal, ROUND_HALF_UP

from exams.app_utils.pdf.pdf_tables import pdf_best_worst_group
from exams.app_utils.pdf.styles import VerticalText

from .models import (
    Region, District, Ward, School,
    ExamType, ExamSubject,
    ExamCandidate, StudentSubjectMark,
    SchoolYearSummary, UserProfile, SchoolSubjectPerformance,ExamSubject,
    SchoolDivisionSummary,SchoolSubjectGradeSummary
)



from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .models import (
    AcademicLevel,
    JointExamProject,
    GradingRule,
    DivisionRule,
    GpaRule,
    ExamCombination,
    ProjectStudent,
    ExamSubjectPaper,
    CombinationPaper,
    SubjectProcessingRule,
    UploadProgress,
    StudentFinalResult,
    ExamSubjectResult,
    ExamSchoolResult, 
    ProcessedSubjectScore,   # ✅ ADD THIS
    ExamScore
)
from .forms import (
    AcademicLevelForm,
    JointExamProjectForm,
    GradingRuleForm,
    DivisionRuleForm,
    ExamScoreForm
)

from openpyxl.styles import PatternFill
from datetime import datetime

import logging
logger = logging.getLogger(__name__)


User = get_user_model()



from reportlab.platypus import Flowable

class VerticalText(Flowable):
    def __init__(self, text, width=28, height=95):
        super().__init__()
        self.text = text
        self.width = width
        self.height = height

    def draw(self):
        c = self.canv
        c.saveState()
        c.rotate(90)
        c.setFont("Helvetica-Bold", 8)
        c.drawString(6, -self.width + 6, self.text)
        c.restoreState()

wrap_style = ParagraphStyle(
    name="WrapText",
    fontName="Helvetica",
    fontSize=8,
    leading=10,
    wordWrap="CJK",     # ✅ forces wrapping even without spaces
)


def draw_header_footer(canvas, doc):
    canvas.saveState()

    # ===== Top-right date & time =====
    canvas.setFont("Helvetica", 8)
    canvas.drawRightString(
        doc.pagesize[0] - doc.rightMargin,
        doc.pagesize[1] - doc.topMargin + 10,
        now().strftime("%A, %d %B %Y  %H:%M:%S")
    )

    # ===== Footer page number =====
    canvas.drawCentredString(
        doc.pagesize[0] / 2,
        15,
        f"Page {doc.page}"
    )

    canvas.restoreState()

from reportlab.platypus import Flowable
from reportlab.lib.colors import black
from reportlab.lib.units import mm

from reportlab.platypus import Flowable

class RoundedHeader(Flowable):
    def __init__(self, title, subtitle, width, height=35):
        Flowable.__init__(self)
        self.title = title
        self.subtitle = subtitle
        self.width = width

        # Split lines
        self.title_lines = self.title.split("\n")

        # Dynamically increase height based on number of lines
        self.line_height = 14
        self.height = max(height, 20 + len(self.title_lines) * self.line_height + 15)

    def draw(self):
        c = self.canv

        # Draw rounded rectangle
        c.roundRect(
            0, 0,
            self.width,
            self.height,
            radius=10,
            stroke=1,
            fill=0
        )

        # === TITLE (MULTI-LINE SUPPORT) ===
        c.setFont("Helvetica-Bold", 14)

        y = self.height - 14

        for line in self.title_lines:
            c.drawCentredString(self.width / 2, y, line.strip())
            y -= self.line_height

        # === SUBTITLE ===
        c.setFont("Helvetica-Bold", 11)
        c.drawCentredString(
            self.width / 2,
            y - 5,  # small gap after title
            self.subtitle
        )

def pct(part, whole):
    try:
        return round((part / whole) * 100, 2) if whole else 0
    except Exception:
        return 0

def round_no_decimal(value):
    if value is None or value == "":
        return ""
    return int(Decimal(str(value)).quantize(0, rounding=ROUND_HALF_UP))


def gpa_to_grade_status(gpa):
    if 1.0000 <= gpa <= 1.5999: return "A","EXCELLENT"
    if 1.6000 <= gpa <= 2.5999: return "B","VERY GOOD"
    if 2.6000 <= gpa <= 3.5999: return "C","GOOD"
    if 3.6000 <= gpa <= 4.5999: return "D","SATISFACTORY"
    if 4.6000 <= gpa <= 5.0000: return "F","FAIL"
    return "-", "-"

def safe_sheet_title(value: str) -> str:
    """
    Make Excel-safe sheet title (max 31 chars, no illegal chars)
    """
    if not value:
        return "Sheet1"

    # Replace illegal Excel characters
    for ch in ['\\', '/', '*', '?', ':', '[', ']']:
        value = value.replace(ch, '-')

    return value[:31]


SUBJECT_GPA_RULES = {
    1: {"A":1,"B":2,"C":3,"D":4,"F":5},
    2: {"A":1,"B":2,"C":3,"D":4,"E":5,"S":6,"F":7},
    3: {"A":1,"B":2,"C":3,"D":4,"E":5},
}


DIVISION_GPA_RULES = {
    "I": 1,
    "II": 2,
    "III": 3,
    "IV": 4,
    "0": 5,
}


def normalize_score(total, papers):
    if papers == 1:
        return total
    if papers == 1.5:
        return round((total / 150) * 100, 2)
    if papers == 2:
        return round((total / 200) * 100, 2)
    if papers == 2.5:
        return round((total / 250) * 100, 2)
    return 0


def resolve_average_grade(avg, grading_rules):
    for r in grading_rules:
        if r.min_score <= avg <= r.max_score:
            return r.grade
    return "X"


def calculate_grade_point(score, academic_level_id):
    rule = GradingRule.objects.filter(
        academic_level_id=academic_level_id,
        min_score__lte=score,
        max_score__gte=score
    ).first()

    if not rule:
        return "F", 0

    return rule.grade, rule.point

# --------------------------------------------------------------
# AUTH: REGISTER
# --------------------------------------------------------------
def register_page(request):
    if request.method == "POST":
        username = request.POST.get("username")
        email = request.POST.get("email")
        password = request.POST.get("password")

        if User.objects.filter(username=username).exists():
            return render(request, "exams/register.html", {
                "error": "Username already exists"
            })

        User.objects.create_user(username=username, email=email, password=password)
        return render(request, "exams/register.html", {
            "message": "User registered successfully!"
        })
    return render(request, "exams/register.html")


# --------------------------------------------------------------
# AUTH: LOGIN
# --------------------------------------------------------------
from django.contrib.auth import login
from django.contrib.auth.models import User
from django.shortcuts import render, redirect

def login_view(request):
    if request.method == "POST":
        username = request.POST.get("username")
        password = request.POST.get("password")

        user = User.objects.filter(username=username).first()

        # ❌ USER NOT FOUND
        if not user:
            return render(
                request,
                "exams/login.html",
                {"error": "Invalid username or password"}
            )

        # ❌ WRONG PASSWORD
        if not user.check_password(password):
            return render(
                request,
                "exams/login.html",
                {"error": "Invalid username or password"}
            )

        # 🚫 INACTIVE ACCOUNT
        if not user.is_active:
            return render(
                request,
                "exams/login.html",
                {"error": "Your account is OFF. Contact/SMS to 0763169695."}
            )

        # ✅ LOGIN
        login(request, user)
        return redirect("exams:dashboard")

    return render(request, "exams/login.html")




from django.contrib.auth import logout
from django.shortcuts import redirect
from django.conf import settings

def logout_view(request):
    logout(request)
    return redirect(settings.LOGIN_URL)
# --------------------------------------------------------------
# DASHBOARD
# --------------------------------------------------------------
@login_required
def dashboard(request):

    user = request.user
    profile = getattr(user, "profile", None)

    years = ["2022", "2023", "2024", "2025"]

    regions = Region.objects.all()
    subjects = ExamSubject.objects.all().order_by("subject_code")

    districts = []
    wards = []
    schools = []

    if profile:
        if profile.role == "region":
            districts = District.objects.filter(region=profile.region)

        elif profile.role == "district":
            districts = District.objects.filter(region=profile.region)
            wards = Ward.objects.filter(district=profile.district)

        elif profile.role == "ward":
            districts = District.objects.filter(region=profile.region)
            wards = Ward.objects.filter(district=profile.district)
            schools = School.objects.filter(ward=profile.ward)

        elif profile.role == "school":
            districts = District.objects.filter(region=profile.region)
            wards = Ward.objects.filter(district=profile.district)
            schools = School.objects.filter(ward=profile.ward)

    exam_types = ExamType.objects.all().order_by("id")
    exams = ExamType.objects.filter(is_active=True).order_by("-year")

    # ✅ ADD PROJECTS HERE
    projects = (
        JointExamProject.objects
        .select_related("academic_level")
        .order_by("-year", "name")
    )

    context = {
        "regions": regions,
        "districts": districts,
        "wards": wards,
        "schools": schools,
        "exam_types": exam_types,
        "subjects": subjects,
        "exams": exams,
        "projects": projects,  # <-- added
        "user": user,
        "years": years,
        "user_profile": profile,
    }

    return render(request, "exams/dashboard.html", context)











# --------------------------------------------------------------
# DASHBOARD dashboard_subjectwise
# --------------------------------------------------------------
@login_required
def dashboard_subjectwise(request):
    user = request.user
    profile = user.profile

    subjects = ExamSubject.objects.only("subject_id", "subject_code", "subject_name_sw")
    exam_types = ExamType.objects.only("id", "name")

    regions = Region.objects.all() if profile.role == "super" else Region.objects.filter(id=profile.region_id)

    districts = []
    wards = []
    schools = []

    if profile.role == "region":
        districts = District.objects.filter(region=profile.region)
    elif profile.role == "district":
        districts = District.objects.filter(region=profile.region)
        wards = Ward.objects.filter(district=profile.district)
    elif profile.role == "ward":
        districts = District.objects.filter(region=profile.region)
        wards = Ward.objects.filter(district=profile.district)
        schools = School.objects.filter(ward=profile.ward)
    elif profile.role == "school":
        schools = School.objects.filter(id=profile.school_id)

    return render(request, "exams/dashboard_subjectwise.html", {
        "subjects": subjects,
        "regions": regions,
        "districts": districts,
        "wards": wards,
        "schools": schools,
        "exam_types": exam_types,
    })

# --------------------------------------------------------------
# CASCADE AJAX
# --------------------------------------------------------------
@login_required
def ajax_get_districts(request, region_id):
    return JsonResponse(
        list(District.objects.filter(region_id=region_id).values("id", "name")),
        safe=False
    )


@login_required
def ajax_get_wards(request, district_id):
    return JsonResponse(
        list(Ward.objects.filter(district_id=district_id).values("id", "name")),
        safe=False
    )


@login_required
def ajax_get_schools(request, ward_id):
    # return school_code and name so frontend can use code as id
    return JsonResponse(
        list(School.objects.filter(ward_id=ward_id).values("school_code", "name")),
        safe=False
    )


def validate_role_requirements(role, row, line):
    if role == "region" and not row.get("region_id"):
        return f"Row {line}: region_id required for region role"

    if role == "district" and not (row.get("region_id") and row.get("district_id")):
        return f"Row {line}: region_id & district_id required"

    if role == "ward" and not (row.get("region_id") and row.get("district_id") and row.get("ward_id")):
        return f"Row {line}: region_id, district_id & ward_id required"

    if role == "school" and not (
        row.get("region_id") and row.get("district_id")
        and row.get("ward_id") and row.get("school_code")
    ):
        return f"Row {line}: full location required for school role"

    return None


# manage_users.py
User = get_user_model()
@login_required
@permission_required('auth.add_user', raise_exception=True)
def manage_users(request):
    """
    GET: render manage users page.
    POST: supports two modes:
      - add_single: form fields for one user
      - upload_csv: CSV bulk upload
    """
    regions = Region.objects.all().order_by('name')
    districts = District.objects.all().order_by('name')
    wards = Ward.objects.all().order_by('name')
    schools = School.objects.all().order_by('name')

    if request.method == 'POST':
        mode = request.POST.get('mode')

        if mode == 'add_single':
            username = request.POST.get('username', '').strip()
            email = request.POST.get('email', '').strip()
            password = request.POST.get('password', '').strip()
            role = request.POST.get('role')
            region_id = request.POST.get('region')
            district_id = request.POST.get('district')
            ward_id = request.POST.get('ward')
            school_code = request.POST.get('school')

            # server-side validation depending on role:
            errors = []
            def require_field(cond, msg):
                if not cond:
                    errors.append(msg)

            if not username:
                errors.append('Username is required.')
            if not role:
                errors.append('Role is required.')

            # conditional requirements:
            if role == 'region':
                require_field(region_id, 'Region must be selected for Region Admin.')
            elif role == 'district':
                require_field(region_id, 'Region must be selected for District Admin.')
                require_field(district_id, 'District must be selected for District Admin.')
            elif role == 'ward':
                require_field(region_id, 'Region required for Ward Admin.')
                require_field(district_id, 'District required for Ward Admin.')
                require_field(ward_id, 'Ward required for Ward Admin.')
            elif role == 'school':
                require_field(region_id, 'Region required for School Admin.')
                require_field(district_id, 'District required for School Admin.')
                require_field(ward_id, 'Ward required for School Admin.')
                require_field(school_code, 'School required for School Admin.')

            if errors:
                for e in errors:
                    messages.error(request, e)
                return redirect('exams:manage_users')

            # create user + profile inside a transaction
            try:
                with transaction.atomic():
                    # If password empty, use create_user without password or set unusable
                    if password:
                        user = User.objects.create_user(username=username, email=email, password=password)
                    else:
                        user = User.objects.create_user(username=username, email=email)
                        user.set_unusable_password()
                        user.save()

                    # Create profile
                    profile = UserProfile.objects.create(user=user, role=role)

                    # set relation fields if provided
                    if region_id:
                        profile.region_id = int(region_id)
                    if district_id:
                        profile.district_id = int(district_id)
                    if ward_id:
                        profile.ward_id = int(ward_id)
                    if school_code:
                        # adapt depending on your School PK - here we assume school_code is the identifier
                        try:
                            school_obj = School.objects.get(school_code=school_code)
                            profile.school = school_obj
                        except School.DoesNotExist:
                            messages.warning(request, f"School code {school_code} not found; profile saved without school.")
                    profile.save()

                messages.success(request, f"User {username} created.")
                return redirect('exams:manage_users')

            except Exception as exc:
                messages.error(request, f"Failed to create user: {exc}")
                return redirect('exams:manage_users')


        elif mode == 'upload_csv':

            DEFAULT_PASSWORD = "123456"

            csv_file = request.FILES.get('csv_file')
            if not csv_file:
                messages.error(request, "No CSV uploaded.")
                return redirect('exams:manage_users')

            try:
                data = csv_file.read().decode('utf-8')
                reader = csv.DictReader(io.StringIO(data))

                created = 0
                errors = []

                with transaction.atomic():
                    for i, row in enumerate(reader, start=1):

                        username = str(row.get('username', '')).strip()
                        email = str(row.get('email', '')).strip()
                        role = str(row.get('role', '')).strip()

                        region_id = row.get('region_id')
                        district_id = row.get('district_id')
                        ward_id = row.get('ward_id')
                        school_code = row.get('school_code')

                        # -------------------------
                        # BASIC VALIDATION
                        # -------------------------
                        if not username or not role:
                            errors.append(f"Row {i}: username or role missing.")
                            continue

                        if User.objects.filter(username=username).exists():
                            errors.append(f"Row {i}: username '{username}' already exists.")
                            continue

                        # -------------------------
                        # RESOLVE FOREIGN KEYS FIRST
                        # -------------------------
                        region = Region.objects.filter(id=region_id).first() if region_id else None
                        district = District.objects.filter(id=district_id).first() if district_id else None
                        ward = Ward.objects.filter(id=ward_id).first() if ward_id else None
                        school = School.objects.filter(
                            school_code=str(school_code).strip()
                        ).first() if school_code else None

                        # Optional: role-based enforcement
                        if role == "district" and not district:
                            errors.append(f"Row {i}: invalid or missing district_id")
                            continue

                        if role == "ward" and not ward:
                            errors.append(f"Row {i}: invalid or missing ward_id")
                            continue

                        if role == "school" and not school:
                            errors.append(f"Row {i}: invalid or missing school_code")
                            continue

                        # -------------------------
                        # CREATE USER (SAFE)
                        # -------------------------
                        user = User.objects.create_user(
                            username=username,
                            email=email,
                            password=DEFAULT_PASSWORD
                        )

                        # -------------------------
                        # CREATE PROFILE WITH ALL FKs SET
                        # -------------------------
                        UserProfile.objects.create(
                            user=user,
                            role=role,
                            region=region,
                            district=district,
                            ward=ward,
                            school=school
                        )

                        created += 1

                msg = f"Bulk upload completed: {created} users created."
                if errors:
                    msg += " Errors: " + "; ".join(errors[:5])

                messages.success(request, msg)
                return redirect('exams:manage_users')

            except Exception as e:
                messages.error(request, f"CSV processing failed: {e}")
                return redirect('exams:manage_users')

#bulk_create_excel
        elif mode == "bulk_create_excel":
            wb = openpyxl.load_workbook(request.FILES["excel_file"])
            ws = wb.active

            headers = [c.value for c in ws[1]]
            created, errors = 0, []

            with transaction.atomic():
                for i, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
                    data = dict(zip(headers, row))

                    username = str(data.get("username", "")).strip()
                    role = str(data.get("role", "")).strip()

                    if not username or not role:
                        errors.append(f"Row {i}: username or role missing")
                        continue

                    if User.objects.filter(username=username).exists():
                        errors.append(f"Row {i}: username exists")
                        continue

                    err = validate_role_requirements(role, data, i)
                    if err:
                        errors.append(err)
                        continue

                    user = User.objects.create_user(
                        username=username,
                        email=data.get("email"),
                        password=data.get("password") or None
                    )

                    profile = UserProfile.objects.create(user=user, role=role)

                    profile.region_id = data.get("region_id") or None
                    profile.district_id = data.get("district_id") or None
                    profile.ward_id = data.get("ward_id") or None

                    if data.get("school_code"):
                        profile.school = School.objects.filter(
                            school_code=data["school_code"]
                        ).first()

                    profile.save()
                    created += 1

            messages.success(
                request,
                f"{created} users created. Errors: {len(errors)}"
            )
            return redirect("exams:manage_users")


        elif mode == "bulk_update_roles_excel":
            wb = openpyxl.load_workbook(request.FILES["excel_file"])
            ws = wb.active

            headers = [c.value for c in ws[1]]
            updated, errors = 0, []

            with transaction.atomic():
                for i, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
                    data = dict(zip(headers, row))
                    username = str(data.get("username", "")).strip()
                    new_role = str(data.get("new_role", "")).strip()

                    if not username or not new_role:
                        errors.append(f"Row {i}: username or new_role missing")
                        continue

                    try:
                        profile = User.objects.get(username=username).profile
                    except User.DoesNotExist:
                        errors.append(f"Row {i}: user not found")
                        continue

                    err = validate_role_requirements(new_role, data, i)
                    if err:
                        errors.append(err)
                        continue

                    profile.role = new_role
                    profile.region = profile.district = profile.ward = profile.school = None

                    profile.region_id = data.get("region_id") or None
                    profile.district_id = data.get("district_id") or None
                    profile.ward_id = data.get("ward_id") or None

                    if data.get("school_code"):
                        profile.school = School.objects.filter(
                            school_code=data["school_code"]
                        ).first()

                    profile.save()
                    updated += 1

            messages.success(
                request,
                f"{updated} users updated. Errors: {len(errors)}"
            )
            return redirect("exams:manage_users")



        elif mode == "download_template":
            template = request.POST.get("template")

            wb = openpyxl.Workbook()
            ws = wb.active

            if template == "create":
                ws.append([
                    "username","email","role","region_id",
                    "district_id","ward_id","school_code","password"
                ])
                fname = "users_bulk_create.xlsx"

            else:
                ws.append([
                    "username","new_role","region_id",
                    "district_id","ward_id","school_code"
                ])
                fname = "users_bulk_role_update.xlsx"

            response = HttpResponse(
                content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
            )
            response["Content-Disposition"] = f'attachment; filename="{fname}"'
            wb.save(response)
            return response







    # GET
    # users = User.objects.all().select_related("profile")
    from django.db.models import Q
    from django.core.paginator import Paginator

    search = request.GET.get("q", "").strip()

    users_qs = (
        User.objects
        .select_related(
            "profile",
            "profile__region",
            "profile__district",
            "profile__ward",
            "profile__school",
        )
        .only("id", "username", "email", "is_active")
        .order_by("username")
    )

    # ✅ SERVER-SIDE SEARCH (GLOBAL)
    if search:
        users_qs = users_qs.filter(
            Q(username__icontains=search) |
            Q(email__icontains=search) |
            Q(profile__role__icontains=search) |
            Q(profile__region__name__icontains=search) |
            Q(profile__district__name__icontains=search) |
            Q(profile__ward__name__icontains=search) |
            Q(profile__school__name__icontains=search)
        )

    paginator = Paginator(users_qs, 50)
    page_number = request.GET.get("page")
    users = paginator.get_page(page_number)



    return render(request, 'exams/manage_users.html', {
        'users': users,
        'search': search,
        'regions': regions,
        'districts': districts,
        'wards': wards,
        'schools': schools,
    })


from django.views.decorators.http import require_POST
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required

@login_required
@require_POST
def toggle_user_active(request, user_id):
    user = get_object_or_404(User, id=user_id)

    if user == request.user:
        return JsonResponse({"ok": False, "error": "Cannot disable yourself"})

    user.is_active = not user.is_active
    user.save(update_fields=["is_active"])

    return JsonResponse({
        "ok": True,
        "active": user.is_active
    })


from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_POST
from django.http import JsonResponse
from django.shortcuts import get_object_or_404

@login_required
@require_POST
def reset_user_password(request, user_id):
    user = get_object_or_404(User, id=user_id)

    # 🔒 prevent self reset (optional but recommended)
    if user == request.user:
        return JsonResponse(
            {"ok": False, "error": "You cannot reset your own password"},
            status=400
        )

    # 🔑 reset password
    user.set_password("12345678")
    user.save(update_fields=["password"])

    return JsonResponse({"ok": True})



from django.views.decorators.http import require_POST
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User

@login_required
@require_POST
def edit_user_ajax(request, user_id):
    user = get_object_or_404(User, id=user_id)

    # Optional: permission check
    if not request.user.is_superuser:
        return JsonResponse({"ok": False, "error": "Permission denied"}, status=403)

    username = request.POST.get("username", "").strip()
    email = request.POST.get("email", "").strip()
    role = request.POST.get("role", "").strip()
    is_active = request.POST.get("is_active") == "1"

    if not username:
        return JsonResponse({"ok": False, "error": "Username required"})

    # unique username check
    if User.objects.exclude(id=user.id).filter(username=username).exists():
        return JsonResponse({"ok": False, "error": "Username already exists"})

    user.username = username
    user.email = email
    user.is_active = is_active
    user.save(update_fields=["username", "email", "is_active"])

    # update profile role (if you use profile)
    if hasattr(user, "profile") and role:
        user.profile.role = role
        user.profile.save(update_fields=["role"])

    return JsonResponse({
        "ok": True,
        "username": user.username,
        "email": user.email,
        "role": role,
        "active": user.is_active
    })



@login_required
@require_POST
def update_user(request, user_id):
    user = get_object_or_404(User, id=user_id)
    form = UserAdminUpdateForm(request.POST, instance=user)
    if form.is_valid():
        form.save()
        return JsonResponse({"ok": True})
    return JsonResponse({"errors": form.errors}, status=400)


# # ========== AJAX UPDATE USER ==========
# @login_required
# def update_user(request, user_id):
#     if request.method == "POST":
#         user = get_object_or_404(User, pk=user_id)

#         data = request.POST
#         user.username = data.get("username")
#         user.email = data.get("email")
#         user.role = data.get("role")
#         user.save()

#         return JsonResponse({"message": "User updated successfully"})

#     return JsonResponse({"error": "Invalid request"}, status=400)


# ========== DELETE USER ==========
@login_required
def delete_user(request, user_id):
    user = get_object_or_404(User, pk=user_id)
    user.delete()
    return JsonResponse({"message": "User deleted successfully"})


# # ========== RESET PASSWORD ==========
# @login_required
# def reset_user_password(request, user_id):
#     if request.method == "POST":
#         user = get_object_or_404(User, pk=user_id)
#         new_password = request.POST.get("new_password")
#         user.set_password(new_password)
#         user.save()
#         return JsonResponse({"message": "Password updated successfully"})




@login_required
def profile_view(request):
    profile = request.user.profile

    if request.method == "POST":
        form = ProfileUpdateForm(request.POST, request.FILES, instance=profile)
        if form.is_valid():
            form.save()
            messages.success(request, "Profile updated successfully!")
            return redirect("exams:profile")
    else:
        form = ProfileUpdateForm(instance=profile)

    return render(request, "exams/profile.html", {"form": form})



@login_required
def profile_update(request):
    user = request.user
    profile = user.profile

    if request.method == "POST":
        u_form = UserUpdateForm(request.POST, instance=user)
        p_form = ProfileUpdateForm(request.POST, request.FILES, instance=profile)

        if u_form.is_valid() and p_form.is_valid():
            u_form.save()
            p_form.save()

            messages.success(request, "Profile updated successfully!")
            return redirect("exams:profile")

        else:
            # FORM INVALID – RETURN PAGE WITH ERRORS
            messages.error(request, "Please correct the errors below.")
            return render(request, "exams/profile.html", {
                "u_form": u_form,
                "p_form": p_form
            })

    else:
        u_form = UserUpdateForm(instance=user)
        p_form = ProfileUpdateForm(instance=profile)

    return render(request, "exams/profile.html", {
        "u_form": u_form,
        "p_form": p_form
    })



@login_required
def change_password(request):
    if request.method == "POST":
        form = PasswordChangeForm(request.user, request.POST)

        if form.is_valid():
            user = form.save()
            update_session_auth_hash(request, user)
            messages.success(request, "Password updated!")
            return redirect("exams:profile")
    else:
        form = PasswordChangeForm(request.user)

    return render(request, "exams/change_password.html", {"form": form})


























def compute_rank_aggregates(qs):
    """
    Computes all aggregates needed for ranking:
      - total
      - pass_count
      - fail_count
      - pass_pct
      - average score (avg_numeric)
      - grades A–E (female/male/total)
    """

    total = qs.count()
    pass_count = qs.filter(status__iexact="PASS").count()
    fail_count = qs.filter(status__iexact="FAIL").count()

    pass_pct = round((pass_count / total * 100), 1) if total else 0

    avg_row = qs.aggregate(avg=Avg("avg_numeric"))
    avg_score = round(float(avg_row["avg"] or 0), 2)


    # Grade buckets
    def gcount(g, sex=None):
        f = {"avg_grade__iexact": g}
        if sex:
            f["sex__iexact"] = sex
        return qs.filter(**f).count()

    grades = {
        "A_f": gcount("A", "F"), "A_m": gcount("A", "M"),
        "B_f": gcount("B", "F"), "B_m": gcount("B", "M"),
        "C_f": gcount("C", "F"), "C_m": gcount("C", "M"),
        "D_f": gcount("D", "F"), "D_m": gcount("D", "M"),
        "E_f": gcount("E", "F"), "E_m": gcount("E", "M"),
    }

    # Total for each A–E
    grades["A"] = grades["A_f"] + grades["A_m"]
    grades["B"] = grades["B_f"] + grades["B_m"]
    grades["C"] = grades["C_f"] + grades["C_m"]
    grades["D"] = grades["D_f"] + grades["D_m"]
    grades["E"] = grades["E_f"] + grades["E_m"]

    # Pass A–C
    grades["AC_total"] = grades["A"] + grades["B"] + grades["C"]

    return {
        "total": total,
        "pass_count": pass_count,
        "fail_count": fail_count,
        "pass_pct": pass_pct,
        "avg_score": avg_score,
        "grades": grades,
    }





















# -------------------------
# Regionwise listing page
# -------------------------
@login_required
def regionwise(request):

    user = request.user
    years = ["2022", "2023", "2024", "2025"]
    # SUPER ADMIN → ALL REGIONS
    if user.profile.role == "super":
        regions = Region.objects.all().order_by("name")

    # REGION ACCOUNT → ONLY THEIR REGION
    elif user.profile.role == "region":
        regions = Region.objects.filter(id=user.profile.region_id)

    # DISTRICT ACCOUNT → THEIR REGION ONLY
    elif user.profile.role == "district":
        regions = Region.objects.filter(id=user.profile.district.region_id)

    # WARD ACCOUNT → THEIR REGION
    elif user.profile.role == "ward":
        regions = Region.objects.filter(id=user.profile.ward.district.region_id)

    # SCHOOL ACCOUNT → THEIR REGION
    elif user.profile.role == "school":
        regions = Region.objects.filter(id=user.profile.school.region_id)

    else:
        regions = Region.objects.none()

    subjects = ExamSubject.objects.all().order_by("subject_code") 
    # ✅ LOAD EXAM TYPES (ID + NAME)
    exam_types = ExamType.objects.filter(is_active=True).only("id", "name").order_by("-year")

    # ============= AJAX SEARCH ==============
    if request.headers.get("x-requested-with") == "XMLHttpRequest":
        query = request.GET.get("q", "").strip().lower()

        results = []
        for r in regions:
            if query in r.name.lower():
                results.append({
                    "id": r.id,
                    "name": r.name,
                    "district_count": r.districts.count(),   # ✅ FIXED HERE
                })

        return JsonResponse({
            "districts": results,
            "exam_types": [
                {"id": e.id, "name": e.name} for e in exam_types
            ]
        })

    # Normal page load
    return render(request, "exams/reports/regionwise.html", {
        "regions": regions,
        "subjects": subjects,
        "years": years,
        "exam_types": exam_types,   # ✅ now available in template
    })



@login_required
def region_school_ranking_html(request):

    region = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_school_rank(region, year, exam_type_id)

    exam_type = None
    if exam_type_id:
        exam_type = ExamType.objects.filter(id=exam_type_id).first()

    return render(request,"exams/reports/region_school_rank_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "exam_type": exam_type,
        "region": Region.objects.get(id=region)
    })





@login_required
def region_school_ranking_print(request):

    region = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_school_rank(region, year, exam_type_id)

    return render(request,"exams/reports/region_school_rank_print.html",{
        "rows": rows,
        "year": year,
        "exam_type": ExamType.objects.get(id=exam_type_id),
        "region": Region.objects.get(id=region),
    })


@login_required
def region_school_ranking_excel(request):

    from openpyxl import Workbook
    from django.http import HttpResponse

    rows = compute_region_school_rank(
        request.GET["region"],
        request.GET["year"],
        request.GET["exam_type_id"]
    )

    wb=Workbook()
    ws=wb.active

    ws.append(["#","CODE","NAME","DISTRICT","REGION",
               "I","II","III","IV","0","#I-IV","%I-IV","GPA"])

    for r in rows:
        ws.append([
            r["rank"],r["code"],r["name"],r["district"],r["region"],
            r["d1"],r["d2"],r["d3"],r["d4"],r["d0"],
            r["pass"],r["pass_pct"],r["gpa"]
        ])

    resp=HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    resp["Content-Disposition"]="attachment; filename=Region_School_Rank.xlsx"
    wb.save(resp)
    return resp


@login_required
def region_school_ranking_pdf(request):

    rows = compute_region_school_rank(
        request.GET["region"],
        request.GET["year"],
        request.GET["exam_type_id"]
    )

    from reportlab.platypus import SimpleDocTemplate,Table,TableStyle
    from reportlab.lib.pagesizes import A4
    from reportlab.lib import colors
    from django.http import HttpResponse

    resp=HttpResponse(content_type="application/pdf")
    resp["Content-Disposition"]="attachment; filename=Region_School_Rank.pdf"

    data=[["#","CODE","NAME","DISTRICT","REGION","I","II","III","IV","0","#I-IV","%I-IV","GPA"]]

    for r in rows:
        data.append([
            r["rank"],r["code"],r["name"],r["district"],r["region"],
            r["d1"],r["d2"],r["d3"],r["d4"],r["d0"],
            r["pass"],r["pass_pct"],r["gpa"]
        ])

    table=Table(data,repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID",(0,0),(-1,-1),1,colors.black),
        ("BACKGROUND",(0,0),(-1,0),colors.lightgrey),
        ("ALIGN",(0,0),(-1,-1),"CENTER")
    ]))

    doc=SimpleDocTemplate(resp,pagesize=A4)
    doc.build([table])
    return resp


@login_required
def region_school_ranking_ext_html(request):

    region = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_school_rank_ext(region, year, exam_type_id)

    exam_type = None
    if exam_type_id:
        exam_type = ExamType.objects.filter(id=exam_type_id).first()

    return render(request,"exams/reports/region_school_rank_ext_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "exam_type": exam_type,
        "region": Region.objects.get(id=region)
    })






@login_required
def district_school_ranking_ext_html(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank_ext(district, year, exam_type_id)

    exam_type = None
    if exam_type_id:
        exam_type = ExamType.objects.filter(id=exam_type_id).first()

    return render(request,"exams/reports/district_school_rank_ext_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "exam_type": exam_type,
        "district": Region.objects.get(id=district)
    })



from openpyxl import Workbook
from django.http import HttpResponse

@login_required
def region_school_ranking_ext_excel(request):

    region = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_school_rank_ext(region, year, exam_type_id)

    wb = Workbook()
    ws = wb.active
    ws.title = "Region Ranking"

    # ---------------- HEADER ROW ----------------
    headers = [
        "#","CNO","SCHOOL NAME","REGION","DISTRICT","OWNERSHIP",

        "REG F","REG M","REG T",

        "SAT F","SAT M","SAT T",

        "I F","I M","I T","I %",

        "II F","II M","II T","II %",

        "III F","III M","III T","III %",

        "PASS I-III F","PASS I-III M","PASS I-III T","PASS I-III %",

        "IV F","IV M","IV T","IV %",

        "FAIL F","FAIL M","FAIL T","FAIL %",

        "PASS I-IV F","PASS I-IV M","PASS I-IV T","PASS I-IV %",

        "GPA","GRADE","STATUS","POSITION"
    ]

    ws.append(headers)

    # ---------------- DATA ROWS ----------------
    for r in rows:
        ws.append([
            r["no"], r["cno"], r["name"], r["region"], r["district"], r["ownership"],

            r["reg_f"], r["reg_m"], r["reg_t"],

            r["sat_f"], r["sat_m"], r["sat_t"],

            r["i_f"], r["i_m"], r["i_t"], r["i_pct"],

            r["ii_f"], r["ii_m"], r["ii_t"], r["ii_pct"],

            r["iii_f"], r["iii_m"], r["iii_t"], r["iii_pct"],

            r["pass_i_iii_f"], r["pass_i_iii_m"], r["pass_i_iii_t"], r["pass_i_iii_pct"],

            r["iv_f"], r["iv_m"], r["iv_t"], r["iv_pct"],

            r["f_f"], r["f_m"], r["f_t"], r["f_pct"],

            r["pass_i_iv_f"], r["pass_i_iv_m"], r["pass_i_iv_t"], r["pass_i_iv_pct"],

            r["gpa"], r["grade"], r["status"], r["position"]
        ])

    # ---------------- RESPONSE ----------------
    filename = f"REGION_SCHOOL_RANK_EXT_{year}.xlsx"

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response


@login_required
def region_school_ranking_ext_print(request):

    region = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_school_rank_ext(region, year, exam_type_id)

    return render(request,"exams/reports/region_school_rank_ext_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "region": Region.objects.get(id=region)
    })





















@login_required
def region_ranking(request, region_id):
    year = int(request.GET.get("year", 2024))

    qs = SchoolYearSummary.objects.filter(
        school__region_id=region_id,
        year=year
    ).values(
        "school__name",
        "school__school_code"
    ).annotate(
        avg_mark=Avg("avg_numeric"),
        candidates=Sum("candidates")
    ).order_by("-avg_mark")

    region = Region.objects.get(id=region_id)

    return render(request, "exams/region_ranking.html", {
        "region": region,
        "year": year,
        "ranking": qs
    })


# -----------------------
# Helper: average -> band letter
# -----------------------
def avg_to_band(avg_value):
    """
    Map numeric average (0-300) to band A-E according to user's mapping:
    0-60 -> E
    61-120 -> D
    121-180 -> C
    181-240 -> B
    241-300 -> A
    """
    try:
        v = float(avg_value or 0)
    except Exception:
        return "-"
    if v <= 60:
        return "E"
    if v <= 120:
        return "D"
    if v <= 180:
        return "C"
    if v <= 240:
        return "B"
    return "A"
# -----------------------
# Region summary HTML (you likely already have)
# -----------------------
@login_required
def region_summary(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    if not region_id or not year:
        return render(request, "exams/reports/region_summary.html", {"error": "Please select region and year."})

    region = get_object_or_404(Region, pk=region_id)
    year = int(year)

    cand_qs = ExamCandidate.objects.filter(year=year, school__region_id=region.id)

    # per-district aggregations
    district_stats = (
        cand_qs.values("school__district__id", "school__district__name")
               .annotate(
                   total=Count("id"),
                   pass_count=Count("id", filter=Q(status__iexact="PASS")),
                   fail_count=Count("id", filter=Q(status__iexact="FAIL")),
                   male=Count("id", filter=Q(sex__iexact="M")),
                   female=Count("id", filter=Q(sex__iexact="F")),
                   male_pass=Count("id", filter=Q(sex__iexact="M") & Q(status__iexact="PASS")),
                   female_pass=Count("id", filter=Q(sex__iexact="F") & Q(status__iexact="PASS")),
               )
               .order_by("school__district__name")
    )

    districts = []
    totals = {"total":0, "pass":0, "fail":0, "male":0, "female":0}
    for row in district_stats:
        total = row["total"] or 0
        pc = row["pass_count"] or 0
        fc = row["fail_count"] or 0
        male = row["male"] or 0
        female = row["female"] or 0
        male_pass = row.get("male_pass") or 0
        female_pass = row.get("female_pass") or 0
        pass_pct = round((pc / total * 100), 1) if total else 0
        districts.append({
            "district_name": row.get("school__district__name") or "Unknown",
            "total": total,
            "pass": pc,
            "fail": fc,
            "pass_pct": pass_pct,
            "male": male,
            "female": female,
            "male_pass": male_pass,
            "female_pass": female_pass,
        })
        totals["total"] += total
        totals["pass"] += pc
        totals["fail"] += fc
        totals["male"] += male
        totals["female"] += female

    totals["pass_pct"] = round((totals["pass"] / totals["total"] * 100), 1) if totals["total"] else 0

    # header numbers
    registered = cand_qs.count()
    pass_fail = cand_qs.filter(status__in=["PASS","pass","Pass","FAIL","fail","Fail"]).count()
    other_status = registered - pass_fail

    # region average — use SchoolYearSummary average of avg_numeric (if available for this year)
    school_summary_qs = SchoolYearSummary.objects.filter(year=year, school__region_id=region.id)
    total_schools = school_summary_qs.values_list("school_id", flat=True).distinct().count()
    region_avg = school_summary_qs.aggregate(avg=Avg("avg_numeric"))["avg"] or 0
    region_avg_rounded = round(region_avg, 4)
    region_avg_band = avg_to_band(region_avg_rounded)

    context = {
        "region": region,
        "year": year,
        "districts": districts,
        "totals": totals,
        "registered": registered,
        "pass_fail": pass_fail,
        "other_status": other_status,
        "region_avg": region_avg_rounded,
        "region_avg_band": region_avg_band,
        "total_schools": total_schools,
    }
    return render(request, "exams/reports/region_summary.html", context)






# View: redesigned HTML preview
# -----------------------
# View: PDF export using xhtml2pdf
def _on_page(canvas, doc):
    # simple page number footer at bottom center
    canvas.saveState()
    canvas.setFont("Helvetica", 9)
    page_no = f"Page {doc.page}"
    canvas.drawString(30, 20, page_no)
    canvas.restoreState()

@login_required
def region_summary_pdf(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    if not region_id or not year:
        return HttpResponse("Missing parameters", status=400)

    context = build_region_summary_context(region_id, year)

    # PDF response
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = f'inline; filename="Region_{year}.pdf"'

    # Use SimpleDocTemplate with small margins
    doc = SimpleDocTemplate(response, pagesize=A4,
                            leftMargin=20, rightMargin=20,
                            topMargin=20, bottomMargin=30)

    story = []

    # Header (single table flowable)
    
    hdr = pdf_header_block_simple(context["region"], year)
    story.append(hdr)
    story.append(Spacer(1, 8))

    # Students block
    story.append(pdf_students_block(context.get("totals", {})))
    story.append(Spacer(1, 10))

    # Grades summary
    story.append(pdf_grades_block(context.get("grades", {}), context.get("totals", {})))
    story.append(Spacer(1, 12))

    # Ownership blocks (each one spaced)
    for tbl in pdf_ownership_block(context.get("ownership", [])):
        story.append(tbl)
        story.append(Spacer(1, 12))

    # District summary table last (if present)
    if context.get("districts"):
        story.append(pdf_district_summary_block(context.get("districts", []), context.get("districts_totals", {})))
        story.append(Spacer(1, 8))

    # Footer text (example)
    footer_text = f"STD-VII : {year} - MKOA - {getattr(context['region'], 'name', '')}"
    story.append(Spacer(1, 20))
    story.append(pdf_footer_block(footer_text))

    # Build
    doc.build(story, onFirstPage=_on_page, onLaterPages=_on_page)
    return response





# -----------------------
# Region Excel export using openpyxl
# -----------------------
@login_required
def region_summary_excel(request):

    region_id = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    ctx = build_region_summary_context(region_id, year, exam_type_id)

    wb = Workbook()
    ws = wb.active
    ws.title = "Region Summary"

    row = 1

    def write_row(values):
        nonlocal row
        for col, v in enumerate(values, 1):
            ws.cell(row=row, column=col, value=v)
        row += 1

    write_row(["PRIME MINISTER'S OFFICE - RALG"])
    write_row([f"FORM TWO NATIONAL ASSESSMENT {ctx['year']}"])
    write_row([f"REGION: {ctx['region'].name}"])
    write_row([])

    write_row(["STUDENTS REGISTRATION AND PERFORMANCE"])
    write_row(["", "FEMALE", "", "MALE", "", "TOTAL"])
    write_row(["REGISTERED", ctx["totals"]["reg_f"], "", ctx["totals"]["reg_m"], "", ctx["totals"]["reg"]])
    write_row(["WITHs RESULTS", ctx["totals"]["sat_f"], "", ctx["totals"]["sat_m"], "", ctx["totals"]["sat"]])
    write_row(["ABSENTEES", ctx["totals"]["abs_f"], "", ctx["totals"]["abs_m"], "", ctx["totals"]["abs"]])
    write_row(["REGION GPA", ctx["totals"]["avg"], ctx["totals"]["grade"], ctx["totals"]["status"]])
    write_row([])

    write_row(["DIVISION PERFORMANCE"])
    write_row(["SEX","I","II","III","IV","0","PASS I-III","PASS I-IV"])

    write_row([
        "F",
        ctx["grades"]["af"],
        ctx["grades"]["bf"],
        ctx["grades"]["cf"],
        ctx["grades"]["df"],
        ctx["grades"]["ef"],
        ctx["a_c_f"],
        ctx["a_d_f"]
    ])

    write_row([
        "M",
        ctx["grades"]["am"],
        ctx["grades"]["bm"],
        ctx["grades"]["cm"],
        ctx["grades"]["dm"],
        ctx["grades"]["em"],
        ctx["a_c_m"],
        ctx["a_d_m"]
    ])

    write_row([
        "T",
        ctx["grades"]["a"],
        ctx["grades"]["b"],
        ctx["grades"]["c"],
        ctx["grades"]["d"],
        ctx["grades"]["e"],
        ctx["a_c_total"],
        ctx["a_d_total"]
    ])

    write_row([])

    write_row(["DISTRICT PERFORMANCE"])
    write_row([
        "#","NAME","SAT",
        "I(F)","I(M)","I(T)",
        "II(F)","II(M)","II(T)",
        "III(F)","III(M)","III(T)",
        "IV(F)","IV(M)","IV(T)",
        "0(F)","0(M)","0(T)",
        "PASS","PASS %","GPA","GRADE","STATUS"
    ])

    for d in ctx["district_perf"]:
        write_row([
            d["no"], d["name"], d["sat"],
            d["af"], d["am"], d["at"],
            d["bf"], d["bm"], d["bt"],
            d["cf"], d["cm"], d["ct"],
            d["df"], d["dm"], d["dt"],
            d["ff"], d["fm"], d["ft"],
            d["pass_no"], d["pass_pct"],
            d["gpa"], d["grade"], d["status"]
        ])

    write_row([])

    write_row(["SUBJECT PERFORMANCE"])
    write_row([
        "CODE","NAME","SAT",
        "A(F)","A(M)","A(T)",
        "B(F)","B(M)","B(T)",
        "C(F)","C(M)","C(T)",
        "D(F)","D(M)","D(T)",
        "F(F)","F(M)","F(T)",
        "PASS","PASS %","GPA","GRADE","STATUS"
    ])

    for s in ctx["subjects"]:
        write_row([
            s["code"], s["subject"], s["sat"],
            s["af"], s["am"], s["at"],
            s["bf"], s["bm"], s["bt"],
            s["cf"], s["cm"], s["ct"],
            s["df"], s["dm"], s["dt"],
            s["ff"], s["fm"], s["ft"],
            s["pass_no"], s["pass_pct"],
            s["gpa"], s["grade"], s["status"]
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    filename = f"Region_Summary_{ctx['region'].id}_{ctx['year']}.xlsx"
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response


@login_required
def region_summary_html(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    profile = getattr(request.user, "profile", None)

    if profile and profile.role != "super":
        if profile.role == "region":
            if int(region_id) != profile.region_id:
                return HttpResponseForbidden("Access denied")
        else:
            return HttpResponseForbidden("Access denied")

    ctx = build_region_summary_context(
        region_id, year, exam_type_id
    )
    return render(request, "exams/reports/region_summary_html.html", ctx)




@login_required
def region_trend_html(request):

    region_id = request.GET.get("region")
    start = request.GET.get("start")
    end = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_trend(region_id, start, end, exam_type_id)

    region = Region.objects.get(id=region_id)
    exam_type = ExamType.objects.get(id=exam_type_id)
    exam = ExamType.objects.get(id=exam_type_id)
    return render(request, "exams/reports/region_trend_html.html", {
        "region": region,
        "exam": exam,
        "rows": rows,
        "start": start,
        "end": end,
        "exam_type": exam_type,
        "exam_type_id": exam_type_id
    })




@login_required
def region_trend_print(request):

    region_id = request.GET.get("region")
    start_year = request.GET.get("start")
    end_year = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_trend(region_id, start_year, end_year, exam_type_id)

    region = Region.objects.get(id=region_id)
    exam_type = ExamType.objects.get(id=exam_type_id)
    exam = ExamType.objects.get(id=exam_type_id)
    return render(request, "exams/reports/region_trend_print.html", {
        "region": region,
        "exam": exam,
        "rows": rows,
        "start": start_year,
        "end": end_year,
        "exam_type": exam_type,
        "exam_type_id": exam_type_id
    })



@login_required
def district_trend_print(request):

    district_id = request.GET.get("district")
    start_year = request.GET.get("start")
    end_year = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = build_district_trend(district_id, start_year, end_year, exam_type_id)

    district = District.objects.get(id=district_id)
    exam_type = ExamType.objects.get(id=exam_type_id)
    
    return render(request, "exams/reports/district_trend_print.html", {
        "district": district,
        "rows": rows,
        "start": start_year,
        "end": end_year,
        "exam_type": exam_type,
        "exam_type_id": exam_type_id
    })



@login_required
def region_summary_print(request):
    region = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    ctx = build_region_summary_context(region, year, exam_type_id)
    return render(request, "exams/reports/region_summary_print.html", ctx)



@login_required
def district_summary_print(request):
    district = request.GET.get("district")
    year = request.GET.get("year")

    exam_type_id = request.GET.get("exam_type_id")
    exam_type = ExamType.objects.get(id=exam_type_id)

    ctx = build_district_summary_context(district, year, exam_type_id)
    return render(request, "exams/reports/district_summary_print.html", ctx)




@login_required
def district_ranking_html(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")

    context = compute_district_ranking(region_id, year)

    return render(
        request,
        "exams/reports/district_ranking_html.html",
        {
            "region": context["region"],
            "year": year,
            "districts": context["rows"],   # <-- IMPORTANT
            "totals": context["totals"],
        }
    )







@login_required
def district_ranking_pdf(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")

    context = compute_district_ranking(region_id, year)
    region = context["region"]
    region_name = region.name if hasattr(region, "name") else str(region)

    # Fix tuple rows → dict rows
    clean_rows = []
    for r in context["rows"]:
        if isinstance(r, dict):
            clean_rows.append(r)
        else:
            # tuple → dict conversion (assumes standardized order)
            clean_rows.append({
                "district": r[1],
                "total": r[2],
                "pass": r[3],
                "fail": r[4],
                "pass_pct": r[5],
                "avg_grade": r[6] if len(r) > 6 else "",
                "status": r[7] if len(r) > 7 else "",
            })

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f"inline; filename=District_Rank_{region_name}_{year}.pdf"
    )

    doc = SimpleDocTemplate(response)
    story = []

    story.append(pdf_header_block_simple(region, year))
    story.append(Spacer(1, 10))

    # Use the cleaned rows
    story.append(pdf_district_summary_block(context["rows"], context["totals"]))

    doc.build(story)
    return response





@login_required
def district_ranking_excel(request):

    context = compute_district_ranking(
        request.GET.get("region"),
        request.GET.get("year")
    )

    region = context["region"]
    districts = context["rows"]
    totals = context["totals"]

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "District Ranking"

    ws.append([
        "District", "A-F", "A-M", "A-T",
        "B-F", "B-M", "B-T",
        "C-F", "C-M", "C-T",
        "D-F", "D-M", "D-T",
        "E-F", "E-M", "E-T",
        "PASS F", "PASS M", "PASS T",
        "%PASS", "Avg Score", "Total Std", "Schools"
    ])

    for d in districts:
        ws.append([
            d["district"],
            d["af"], d["am"], d["a"],
            d["bf"], d["bm"], d["b"],
            d["cf"], d["cm"], d["c"],
            d["df"], d["dm"], d["d"],
            d["ef"], d["em"], d["e"],
            d["pass_f"], d["pass_m"], d["pass_t"],
            d["pass_ac_pct"], d["avg_score"],
            d["total_std"], d["total_schools"]
        ])

    # ADD TOTAL ROW
    ws.append([
        "TOTAL",
        totals["af"], totals["am"], totals["a"],
        totals["bf"], totals["bm"], totals["b"],
        totals["cf"], totals["cm"], totals["c"],
        totals["df"], totals["dm"], totals["d"],
        totals["ef"], totals["em"], totals["e"],
        totals["pass_f"], totals["pass_m"], totals["pass_t"],
        "-", totals["avg_score"],
        totals["total_std"], totals["total_schools"]
    ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = "attachment; filename=district_ranking.xlsx"

    wb.save(response)
    return response















# --------------------------------------------------------------
# EVERYTHING-WARD
# --------------------------------------------------------------
@login_required
def ward_ranking_html(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year", 2024))

    region = Region.objects.get(id=region_id)

    wards, totals = build_ward_rank_data(region_id, year)

    return render(request, "exams/reports/ward_ranking_html.html", {
        "region": region,
        "year": year,
        "wards": wards,
        "totals": totals
    })


@login_required
def ward_ranking_pdf(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")

    region = get_object_or_404(Region, pk=region_id)
    wards, totals = build_ward_rank_data(region_id, year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Ward_Ranking_{region.name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A4),
        leftMargin=20,
        rightMargin=20,
        topMargin=20,
        bottomMargin=20
    )

    story = []
    story.append(pdf_ward_ranking_table(region, year, wards))

    doc.build(story)
    return response


@login_required
def ward_ranking_excel(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year", 2024))

    region = Region.objects.get(id=region_id)

    wards, totals = build_ward_rank_data(region_id, year)

    wb = Workbook()
    ws = wb.active
    ws.title = "Ward Ranking"

    headers = [
        "Rank","Ward",
        "A(F)","A(M)","A(T)",
        "B(F)","B(M)","B(T)",
        "C(F)","C(M)","C(T)",
        "D(F)","D(M)","D(T)",
        "E(F)","E(M)","E(T)",
        "Total Std","Schools",
        "Pass(F)","Pass(M)","Pass(T)",
        "Avg Score"
    ]
    ws.append(headers)

    for w in wards:
        ws.append([
            w["rank"], w["name"],
            w["af"], w["am"], w["a"],
            w["bf"], w["bm"], w["b"],
            w["cf"], w["cm"], w["c"],
            w["df"], w["dm"], w["d"],
            w["ef"], w["em"], w["e"],
            w["total_std"],
            w["total_schools"],
            w["pass_f"], w["pass_m"], w["pass_t"],
            w["avg_score"]
        ])

    response = HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    response["Content-Disposition"] = f"attachment; filename=ward_ranking_{region.name}_{year}.xlsx"
    wb.save(response)
    return response


@login_required
def ward_ranking(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    district_id = request.GET.get("district")  # optional: restrict to a district
    page = int(request.GET.get("page", 1))

    if not region_id or not year:
        return render(request, "exams/error.html", {"message":"Missing region or year"})

    region = get_object_or_404(Region, pk=region_id)
    year = int(year)

    base_qs = ExamCandidate.objects.filter(year=year, school__region_id=region.id)

    ward_qs = Ward.objects.filter(district__region_id=region.id)
    if district_id:
        ward_qs = ward_qs.filter(district_id=district_id)

    ranking_rows = []
    for w in ward_qs:
        w_qs = base_qs.filter(school__ward_id=w.id)
        agg = compute_rank_aggregates(w_qs)
        ranking_rows.append({
            "ward": w,
            "total": agg["total"],
            "pass_count": agg["pass_count"],
            "fail_count": agg["fail_count"],
            "pass_pct": agg["pass_pct"],
            "avg_score": agg["avg_score"],
            "grades": agg["grades"],
        })

    ranking_rows.sort(key=lambda r: (r["pass_pct"], r["avg_score"], r["pass_count"]), reverse=True)

    paginator = Paginator(ranking_rows, 50)
    page_obj = paginator.get_page(page)

    return render(request, "exams/reports/ward_ranking.html", {
        "region": region,
        "year": year,
        "district_id": district_id,
        "page_obj": page_obj,
    })






















@login_required
def school_ranking_html(request):
    # expects region & year (district/ward optional)
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    district_id = request.GET.get("district")
    ward_id = request.GET.get("ward")

    if not region_id or not year:
        return render(request, "exams/error.html", {"message":"Missing region or year"})

    region = get_object_or_404(Region, pk=region_id)
    schools, totals = build_school_rank_data(region_id, year, district_id, ward_id)

    return render(request, "exams/reports/school_ranking_html.html", {
        "region": region,
        "year": int(year),
        "schools": schools,
        "totals": totals,
    })



@login_required
def best_schools_pdf(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")

    if not region_id or not year:
        return HttpResponse("Missing parameters", status=400)

    # MUST load region object (pdf_header_block_simple expects an object not ID)
    region = get_object_or_404(Region, pk=region_id)

    # PDF response
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = f'inline; filename="Best_Schools_{region.name}_{year}.pdf"'

    doc = SimpleDocTemplate(response)
    story = []

    # HEADER BLOCK
    story.append(pdf_header_block_simple(region, year))
    story.append(Spacer(1, 10))

    # BEST & WORST TABLE BLOCKS (Top10, Bottom10, etc.)
    blocks = pdf_best_worst_group(region_id, year)
    for b in blocks:
        story.append(b)

    # Build final PDF
    doc.build(story)
    return response






@login_required
def school_ranking_excel(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    district_id = request.GET.get("district")
    ward_id = request.GET.get("ward")

    if not region_id or not year:
        return HttpResponse("Missing parameters", status=400)

    region = get_object_or_404(Region, pk=region_id)
    schools, totals = build_school_rank_data(region_id, year, district_id, ward_id)

    wb = Workbook()
    ws = wb.active
    ws.title = "School Ranking"

    headers = [
        "Rank","School Code","School Name",
        "A(F)","A(M)","A(T)",
        "B(F)","B(M)","B(T)",
        "C(F)","C(M)","C(T)",
        "D(F)","D(M)","D(T)",
        "E(F)","E(M)","E(T)",
        "Total Std","Pass F","Pass M","Pass T","% Pass","Avg Score"
    ]
    ws.append(headers)

    for s in schools:
        ws.append([
            s["rank"], s["school_code"], s["name"],
            s["af"], s["am"], s["a"],
            s["bf"], s["bm"], s["b"],
            s["cf"], s["cm"], s["c"],
            s["df"], s["dm"], s["d"],
            s["ef"], s["em"], s["e"],
            s["total_std"],
            s["pass_f"], s["pass_m"], s["pass_t"],
            s["pass_pct"],
            s["avg_score"]
        ])

    # totals row
    ws.append([
        "TOTAL", "", "",
        totals["af"], totals["am"], totals["a"],
        totals["bf"], totals["bm"], totals["b"],
        totals["cf"], totals["cm"], totals["c"],
        totals["df"], totals["dm"], totals["d"],
        totals["ef"], totals["em"], totals["e"],
        totals["total_std"],
        totals["pass_f"], totals["pass_m"], totals["pass_t"],
        "-", totals["avg_score"]
    ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    fname = f"school_ranking_{(region.name)}_{year}.xlsx"
    response["Content-Disposition"] = f'attachment; filename="{fname}"'
    wb.save(response)
    return response
    
@login_required
def school_ranking_pdf(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    district_id = request.GET.get("district")  # optional
    ward_id = request.GET.get("ward")  # optional
    page = int(request.GET.get("page", 1))

    if not region_id or not year:
        return render(request, "exams/error.html", {"message":"Missing region or year"})

    region = get_object_or_404(Region, pk=region_id)
    year = int(year)

    base_qs = ExamCandidate.objects.filter(year=year, school__region_id=region.id)

    # iterate schools with candidates OR use a join to distinct school ids
    school_ids = base_qs.values_list("school_id", flat=True).distinct()
    schools = School.objects.filter(id__in=school_ids)
    if district_id:
        schools = schools.filter(district_id=district_id)
    if ward_id:
        schools = schools.filter(ward_id=ward_id)

    ranking_rows = []
    for s in schools:
        s_qs = base_qs.filter(school_id=s.id)
        agg = compute_rank_aggregates(s_qs)
        ranking_rows.append({
            "school": s,
            "total": agg["total"],
            "pass_count": agg["pass_count"],
            "fail_count": agg["fail_count"],
            "pass_pct": agg["pass_pct"],
            "avg_score": agg["avg_score"],
            "grades": agg["grades"],
        })

    ranking_rows.sort(key=lambda r: (r["pass_pct"], r["avg_score"], r["pass_count"]), reverse=True)

    paginator = Paginator(ranking_rows, 100)
    page_obj = paginator.get_page(page)

    return render(request, "exams/reports/school_ranking.html", {
        "region": region,
        "year": year,
        "page_obj": page_obj,
    })


















@login_required
def best_schools_tables(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")

    if not region_id or not year:
        return HttpResponse("Missing parameters", status=400)

    region = get_object_or_404(Region, pk=region_id)

    data = build_best_worst_tables(region_id, year)

    return render(request, "exams/reports/best10_tables.html", {
        "region": region,
        "year": year,
        **data
    })


@login_required
def best_schools_pdf(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")

    if not region_id or not year:
        return HttpResponse("Missing parameters", status=400)

    # MUST load region object (pdf_header_block_simple expects an object not ID)
    region = get_object_or_404(Region, pk=region_id)

    # PDF response
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = f'inline; filename="Best_Schools_{region.name}_{year}.pdf"'

    doc = SimpleDocTemplate(response)
    story = []

    # HEADER BLOCK
    story.append(pdf_header_block_simple(region, year))
    story.append(Spacer(1, 10))

    # BEST & WORST TABLE BLOCKS (Top10, Bottom10, etc.)
    blocks = pdf_best_worst_group(region_id, year)
    for b in blocks:
        story.append(b)

    # Build final PDF
    doc.build(story)
    return response









@login_required
def best_schools_excel(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")

    region = get_object_or_404(Region, pk=region_id)
    tables = build_best_worst_tables(region_id, year)

    wb = Workbook()

    def add_sheet(title, rows):
        ws = wb.create_sheet(title)
        headers = [
            "CODE","NAME","DISTRICT","TOTAL",
            "A","B","C","D","E",
            "PASS A-C","% PASS","AVERAGE","AVG GRADE","STATUS"
        ]
        ws.append(headers)

        for r in rows:
            ws.append([
                r["school_code"], r["name"], r["district"], r["total"],
                r["A"], r["B"], r["C"], r["D"], r["E"],
                r["pass_ac"], r["pass_pct"],
                r["avg"], r["avg_grade"], r["status"],
            ])

    add_sheet("Top10", tables["top10"])
    add_sheet("Bottom10", tables["bottom10"])
    add_sheet("Top10_30plus", tables["top10_big"])
    add_sheet("Bottom10_30plus", tables["bottom10_big"])
    add_sheet("Top10_Gov", tables["top10_gov"])

    # remove default empty sheet
    if "Sheet" in wb.sheetnames:
        wb.remove(wb["Sheet"])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="best10_{region.name}_{year}.xlsx"'
    wb.save(response)
    return response










# --------------------------------------------------------------
# SUBJECTS
# --------------------------------------------------------------
@login_required
def general_subject_summary(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject", "ALL")

    region, summary = get_region_subject_summary(region_id, year, subject)

    return render(request, "exams/reports/subject_summary.html", {
        "summary": summary,
        "subject": subject,
        "year": year,
        "region": region,
    })





@login_required
def region_subject_summary_pdf(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject", "ALL")

    region, summary = get_region_subject_summary(region_id, year, subject)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Subject_Summary_{region.name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response, pagesize=A4,
        leftMargin=20, rightMargin=20,
        topMargin=20, bottomMargin=30
    )

    story = []
    styles = getSampleStyleSheet()

    story.append(Paragraph(
        f"<b>SUBJECT SUMMARY – {year}</b><br/>{region.name}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    data = [[
        "Subject", "Name", "Total",
        "A", "B", "C", "D", "E",
        "Pass A–C", "% Pass"
    ]]

    for r in summary:
        data.append([
            r["subject_code"],
            r["subject_name_sw"],
            r["total_std"],
            r["A"], r["B"], r["C"], r["D"], r["E"],
            r["pass_ac"],
            f'{r["pass_pct"]}%',
        ])

    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (2,1), (-1,-1), "CENTER"),
    ]))

    story.append(table)
    doc.build(story)
    return response


@login_required
def region_subject_summary_excel(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject", "ALL")

    region, summary = get_region_subject_summary(region_id, year, subject)

    wb = Workbook()
    ws = wb.active
    ws.title = "Subject Summary"

    headers = [
        "Subject", "Name", "Total",
        "A", "B", "C", "D", "E",
        "Pass A–C", "% Pass"
    ]
    ws.append(headers)

    for r in summary:
        ws.append([
            r["subject_code"],
            r["subject_name_sw"],
            r["total_std"],
            r["A"], r["B"], r["C"], r["D"], r["E"],
            r["pass_ac"],
            r["pass_pct"],
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="Subject_Summary_{region.name}_{year}.xlsx"'
    )

    wb.save(response)
    return response








@login_required
def subject_summary_pdf(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    subject = request.GET.get("subject")

    region = get_object_or_404(Region, pk=region_id)

    # Build summary again (reuse logic)
    response_html = general_subject_summary(request).content.decode()

    response = HttpResponse(content_type="application/pdf")
    response['Content-Disposition'] = (
        f'attachment; filename="subject_summary_{region.name}_{subject}_{year}.pdf"'
    )

    pisa.CreatePDF(response_html, dest=response)
    return response


import openpyxl
from openpyxl.styles import Font, Alignment
from django.http import HttpResponse


@login_required
def subject_summary_excel(request):
    region_id = request.GET.get("region")
    year = request.GET.get("year")
    subject = request.GET.get("subject")

    region = get_object_or_404(Region, pk=region_id)

    # Fetch summary
    res = general_subject_summary(request)
    context = res.context_data
    summary = context["summary"]

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Subject Summary"

    headers = ["SUBJECT", "TOTAL", "A", "B", "C", "D", "E", "PASS A-C", "% PASS"]
    ws.append(headers)

    for cell in ws[1]:
        cell.font = Font(bold=True)
        cell.alignment = Alignment(horizontal="center")

    for row in summary:
        pass_ac = (row["A"] or 0) + (row["B"] or 0) + (row["C"] or 0)
        pct = round(pass_ac / row["total_std"] * 100, 2) if row["total_std"] else 0

        ws.append([
            row["subject_code"],
            row["total_std"],
            row["A"], row["B"], row["C"], row["D"], row["E"],
            pass_ac,
            pct
        ])

    response = HttpResponse(
        content=wb,
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="subject_summary_{subject}_{year}.xlsx"'

    wb.save(response)
    return response









@login_required
def regional_subject_school_ranking(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject")  # subject CODE

    region = get_object_or_404(Region, pk=region_id)

    # ✅ RESOLVE SUBJECT NAME
    subject_obj = ExamSubject.objects.filter(subject_id=subject).first()
    subject_name_sw = subject_obj.subject_name_sw if subject_obj else "UNKNOWN"

    schools = School.objects.filter(region_id=region_id)
    rows = []

    for s in schools:
        perf = SchoolSubjectPerformance.objects.filter(
            school_code=s.school_code,
            year=year,
            subject_code=subject
        ).first()

        if perf:
            total_std = perf.total_std or 0
            avg_subj = float(perf.subj_avg or 0)

            pass_ac = total_std if perf.subj_grade in ["A", "B", "C"] else 0
            pass_pct = round((pass_ac / total_std * 100), 2) if total_std else 0

            rows.append({
                "school_code": s.school_code,
                "school": s.name,
                "district": s.district.name if s.district else "",
                "total": total_std,
                "pass_ac": pass_ac,
                "pass_pct": pass_pct,
                "avg": round(avg_subj, 4),
            })

    rows.sort(key=lambda r: (r["avg"], r["pass_pct"]), reverse=True)

    for i, r in enumerate(rows, start=1):
        r["rank"] = i

    return render(
        request,
        "exams/reports/regional_subject_school_ranking.html",
        {
            "rows": rows,
            "region": region,
            "year": year,
            "subject_code": subject,
            "subject_name_sw": subject_name_sw,
        }
    )




 
@login_required
def regional_subject_school_ranking_pdf(request):
    ctx = get_regional_subject_school_ranking(
        request.GET.get("region"),
        int(request.GET.get("year")),
        request.GET.get("subject"),
    )

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Regional_Subject_School_Rank_{ctx["year"]}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=20,
        rightMargin=20,
        topMargin=20,
        bottomMargin=30
    )

    styles = getSampleStyleSheet()
    story = []

    # -------- HEADER --------
    story.append(Paragraph(
        f"<b>REGIONAL SUBJECT SCHOOL RANKING</b><br/>"
        f"{ctx['subject_name_sw']} ({ctx['subject_code']}) – {ctx['year']}<br/>"
        f"{ctx['region'].name}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    # -------- TABLE --------
    data = [[
        "Rank", "School Code", "School",
        "District", "Total",
        "Pass A–C", "% Pass", "Average"
    ]]

    for r in ctx["rows"]:
        data.append([
            r["rank"],
            r["school_code"],
            r["school"],
            r["district"],
            r["total"],
            r["pass_ac"],
            f'{r["pass_pct"]}%',
            r["avg"],
        ])

    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#E2F7FD")),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (4,1), (-1,-1), "CENTER"),
    ]))

    story.append(table)
    doc.build(story)
    return response





@login_required
def regional_subject_school_ranking_excel(request):
    ctx = get_regional_subject_school_ranking(
        request.GET.get("region"),
        int(request.GET.get("year")),
        request.GET.get("subject"),
    )

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Regional Subject Ranking"

    ws.append([
        "Rank", "School Code", "School",
        "District", "Total",
        "Pass A–C", "% Pass", "Average"
    ])

    for r in ctx["rows"]:
        ws.append([
            r["rank"],
            r["school_code"],
            r["school"],
            r["district"],
            r["total"],
            r["pass_ac"],
            r["pass_pct"],
            r["avg"],
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="Regional_Subject_School_Rank_{ctx["year"]}.xlsx"'
    )

    wb.save(response)
    return response




















def compute_trend(region_id, start_year, end_year):

    print("DEBUG TREND INPUT:", region_id, start_year, end_year)

    start_year = int(start_year)
    end_year = int(end_year)

    years = list(range(start_year, end_year + 1))
    rows = []
    prev_pct = None

    for y in years:

        print("\n===== DEBUG YEAR:", y, "=====")

        qs = ExamCandidate.objects.filter(
            year=y,
            school__district__region_id=region_id
        )

        total = qs.filter(avg_grade__in=["A", "B", "C", "D", "E"]).count()
        passed = qs.filter(avg_grade__in=["A", "B", "C"]).count()

        pct = (passed / total * 100) if total else 0
        pct = round(pct, 3)

        # Determine remark + icon
        if prev_pct is None:
            remark = "START"
            icon = "◎"
        elif pct > prev_pct:
            remark = "RISE"
            icon = "▲"
        elif pct < prev_pct:
            remark = "DROP"
            icon = "▼"
        else:
            remark = "SAME"
            icon = "■"

        prev_pct = pct

        rows.append({
            "year": y,
            "total": total,
            "passed": passed,
            "pct": pct,
            "remark": remark,
            "icon": icon,
        })
    return rows



@login_required
def region_trend_excel(request):

    region = request.GET.get("region")
    start = request.GET.get("start")
    end = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_trend(region, start, end, exam_type_id)

    from openpyxl import Workbook
    from django.http import HttpResponse

    wb = Workbook()
    ws = wb.active
    ws.title = "Region Trend"

    ws.append([
        "Year","I","II","III","IV","0","Total",
        "Pass I-III","%","Pass I-IV","%","GPA","Trend","Remark"
    ])

    for r in rows:
        ws.append([
            r["year"], r["d1"], r["d2"], r["d3"], r["d4"], r["d0"],
            r["total"],
            r["pass_i_iii"], r["pass_i_iii_pct"],
            r["pass_i_iv"], r["pass_i_iv_pct"],
            r["gpa"], r["icon"], r["remark"]
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = "attachment; filename=Region_Trend.xlsx"

    wb.save(response)
    return response


@login_required
def region_trend_pdf(request):

    region_id = request.GET.get("region")
    start = request.GET.get("start")
    end = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_trend(region_id, start, end, exam_type_id)

    region = Region.objects.get(id=region_id)
    exam_type = ExamType.objects.get(id=exam_type_id)

    from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
    from reportlab.lib.pagesizes import A4
    from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
    from reportlab.lib.enums import TA_CENTER
    from reportlab.lib import colors
    from django.http import HttpResponse

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = "attachment; filename=Region_Trend_Report.pdf"

    doc = SimpleDocTemplate(response, pagesize=A4)

    styles = getSampleStyleSheet()

    title_style = ParagraphStyle(
        "TitleStyle",
        parent=styles["Heading1"],
        alignment=TA_CENTER,
        textColor=colors.black,
        fontSize=14
    )

    subtitle_style = ParagraphStyle(
        "SubtitleStyle",
        parent=styles["Normal"],
        alignment=TA_CENTER,
        fontSize=11
    )

    elements = []

    # ---------- TITLE ----------
    elements.append(Paragraph(
        f"{region.name} – {exam_type.name}",
        title_style
    ))

    elements.append(Paragraph(
        f"GPA Performance Trend ({start} – {end})",
        subtitle_style
    ))

    elements.append(Spacer(1, 12))

    # ---------- TABLE ----------
    data = [[
        "Year","I","II","III","IV","0","Total",
        "Pass I-III","%","Pass I-IV","%","GPA","Trend","Remark"
    ]]

    for r in rows:
        data.append([
            r["year"], r["d1"], r["d2"], r["d3"], r["d4"], r["d0"],
            r["total"],
            r["pass_i_iii"], r["pass_i_iii_pct"],
            r["pass_i_iv"], r["pass_i_iv_pct"],
            r["gpa"], r["icon"], r["remark"]
        ])

    table = Table(data, repeatRows=1)

    table.setStyle(TableStyle([
        ("GRID",(0,0),(-1,-1),1,colors.black),
        ("BACKGROUND",(0,0),(-1,0),colors.lightgrey),
        ("ALIGN",(0,0),(-1,-1),"CENTER"),
        ("FONT",(0,0),(-1,0),"Helvetica-Bold"),
        ("BOTTOMPADDING",(0,0),(-1,0),8),
    ]))

    elements.append(table)

    doc.build(elements)

    return response








def compute_district_trend(region_id, start_year, end_year):
    from exams.models import District, ExamCandidate

    start_year = int(start_year)
    end_year = int(end_year)

    years = list(range(start_year, end_year + 1))
    districts = District.objects.filter(region_id=region_id).order_by("name")

    result = []

    for d in districts:
        prev_pct = None
        year_rows = []

        for y in years:
            qs = ExamCandidate.objects.filter(
                year=y,
                school__district_id=d.id
            )

            total = qs.count()
            passed = qs.filter(avg_grade__in=["A", "B", "C"]).count()
            pct = round((passed / total * 100), 3) if total else 0

            # Remark + icon
            if prev_pct is None:
                remark = "START"
                icon = "◎"
            elif pct > prev_pct:
                remark = "RISE"
                icon = "▲"
            elif pct < prev_pct:
                remark = "DROP"
                icon = "▼"
            else:
                remark = "SAME"
                icon = "■"

            prev_pct = pct

            year_rows.append({
                "year": y,
                "total": total,
                "passed": passed,
                "pct": pct,
                "remark": remark,
                "icon": icon,
            })

        result.append({
            "district": d.name,
            "years": year_rows
        })

    return years, result








def district_trend_pdf(request):
    region_id = request.GET.get("region")
    start = request.GET.get("start")
    end = request.GET.get("end")

    years, rows = compute_district_trend(region_id, start, end)
    region = Region.objects.get(id=region_id)

    html = render_to_string("exams/reports/district_trend_pdf.html", {
        "region": region,
        "years": years,
        "rows": rows
    })

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = "attachment; filename=district_trend.pdf"

    pisa.CreatePDF(html, dest=response)
    return response





import openpyxl
from openpyxl.styles import Font, Alignment
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required

@login_required
def district_trend_excel(request):

    district_id = request.GET.get("district")
    start = request.GET.get("start")
    end = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    # ---- build data from SINGLE source ----
    rows = build_district_trend(
        district_id=district_id,
        start_year=start,
        end_year=end,
        exam_type_id=exam_type_id
    )

    district = District.objects.get(id=district_id)
    exam_type = ExamType.objects.get(id=exam_type_id)

    # ---- workbook ----
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "District Trend"

    bold = Font(bold=True)
    center = Alignment(horizontal="center")

    r = 1

    # ================= HEADER =================
    ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=14)
    ws.cell(r, 1).value = (
        f"{exam_type} – {district.name.upper()} "
        f"GPA TREND ({start}–{end})"
    )
    ws.cell(r, 1).font = Font(bold=True, size=14)
    ws.cell(r, 1).alignment = center
    r += 2

    # ================= TABLE HEADER =================
    headers = [
        "Year",
        "I", "II", "III", "IV", "0",
        "Total",
        "Pass I–III", "% I–III",
        "Pass I–IV", "% I–IV",
        "Division GPA",
        "Subject GPA",
        "Final GPA",
        "Trend",
        "Remark",
    ]

    ws.append(headers)
    for c in range(1, len(headers) + 1):
        ws.cell(r, c).font = bold
        ws.cell(r, c).alignment = center
    r += 1

    # ================= DATA ROWS =================
    for row in rows:
        ws.append([
            row["year"],
            row["d1"],
            row["d2"],
            row["d3"],
            row["d4"],
            row["d0"],
            row["total"],
            row["pass_i_iii"],
            row["pass_i_iii_pct"],
            row["pass_i_iv"],
            row["pass_i_iv_pct"],
            row["div_gpa"],
            row["subj_gpa"],
            row["gpa"],
            row["icon"],
            row["remark"],
        ])
        r += 1

    # ================= COLUMN WIDTHS =================
    widths = {
        1: 10,  # Year
        7: 10,  # Total
        12: 12, # Div GPA
        13: 12, # Sub GPA
        14: 12, # Final GPA
        16: 12  # Remark
    }
    for col, w in widths.items():
        ws.column_dimensions[openpyxl.utils.get_column_letter(col)].width = w

    # ================= RESPONSE =================
    filename = (
        f"{exam_type}-"
        f"{start}-{end}-"
        f"{district.name.replace(' ','_')}-Trend.xlsx"
    )

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response




@login_required
def region_trend_pdf_reportlab(request):
    region_id = request.GET.get("region")
    start_year = request.GET.get("start")
    end_year = request.GET.get("end")

    if not region_id or not start_year or not end_year:
        return HttpResponse("Missing parameters", status=400)

    region = get_object_or_404(Region, pk=region_id)
    rows = compute_trend(region_id, start_year, end_year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Region_Trend_{region.name}_{start_year}_{end_year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=25, rightMargin=25,
        topMargin=20, bottomMargin=30
    )

    styles = getSampleStyleSheet()
    story = []

    # -------- HEADER --------
    story.append(Paragraph(
        f"<b>REGION PASS TREND</b><br/>"
        f"{region.name}<br/>"
        f"{start_year} – {end_year}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    # -------- TABLE --------
    data = [[
        "Year", "Total Candidates",
        "Passed (A–C)", "% Pass",
        "Remark"
    ]]

    for r in rows:
        data.append([
            r["year"],
            r["total"],
            r["passed"],
            f'{r["pct"]}%',
            f'{r["remark"]} {r["icon"]}'
        ])

    table = Table(data, repeatRows=1, hAlign="LEFT")
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#CFEAFB")),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
        ("BOTTOMPADDING", (0,0), (-1,-1), 6),
        ("TOPPADDING", (0,0), (-1,-1), 6),
    ]))

    story.append(table)
    doc.build(story)

    return response







@login_required
def district_trend_pdf_reportlab(request):
    region_id = request.GET.get("region")
    start_year = request.GET.get("start")
    end_year = request.GET.get("end")

    if not region_id or not start_year or not end_year:
        return HttpResponse("Missing parameters", status=400)

    region = get_object_or_404(Region, pk=region_id)
    years, rows = compute_district_trend(region_id, start_year, end_year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="District_Trend_{region.name}_{start_year}_{end_year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A4),
        leftMargin=20, rightMargin=20,
        topMargin=20, bottomMargin=20
    )

    styles = getSampleStyleSheet()
    story = []

    # -------- HEADER --------
    story.append(Paragraph(
        f"<b>DISTRICT PASS TREND</b><br/>"
        f"{region.name}<br/>"
        f"{start_year} – {end_year}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    # -------- TABLE HEADER --------
    header = ["District"]
    for y in years:
        header += [
            f"{y} Total",
            f"{y} Pass",
            f"{y} %",
            f"{y} Remark"
        ]

    data = [header]

    # -------- ROWS --------
    for d in rows:
        row = [d["district"]]
        for yr in d["years"]:
            row += [
                yr["total"],
                yr["passed"],
                f'{yr["pct"]}%',
                f'{yr["remark"]} {yr["icon"]}'
            ]
        data.append(row)

    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#E2F7FD")),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
        ("VALIGN", (0,0), (-1,-1), "MIDDLE"),
        ("FONTSIZE", (0,0), (-1,-1), 8),
        ("BOTTOMPADDING", (0,0), (-1,-1), 4),
        ("TOPPADDING", (0,0), (-1,-1), 4),
    ]))

    story.append(table)
    doc.build(story)

    return response



















# --------------------------------------------------------------
# MAIN DASHBOARD API (based on SchoolYearSummary)
# --------------------------------------------------------------
@require_GET
@login_required
def ajax_dashboard_data(request):

    region = request.GET.get("region")
    district = request.GET.get("district")
    ward = request.GET.get("ward")
    school = request.GET.get("school")
    examtype = request.GET.get("examtype")
    ownership = request.GET.get("ownership")
    start_year = request.GET.get("start_year")
    end_year = request.GET.get("end_year")
    subject_param = request.GET.get("subject")
    gender = request.GET.get("gender")
 
    # =======================
    # School summary queryset
    # =======================
    qs = SchoolYearSummary.objects.select_related("school", "exam_type")

    if region:
        qs = qs.filter(school__region_id=region)
    if district:
        qs = qs.filter(school__district_id=district)
    if ward:
        qs = qs.filter(school__ward_id=ward)
    if school:
        qs = qs.filter(school__school_code=school)
    if examtype:
        qs = qs.filter(exam_type_id=examtype)
    if ownership:
        qs = qs.filter(school__ownership=ownership)
    if start_year:
        qs = qs.filter(year__gte=start_year)
    if end_year:
        qs = qs.filter(year__lte=end_year)

    profile = getattr(request.user, "profile", None)
    if profile and profile.role != "super":
        if profile.role == "region":
            qs = qs.filter(school__region_id=profile.region_id)
        elif profile.role == "district":
            qs = qs.filter(school__district_id=profile.district_id)
        elif profile.role == "ward":
            qs = qs.filter(school__ward_id=profile.ward_id)
        elif profile.role == "school":
            qs = qs.filter(school__school_code=profile.school.school_code)

    # =====================================
    # Division summary queryset (IMPORTANT)
    # =====================================
    div_qs = SchoolDivisionSummary.objects.select_related("school")

    if region:
        div_qs = div_qs.filter(school__region_id=region)
    if district:
        div_qs = div_qs.filter(school__district_id=district)
    if ward:
        div_qs = div_qs.filter(school__ward_id=ward)
    if school:
        div_qs = div_qs.filter(school__school_code=school)
    if examtype:
        div_qs = div_qs.filter(exam_type_id=examtype)
    if ownership:
        div_qs = div_qs.filter(school__ownership=ownership)
    if start_year:
        div_qs = div_qs.filter(year__gte=start_year)
    if end_year:
        div_qs = div_qs.filter(year__lte=end_year)
    if gender in ["M", "F"]:
        div_qs = div_qs.filter(sex=gender)

    if profile and profile.role != "super":
        if profile.role == "region":
            div_qs = div_qs.filter(school__region_id=profile.region_id)
        elif profile.role == "district":
            div_qs = div_qs.filter(school__district_id=profile.district_id)
        elif profile.role == "ward":
            div_qs = div_qs.filter(school__ward_id=profile.ward_id)
        elif profile.role == "school":
            div_qs = div_qs.filter(school__school_code=profile.school.school_code)

    # =====================
    # PASS / FAIL KPIs
    # =====================
    PASS_DIVS = ["I", "II", "III", "IV"]

    FAIL_DIVS = ["DIV 0","DIV O","0","F"]

    fail_candidates = div_qs.filter(
        division__in=FAIL_DIVS
    ).aggregate(t=Sum("total"))["t"] or 0

    pass_candidates = div_qs.filter(division__in=PASS_DIVS).aggregate(t=Sum("total"))["t"] or 0
    # fail_candidates = div_qs.filter(division="O").aggregate(t=Sum("total"))["t"] or 0

    total_candidates = pass_candidates + fail_candidates

    pass_pct = round((pass_candidates / max(total_candidates,1)) * 100, 1)

    donut = [
        {"name": "Pass", "value": int(pass_candidates)},
        {"name": "Fail", "value": int(fail_candidates)},
    ]


    # =====================
    # PASS / FAIL KPIs
    # =====================
    PASS_I_III_DIVS = ["I", "II", "III"]

    pass_i_iii_candidates = div_qs.filter(
        division__in=PASS_I_III_DIVS
    ).aggregate(t=Sum("total"))["t"] or 0

    total_candidates = pass_candidates + fail_candidates

    pass_i_iii_pct = round((pass_i_iii_candidates / max(total_candidates,1)) * 100, 1)


    # =====================
    # Gender KPIs
    # =====================
    male_total = div_qs.filter(sex="M").aggregate(t=Sum("total"))["t"] or 0
    female_total = div_qs.filter(sex="F").aggregate(t=Sum("total"))["t"] or 0

    male_pass = div_qs.filter(sex="M", division__in=PASS_DIVS).aggregate(t=Sum("total"))["t"] or 0
    female_pass = div_qs.filter(sex="F", division__in=PASS_DIVS).aggregate(t=Sum("total"))["t"] or 0

    male_pct = round((male_pass / male_total * 100),1) if male_total else 0
    female_pct = round((female_pass / female_total * 100),1) if female_total else 0

    male_i_iii_pass = div_qs.filter(sex="M", division__in=PASS_I_III_DIVS).aggregate(t=Sum("total"))["t"] or 0
    female_i_iii_pass = div_qs.filter(sex="F", division__in=PASS_I_III_DIVS).aggregate(t=Sum("total"))["t"] or 0

    male_i_iii_pct = round((male_i_iii_pass / male_total * 100),1) if male_total else 0
    female_i_iii_pct = round((female_i_iii_pass / female_total * 100),1) if female_total else 0


    # =====================
    # Grades distribution
    # =====================
    grade_order = ["I","II","III","IV","0"]
    grade_counts_raw = div_qs.values("division").annotate(t=Sum("total"))

    grade_values = [
        next((g["t"] for g in grade_counts_raw if g["division"] == gr),0)
        for gr in grade_order
    ]

    # =====================
    # Gender pass per year
    # =====================
    gender_stats = (
        div_qs.values("year")
        .annotate(
            male_pass=Sum("total", filter=Q(sex="M", division__in=PASS_DIVS)),
            male_total=Sum("total", filter=Q(sex="M")),
            female_pass=Sum("total", filter=Q(sex="F", division__in=PASS_DIVS)),
            female_total=Sum("total", filter=Q(sex="F")),
        )
        .order_by("year")
    )

    gender_years, male_rates, female_rates = [], [], []

    for r in gender_stats:
        gender_years.append(r["year"])
        m = (r["male_pass"] or 0) / (r["male_total"] or 1) * 100
        f = (r["female_pass"] or 0) / (r["female_total"] or 1) * 100
        male_rates.append(round(m,1))
        female_rates.append(round(f,1))

    # =====================
    # Line chart (GPA per year – NECTA)
    # =====================
    from exams.gpa_calculation import (
        region_division_gpa,
        region_subject_gpa_value,
        district_division_gpa,
        district_subject_gpa_value,
    )

    year_list = (
        qs.values_list("year", flat=True)
        .distinct()
        .order_by("year")
    )

    years = []
    avg_series = []

    for y in year_list:

        # -------------------
        # REGION LEVEL
        # -------------------
        if region and not district and not ward and not school:
            div_gpa = region_division_gpa(
                year=y,
                exam_type_id=examtype,
                region_id=region,
            )

            subj_gpa = region_subject_gpa_value(
                year=y,
                exam_type_id=examtype,
                region_id=region,
            )

            final_gpa = (div_gpa + subj_gpa) / 2 if div_gpa and subj_gpa else 0

        # -------------------
        # DISTRICT LEVEL
        # -------------------
        elif district and not ward and not school:
            div_gpa = district_division_gpa(
                year=y,
                exam_type_id=examtype,
                district_id=district,
            )

            subj_gpa = district_subject_gpa_value(
                year=y,
                exam_type_id=examtype,
                district_id=district,
            )

            final_gpa = (div_gpa + subj_gpa) / 2 if div_gpa and subj_gpa else 0

        # -------------------
        # SCHOOL LEVEL
        # -------------------
        elif school:
            sy = SchoolYearSummary.objects.filter(
                year=y,
                exam_type_id=examtype,
                school__school_code=school
            ).first()

            final_gpa = float(sy.avg_gpa) if sy and sy.avg_gpa else 0

        # -------------------
        # FALLBACK (mixed filters)
        # -------------------
        else:
            final_gpa = qs.filter(year=y).aggregate(
                gpa=Avg("avg_gpa")
            )["gpa"] or 0

        years.append(y)
        avg_series.append(round(float(final_gpa), 4))






    # =====================
    # Line chart (school avg)
    # =====================
    # year_stats = qs.values("year").annotate(avg_mark=Avg("avg_numeric")).order_by("year")
    # years = [r["year"] for r in year_stats]
    # avg_series = [float(r["avg_mark"] or 0) for r in year_stats]

    # =====================
    # Top schools
    # =====================
    top_schools_raw = qs.values("school__school_code","school__name")\
        .annotate(avg_mark=Avg("avg_numeric"), candidates=Sum("candidates"))\
        .order_by("-avg_mark")[:10]

    top_schools = [
        {
            "school_code": r["school__school_code"],
            "school": r["school__name"],
            "avg": round(float(r["avg_mark"] or 0),2),
            "candidates": int(r["candidates"] or 0),
        }
        for r in top_schools_raw
    ]

    # =====================
    # Final JSON
    # =====================
    data = {
        "cards": [
            {"title":"Total Schools","value":qs.values("school__school_code").distinct().count()},
            {"title":"Total Candidates","value":int(total_candidates)},
            {"title":"Avg Marks","value":round(qs.aggregate(avg=Avg("avg_numeric"))["avg"] or 0,2)},
            {"title":"Pass %","value":f"{pass_pct}%"},
        ],
        "donut":donut,
        "line":{"years":years,"avg_marks":avg_series},
        "top_schools":top_schools,
        "grades":{"labels":grade_order,"values":grade_values},
        "gender_pass":{"years":gender_years,"male":male_rates,"female":female_rates},
        "gender_kpi":{
            "pass_i_iii": pass_i_iii_candidates,
            "pass_i_iii_pct": pass_i_iii_pct,

            "male_total":male_total,
            "female_total":female_total,
            "male_pass":male_pass,
            "female_pass":female_pass,
            "male_pct":male_pct,
            "female_pct":female_pct,

            "male_i_iii_pass":male_i_iii_pass,
            "female_i_iii_pass":female_i_iii_pass,
            "male_i_iii_pct":male_i_iii_pct,
            "female_i_iii_pct":female_i_iii_pct,
        }
    }

    return JsonResponse(data, safe=False)










@login_required
def bulk_uploads_page(request):
    stats = {
        "school_year_summary": SchoolYearSummary.objects.count(),
        "school_division_summary": SchoolDivisionSummary.objects.count(),
        "school_subject_grade_summary": SchoolSubjectGradeSummary.objects.count(),
        "exam_candidate": ExamCandidate.objects.count(),
        "school_subject_performance": SchoolSubjectPerformance.objects.count(),
        "student_subject_mark": StudentSubjectMark.objects.count(),
        "auth_user": UserProfile.objects.count(),
    }

    return render(request, "exams/bulk_uploads.html", {
        "stats": stats,
        "exam_types": ExamType.objects.all()
    })


TABLES = {
    "exam_candidate": {
        "table": "exam_candidate",
        "columns": "(year, school_code, cand_no, prem_no, name, sex, avg_numeric, avg_grade, full_results, division, points, cand_comb)",
        "requires_exam_type": True,
    },

    "student_subject_mark": {
        "table": "student_subject_mark",
        "columns": "(year, school_code, cand_no, subject_code, grade, numeric_score)",
        "requires_exam_type": True,
    },
    "school_year_summary": {
        "table": "school_year_summary",
        "columns": "(year, candidates, avg_numeric, avg_grade, status, school_code, avg_gpa)",
        "requires_exam_type": True,
    },
    "school_subject_performance": {
        "table": "school_subject_performance",
        "columns": "(school_code, year, subject_code, total_std, subj_avg, subj_grade, subj_status,subj_gpa, total_pass)",
        "requires_exam_type": True,
    },


    "school_division_summary": {
        "table": "school_division_summary",
        "columns": "(year, sex, division, total, school_code)",
        "requires_exam_type": True,
    },

    "school_subject_grade_summary": {
        "table": "school_subject_grade_summary",
        "columns": "(year, subject_code, sex, grade, total, school_code)",
        "requires_exam_type": True,
    },
    "auth_user": {
        "table": "auth_user",
        "columns": "(password, username, email)",
    },

}




@login_required
def bulk_upload_handler(request, target):
    if request.method != "POST":
        return redirect("exams:bulk_uploads")

    csv_file = request.FILES.get("csv_file")
    exam_type_id = request.POST.get("exam_type_id")

    if not csv_file:
        messages.error(request, "No CSV file selected.")
        return redirect("exams:bulk_uploads")

    if target not in TABLES:
        messages.error(request, "Invalid upload target.")
        return redirect("exams:bulk_uploads")

    cfg = TABLES[target]

    # Validate exam_type if required
    if cfg["requires_exam_type"]:
        if not exam_type_id or not ExamType.objects.filter(id=exam_type_id).exists():
            messages.error(request, "Valid Exam Type is required for this upload.")
            return redirect("exams:bulk_uploads")

    # Save temp file
    with tempfile.NamedTemporaryFile(delete=False, suffix=".csv", mode="wb") as tmp:
        for chunk in csv_file.chunks():
            tmp.write(chunk)
        file_path = tmp.name.replace("\\", "/")

    try:
        from django.db import connection

        with connection.cursor() as cursor:
            cursor.execute("SET GLOBAL local_infile = 1")

            set_clause = ""
            if cfg["requires_exam_type"]:
                set_clause = f" SET exam_type_id = {int(exam_type_id)}"

            sql = f"""
                LOAD DATA LOCAL INFILE '{file_path}'
                INTO TABLE {cfg['table']}
                FIELDS TERMINATED BY ','
                OPTIONALLY ENCLOSED BY '"'
                LINES TERMINATED BY '\\r\\n'
                IGNORE 1 LINES
                {cfg['columns']}
                {set_clause}
            """

            cursor.execute(sql)
            rows = cursor.rowcount
            connection.commit()

        messages.success(
            request,
            f"{target.replace('_',' ').title()} uploaded successfully. {rows} rows inserted."
        )

    except Exception as e:
        messages.error(request, f"Upload failed: {e}")

    finally:
        if os.path.exists(file_path):
            os.remove(file_path)

    return redirect("exams:bulk_uploads")











@login_required
@require_POST
def bulk_upload_delete(request, target):

    TABLE_MAP = {
        "exam_candidate": "exam_candidate",
        "school_year_summary": "school_year_summary",
        "student_subject_mark": "student_subject_mark",
        "school_subject_performance": "school_subject_performance",

        "school_division_summary": "school_division_summary",
        "school_subject_grade_summary": "school_subject_grade_summary", 

    }

    if target not in TABLE_MAP:
        messages.error(request, "Invalid delete target.")
        return redirect("exams:bulk_uploads")

    table = TABLE_MAP[target]

    with connection.cursor() as cursor:
        cursor.execute(f"TRUNCATE TABLE {table}")

    messages.success(
        request,
        f"All records deleted from {target.upper()}."
    )

    return redirect("exams:bulk_uploads")



@login_required
def bulk_upload_sample(request, target):

    SAMPLES = {
        "exam_candidate": [
            ["year", "school_code", "cand_no", "prem_no","name", "sex", "avg_grade", "status", "full_results", "division", "points", "cand_comb"]
        ],
        "student_subject_mark": [
            ["year","school_code","cand_no","subject_code","grade","numeric_score"]
        ],
        "school_year_summary": [
            ["year","candidates","avg_numeric","avg_grade","status","school_code","avg_gpa"]
        ],
        "school_subject_performance": [
            ["school_code","year","subject_code","total_std","subj_avg","subj_grade","subj_status","subj_gpa","total_pass"]
        ],

        "school_division_summary": [
            ["year","sex","division","total","school_code"]
        ],

        "school_subject_grade_summary": [
            ["year","subject_code","sex","grade","total","school_code"]
        ],
    }

    if target not in SAMPLES:
        return HttpResponse("Invalid sample", status=400)

    response = HttpResponse(content_type="text/csv")
    response["Content-Disposition"] = f"attachment; filename={target}_sample.csv"

    writer = csv.writer(response)
    for row in SAMPLES[target]:
        writer.writerow(row)

    return response






@login_required
def district_subject_rank_pdf(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject")

    ctx = get_district_subject_ranking(region_id, year, subject)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="District_Subject_Rank_{ctx["region"].name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=20,
        rightMargin=20,
        topMargin=20,
        bottomMargin=30
    )

    styles = getSampleStyleSheet()
    story = []

    story.append(Paragraph(
        f"<b>DISTRICT SUBJECT RANKING – {ctx['subject_name_sw']} ({year})</b><br/>{ctx['region'].name}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    data = [[
        "Rank", "District",
        "Total Std", "Pass A–C", "% Pass", "Average"
    ]]

    for r in ctx["rows"]:
        data.append([
            r["rank"],
            r["district"],
            r["total"],
            r["pass_ac"],
            f'{r["pass_pct"]}%',
            r["avg"],
        ])

    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (2,1), (-1,-1), "CENTER"),
    ]))

    story.append(table)
    doc.build(story)
    return response






@login_required
def district_subject_rank_excel(request):
    region_id = request.GET.get("region")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject")

    ctx = get_district_subject_ranking(region_id, year, subject)

    wb = Workbook()
    ws = wb.active
    ws.title = "District Subject Rank"

    ws.append([
        "Rank", "District",
        "Total Std", "Pass I–IV", "% Pass", "Average"
    ])

    for r in ctx["rows"]:
        ws.append([
            r["rank"],
            r["district"],
            r["total"],
            r["pass_ac"],
            r["pass_pct"],
            r["avg"],
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="District_Subject_Rank_{ctx["region"].name}_{year}.xlsx"'
    )

    wb.save(response)
    return response









# views.py
@login_required
def districtwise(request):

    user = request.user
    years = ["2022", "2023", "2024", "2025"]

    # ---------------- ROLE FILTERING ----------------
    if user.profile.role == "super":
        districts = District.objects.select_related("region").all().order_by("name")

    elif user.profile.role == "region":
        districts = District.objects.filter(
            region_id=user.profile.region_id
        ).select_related("region").order_by("name")

    elif user.profile.role == "district":
        districts = District.objects.filter(
            id=user.profile.district_id
        ).select_related("region").order_by("name")

    elif user.profile.role == "ward":
        districts = District.objects.filter(
            region_id=user.profile.ward.district.region_id
        ).select_related("region").order_by("name")

    elif user.profile.role == "school":
        districts = District.objects.filter(
            region_id=user.profile.school.region_id
        ).select_related("region").order_by("name")

    else:
        districts = District.objects.none()

    subjects = ExamSubject.objects.all().order_by("subject_code")

    # ✅ LOAD EXAM TYPES (ID + NAME)
    exam_types = ExamType.objects.filter(is_active=True).only("id", "name").order_by("-year")

    # ---------------- AJAX SEARCH ----------------
    if request.headers.get("x-requested-with") == "XMLHttpRequest":
        query = request.GET.get("q", "").strip().lower()

        results = []
        for d in districts:
            if query in d.name.lower():
                results.append({
                    "id": d.id,
                    "name": d.name,
                    "region": d.region.name,
                    "ward_count": d.wards.count(),
                })

        return JsonResponse({
            "districts": results,
            "exam_types": [
                {"id": e.id, "name": e.name} for e in exam_types
            ]
        })

    # ---------------- NORMAL PAGE LOAD ----------------
    return render(
        request,
        "exams/reports/districtwise.html",
        {
            "districts": districts,
            "subjects": subjects,
            "years": years,
            "exam_types": exam_types,   # ✅ now available in template
        }
    )




@login_required
def district_summary_html(request):
    district_id = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    profile = getattr(request.user, "profile", None)

    if profile and profile.role != "super":
        if profile.role == "district":
            if int(district_id) != profile.district_id:
                return HttpResponseForbidden("Access denied")

        elif profile.role == "region":
            if not District.objects.filter(
                id=district_id,
                region=profile.region
            ).exists():
                return HttpResponseForbidden("Access denied")

        else:
            return HttpResponseForbidden("Access denied")

    ctx = build_district_summary_context(
        district_id, year, exam_type_id
    )
    return render(request, "exams/reports/district_summary_html.html", ctx)



@login_required
def district_trend_html(request):

    district_id = request.GET.get("district")
    start = request.GET.get("start")
    end = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = build_district_trend(
        district_id=district_id,
        start_year=start,
        end_year=end,
        exam_type_id=exam_type_id
    )

    district = District.objects.get(id=district_id)
    exam_type = ExamType.objects.get(id=exam_type_id)

    return render(
        request,
        "exams/reports/district_trend_html.html",
        {
            "district": district,
            "exam_type": exam_type,
            "exam_type_id": exam_type_id,
            "start": start,
            "end": end,
            "rows": rows,
        }
    )



@login_required
def district_ward_ranking_html(request):
    rows = build_district_ward_ranking_context(
        request.GET["district"],
        request.GET["year"],
        request.GET["exam_type_id"]
    )

    return render(request,"exams/reports/district_ward_rank_html.html",{
        "rows":rows,
        "district":District.objects.get(id=request.GET["district"]),
        "year":request.GET["year"]
    })






# views.py
from exams.views_helpers import build_district_summary_context
from exams.app_utils.pdf.pdf_tables import pdf_district_summary_block

from reportlab.platypus import SimpleDocTemplate, Paragraph, Table, TableStyle, Spacer
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import cm   # ← REQUIRED

@login_required
def district_summary_pdf(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))

    ctx = build_district_summary_context(district_id, year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="District_Summary_{ctx["district"].name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=1.2 * cm,
        rightMargin=1.2 * cm,
        topMargin=1.5 * cm,
        bottomMargin=1.5 * cm,
    )

    styles = getSampleStyleSheet()
    story = []

    # =========================================================
    # HEADER
    # =========================================================
    story.append(Paragraph(
        "<b>OFISI YA RAIS – TAMISEMI</b><br/>"
        f"<b>MATOKEO YA DARASA LA NNE – {year}</b><br/><br/>"
        f"<b>WILAYA:</b> {ctx['district'].name}",
        styles["Title"]
    ))
    story.append(Spacer(1, 10))

    # =========================================================
    # SUMMARY (WANAFUNZI)
    # =========================================================
    t = ctx["totals"]

    summary_data = [
        ["", "WASICHANA", "%", "WAVULANA", "%", "JUMLA", "%"],
        [
            "WALIOSAJILIWA",
            t["reg_f"], pct(t["reg_f"], t["reg"]),
            t["reg_m"], pct(t["reg_m"], t["reg"]),
            t["reg"], "100",
        ],
        [
            "WENYE MATOKEO",
            t["sat_f"], pct(t["sat_f"], t["reg"]),
            t["sat_m"], pct(t["sat_m"], t["reg"]),
            t["sat"], pct(t["sat"], t["reg"]),
        ],
        [
            "WASIOFANYA",
            t["abs_f"], pct(t["abs_f"], t["reg"]),
            t["abs_m"], pct(t["abs_m"], t["reg"]),
            t["abs"], pct(t["abs"], t["reg"]),
        ],
    ]

    table = Table(summary_data, colWidths=[5*cm, 2*cm, 2*cm, 2*cm, 2*cm, 2*cm, 2*cm])
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.lightblue),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
    ]))
    story.append(table)
    story.append(Spacer(1, 12))

    # =========================================================
    # GRADE DISTRIBUTION (A–E)
    # =========================================================
    g = ctx["grades"]

    grade_data = [
        ["DARAJA", "A", "B", "C", "D", "E", "PASS A–C", "%"],
        [
            "JUMLA",
            g["a"], g["b"], g["c"], g["d"], g["e"],
            ctx["a_c_total"], pct(ctx["a_c_total"], t["sat"]),
        ],
    ]

    grade_table = Table(grade_data, colWidths=[4*cm]*8)
    grade_table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
    ]))
    story.append(grade_table)
    story.append(Spacer(1, 14))

    # =========================================================
    # SCHOOL RANKING
    # =========================================================
    story.append(Paragraph("<b>UPANGAJI WA SHULE (WASTANI)</b>", styles["Heading2"]))
    story.append(Spacer(1, 6))

    school_data = [
        ["NA", "SHULE", "WASTANI", "DARAJA", "HALI"]
    ]

    for s in ctx["schools"]:
        school_data.append([
            s["rank"],
            s["name"],
            s["avg"],
            s["grade"],
            s["status"],
        ])

    school_table = Table(
        school_data,
        colWidths=[1.2*cm, 7*cm, 2.5*cm, 2.5*cm, 3*cm],
        repeatRows=1
    )
    school_table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#E2F7FD")),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (0,1), (-1,-1), "CENTER"),
        ("ALIGN", (1,1), (1,-1), "LEFT"),
    ]))

    story.append(school_table)

    doc.build(story)
    return response




import openpyxl
from openpyxl.styles import Font, Alignment
from django.http import HttpResponse

@login_required
def district_summary_excel(request):

    district_id = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    ctx = build_district_summary_context(
        district_id=district_id,
        year=year,
        exam_type_id=exam_type_id
    )

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "District Summary"

    bold = Font(bold=True)
    center = Alignment(horizontal="center")

    r = 1

    # ================= HEADER =================
    ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=6)
    ws.cell(r, 1).value = (
        f"{ctx['exam_type_obj']} - "
        f"{ctx['district'].name.upper()} - {year}"
    )
    ws.cell(r, 1).font = Font(bold=True, size=14)
    ws.cell(r, 1).alignment = center
    r += 2

    # ================= TOTALS =================
    ws.cell(r,1).value = "SUMMARY"
    ws.cell(r,1).font = bold
    r += 1

    totals = ctx["totals"]
    rows = [
        ("Registered", totals["reg"]),
        ("Sat", totals["sat"]),
        ("Absent", totals["abs"]),
        ("GPA", totals["avg"]),
        ("Grade", totals["grade"]),
        ("Status", totals["status"]),
    ]

    for k,v in rows:
        ws.cell(r,1).value = k
        ws.cell(r,2).value = v
        r += 1

    r += 1

    # ================= DIVISION SUMMARY =================
    ws.cell(r,1).value = "DIVISION SUMMARY"
    ws.cell(r,1).font = bold
    r += 1

    ws.append(["Division","Female","Male","Total"])
    for c in range(1,5):
        ws.cell(r,c).font = bold
    r += 1

    g = ctx["grades"]
    divs = [
        ("I", g["af"], g["am"], g["a"]),
        ("II", g["bf"], g["bm"], g["b"]),
        ("III", g["cf"], g["cm"], g["c"]),
        ("IV", g["df"], g["dm"], g["d"]),
        ("0", g["ef"], g["em"], g["e"]),
    ]

    for d in divs:
        ws.append(d)
        r += 1

    r += 1

    # ================= WARD PERFORMANCE =================
    if "ward_perf" in ctx and ctx["ward_perf"]:
        ws.cell(r,1).value = "WARD PERFORMANCE"
        ws.cell(r,1).font = bold
        r += 1

        ws.append(["Rank","Ward","GPA","Grade","Status"])
        for c in range(1,6):
            ws.cell(r,c).font = bold
        r += 1

        for w in ctx["ward_perf"]:
            ws.append([
                w["no"],
                w["name"],
                w["gpa"],
                w["grade"],
                w["status"]
            ])
            r += 1

        r += 1

    # ================= BEST SCHOOLS =================
    if "best_schools" in ctx and ctx["best_schools"]:
        ws.cell(r,1).value = "BEST 10 SCHOOLS"
        ws.cell(r,1).font = bold
        r += 1

        ws.append([
            "#","School","Sat",
            "I","II","III","IV","0",
            "Pass","%","GPA","Grade","Status"
        ])
        for c in range(1,14):
            ws.cell(r,c).font = bold
        r += 1

        for s in ctx["best_schools"]:
            ws.append([
                s["no"],
                s["name"],
                s["sat"],
                s["at"],
                s["bt"],
                s["ct"],
                s["dt"],
                s["ft"],
                s["pass_no"],
                s["pass_pct"],
                s["gpa"],
                s["grade"],
                s["status"],
            ])
            r += 1

    # ================= RESPONSE =================
    filename = (
        f"{ctx['exam_type_obj']}-"
        f"{year}-{ctx['district'].name.replace(' ','_')}-Summary.xlsx"
    )

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response




@login_required
def district_school_ranking_html(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank(district, year, exam_type_id)

    exam_type = None
    if exam_type_id:
        exam_type = ExamType.objects.filter(id=exam_type_id).first()

    return render(request,"exams/reports/district_school_rank_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "exam_type": exam_type,
        "district": Region.objects.get(id=district)
    })





@login_required
def district_ward_ranking_pdf(request):
    ctx = build_district_ward_ranking_context(
        request.GET.get("district"),
        int(request.GET.get("year"))
    )

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = "inline; filename=ward_ranking.pdf"

    doc = SimpleDocTemplate(response)
    story = []

    story.append(Paragraph(
        f"<b>WARD RANKING – {ctx['district'].name} ({ctx['year']})</b>",
        getSampleStyleSheet()["Title"]
    ))

    rows = [
        [r["rank"], r["ward"], r["total"], r["pass"], f"{r['pass_pct']}%", r["avg"]]
        for r in ctx["rows"]
    ]

    story.append(simple_rank_table(
        ["#", "Ward", "Total", "Pass", "% Pass", "Avg"], rows
    ))

    doc.build(story)
    return response
 

@login_required
def district_school_ranking_excel(request):
    ctx = build_district_school_ranking_context(
        request.GET.get("district"),
        int(request.GET.get("year"))
    )

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "School Ranking"

    ws.append(["#", "School", "Ward", "Ownership", "Total", "Pass", "% Pass", "Average"])

    for r in ctx["rows"]:
        ws.append([
            r["rank"], r["school"], r["ward"], r["ownership"],
            r["total"], r["pass"], r["pass_pct"], r["avg"]
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = "attachment; filename=school_ranking.xlsx"
    wb.save(response)
    return response




@login_required
def district_ward_ranking_excel(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))

    ctx = build_district_ward_ranking_context(district_id, year)

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Ward Ranking"

    bold = Font(bold=True)

    # ================= HEADER =================
    ws.append([f"WARD RANKING – {ctx['district'].name} – {year}"])
    ws["A1"].font = Font(bold=True, size=14)
    ws.append([])

    # ================= TABLE HEADER =================
    headers = [
        "Rank",
        "Ward",
        "Total Candidates",
        "Pass A–C",
        "% Pass",
        "Average Score",
    ]
    ws.append(headers)

    for col in range(1, len(headers) + 1):
        ws.cell(row=ws.max_row, column=col).font = bold

    # ================= DATA ROWS =================
    for r in ctx["rows"]:
        ws.append([
            r["rank"],
            r["ward"],
            r["total"],
            r["pass"],
            r["pass_pct"],
            r["avg"],
        ])

    # ================= AUTO WIDTH =================
    for col in ws.columns:
        max_length = 0
        col_letter = col[0].column_letter
        for cell in col:
            if cell.value:
                max_length = max(max_length, len(str(cell.value)))
        ws.column_dimensions[col_letter].width = max_length + 3

    # ================= RESPONSE =================
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="District_Ward_Ranking_{ctx["district"].name}_{year}.xlsx"'
    )

    wb.save(response)
    return response


@login_required
def district_school_ranking_pdf(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))

    ctx = build_district_summary_context(district_id, year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="District_School_Ranking_{ctx["district"].name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=20,
        rightMargin=20,
        topMargin=20,
        bottomMargin=30
    )

    styles = getSampleStyleSheet()
    story = []

    # ================= HEADER =================
    story.append(Paragraph(
        f"<b>DISTRICT SCHOOL RANKING</b><br/>"
        f"{ctx['district'].name} – {year}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    # ================= TABLE DATA =================
    data = [[
        "Rank",
        "School",
        "Ownership",
        "Total",
        "Pass A–C",
        "% Pass",
        "Average"
    ]]

    for s in ctx["schools"]:
        pass_pct = round((s["pass"] / s["total"] * 100), 2) if s["total"] else 0

        data.append([
            s["rank"],
            s["name"],
            s["ownership"],
            s["total"],
            s["pass"],
            f"{pass_pct}%",
            s["avg"],
        ])

    # ================= TABLE =================
    table = Table(data, repeatRows=1, colWidths=[
        40, 140, 90, 60, 60, 60, 60
    ])

    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#E8F1FA")),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (3,1), (-1,-1), "CENTER"),
        ("VALIGN", (0,0), (-1,-1), "MIDDLE"),
    ]))

    story.append(table)

    doc.build(story)
    return response


@login_required
def district_top_bottom_pdf(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))

    ctx = build_district_top_bottom_context(district_id, year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="District_Top_Bottom_{ctx["district"].name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=20,
        rightMargin=20,
        topMargin=20,
        bottomMargin=30
    )

    styles = getSampleStyleSheet()
    story = []

    story.append(Paragraph(
        f"<b>DISTRICT TOP / BOTTOM SCHOOLS</b><br/>"
        f"{ctx['district'].name} – {year}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    def table_block(title, rows):
        data = [[
            "Rank", "School", "District",
            "Total", "Pass A–C", "% Pass",
            "Average", "Grade", "Status"
        ]]
        for r in rows:
            data.append([
                r["rank"], r["name"], r["district"],
                r["total"], r["pass_ac"],
                f'{r["pass_pct"]}%',
                r["avg"], r["avg_grade"], r["status"]
            ])

        t = Table(data, repeatRows=1)
        t.setStyle(TableStyle([
            ("GRID", (0,0), (-1,-1), 0.5, colors.black),
            ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#E2F7FD")),
            ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
            ("ALIGN", (3,1), (-1,-1), "CENTER"),
        ]))

        return [
            Paragraph(f"<b>{title}</b>", styles["Heading3"]),
            Spacer(1, 6),
            t,
            Spacer(1, 12),
        ]

    story += table_block("TOP 10 SCHOOLS", ctx["top10"])
    story += table_block("BOTTOM 10 SCHOOLS", ctx["bottom10"])
    story += table_block("TOP 10 LARGE SCHOOLS (≥30)", ctx["top10_big"])
    story += table_block("BOTTOM 10 LARGE SCHOOLS (≥30)", ctx["bottom10_big"])
    story += table_block("TOP 10 GOVERNMENT SCHOOLS", ctx["top10_gov"])

    doc.build(story)
    return response


@login_required
def district_top_bottom_excel(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))

    ctx = build_district_top_bottom_context(district_id, year)

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Top Bottom Schools"

    bold = Font(bold=True)

    ws.append([f"DISTRICT TOP / BOTTOM SCHOOLS – {ctx['district'].name} – {year}"])
    ws["A1"].font = Font(bold=True, size=14)
    ws.append([])

    headers = [
        "Rank", "School", "District",
        "Total", "Pass A–C", "% Pass",
        "Average", "Grade", "Status"
    ]

    def write_block(title, rows):
        ws.append([title])
        ws[ws.max_row][0].font = bold
        ws.append(headers)
        for r in rows:
            ws.append([
                r["rank"], r["name"], r["district"],
                r["total"], r["pass_ac"],
                r["pass_pct"], r["avg"],
                r["avg_grade"], r["status"]
            ])
        ws.append([])

    write_block("TOP 10 SCHOOLS", ctx["top10"])
    write_block("BOTTOM 10 SCHOOLS", ctx["bottom10"])
    write_block("TOP 10 LARGE SCHOOLS (≥30)", ctx["top10_big"])
    write_block("BOTTOM 10 LARGE SCHOOLS (≥30)", ctx["bottom10_big"])
    write_block("TOP 10 GOVERNMENT SCHOOLS", ctx["top10_gov"])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="District_Top_Bottom_{ctx["district"].name}_{year}.xlsx"'
    )

    wb.save(response)
    return response







@login_required
def district_top_bottom_html(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))

    district = get_object_or_404(District, pk=district_id)

    # --------------------------------------------------
    # Get ALL schools in this district for the year
    # --------------------------------------------------
    summaries = SchoolYearSummary.objects.filter(
        year=year,
        school__district=district
    ).select_related("school", "school__district")

    rows = []

    for s in summaries:
        school = s.school

        # Candidate stats per school
        cand_qs = ExamCandidate.objects.filter(
            year=year,
            school=school
        )

        def g(grade):
            return cand_qs.filter(avg_grade__iexact=grade).count()

        A = g("A")
        B = g("B")
        C = g("C")
        D = g("D")
        E = g("E")

        total = cand_qs.exclude(avg_grade="A-ABS").count()
        pass_ac = A + B + C
        pass_pct = round((pass_ac / total * 100), 2) if total else 0

        rows.append({
            "school_code": school.school_code,
            "name": school.name,
            "district": district.name,
            "total": total,
            "A": A, "B": B, "C": C, "D": D, "E": E,
            "pass_ac": pass_ac,
            "pass_pct": pass_pct,
            "avg": round(float(s.avg_numeric or 0), 4),
            "avg_grade": s.avg_grade or "-",
            "status": s.status or "-",
            "ownership": (school.ownership or "").upper(),
        })

    # --------------------------------------------------
    # BUILD TOP / BOTTOM TABLES
    # --------------------------------------------------
    top10 = sorted(rows, key=lambda x: x["avg"], reverse=True)[:10]
    bottom10 = sorted(rows, key=lambda x: x["avg"])[:10]

    large = [r for r in rows if r["total"] >= 30]
    top10_big = sorted(large, key=lambda x: x["avg"], reverse=True)[:10]
    bottom10_big = sorted(large, key=lambda x: x["avg"])[:10]

    gov = [r for r in rows if r["ownership"] == "GVT"]
    top10_gov = sorted(gov, key=lambda x: x["avg"], reverse=True)[:10]

    # --------------------------------------------------
    # Assign ranks (IMPORTANT for HTML/PDF/Excel)
    # --------------------------------------------------
    def add_rank(data):
        for i, r in enumerate(data, start=1):
            r["rank"] = i
        return data

    context = {
        "district": district,
        "year": year,
        "top10": add_rank(top10),
        "bottom10": add_rank(bottom10),
        "top10_big": add_rank(top10_big),
        "bottom10_big": add_rank(bottom10_big),
        "top10_gov": add_rank(top10_gov),
    }

    return render(
        request,
        "exams/reports/district_top_bottom.html",
        context
    )






@login_required
def district_subject_summary(request):
    ctx = build_district_subject_summary(
        request.GET.get("district"),
        int(request.GET.get("year")),
        request.GET.get("subject", "ALL"),
    )
    return render(request, "exams/reports/district_subject_summary.html", ctx)













from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required

@login_required
def district_subject_school_ranking(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject")  # subject CODE

    district = get_object_or_404(District, pk=district_id)

    # ✅ RESOLVE SUBJECT NAME (SAME PATTERN AS REGION)
    subject_obj = ExamSubject.objects.filter(subject_id=subject).first()
    subject_name_sw = subject_obj.subject_name_sw if subject_obj else "UNKNOWN"

    # ✅ ONLY SCHOOLS IN THIS DISTRICT
    schools = School.objects.filter(district=district)

    rows = []

    for s in schools:
        # ✅ USE SchoolSubjectPerformance (FAST, NO JOIN ISSUES)
        perf = SchoolSubjectPerformance.objects.filter(
            school_code=s.school_code,
            year=year,
            subject_code=subject
        ).first()

        if not perf:
            continue

        total_std = perf.total_std or 0
        avg_subj = float(perf.subj_avg or 0)

        # PASS A–C BASED ON SUBJECT GRADE
        pass_ac = total_std if perf.subj_grade in ["A", "B", "C"] else 0
        pass_pct = round((pass_ac / total_std * 100), 2) if total_std else 0

        rows.append({
            "school_code": s.school_code,
            "school": s.name,
            "ward": s.ward.name if s.ward else "",
            "ownership": s.ownership,
            "total": total_std,
            "pass_ac": pass_ac,
            "pass_pct": pass_pct,
            "avg": round(avg_subj, 4),
        })

    # ✅ SAME SORT LOGIC AS REGION
    rows.sort(key=lambda r: (r["avg"], r["pass_pct"]), reverse=True)

    # ✅ ASSIGN RANK
    for i, r in enumerate(rows, start=1):
        r["rank"] = i

    return render(
        request,
        "exams/reports/district_subject_rank.html",
        {
            "rows": rows,
            "district": district,
            "year": year,
            "subject_code": subject,
            "subject_name_sw": subject_name_sw,
        }
    )

@login_required
def district_subject_summary_pdf(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject", "ALL")

    ctx = build_district_subject_summary(district_id, year, subject)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="District_Subject_Summary_{ctx["district"].name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=25,
        rightMargin=25,
        topMargin=25,
        bottomMargin=30
    )

    styles = getSampleStyleSheet()
    story = []

    # ---------- HEADER ----------
    title = (
        f"<b>DISTRICT SUBJECT SUMMARY</b><br/>"
        f"{ctx['district'].name} – {year}<br/>"
        f"SUBJECT: {ctx['subject']}"
    )
    story.append(Paragraph(title, styles["Title"]))
    story.append(Spacer(1, 12))

    # ---------- TABLE ----------
    table_data = [[
        "Code", "Subject",
        "Total",
        "A", "B", "C", "D", "E",
        "Pass A–C", "% Pass",
        "Average"
    ]]

    for r in ctx["rows"]:
        table_data.append([
            r["subject_code"],
            r["subject_name_sw"],
            r["total_std"],
            r["A"], r["B"], r["C"], r["D"], r["E"],
            r["pass_ac"],
            f'{r["pass_pct"]}%',
            r["avg_subj"],
        ])

    table = Table(table_data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.HexColor("#E2F7FD")),
        ("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (2,1), (-1,-1), "CENTER"),
        ("VALIGN", (0,0), (-1,-1), "MIDDLE"),
    ]))

    story.append(table)
    doc.build(story)

    return response


@login_required
def district_subject_summary_excel(request):
    district_id = request.GET.get("district")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject", "ALL")

    ctx = build_district_subject_summary(district_id, year, subject)

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "District Subject Summary"

    bold = Font(bold=True)

    # ---------- HEADER ----------
    ws.append([f"DISTRICT SUBJECT SUMMARY – {ctx['district'].name} – {year}"])
    ws["A1"].font = Font(bold=True, size=14)
    ws.append([f"SUBJECT: {ctx['subject']}"])
    ws.append([])

    # ---------- TABLE HEADER ----------
    headers = [
        "Code", "Subject",
        "Total",
        "A", "B", "C", "D", "E",
        "Pass A–C", "% Pass",
        "Average"
    ]
    ws.append(headers)
    for col in range(1, len(headers) + 1):
        ws.cell(row=ws.max_row, column=col).font = bold

    # ---------- DATA ----------
    for r in ctx["rows"]:
        ws.append([
            r["subject_code"],
            r["subject_name_sw"],
            r["total_std"],
            r["A"], r["B"], r["C"], r["D"], r["E"],
            r["pass_ac"],
            r["pass_pct"],
            r["avg_subj"],
        ])

    # ---------- AUTO WIDTH ----------
    for col in ws.columns:
        max_len = 0
        col_letter = col[0].column_letter
        for cell in col:
            if cell.value:
                max_len = max(max_len, len(str(cell.value)))
        ws.column_dimensions[col_letter].width = max_len + 3

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="District_Subject_Summary_{ctx["district"].name}_{year}.xlsx"'
    )

    wb.save(response)
    return response


@login_required
def district_d_trend_html(request):
    district_id = request.GET.get("district")

    ctx = build_district_trend(district_id)

    return render(
        request,
        "exams/reports/district_d_trend_html.html",
        ctx
    )




@login_required
def district_d_trend_pdf(request):
    district_id = request.GET.get("district")
    ctx = build_district_trend(district_id)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="District_Trend_{ctx["district"].name}.pdf"'
    )

    doc = SimpleDocTemplate(response, pagesize=A4)
    styles = getSampleStyleSheet()
    story = []

    story.append(Paragraph(
        f"<b>DISTRICT PERFORMANCE TREND</b><br/>{ctx['district'].name}",
        styles["Title"]
    ))
    story.append(Spacer(1, 12))

    data = [["Year", "Average", "Grade", "Total Candidates"]]

    for r in ctx["rows"]:
        data.append([r["year"], r["avg"], r["grade"], r["total"]])

    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
    ]))

    story.append(table)
    doc.build(story)

    return response




@login_required
def district_d_trend_excel(request):
    district_id = request.GET.get("district")
    ctx = build_district_trend(district_id)

    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "District Trend"

    ws.append(["Year", "Average", "Grade", "Total Candidates"])

    for r in ctx["rows"]:
        ws.append([r["year"], r["avg"], r["grade"], r["total"]])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="District_Trend_{ctx["district"].name}.xlsx"'
    )

    wb.save(response)
    return response
















@login_required
def wardwise(request):
    user = request.user
    profile = user.profile
    years = ["2022", "2023", "2024", "2025"]

    # ----------------------------------
    # ROLE-BASED VISIBILITY
    # ----------------------------------
    if profile.role == "super":
        wards = Ward.objects.select_related("district", "district__region")

    elif profile.role == "region":
        wards = Ward.objects.filter(
            district__region=profile.region
        ).select_related("district", "district__region")

    elif profile.role == "district":
        wards = Ward.objects.filter(
            district=profile.district
        ).select_related("district", "district__region")

    elif profile.role == "ward":
        wards = Ward.objects.filter(
            id=profile.ward_id
        ).select_related("district", "district__region")

    elif profile.role == "school":
        wards = Ward.objects.filter(
            id=profile.school.ward_id
        ).select_related("district", "district__region")

    else:
        wards = Ward.objects.none()

    # ----------------------------------
    # AJAX SEARCH (same as regionwise)
    # ----------------------------------
    if request.headers.get("x-requested-with") == "XMLHttpRequest":
        query = request.GET.get("q", "").strip().lower()

        results = []
        for w in wards:
            if query in w.name.lower():
                results.append({
                    "id": w.id,
                    "name": w.name,
                    "district": w.district.name,
                    "region": w.district.region.name,
                })

        return JsonResponse({"wards": results})

    return render(
        request,
        "exams/reports/wardwise.html",
        {
            "wards": wards,
            "years": years,
        }
    )



@login_required
def ward_summary_html(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")

    if not ward_id or not year:
        return HttpResponse("Missing ward or year", status=400)

    context = build_ward_summary_context(ward_id, year)

    return render(
        request,
        "exams/reports/ward_summary_html.html",
        context
    )


@login_required
def ward_summary_pdf(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")

    if not ward_id or not year:
        return HttpResponse("Missing parameters", status=400)

    context = build_ward_summary_context(ward_id, year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Ward_{context["ward"].name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        leftMargin=20,
        rightMargin=20,
        topMargin=20,
        bottomMargin=30
    )

    story = []

    # Header
    story.append(
        pdf_header_block_simple(context["ward"], year)
    )
    story.append(Spacer(1, 8))

    # Students
    story.append(pdf_students_block(context["totals"]))
    story.append(Spacer(1, 10))

    # Grades
    story.append(
        pdf_grades_block(context["grades"], context["totals"])
    )
    story.append(Spacer(1, 12))

    # Ownership
    for tbl in pdf_ownership_block(context["ownership"]):
        story.append(tbl)
        story.append(Spacer(1, 12))

    # Schools summary
    if context.get("schools"):
        story.append(
            pdf_district_summary_block(
                context["schools"],
                context["schools_totals"]
            )
        )

    doc.build(story, onFirstPage=_on_page, onLaterPages=_on_page)
    return response


@login_required
def ward_summary_excel(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")

    if not ward_id or not year:
        return HttpResponse("Missing parameters", status=400)

    context = build_ward_summary_context(ward_id, year)

    wb = Workbook()
    ws = wb.active
    ws.title = "Ward Summary"

    bold = Font(bold=True)
    head = PatternFill(start_color="DDDDDD", end_color="DDDDDD", fill_type="solid")

    # Title
    ws.merge_cells("A1:G1")
    ws["A1"] = f"WARD SUMMARY – {context['ward'].name} – {year}"
    ws["A1"].font = Font(bold=True, size=14)
    ws["A1"].alignment = Alignment(horizontal="center")

    ws.append([])

    # Registered / Sat / Absent
    ws.append(["Category", "Female", "Female %", "Male", "Male %", "Total", "Total %"])
    for c in ws[3]:
        c.font = bold
        c.fill = head

    t = context["totals"]

    def pct(a, b): return round((a / b * 100), 1) if b else 0

    ws.append(["Registered", t["reg_f"], pct(t["reg_f"], t["reg"]),
               t["reg_m"], pct(t["reg_m"], t["reg"]), t["reg"], 100])

    ws.append(["With Results", t["sat_f"], pct(t["sat_f"], t["reg"]),
               t["sat_m"], pct(t["sat_m"], t["reg"]), t["sat"], pct(t["sat"], t["reg"])])

    ws.append(["Absent", t["abs_f"], pct(t["abs_f"], t["reg"]),
               t["abs_m"], pct(t["abs_m"], t["reg"]), t["abs"], pct(t["abs"], t["reg"])])

    ws.append(["Average Score", t["avg"], "", "", "", "", ""])
    ws.append(["Grade", t["grade"], "", "", "", "", ""])
    ws.append(["Status", t["status"], "", "", "", "", ""])
    ws.append(["Total Schools", t["school_count"], "", "", "", "", ""])

    ws.append([])

    # Schools summary
    ws.append(["SCHOOL SUMMARY"])
    ws["A12"].font = bold

    ws.append(["School", "Total", "Pass", "Fail", "Pass %", "Male", "Female"])
    for c in ws[13]:
        c.font = bold
        c.fill = head

    for s in context["schools"]:
        ws.append([
            s["school_name"],
            s["total"],
            s["pass"],
            s["fail"],
            s["pass_pct"],
            s["male"],
            s["female"],
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="Ward_{context["ward"].name}_{year}.xlsx"'
    )

    wb.save(response)
    return response


@login_required
def ward_school_ranking_html(request):
    ward_id = request.GET.get("ward")
    year = int(request.GET.get("year", 2024))

    ward = get_object_or_404(Ward, pk=ward_id)

    rows, totals = build_school_rank_data(
        region_id=None,   # ✅ DO NOT FORCE REGION
        year=year,
        ward_id=ward.id
    )

    return render(
        request,
        "exams/reports/ward_school_ranking_html.html",
        {
            "ward": ward,
            "year": year,
            "rows": rows,
            "totals": totals,
        }
    )





@login_required
def ward_school_ranking_pdf(request):
    ward_id = request.GET.get("ward")
    year = int(request.GET.get("year", 2024))

    ward = get_object_or_404(Ward, pk=ward_id)

    rows, totals = build_school_rank_data(
        ward.district.region_id,
        year,
        ward_id=ward.id
    )

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Ward_School_Ranking_{ward.name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A4),
        leftMargin=20,
        rightMargin=20,
        topMargin=20,
        bottomMargin=20
    )

    story = []

    # Header
    story.append(
        pdf_header_block_simple(ward, year)
    )
    story.append(Spacer(1, 10))

    # Ranking Table
    story.append(
        pdf_school_ranking_block(rows, totals)
    )

    doc.build(story)
    return response


@login_required
def ward_school_ranking_excel(request):
    ward_id = request.GET.get("ward")
    year = int(request.GET.get("year", 2024))

    ward = get_object_or_404(Ward, pk=ward_id)

    rows, totals = build_school_rank_data(
        ward.district.region_id,
        year,
        ward_id=ward.id
    )

    wb = Workbook()
    ws = wb.active
    ws.title = "Ward School Ranking"

    bold = Font(bold=True)

    ws.append([
        "Rank", "School Code", "School Name",
        "A", "B", "C", "D", "E",
        "Pass A–C", "% Pass", "Average", "Total"
    ])

    for c in ws[1]:
        c.font = bold

    for r in rows:
        ws.append([
            r["rank"],
            r["school_code"],
            r["name"],
            r["a"],
            r["b"],
            r["c"],
            r["d"],
            r["e"],
            r["pass_t"],
            r["pass_pct"],
            r["avg_score"],
            r["total_std"],
        ])

    # TOTAL ROW
    ws.append([
        "TOTAL", "", "",
        totals["a"],
        totals["b"],
        totals["c"],
        totals["d"],
        totals["e"],
        totals["pass_t"],
        "",
        totals["avg_score"],
        totals["total_std"],
    ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="Ward_School_Ranking_{ward.name}_{year}.xlsx"'
    )

    wb.save(response)
    return response


@login_required
def ward_subject_summary_html(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")
    subject = request.GET.get("subject", "ALL")

    context = build_ward_subject_summary(ward_id, year, subject)

    return render(
        request,
        "exams/reports/ward_subject_summary_html.html",
        context
    )


@login_required
def ward_subject_summary_pdf(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")
    subject = request.GET.get("subject", "ALL")

    context = build_ward_subject_summary(ward_id, year, subject)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Ward_Subject_Summary_{context["ward"].name}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(response, pagesize=A4)
    story = []

    story.append(pdf_header_block_simple(context["ward"], year))
    story.append(Spacer(1, 10))
    story.append(pdf_subject_summary_block(context["rows"], context["totals"]))

    doc.build(story)
    return response

@login_required
def ward_subject_summary_excel(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")
    subject = request.GET.get("subject", "ALL")

    context = build_ward_subject_summary(ward_id, year, subject)

    wb = Workbook()
    ws = wb.active
    ws.title = "Ward Subject Summary"

    bold = Font(bold=True)

    ws.append([
        "Subject Code", "Subject Name",
        "Total", "Pass A–C", "% Pass", "Fail", "Average"
    ])

    for c in ws[1]:
        c.font = bold

    for r in context["rows"]:
        ws.append([
            r["subject_code"],
            r["subject_name"],
            r["total"],
            r["pass_ac"],
            r["pass_pct"],
            r["fail"],
            r["avg"],
        ])

    ws.append([
        "TOTAL", "",
        context["totals"]["total"],
        context["totals"]["pass_ac"],
        "",
        context["totals"]["fail"],
        context["totals"]["avg"],
    ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="Ward_Subject_Summary_{context["ward"].name}_{year}.xlsx"'
    )

    wb.save(response)
    return response


@login_required
def ward_subject_school_ranking_html(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")
    subject = request.GET.get("subject")

    context = build_ward_subject_school_ranking(
        ward_id=ward_id,
        year=year,
        subject_code=subject
    )

    subject_obj = ExamSubject.objects.get(subject_code=subject)
    context["subject_name"] = subject_obj.subject_name_sw

    return render(
        request,
        "exams/reports/ward_subject_school_ranking.html",
        context
    )

@login_required
def ward_subject_school_ranking_pdf(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")
    subject = request.GET.get("subject")

    context = build_ward_subject_school_ranking(
        ward_id=ward_id,
        year=year,
        subject_code=subject
    )

    subject_obj = ExamSubject.objects.get(subject_code=subject)
    context["subject_name"] = subject_obj.subject_name_sw

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="Ward_Subject_Ranking_{subject}_{year}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A4),
        leftMargin=20,
        rightMargin=20
    )

    story = []
    story.append(pdf_header_block_simple(context["ward"], year))
    story.append(Spacer(1, 10))

    story.append(
        pdf_subject_school_ranking_block(
            context["rows"],
            context["subject_code"],
            context["subject_name"]
        )
    )

    doc.build(story)
    return response


@login_required
def ward_subject_school_ranking_excel(request):
    ward_id = request.GET.get("ward")
    year = request.GET.get("year")
    subject = request.GET.get("subject")

    context = build_ward_subject_school_ranking(
        ward_id=ward_id,
        year=year,
        subject_code=subject
    )

    subject_obj = ExamSubject.objects.get(subject_code=subject)
    subject_name = subject_obj.subject_name_sw

    wb = Workbook()
    ws = wb.active
    ws.title = "Ward Subject Ranking"

    bold = Font(bold=True)

    ws.append([
        "Rank", "School Code", "School",
        "District", "Total",
        "Pass A–C", "% Pass", "Average"
    ])

    for c in ws[1]:
        c.font = bold

    for r in context["rows"]:
        ws.append([
            r["rank"],
            r["school_code"],
            r["school"],
            r["district"],
            r["total"],
            r["pass_ac"],
            r["pass_pct"],
            r["avg"],
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="Ward_Subject_Ranking_{subject}_{year}.xlsx"'
    )

    wb.save(response)
    return response







@login_required
def schoolwise(request):
    user = request.user
    profile = user.profile

    years = ["2022", "2023", "2024", "2025"]

    qs = School.objects.select_related(
        "ward",
        "district",
        "district__region"
    )

    # ---------------- ROLE FILTERING ----------------
    if profile.role == "super":
        pass

    elif profile.role == "region":
        qs = qs.filter(district__region=profile.region)

    elif profile.role == "district":
        qs = qs.filter(district=profile.district)

    elif profile.role == "ward":
        qs = qs.filter(ward=profile.ward)

    elif profile.role == "school":
        qs = qs.filter(school_code=profile.school.school_code)

    else:
        qs = School.objects.none()

    # ---------------- LOAD EXAM TYPES ----------------
    exam_types = ExamType.objects.filter(is_active=True).only("id", "name")

    # ---------------- AJAX SEARCH ----------------
    if request.headers.get("x-requested-with") == "XMLHttpRequest":
        q = request.GET.get("q", "").lower()
        data = []

        for s in qs:
            if q in s.name.lower():
                data.append({
                    "id": s.school_code,
                    "name": s.name,
                    "ward": s.ward.name,
                    "district": s.district.name,
                    "region": s.district.region.name,
                })

        return JsonResponse({
            "schools": data,
            "exam_types": [
                {"id": e.id, "name": e.name} for e in exam_types
            ]
        })

    # ---------------- NORMAL PAGE LOAD ----------------
    return render(
        request,
        "exams/reports/schoolwise.html",
        {
            "schools": qs,
            "years": years,
            "subjects": ExamSubject.objects.all(),
            "exam_types": exam_types,   # ✅ NOW AVAILABLE IN TEMPLATE
        }
    )





@login_required
def school_summary_html(request):
    school_code = request.GET.get("school")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    profile = getattr(request.user, "profile", None)

    # 🔒 BLOCK UNAUTHORIZED ACCESS
    if profile and profile.role != "super":
        if profile.role == "school":
            if school_code != profile.school.school_code:
                return HttpResponseForbidden("Access denied")

        elif profile.role == "ward":
            if not School.objects.filter(
                school_code=school_code,
                ward=profile.ward
            ).exists():
                return HttpResponseForbidden("Access denied")

        elif profile.role == "district":
            if not School.objects.filter(
                school_code=school_code,
                district=profile.district
            ).exists():
                return HttpResponseForbidden("Access denied")

        elif profile.role == "region":
            if not School.objects.filter(
                school_code=school_code,
                region=profile.region
            ).exists():
                return HttpResponseForbidden("Access denied")

    # ✅ SAFE TO PROCEED
    ctx = build_school_summary_context(
        school_code, year, exam_type_id
    )
    return render(request, "exams/reports/school_summary_html.html", ctx)







from django.http import JsonResponse
from exams.models import SchoolYearSummary

@login_required
def check_school_summary_exists(request):
    school = request.GET.get("school")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    exists = SchoolYearSummary.objects.filter(
        school__school_code=school,
        year=year,
        exam_type_id=exam_type_id
    ).exists()

    return JsonResponse({"exists": exists})


from django.http import HttpResponse
from django.contrib.auth.decorators import login_required

from reportlab.platypus import (
    SimpleDocTemplate, Table, TableStyle,
    Paragraph, Spacer, PageBreak
)
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib import colors

from exams.views_helpers import build_school_summary_context


@login_required
def school_summary_pdf(request):
    school_code = request.GET.get("school")
    year = request.GET.get("year")

    context = build_school_summary_context(school_code, year)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename=School_{school_code}_{year}.pdf'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        rightMargin=20,
        leftMargin=20,
        topMargin=20,
        bottomMargin=20
    )

    styles = getSampleStyleSheet()
    styles.add(ParagraphStyle(
        name="Header",
        alignment=1,
        fontSize=14,
        spaceAfter=8,
        fontName="Helvetica-Bold"
    ))
    styles.add(ParagraphStyle(
        name="SubHeader",
        alignment=1,
        fontSize=11,
        spaceAfter=6
    ))
    styles.add(ParagraphStyle(
        name="Bold",
        fontName="Helvetica-Bold"
    ))

    story = []

    school = context["school"]
    totals = context["totals"]
    grades = context["grades"]

    # =====================================================
    # HEADER
    # =====================================================
    story.append(Paragraph("OFISI YA RAIS - TAMISEMI", styles["Header"]))
    story.append(Paragraph(
        f"MATOKEO YA DARASA LA NNE - {context['year']}",
        styles["Header"]
    ))
    story.append(Spacer(1, 10))

    story.append(Paragraph(
        f"<b>SHULE:</b> {school.name}", styles["Normal"]
    ))
    story.append(Paragraph(
        f"<b>KATA:</b> {school.ward.name} &nbsp;&nbsp; "
        f"<b>WILAYA:</b> {school.district.name}",
        styles["Normal"]
    ))
    story.append(Paragraph(
        f"<b>MKOA:</b> {school.district.region.name}",
        styles["Normal"]
    ))
    story.append(Spacer(1, 12))

    # =====================================================
    # WANAFUNZI TABLE
    # =====================================================
    student_table = Table([
        ["WANAFUNZI", "", "", "", "", ""],
        ["", "WASICHANA", "", "WAVULANA", "", "JUMLA"],
        ["", "NA", "%", "NA", "%", "NA"],

        ["Waliosajiliwa",
         totals["reg_f"], "100",
         totals["reg_m"], "100",
         totals["reg"]],

        ["Wenye Matokeo",
         totals["sat_f"], "",
         totals["sat_m"], "",
         totals["sat"]],

        ["Wasiofanya",
         totals["abs_f"], "",
         totals["abs_m"], "",
         totals["abs"]],
    ], colWidths=[90, 50, 40, 50, 40, 50])

    student_table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("SPAN", (0,0), (-1,0)),
        ("BACKGROUND", (0,0), (-1,0), colors.lightblue),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
        ("FONT", (0,0), (-1,0), "Helvetica-Bold"),
        ("FONT", (0,1), (-1,2), "Helvetica-Bold"),
    ]))

    story.append(student_table)
    story.append(Spacer(1, 12))

    # =====================================================
    # GRADES TABLE
    # =====================================================
    grade_table = Table([
        ["UFAULU WA MADARAJA", "", "", "", "", "", ""],
        ["", "A", "B", "C", "D", "E", "PASS A–C"],

        ["Wasichana",
         grades["af"], grades["bf"], grades["cf"],
         grades["df"], grades["ef"],
         context["a_c_f"]],

        ["Wavulana",
         grades["am"], grades["bm"], grades["cm"],
         grades["dm"], grades["em"],
         context["a_c_m"]],

        ["Jumla",
         grades["a"], grades["b"], grades["c"],
         grades["d"], grades["e"],
         context["a_c_total"]],
    ], colWidths=[90, 45, 45, 45, 45, 45, 60])

    grade_table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("SPAN", (0,0), (-1,0)),
        ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
        ("FONT", (0,0), (-1,0), "Helvetica-Bold"),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
    ]))

    story.append(grade_table)
    story.append(Spacer(1, 12))

    # =====================================================
    # AVERAGE & STATUS
    # =====================================================
    avg_table = Table([
        ["WASTANI WA SHULE", context["totals"]["avg"]],
        ["HALI", context["totals"]["status"]],
    ], colWidths=[120, 80])

    avg_table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("FONT", (0,0), (0,-1), "Helvetica-Bold"),
        ("ALIGN", (1,0), (-1,-1), "CENTER"),
    ]))

    story.append(avg_table)

    # =====================================================
    # BUILD PDF
    # =====================================================
    doc.build(story)

    return response



from django.http import HttpResponse
from django.contrib.auth.decorators import login_required

from openpyxl import Workbook
from openpyxl.styles import Font, Alignment
from openpyxl.utils import get_column_letter

from exams.views_helpers import build_school_summary_context


@login_required
def school_summary_excel(request):

    school_code  = request.GET.get("school")
    year         = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    if not all([school_code, year, exam_type_id]):
        return HttpResponse("Missing parameters", status=400)

    context = build_school_summary_context(
        school_code,
        year,
        exam_type_id
    )

    school = context["school"]
    totals = context["totals"]
    grades = context["grades"]
    subject_rows = context.get("subjects", [])

    wb = Workbook()
    ws = wb.active
    ws.title = "School Summary"

    bold = Font(bold=True)
    center = Alignment(horizontal="center")

    def row(data, bold_cols=None):
        ws.append(data)
        r = ws.max_row
        for c in range(1, len(data) + 1):
            ws.cell(row=r, column=c).alignment = center
        if bold_cols:
            for c in bold_cols:
                ws.cell(row=r, column=c).font = bold

    # --------------------------------------------------
    # HEADER
    # --------------------------------------------------
    ws.merge_cells("A1:J1")
    ws["A1"] = "PRIME MINISTER'S OFFICE -RALG"
    ws["A1"].font = bold
    ws["A1"].alignment = center

    ws.merge_cells("A2:J2")
    ws["A2"] = f"SCHOOL SUMMARY REPORT - {year}"
    ws["A2"].font = bold
    ws["A2"].alignment = center

    ws.append([])

    row(["SCHOOL", school.name], [1])
    row(["DISTRICT", school.district.name], [1])
    row(["REGION", school.district.region.name], [1])
    row(["EXAM TYPE", context.get("exam_type")], [1])

    ws.append([])

    # --------------------------------------------------
    # STUDENT SUMMARY
    # --------------------------------------------------
    row(["STUDENTS REGISTRATION AND PERFORMANCE"], [1])
    row(["", "FEMALE NO", "%", "MALE NO", "%", "TOTAL"])

    row([
        "REGISTERED",
        totals["reg_f"], 100,
        totals["reg_m"], 100,
        totals["reg"]
    ])

    row([
        "WITH RESULTS",
        totals["sat_f"], round(totals["sat_f"] / max(totals["reg"], 1) * 100, 1),
        totals["sat_m"], round(totals["sat_m"] / max(totals["reg"], 1) * 100, 1),
        totals["sat"]
    ])

    row([
        "ABSENT",
        totals["abs_f"], round(totals["abs_f"] / max(totals["reg"], 1) * 100, 1),
        totals["abs_m"], round(totals["abs_m"] / max(totals["reg"], 1) * 100, 1),
        totals["abs"]
    ])

    ws.append([])

    row(["AVERAGE GPA", totals["avg"]])
    row(["STATUS", totals["status"]])

    ws.append([])

    # --------------------------------------------------
    # DIVISION PERFORMANCE
    # --------------------------------------------------
    row(["DIVISION PERFORMANCE"], [1])
    row(["SEX", "I", "II", "III", "IV", "0", "PASS I-III", "PASS I-IV"])

    row([
        "F",
        grades["af"], grades["bf"], grades["cf"],
        grades["df"], grades["ef"],
        context["a_c_f"], context["a_d_f"]
    ])

    row([
        "M",
        grades["am"], grades["bm"], grades["cm"],
        grades["dm"], grades["em"],
        context["a_c_m"], context["a_d_m"]
    ])

    row([
        "T",
        grades["a"], grades["b"], grades["c"],
        grades["d"], grades["e"],
        context["a_c_total"], context["a_d_total"]
    ])

    ws.append([])
    ws.append([])

    # --------------------------------------------------
    # SUBJECT PERFORMANCE
    # --------------------------------------------------
    row(["SUBJECT PERFORMANCE"], [1])

    row([
        "CODE","SUBJECT","SAT",
        "A F","A M","A T",
        "B F","B M","B T",
        "C F","C M","C T",
        "D F","D M","D T",
        "F F","F M","F T",
        "PASS NO","PASS %",
        "GPA","GRADE","STATUS"
    ])

    for s in subject_rows:
        row([
            s["code"],
            s["subject"],
            s["sat"],
            s["af"], s["am"], s["at"],
            s["bf"], s["bm"], s["bt"],
            s["cf"], s["cm"], s["ct"],
            s["df"], s["dm"], s["dt"],
            s["ff"], s["fm"], s["ft"],
            s["pass_no"], s["pass_pct"],
            s["gpa"], s["grade"], s["status"]
        ])

    for col in range(1, 20):
        ws.column_dimensions[get_column_letter(col)].width = 16

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename=School_{school_code}_{year}_Exam{exam_type_id}_Summary.xlsx'
    )

    wb.save(response)
    return response




@login_required
def school_subject_summary_html(request):
    school_code = request.GET.get("school")
    year = int(request.GET.get("year"))
    subject = request.GET.get("subject", "ALL")

    context = build_school_subject_summary_context(
        school_code=school_code,
        year=year,
        subject=subject
    )

    return render(
        request,
        "exams/reports/school_subject_summary_html.html",
        context
    )


from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required

from exams.models import School
from exams.views_helpers import build_school_trend_context


@login_required
def school_trend_html(request):

    school_code  = request.GET.get("school")
    start_year   = request.GET.get("start")
    end_year     = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    if not school_code or not start_year or not end_year or not exam_type_id:
        return HttpResponse("Missing parameters", status=400)

    rows = compute_school_trend(
        school_code,
        start_year,
        end_year,
        exam_type_id
    )

    school = School.objects.get(school_code=school_code)
    exam_type = ExamType.objects.get(id=exam_type_id)

    return render(request, "exams/reports/school_trend_html.html", {
        "school": school,
        "rows": rows,
        "start": int(start_year),
        "end": int(end_year),
        "exam_type_id": int(exam_type_id),
        "exam_type": exam_type.name,   # ✅ THIS LINE FIXES IT
        "years": list(range(int(start_year), int(end_year) + 1))
    })





from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.pagesizes import A4
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors

@login_required
def school_subject_summary_pdf(request):
    school_code = request.GET.get("school")
    year = request.GET.get("year")
    subject = request.GET.get("subject", "ALL")

    context = build_school_subject_summary_context(
        school_code, year, subject
    )

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename=School_Subject_{school_code}_{year}.pdf'
    )

    doc = SimpleDocTemplate(response, pagesize=A4)
    styles = getSampleStyleSheet()
    story = []

    school = context["school"]

    story.append(Paragraph("OFISI YA RAIS - TAMISEMI", styles["Title"]))
    story.append(Paragraph(
        f"{school.name} – {context['subject_name']} ({year})",
        styles["Heading2"]
    ))
    story.append(Spacer(1, 10))

    data = [
        ["Subject", "Total", "Pass (A–C)", "Fail (D–E)", "Pass %", "Average"]
    ]

    for r in context["rows"]:
        data.append([
            r["subject"], r["total"], r["pass"],
            r["fail"], r["pass_pct"], r["avg"]
        ])

    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
        ("ALIGN", (1,1), (-1,-1), "CENTER"),
        ("FONT", (0,0), (-1,0), "Helvetica-Bold"),
    ]))

    story.append(table)
    doc.build(story)

    return response



from openpyxl import Workbook
from openpyxl.styles import Font, Alignment

@login_required
def school_subject_summary_excel(request):
    school_code = request.GET.get("school")
    year = request.GET.get("year")
    subject = request.GET.get("subject", "ALL")

    context = build_school_subject_summary_context(
        school_code, year, subject
    )

    wb = Workbook()
    ws = wb.active
    ws.title = "Subject Summary"

    bold = Font(bold=True)
    center = Alignment(horizontal="center")

    ws.append(["School", context["school"].name])
    ws.append(["Year", year])
    ws.append(["Subject", context["subject_name"]])
    ws.append([])

    ws.append([
        "Subject", "Total", "Pass (A–C)",
        "Fail (D–E)", "Pass %", "Average"
    ])

    for cell in ws[5]:
        cell.font = bold
        cell.alignment = center

    for r in context["rows"]:
        ws.append([
            r["subject"], r["total"], r["pass"],
            r["fail"], r["pass_pct"], r["avg"]
        ])

    for row in ws.iter_rows(min_row=6):
        for cell in row:
            cell.alignment = center

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename=School_Subject_{school_code}_{year}.xlsx'
    )

    wb.save(response)
    return response












from exams.views_helpers import build_school_trend_context

@login_required
def school_trend_pdf(request):
    school_code = request.GET.get("school")
    start = int(request.GET.get("start"))
    end = int(request.GET.get("end"))

    context = build_school_trend_context(
        school_code=school_code,
        start=start,
        end=end
    )

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="School_Trend_{school_code}_{start}_{end}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        rightMargin=30,
        leftMargin=30,
        topMargin=30,
        bottomMargin=30,
    )

    styles = getSampleStyleSheet()
    elements = []

    # -----------------------------
    # TITLE (USE EXISTING STYLE)
    # -----------------------------
    elements.append(
        Paragraph(
            f"School Performance Trend ({start} – {end})",
            styles["Title"]
        )
    )
    elements.append(Spacer(1, 10))

    school = context["school"]
    elements.append(
        Paragraph(
            f"<b>{school.name}</b><br/>"
            f"Ward: {school.ward.name} | "
            f"District: {school.district.name} | "
            f"Region: {school.district.region.name}",
            styles["Normal"]
        )
    )

    elements.append(Spacer(1, 15))

    # -----------------------------
    # TABLE
    # -----------------------------
    table_data = [
        ["Year", "Total Students", "Pass (A–C)", "Fail (D–E)", "Pass %", "Average"]
    ]

    for r in context["rows"]:
        table_data.append([
            r["year"],
            r["total_std"],
            r["pass_ac"],
            r["fail_de"],
            f'{r["pass_pct"]}%',
            r["avg"],
        ])

    table = Table(table_data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0, 0), (-1, -1), 0.5, colors.black),
        ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
        ("ALIGN", (1, 1), (-1, -1), "CENTER"),
        ("FONT", (0, 0), (-1, 0), "Helvetica-Bold"),
    ]))

    elements.append(table)

    doc.build(elements)
    return response










from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from openpyxl import Workbook
from openpyxl.styles import Font, Alignment

from exams.views_helpers import build_school_trend_context


@login_required
def school_trend_excel(request):
    school_code = request.GET.get("school")
    start = int(request.GET.get("start"))
    end = int(request.GET.get("end"))

    context = build_school_trend_context(
        school_code=school_code,
        start=start,
        end=end
    )

    school = context["school"]
    rows = context["rows"]

    wb = Workbook()
    ws = wb.active
    ws.title = "School Trend"

    bold = Font(bold=True)
    center = Alignment(horizontal="center")

    # ---------------- HEADER ----------------
    ws.append([
        "Year",
        "Total Students",
        "Pass (A–C)",
        "Fail (D–E)",
        "Pass %",
        "Average Score",
    ])

    for cell in ws[1]:
        cell.font = bold
        cell.alignment = center

    # ---------------- DATA ----------------
    for r in rows:
        ws.append([
            r["year"],
            r["total_std"],
            r["pass_ac"],
            r["fail_de"],
            r["pass_pct"],
            r["avg"],
        ])

    # ---------------- AUTOSIZE ----------------
    for col in ws.columns:
        max_len = max(len(str(cell.value)) if cell.value else 0 for cell in col)
        ws.column_dimensions[col[0].column_letter].width = max_len + 4

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="School_Trend_{school.school_code}_{start}_{end}.xlsx"'
    )

    wb.save(response)
    return response


































# --------------------------------------------------------------
# MAIN DASHBOARD API (based on SchoolYearSummary)
# --------------------------------------------------------------

from django.http import JsonResponse
from django.views.decorators.http import require_GET
from django.contrib.auth.decorators import login_required
from django.db.models import Avg, Sum

from exams.models import (
    School,
    ExamCandidate,
    StudentSubjectMark,
    SchoolSubjectPerformance,
)


@login_required
@require_GET
def ajax_dashboard_subjectwise(request):

    region     = request.GET.get("region")
    district   = request.GET.get("district")
    ward       = request.GET.get("ward")
    school     = request.GET.get("school")
    examtype   = request.GET.get("examtype")
    subject    = request.GET.get("subject")
    gender     = request.GET.get("gender")
    ownership  = request.GET.get("ownership")
    start_year = request.GET.get("start_year")
    end_year   = request.GET.get("end_year")

    # =====================================================
    # 1. SCHOOL SCOPE
    # =====================================================
    school_qs = School.objects.all()

    if region:
        school_qs = school_qs.filter(region_id=region)
    if district:
        school_qs = school_qs.filter(district_id=district)
    if ward:
        school_qs = school_qs.filter(ward_id=ward)
    if ownership:
        school_qs = school_qs.filter(ownership=ownership)
    if school:
        school_qs = school_qs.filter(school_code=school)

    school_codes = list(school_qs.values_list("school_code", flat=True))

    # =====================================================
    # 2. SUBJECT PERFORMANCE TABLE
    # =====================================================


    perf_qs = SchoolSubjectPerformance.objects.filter(
        school_code__in=school_codes
    )

    if examtype:
        perf_qs = perf_qs.filter(exam_type_id=examtype)

    if subject:
        perf_qs = perf_qs.filter(subject_code=subject)
    if start_year:
        perf_qs = perf_qs.filter(year__gte=int(start_year))
    if end_year:
        perf_qs = perf_qs.filter(year__lte=int(end_year))

    # =====================================================
    # 3. KPI COUNTS
    # =====================================================
    grade_kpi_qs = SchoolSubjectGradeSummary.objects.filter(
        school__school_code__in=school_codes,
        grade__in=["A", "B", "C", "D", "F"]   # ✅ exclude X
    )

    if examtype:
        grade_kpi_qs = grade_kpi_qs.filter(exam_type_id=examtype)
    if subject:
        grade_kpi_qs = grade_kpi_qs.filter(subject_code=subject)
    if start_year:
        grade_kpi_qs = grade_kpi_qs.filter(year__gte=int(start_year))
    if end_year:
        grade_kpi_qs = grade_kpi_qs.filter(year__lte=int(end_year))
    if gender in ["M", "F"]:
        grade_kpi_qs = grade_kpi_qs.filter(sex=gender)

    total_students = grade_kpi_qs.aggregate(
        t=Sum("total")
    )["t"] or 0

    pass_count = grade_kpi_qs.filter(
        grade__in=["A", "B", "C", "D"]
    ).aggregate(
        t=Sum("total")
    )["t"] or 0

    fail_count = grade_kpi_qs.filter(
        grade="F"
    ).aggregate(
        t=Sum("total")
    )["t"] or 0

    # =====================================================
    # 4. GRADE DISTRIBUTION (FROM SUMMARY TABLE)
    # =====================================================
    grade_qs = SchoolSubjectGradeSummary.objects.filter(
        school__school_code__in=school_codes
    )

    if examtype:
        grade_qs = grade_qs.filter(exam_type_id=examtype)

    if subject:
        grade_qs = grade_qs.filter(subject_code=subject)
    if start_year:
        grade_qs = grade_qs.filter(year__gte=int(start_year))
    if end_year:
        grade_qs = grade_qs.filter(year__lte=int(end_year))
    if gender in ["M","F"]:
        grade_qs = grade_qs.filter(sex=gender)

    grades = ["A","B","C","D","F"]

    grade_values = [
        grade_qs.filter(grade=g).aggregate(t=Sum("total"))["t"] or 0
        for g in grades
    ]

    # =====================================================
    # 5. YEARLY AVERAGE GPA (LINE)
    # =====================================================
    SUB_WEIGHT = {
        "A": 1,
        "B": 2,
        "C": 3,
        "D": 4,
        "F": 5,
    }

    yearly_qs = SchoolSubjectGradeSummary.objects.filter(
        school__school_code__in=school_codes,
        grade__in=["A", "B", "C", "D", "F"]   # exclude X
    )

    if examtype:
        yearly_qs = yearly_qs.filter(exam_type_id=examtype)
    if subject:
        yearly_qs = yearly_qs.filter(subject_code=subject)
    if start_year:
        yearly_qs = yearly_qs.filter(year__gte=int(start_year))
    if end_year:
        yearly_qs = yearly_qs.filter(year__lte=int(end_year))
    if gender in ["M", "F"]:
        yearly_qs = yearly_qs.filter(sex=gender)


    yearly_stats = {}

    for r in yearly_qs:
        y = r.year
        w = SUB_WEIGHT[r.grade]

        if y not in yearly_stats:
            yearly_stats[y] = {
                "weighted": 0,
                "total": 0
            }

        yearly_stats[y]["weighted"] += w * r.total
        yearly_stats[y]["total"] += r.total

    years = []
    avg_series = []

    for y in sorted(yearly_stats.keys()):
        t = yearly_stats[y]["total"]
        w = yearly_stats[y]["weighted"]

        gpa = round(w / t, 4) if t else 0

        years.append(y)
        avg_series.append(gpa)

    overall_weighted = sum(v["weighted"] for v in yearly_stats.values())
    overall_total = sum(v["total"] for v in yearly_stats.values())

    avg_overall = round(overall_weighted / overall_total, 4) if overall_total else 0

    # =====================================================
    # 7. STACKED BAR: GRADES BY SUBJECT
    # =====================================================
    # ---------------- STACKED SUBJECT GRADES ----------------
    stack_qs = SchoolSubjectGradeSummary.objects.filter(
        school__school_code__in=school_codes
    )

    if examtype:
        stack_qs = stack_qs.filter(exam_type_id=examtype)
    if subject:
        stack_qs = stack_qs.filter(subject_code=subject)
    if start_year:
        stack_qs = stack_qs.filter(year__gte=int(start_year))
    if end_year:
        stack_qs = stack_qs.filter(year__lte=int(end_year))
    if gender in ["M","F"]:
        stack_qs = stack_qs.filter(sex=gender)

    stack_subjects = list(
        stack_qs.values_list("subject_code", flat=True).distinct()
    )

    subject_names = {
        s.subject_code: s.subject_name_eng
        for s in ExamSubject.objects.all()
    }

    grades_stack = ["A","B","C","D","F"]

    stack_values = {g: [] for g in grades_stack}

    for sub in stack_subjects:
        for g in grades_stack:
            v = stack_qs.filter(subject_code=sub, grade=g)\
                        .aggregate(t=Sum("total"))["t"] or 0
            stack_values[g].append(v)

    stack_labels = [subject_names.get(s, s) for s in stack_subjects]

    # =====================================================
    # 6. FINAL JSON
    # =====================================================
    return JsonResponse({
        "cards": [
            {"value": total_students},
            {"value": pass_count},
            {"value": fail_count},
            {"value": round(avg_overall, 4)},
        ],
        "donut": [
            {"name": "Pass", "value": pass_count},
            {"name": "Fail", "value": fail_count},
        ],
        "grades": {
            "labels": grades,
            "values": grade_values,
        },
        "line": {
            "years": years,
            "avg_marks": avg_series,
        },
        "stacked": {   # 🔥 NEW PART
            "subjects": stack_labels,
            "grades": grades_stack,
            "values": stack_values
        }
    })
























from django.contrib import messages
from django.shortcuts import redirect, render
from django.contrib.auth.decorators import login_required, permission_required
import openpyxl
import csv
import io

@login_required
@permission_required("exams.add_school", raise_exception=True)
def geo_import_upload(request):

    if request.method == "POST":
        file = request.FILES.get("geo_file")

        if not file:
            messages.error(request, "No file selected.")
            return redirect("exams:geo_import")

        try:
            import pandas as pd
            from exams.services.geo_importer import import_geo_dataframe

            if file.name.endswith(".xlsx"):
                df = pd.read_excel(file)
            elif file.name.endswith(".csv"):
                df = pd.read_csv(file)
            else:
                messages.error(request, "Only Excel (.xlsx) or CSV allowed.")
                return redirect("exams:geo_import")

            result = import_geo_dataframe(df)

            messages.success(
                request,
                f"Import completed: "
                f"{result['regions']} regions, "
                f"{result['districts']} districts, "
                f"{result['wards']} wards, "
                f"{result['schools']} schools."
            )

        except Exception as e:
            messages.error(request, f"Import failed: {e}")

        return redirect("exams:geo_import")

    schools = School.objects.select_related(
        "region", "district", "ward"
    ).order_by("name")

    return render(request, "exams/geo_import.html", {
        "schools": schools
    })



@login_required
@permission_required("exams.delete_school", raise_exception=True)
def school_delete(request, pk):
    school = get_object_or_404(School, pk=pk)

    if not school.can_delete():
        messages.error(request, "Cannot delete school with existing data.")
        return redirect("exams:geo_import")

    school.delete()
    messages.success(request, "School deleted successfully.")
    return redirect("exams:geo_import")


from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required

from exams.models import School, District, Ward
from exams.forms import SchoolForm


@login_required
@permission_required("exams.change_school", raise_exception=True)
def school_edit(request, pk):
    school = get_object_or_404(School, pk=pk)

    if request.method == "POST":
        form = SchoolForm(request.POST, instance=school)

        if form.is_valid():
            form.save()
            messages.success(request, "School details updated successfully.")
            return redirect("exams:geo_import")

        messages.error(request, "Please correct the errors below.")

    else:
        form = SchoolForm(instance=school)

    return render(request, "exams/school_edit.html", {
        "form": form,
        "school": school,
    })



import openpyxl
from openpyxl.styles import Font
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required, permission_required

from exams.models import School


@login_required
@permission_required("exams.view_school", raise_exception=True)
def export_schools_excel(request):
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Schools"

    # Header
    headers = [
        "School Code",
        "Registration No",
        "School Name",
        "Ward",
        "District",
        "Region",
        "Ownership",
        "Latitude",
        "Longitude",
    ]
    ws.append(headers)

    # Bold header
    for cell in ws[1]:
        cell.font = Font(bold=True)

    schools = School.objects.select_related(
        "region", "district", "ward"
    ).order_by("region__name", "district__name", "ward__name", "name")

    for s in schools:
        ws.append([
            s.school_code,
            s.school_regno or "",
            s.name,
            s.ward.name,
            s.district.name,
            s.region.name,
            s.ownership,
            float(s.latitude) if s.latitude is not None else "",
            float(s.longitude) if s.longitude is not None else "",
        ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = 'attachment; filename="schools_list.xlsx"'
    wb.save(response)

    return response


@login_required
@permission_required("exams.view_examtype", raise_exception=True)
def examtype_list(request):
    examtypes = ExamType.objects.all()
    return render(request, "exams/examtype_list.html", {
        "examtypes": examtypes
    })


@login_required
@permission_required("exams.add_examtype", raise_exception=True)
def examtype_create(request):
    if request.method == "POST":
        form = ExamTypeForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, "Exam type created successfully.")
            return redirect("exams:examtype_list")
    else:
        form = ExamTypeForm()

    return render(request, "exams/examtype_form.html", {
        "form": form,
        "title": "Add Exam Type"
    })


@login_required
@permission_required("exams.change_examtype", raise_exception=True)
def examtype_edit(request, pk):
    examtype = get_object_or_404(ExamType, pk=pk)

    if request.method == "POST":
        form = ExamTypeForm(request.POST, instance=examtype)
        if form.is_valid():
            form.save()
            messages.success(request, "Exam type updated successfully.")
            return redirect("exams:examtype_list")
    else:
        form = ExamTypeForm(instance=examtype)

    return render(request, "exams/examtype_form.html", {
        "form": form,
        "title": "Edit Exam Type"
    })


@login_required
@permission_required("exams.delete_examtype", raise_exception=True)
def examtype_delete(request, pk):
    examtype = get_object_or_404(ExamType, pk=pk)

    # Optional safety check
    if examtype.schoolyearsummary_set.exists():
        messages.error(
            request,
            "Cannot delete exam type already used in results."
        )
        return redirect("exams:examtype_list")

    examtype.delete()
    messages.success(request, "Exam type deleted successfully.")
    return redirect("exams:examtype_list")



from exams.forms import ExamSubjectForm

@login_required
@permission_required("exams.view_examsubject", raise_exception=True)
def subject_list(request):
    subjects = ExamSubject.objects.all()
    return render(request, "exams/subject_list.html", {
        "subjects": subjects
    })


@login_required
@permission_required("exams.add_examsubject", raise_exception=True)
def subject_create(request):
    if request.method == "POST":
        form = ExamSubjectForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, "Subject added successfully.")
            return redirect("exams:subject_list")
    else:
        form = ExamSubjectForm()

    return render(request, "exams/subject_form.html", {
        "form": form,
        "title": "Add Subject"
    })


@login_required
@permission_required("exams.change_examsubject", raise_exception=True)
def subject_edit(request, pk):
    subject = get_object_or_404(ExamSubject, pk=pk)

    if request.method == "POST":
        form = ExamSubjectForm(request.POST, instance=subject)
        if form.is_valid():
            form.save()
            messages.success(request, "Subject updated successfully.")
            return redirect("exams:subject_list")
    else:
        form = ExamSubjectForm(instance=subject)

    return render(request, "exams/subject_form.html", {
        "form": form,
        "title": "Edit Subject"
    })


@login_required
@permission_required("exams.delete_examsubject", raise_exception=True)
def subject_delete(request, pk):
    subject = get_object_or_404(ExamSubject, pk=pk)

    # SAFE CHECK: subject used anywhere?
    used_in_marks = StudentSubjectMark.objects.filter(
        subject_code=subject.subject_code
    ).exists()

    used_in_summary = SchoolSubjectPerformance.objects.filter(
        subject_code=subject.subject_code
    ).exists()

    if used_in_marks or used_in_summary:
        messages.error(
            request,
            "Cannot delete subject because it is already used in exam data."
        )
        return redirect("exams:subject_list")

    subject.delete()
    messages.success(request, "Subject deleted successfully.")
    return redirect("exams:subject_list")




import openpyxl
from django.contrib import messages
from django.db import transaction
from django.shortcuts import redirect
from django.contrib.auth.decorators import login_required, permission_required

from exams.models import ExamSubject


@login_required
@permission_required("exams.add_examsubject", raise_exception=True)
def subject_bulk_upload(request):
    if request.method != "POST":
        return redirect("exams:subject_list")

    excel_file = request.FILES.get("excel_file")
    if not excel_file:
        messages.error(request, "No file uploaded.")
        return redirect("exams:subject_list")

    if not excel_file.name.endswith(".xlsx"):
        messages.error(request, "Only Excel (.xlsx) files are allowed.")
        return redirect("exams:subject_list")

    wb = openpyxl.load_workbook(excel_file)
    ws = wb.active

    headers = [str(c.value).strip() for c in ws[1]]
    required_cols = [
        "subject_code",
        "subject_name_eng",
        "subject_name_sw",
        "subject_shortname",
        "subject_level",
        "is_practical",
        "is_principal",
        "number_of_papers",
        "color_code",
        "academic_level_id",
    ]

    for col in required_cols:
        if col not in headers:
            messages.error(request, f"Missing column: {col}")
            return redirect("exams:subject_list")

    created, updated, errors = 0, 0, []

    with transaction.atomic():
        for i, row in enumerate(ws.iter_rows(min_row=2, values_only=True), start=2):
            data = dict(zip(headers, row))

            code = str(data.get("subject_code", "")).strip()
            name_eng = str(data.get("subject_name_eng", "")).strip()
            name_sw = str(data.get("subject_name_sw", "")).strip()
            shortname = str(data.get("subject_shortname", "")).strip()
            level = str(data.get("subject_level", "")).strip()
            is_practical = str(data.get("is_practical", "")).strip()
            is_principal = str(data.get("is_principal", "")).strip()
            number_of_papers = str(data.get("number_of_papers", "")).strip()
            color_code = str(data.get("color_code", "")).strip()
            academic_level_id = str(data.get("academic_level_id", "")).strip()
            is_active = data.get("is_active", True)

            if not code or not name_eng or not name_sw:
                errors.append(f"Row {i}: Missing required values")
                continue

            active = True
            if is_active in [0, "0", "FALSE", "false", False]:
                active = False

            obj, created_flag = ExamSubject.objects.update_or_create(
                subject_code=code,
                defaults={
                    "subject_name_eng": name_eng,
                    "subject_name_sw": name_sw,
                    "subject_shortname": shortname,
                    "subject_level": level,
                    "is_practical": is_practical,
                    "is_principal": is_principal,
                    "number_of_papers": number_of_papers,
                    "color_code": color_code,
                    "academic_level_id": academic_level_id,
                    "is_active": active,
                }
            )

            if created_flag:
                created += 1
            else:
                updated += 1

    messages.success(
        request,
        f"Bulk upload completed: {created} created, {updated} updated."
    )

    if errors:
        messages.warning(
            request,
            "Some rows were skipped: " + "; ".join(errors[:5])
        )

    return redirect("exams:subject_list")


@login_required
def subject_template_download(request):
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Subjects"

    ws.append([
        "subject_code",
        "subject_name_eng",
        "subject_name_sw",
        "subject_shortname",
        "subject_level",
        "is_practical",
        "is_principal",
        "number_of_papers",
        "color_code",
        "academic_level_id",
        "is_active"
    ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = 'attachment; filename="subjects_template.xlsx"'
    wb.save(response)
    return response








@login_required
def download_template(request, exam_id):
    exam = get_object_or_404(ExamType, id=exam_id)

    file_path = os.path.join(
        settings.MEDIA_ROOT,
        "templates",
        f"{exam.code}_UPLOAD_TEMPLATE.xlsx"
    )

    if not os.path.exists(file_path):
        raise Http404("Template not found")

    return FileResponse(
        open(file_path, "rb"),
        as_attachment=True,
        filename=os.path.basename(file_path)
    )


@login_required
def upload_results(request, exam_id):
    exam = get_object_or_404(ExamType, id=exam_id)

    if request.method == "POST":
        # handle Excel upload here
        pass

    return render(request, "upload_results.html", {"exam": exam})





@login_required
def exam_schools(request, exam_id):
    exam = get_object_or_404(ExamType, id=exam_id)
    user = request.user
    profile = getattr(user, "profile", None)

    # 🔹 Superuser sees all schools
    if user.is_superuser:
        schools = School.objects.all()

    # 🔹 Role-based filtering for non-superusers
    elif profile:
        role = getattr(profile, "role", None)

        if role == "region":
            # All schools in the user's region
            schools = School.objects.filter(region=profile.region)
        
        elif role == "district":
            # All schools in the user's district
            schools = School.objects.filter(district=profile.district)
        
        elif role == "ward":
            # All schools in the user's ward
            schools = School.objects.filter(ward=profile.ward)
        
        elif role == "school":
            # Only the user's school
            if profile.school:
                schools = School.objects.filter(pk=profile.school.pk)
            else:
                schools = School.objects.none()
        
        else:
            # Unknown role → no schools
            schools = School.objects.none()

    else:
        # No profile & not superuser → no schools
        schools = School.objects.none()

    # Optimize foreign key access
    schools = schools.select_related("region", "district", "ward").order_by("school_code")

    context = {
        "exam": exam,
        "schools": schools,
        "user_profile": profile,  # optional for template use
    }

    return render(request, "exams/exam_schools.html", context)









@login_required
def school_actions(request, exam_id, school_code):
    """
    Dashboard actions for a selected school & exam
    """

    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)

    candidates_count = ExamCandidate.objects.filter(
        exam_type=exam,
        school=school
    ).count()

    context = {
        "exam": exam,
        "school": school,
        "candidates_count": candidates_count,
    }

    return render(request, "school_actions.html", context)





import csv

# ---------------- Print school results
@login_required
def print_school_results(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)
    
    # Fetch results for this school and exam
    results = school.results.filter(exam_type=exam)  # adjust 'results' field/model as per your setup
    
    context = {
        "exam": exam,
        "school": school,
        "results": results,
    }
    return render(request, "exams/print_school_results.html", context)


# ---------------- Print students
@login_required
def print_students(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)
    
    students = school.students.all()  # adjust if your Student model is different
    
    context = {
        "exam": exam,
        "school": school,
        "students": students,
    }
    return render(request, "exams/print_students.html", context)


# ---------------- Download reports (CSV)
@login_required
def download_reports(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)
    
    results = school.results.filter(exam_type=exam)  # adjust as needed

    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = f'attachment; filename="{school.school_code}_{exam.name}_reports.csv"'

    writer = csv.writer(response)
    writer.writerow(['Student No', 'Name', 'Subject', 'Grade', 'Score'])

    for r in results:
        writer.writerow([r.student.student_number, r.student.name, r.subject.name, r.grade, r.numeric_score])

    return response


# ---------------- Download Bulk SMS (CSV)
@login_required
def download_bulk_sms(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)
    
    students = school.students.all()  # adjust field names
    response = HttpResponse(content_type='text/csv')
    response['Content-Disposition'] = f'attachment; filename="{school.school_code}_{exam.name}_bulk_sms.csv"'

    writer = csv.writer(response)
    writer.writerow(['Phone Number', 'Message'])

    for student in students:
        # Example message
        message = f"{student.name}, your {exam.name} results are ready."
        writer.writerow([student.phone_number, message])

    return response


import csv

@login_required
def print_school_results(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)

    # 🔹 1️⃣ Get all candidates for this school and exam
    candidates = ExamCandidate.objects.filter(exam_type=exam, school=school).order_by('cand_no')

    # 🔹 2️⃣ Get all StudentSubjectMarks for this school and exam
    all_marks = StudentSubjectMark.objects.filter(
        exam_type=exam,
        school=school,
        numeric_score__isnull=False  # only subjects with at least one mark
    ).values('subject_code').distinct()

    # Get subject codes that have at least one mark
    subject_codes_with_marks = [m['subject_code'] for m in all_marks]

    # 🔹 3️⃣ Fetch subject details only for those codes
    subjects = ExamSubject.objects.filter(
        subject_code__in=subject_codes_with_marks,
        is_active=True
    ).order_by('subject_code')

    # 🔹 4️⃣ Build candidate results
    results = []
    for cand in candidates:
        row = {
            "school_code": school.school_code,
            "cand_no": cand.cand_no,
            "name": cand.name,
            "sex": cand.sex,
            "subjects": [],
            "points": cand.points,
            "division": cand.division,
            "average": cand.avg_numeric,
            "avg_grade": cand.avg_grade,
            "position": getattr(cand, "position", ""),
        }

        # Get marks for this candidate only for subjects that exist
        marks = StudentSubjectMark.objects.filter(
            exam_type=exam,
            school=school,
            cand_no=cand.cand_no,
            subject_code__in=subject_codes_with_marks
        ).values('subject_code', 'numeric_score', 'grade')

        marks_dict = {m['subject_code']: m for m in marks}

        # Fill subjects in order
        for subj in subjects:
            mark = marks_dict.get(subj.subject_code)
            if mark:
                row["subjects"].append({
                    "numeric_score": mark['numeric_score'],
                    "grade": mark['grade']
                })
            else:
                row["subjects"].append({
                    "numeric_score": "",
                    "grade": ""
                })

        results.append(row)

    context = {
        "exam": exam,
        "school": school,
        "subjects": subjects,
        "results": results,
    }

    return render(request, "exams/print_school_results.html", context)










@login_required
def download_school_results_excel(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)

    # Subjects with at least one mark
    subject_codes = (
        StudentSubjectMark.objects
        .filter(exam_type=exam, school=school, numeric_score__isnull=False)
        .values_list("subject_code", flat=True)
        .distinct()
    )

    subjects = ExamSubject.objects.filter(
        subject_code__in=subject_codes,
        is_active=True
    ).order_by("subject_code")

    response = HttpResponse(content_type="text/csv")
    response["Content-Disposition"] = (
        f'attachment; filename="{school.school_code}_{exam.code}_results.csv"'
    )

    writer = csv.writer(response)

    # Header
    header = [
        "SCHOOL", "CAND NO", "NAME", "SEX"
    ]

    for s in subjects:
        short = s.subject_shortname or s.subject_code
        header.append(VerticalText(f"{short} SCORE"))
        header.append(VerticalText(f"{short} GRADE"))

    header += ["POINT", "DIV", "AVG", "GRADE", "POS"]

    writer.writerow(header)

    candidates = ExamCandidate.objects.filter(
        exam_type=exam, school=school
    ).order_by("cand_no")

    for cand in candidates:
        marks = StudentSubjectMark.objects.filter(
            exam_type=exam,
            school=school,
            cand_no=cand.cand_no,
            subject_code__in=subject_codes
        ).values("subject_code", "numeric_score", "grade")

        mark_map = {m["subject_code"]: m for m in marks}

        row = [
            school.school_code,
            cand.cand_no,
            cand.name,
            cand.sex
        ]

        for s in subjects:
            m = mark_map.get(s.subject_code)
            row.append(round_no_decimal(m["numeric_score"]) if m else "")
            row.append(m["grade"] if m else "")

        row += [
            cand.points,
            cand.division,
            round_no_decimal(cand.avg_numeric),
            cand.avg_grade,
            getattr(cand, "position", "")
        ]


        writer.writerow(row)

    return response







@login_required
def download_school_results_pdf(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)

    # Subjects that have at least one mark (PERFORMANCE OPTIMIZED)
    subject_codes = (
        StudentSubjectMark.objects
        .filter(exam_type=exam, school=school, numeric_score__isnull=False)
        .values_list("subject_code", flat=True)
        .distinct()
    )

    subjects = list(
        ExamSubject.objects
        .filter(subject_code__in=subject_codes, is_active=True)
        .order_by("subject_code")
    )

    candidates = ExamCandidate.objects.filter(
        exam_type=exam, school=school
    ).order_by("cand_no")

    # PDF response
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="{school.school_code}_{exam.code}_results.pdf"'
    )


    # =====================================================
    # 1️⃣ DEFINE COLUMN WIDTHS FIRST  (CRITICAL FIX)
    # =====================================================
    col_widths = [
        40,   # SCHOOL
        60,   # CAND NO
        140,  # NAME
        30,   # SEX
    ]

    for _ in subjects:
        col_widths.extend([18, 18])  # SCORE / GRADE

    col_widths.extend([35, 35, 35, 40, 35])  # tail columns

    LEFT_MARGIN = 15
    RIGHT_MARGIN = 15

    table_width = sum(col_widths)

    a4_width = landscape(A4)[0] - LEFT_MARGIN - RIGHT_MARGIN
    a3_width = landscape(A3)[0] - LEFT_MARGIN - RIGHT_MARGIN

    if table_width > a4_width:
        page_size = landscape(A3)
    else:
        page_size = landscape(A4)

    if table_width > a3_width:
        shrink_ratio = a3_width / table_width
        col_widths = [w * shrink_ratio for w in col_widths]

    doc = SimpleDocTemplate(
        response,
        pagesize=page_size,
        leftMargin=LEFT_MARGIN,
        rightMargin=RIGHT_MARGIN,
        topMargin=20,
        bottomMargin=25,
    )



    styles = getSampleStyleSheet()
    story = []

    # 🔹 Header
    title = f"{school.name} ({school.school_code}) – {exam.name}"
    story.append(Paragraph(f"<b>{title}</b>", styles["Title"]))
    story.append(Spacer(1, 10))

    # 🔹 Build table header
    table_data = []

    header = [
        "SCHOOL", "CAND NO", "NAME", "SEX"
    ]

    for s in subjects:
        short = s.subject_shortname or s.subject_code
        header.append(VerticalText(f"{short} SC"))
        header.append(VerticalText(f"{short} GRD"))

    header += ["POINT", "DIV", "AVG", "GRADE", "POS"]


    table_data.append(header)

    # 🔹 Fill table rows
    for cand in candidates:
        marks = StudentSubjectMark.objects.filter(
            exam_type=exam,
            school=school,
            cand_no=cand.cand_no,
            subject_code__in=subject_codes
        ).values("subject_code", "numeric_score", "grade")

        mark_map = {m["subject_code"]: m for m in marks}

        row = [
            school.school_code,
            cand.cand_no,
            cand.name,
            cand.sex,
        ]

        for s in subjects:
            m = mark_map.get(s.subject_code)
            row.append(round_no_decimal(m["numeric_score"]) if m else "")
            row.append(m["grade"] if m else "")

        row += [
            cand.points,
            cand.division,
            round_no_decimal(cand.avg_numeric),
            cand.avg_grade,
            getattr(cand, "position", "")
        ]



        table_data.append(row)

    # 🔹 Create table
    # table = Table(table_data, repeatRows=1, rowHeights=[40] + [None]*(len(table_data)-1))

    HEADER_HEIGHT = 60
    row_heights = [HEADER_HEIGHT] + [None] * (len(table_data) - 1)



    table = Table(
        table_data,
        repeatRows=1,
        colWidths=col_widths,
        rowHeights=row_heights
    )

    table.setStyle(TableStyle([
        ("GRID", (0, 0), (-1, -1), 0.25, colors.black),
        ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
        ("ALIGN", (0, 0), (-1, 0), "CENTER"),
        ("ALIGN", (4, 1), (-1, -1), "CENTER"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("FONTSIZE", (0, 0), (-1, -1), 7),
        ("TOPPADDING", (0, 0), (-1, 0), 4),
        ("BOTTOMPADDING", (0, 0), (-1, 0), 4),
    ]))


    story.append(table)

    # 🔹 Footer
    story.append(Spacer(1, 12))
    footer = f"{exam.code} {exam.year} – {school.name}"
    story.append(Paragraph(footer, styles["Normal"]))

    # Build PDF (reuse your existing page decorator)
    doc.build(story, onFirstPage=_on_page, onLaterPages=_on_page)

    return response



@login_required
def school_results_html(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)

    subject_codes = (
        StudentSubjectMark.objects
        .filter(exam_type=exam, school=school, numeric_score__isnull=False)
        .values_list("subject_code", flat=True)
        .distinct()
    )

    subjects = list(
        ExamSubject.objects
        .filter(subject_code__in=subject_codes, is_active=True)
        .order_by("subject_code")
    )

    candidates = ExamCandidate.objects.filter(
        exam_type=exam, school=school
    ).order_by("cand_no")

    # Preload all marks ONCE (critical for speed)
    marks_qs = StudentSubjectMark.objects.filter(
        exam_type=exam,
        school=school,
        subject_code__in=subject_codes
    ).values("cand_no", "subject_code", "numeric_score", "grade")

    marks_map = {}
    for m in marks_qs:
        marks_map.setdefault(m["cand_no"], {})[m["subject_code"]] = m

    rows = []
    for cand in candidates:
        row = {
            "school": school.school_code,
            "cand_no": cand.cand_no,
            "name": cand.name,
            "sex": cand.sex,
            "subjects": [],
            "points": cand.points,
            "division": cand.division,
            "avg": round_no_decimal(cand.avg_numeric),
            "grade": cand.avg_grade,
            "pos": getattr(cand, "position", "")
        }

        cand_marks = marks_map.get(cand.cand_no, {})
        for s in subjects:
            m = cand_marks.get(s.subject_code)
            row["subjects"].append({
                "score": round_no_decimal(m["numeric_score"]) if m else "",
                "grade": m["grade"] if m else ""
            })

        rows.append(row)

    return render(request, "exams/school_results_fast.html", {
        "exam": exam,
        "school": school,
        "subjects": subjects,
        "rows": rows,
    })





from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required
from openpyxl import Workbook

@login_required
def export_sms_xlsx(request, exam_id, school_code):
    exam = get_object_or_404(ExamType, id=exam_id)
    school = get_object_or_404(School, school_code=school_code)

    # ✅ AUTO-DETECT LATEST YEAR
    year = (
        ExamCandidate.objects
        .filter(exam_type=exam, school=school)
        .values_list("year", flat=True)
        .order_by("-year")
        .first()
    )

    if not year:
        messages.error(request, "No results found for this school and exam.")
        return redirect("exams:dashboard")

    candidates = ExamCandidate.objects.filter(
        exam_type=exam,
        school=school,
        year=year
    ).order_by("cand_no")

    # -------------------------
    # BUILD SMS CONTENT
    # -------------------------
    rows = []
    for c in candidates:


        points = int(c.avg_numeric) if c.avg_numeric is not None else "N/A"
        division = c.avg_grade or "N/A"
        full_results = c.full_results or "Results pending"

        sms = (
            f"Habari Mzazi/Mlezi wa  {c.name}, Matokeo ya Mitihani ya {exam.name} "
            f"ni Division {division}, Points {points}. "
            f"Matokeo kwa Ujumla ni: {full_results}"
        )


        rows.append({
            "CAND_NO": c.cand_no,
            "NAME": c.name,
            "PHONE": "",  # add if available
            "SMS": sms,
        })

    # -------------------------
    # EXPORT TO EXCEL
    # -------------------------
    from django.http import HttpResponse
    from openpyxl import Workbook

    wb = Workbook()
    ws = wb.active
    ws.title = "Data"

    # Write header (from dict keys)
    if rows:
        headers = list(rows[0].keys())
        ws.append(headers)

        # Write data rows
        for row in rows:
            ws.append([row.get(h) for h in headers])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="{exam.code}_{school.school_code}_SMS.xlsx"'
    )

    wb.save(response)
    return response




















from django.shortcuts import render
from django.contrib import messages

from .models import (
    SchoolClass,
    Stream,
    Teacher,
    TeacherSubject,
    TimetableSubject,
)
from .forms import (
    SchoolClassForm,
    StreamForm,
    TeacherForm,
    TimetableSubjectForm,
    TeacherSubjectForm,
)
from .services.timetable_engine import generate_timetable


@login_required
def timetable_view(request):

    user = request.user
    profile = getattr(user, "profile", None)

    if not profile or not profile.school:
        messages.error(request, "Your account is not linked to any school.")
        return redirect("exams:dashboard")   # or any safe page

    school = profile.school

    classes = SchoolClass.objects.filter(school=school)
    streams = Stream.objects.filter(school=school)
    subjects = TimetableSubject.objects.filter(school=school)
    teachers = Teacher.objects.filter(school=school)
    teacher_subjects = TeacherSubject.objects.filter(school=school)


    DAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]
    PERIODS = [
        "P1", "P2", "P3", "P4", "BREAK",
        "P6", "P7", "P8", "P9", "LUNCH",
        "P11", "P12"
    ]

    timetable = None
    display_table = []

    # -------------------------------------------------
    # FORMS (DEFAULT)
    # -------------------------------------------------
    class_form = SchoolClassForm()
    stream_form = StreamForm()
    teacher_form = TeacherForm()
    subject_form = TimetableSubjectForm()
    assign_form = TeacherSubjectForm()

    # -------------------------------------------------
    # HANDLE POST ACTIONS
    # -------------------------------------------------
    if request.method == "POST":

        # ---------- ADD CLASS ----------
        if "add_class" in request.POST:
            class_form = SchoolClassForm(request.POST, school=school)
            if class_form.is_valid():
                class_form.save()
                request.session.pop("timetable", None)
                messages.success(
                    request,
                    "Class added. Timetable needs regeneration."
                )
            else:
                messages.error(request, "Failed to add class.")

        # ---------- ADD STREAM ----------
        elif "add_stream" in request.POST:
            stream_form = StreamForm(request.POST, school=school)
            if stream_form.is_valid():
                stream_form.save()
                request.session.pop("timetable", None)
                messages.success(
                    request,
                    "Stream added. Timetable needs regeneration."
                )
            else:
                messages.error(request, "Failed to add stream.")

        # ---------- ADD TEACHER ----------
        elif "add_teacher" in request.POST:
            teacher_form = TeacherForm(request.POST, school=school)
            if teacher_form.is_valid():
                teacher_form.save()
                request.session.pop("timetable", None)
                messages.success(
                    request,
                    "Teacher added. Timetable needs regeneration."
                )
            else:
                messages.error(request, "Failed to add teacher.")

        # ---------- ADD SUBJECT ----------
        elif "add_subject" in request.POST:
            subject_form = TimetableSubjectForm(request.POST, school=school)
            if subject_form.is_valid():
                subject_form.save()
                request.session.pop("timetable", None)
                messages.success(
                    request,
                    "Subject added. Timetable needs regeneration."
                )
            else:
                messages.error(request, "Failed to add subject.")

        # ---------- ASSIGN TEACHER ----------
        elif "assign_teacher" in request.POST:
            assign_form = TeacherSubjectForm(request.POST, school=school)
            if assign_form.is_valid():
                assign_form.save()
                request.session.pop("timetable", None)
                messages.success(
                    request,
                    "Teacher assigned. Timetable needs regeneration."
                )
            else:
                messages.error(request, "Assignment failed.")

        # ---------- GENERATE TIMETABLE ----------
        elif "generate" in request.POST:

            # VALIDATION (MUST COME FIRST)
            if not streams.exists():
                messages.error(request, "No streams found. Please add streams first.")

            elif not subjects.exists():
                messages.error(request, "No timetable subjects found.")

            elif not teachers.exists():
                messages.error(request, "No teachers found.")

            elif not teacher_subjects.exists():
                messages.error(
                    request,
                    "No teacher-subject-stream assignments found."
                )

            else:
                timetable = generate_timetable(
                    streams, subjects, teacher_subjects
                )

                

                # ✅ STORE REAL TIMETABLE IN SESSION
                request.session["timetable"] = timetable
                request.session.modified = True
                # ✅ STORE DISPLAY TABLE FOR PDF & EXCEL
                request.session["display_table"] = display_table
                request.session.modified = True

                messages.success(
                    request,
                    "Timetable generated successfully."
                )

    # -------------------------------------------------
    # LOAD TIMETABLE FROM SESSION (ALWAYS)
    # -------------------------------------------------
    timetable = request.session.get("timetable")

    # -------------------------------------------------
    # PREPARE TEMPLATE-READY DATA
    # -------------------------------------------------
    if timetable:
        for day, stream_map in timetable.items():
            for stream_id, periods_map in stream_map.items():

                stream_obj = next(
                    (s for s in streams if s.id == stream_id),
                    None
                )
                if not stream_obj:
                    continue

                row = {
                    "day": day,
                    "stream": str(stream_obj),
                    "cells": []
                }

                for period in PERIODS:
                    cell = periods_map.get(period)

                    if isinstance(cell, dict):
                        row["cells"].append({
                            "name": cell.get("name", ""),
                            "color": cell.get("color", "#FFFFFF"),
                        })
                    elif cell in ["BREAK", "LUNCH"]:
                        row["cells"].append({
                            "name": cell,
                            "color": "#26ffe9",
                        })
                    else:
                        row["cells"].append({
                            "name": cell or "",
                            "color": "#FFFFFF",
                        })

                display_table.append(row)

    # -------------------------------------------------
    # CONTEXT
    # -------------------------------------------------
    context = {
        "classes": classes,
        "streams": streams,
        "subjects": subjects,
        "teachers": teachers,
        "display_table": display_table,
        "days": DAYS,
        "periods": PERIODS,
        "class_form": class_form,
        "stream_form": stream_form,
        "teacher_form": teacher_form,
        "subject_form": subject_form,
        "assign_form": assign_form,
        "school": school,   # 🔴 THIS MUST EXIST
    }
    return render(request, "exams/timetable.html", context)








from django.http import HttpResponse
from reportlab.lib.pagesizes import A3, landscape
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib import colors


def timetable_pdf(request):

    school = request.user.profile.school
    display_table = request.session.get("display_table", [])

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'attachment; filename="{school.school_code}_TIMETABLE.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A3),
        rightMargin=20,
        leftMargin=20,
        topMargin=20,
        bottomMargin=20,
    )

    styles = getSampleStyleSheet()
    elements = []

    # -------------------------------------------------
    # HEADER
    # -------------------------------------------------
    title = Paragraph(
        f"<b>{school.school_code} - {school.name}</b>",
        styles["Title"],
    )
    subtitle = Paragraph(
        f"{school.region} | {school.district} | {school.ward}",
        styles["Normal"],
    )

    elements.append(title)
    elements.append(subtitle)
    elements.append(Spacer(1, 12))

    # -------------------------------------------------
    # TABLE DATA
    # -------------------------------------------------
    headers = [
        "Day","Stream",
        "P1","P2","P3","P4","BREAK",
        "P6","P7","P8","P9","LUNCH",
        "P11","P12"
    ]

    data = [headers]

    for row in display_table:
        data.append(
            [row["day"], row["stream"]] +
            [c["name"] or "-" for c in row["cells"]]
        )

    table = Table(data, repeatRows=1)

    # -------------------------------------------------
    # BASE TABLE STYLE
    # -------------------------------------------------
    style = [
        ("GRID", (0,0), (-1,-1), 0.5, colors.black),
        ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
        ("ALIGN", (2,1), (-1,-1), "CENTER"),
        ("VALIGN", (0,0), (-1,-1), "MIDDLE"),
        ("FONT", (0,0), (-1,0), "Helvetica-Bold"),
        ("FONT", (0,1), (-1,-1), "Helvetica"),
        ("FONTSIZE", (0,0), (-1,-1), 8),
    ]

    # -------------------------------------------------
    # APPLY SUBJECT COLORS FROM DB
    # -------------------------------------------------
    def hex_to_rgb(hex_color):
        hex_color = hex_color.replace("#", "")
        return tuple(int(hex_color[i:i+2], 16)/255 for i in (0, 2, 4))

    for r, row in enumerate(display_table, start=1):
        for c, cell in enumerate(row["cells"], start=2):

            color = cell.get("color")

            if color:
                try:
                    rgb = hex_to_rgb(color)
                    style.append(
                        ("BACKGROUND", (c, r), (c, r), colors.Color(*rgb))
                    )
                except:
                    pass

    table.setStyle(TableStyle(style))

    elements.append(table)
    doc.build(elements)

    return response



from openpyxl import Workbook
from openpyxl.styles import PatternFill, Font, Alignment
from django.http import HttpResponse


def timetable_excel(request):

    school = request.user.profile.school
    display_table = request.session.get("display_table", [])

    wb = Workbook()
    ws = wb.active
    ws.title = "Timetable"

    # ----------------------------------
    # HEADER
    # ----------------------------------
    ws.append([f"{school.school_code} - {school.name}"])
    ws.append([f"{school.region} | {school.district} | {school.ward}"])
    ws.append([])

    ws["A1"].font = Font(bold=True, size=14)
    ws["A2"].font = Font(italic=True)

    headers = [
        "Day","Stream",
        "P1","P2","P3","P4","BREAK",
        "P6","P7","P8","P9","LUNCH",
        "P11","P12"
    ]

    ws.append(headers)

    header_row = ws.max_row
    for col in range(1, len(headers)+1):
        ws.cell(row=header_row, column=col).font = Font(bold=True)
        ws.cell(row=header_row, column=col).alignment = Alignment(horizontal="center")

    # ----------------------------------
    # BODY WITH COLORS
    # ----------------------------------
    row_index = ws.max_row + 1

    for row in display_table:

        values = [row["day"], row["stream"]] + [c["name"] or "-" for c in row["cells"]]
        ws.append(values)

        col_index = 3  # first period column

        for cell in row["cells"]:
            excel_cell = ws.cell(row=row_index, column=col_index)

            excel_cell.alignment = Alignment(horizontal="center", vertical="center")

            color = cell.get("color")

            if color:
                excel_color = color.replace("#", "")
                excel_cell.fill = PatternFill(
                    start_color=excel_color,
                    end_color=excel_color,
                    fill_type="solid"
                )

            col_index += 1

        row_index += 1

    # ----------------------------------
    # AUTO COLUMN WIDTH
    # ----------------------------------
    for col in ws.columns:
        max_len = 0
        col_letter = col[0].column_letter
        for cell in col:
            if cell.value:
                max_len = max(max_len, len(str(cell.value)))
        ws.column_dimensions[col_letter].width = max_len + 2

    # ----------------------------------
    # RESPONSE
    # ----------------------------------
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="{school.school_code}_TIMETABLE.xlsx"'
    )

    wb.save(response)
    return response








@login_required
# def school_summary_pdf(request):
#     school_code = request.GET.get("school")
#     year = request.GET.get("year")

#     context = build_school_summary_context(school_code, year)


@login_required
def school_summary_print(request):

    school_code  = request.GET.get("school")
    year         = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    if not all([school_code, year, exam_type_id]):
        return HttpResponse("Missing parameters", status=400)

    context = build_school_summary_context(
        school_code,
        year,
        exam_type_id
    )

    return render(
        request,
        "exams/reports/school_summary_print.html",
        context
    )


@login_required
def school_trend_print(request):

    school_code  = request.GET.get("school")
    start_year   = request.GET.get("start")
    end_year     = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    if not all([school_code, start_year, end_year, exam_type_id]):
        return HttpResponse("Missing parameters", status=400)

    rows = compute_school_trend(
        school_code,
        start_year,
        end_year,
        exam_type_id   # ✅ FIX
    )

    school = School.objects.get(school_code=school_code)
    exam_type = ExamType.objects.get(id=exam_type_id)

    return render(
        request,
        "exams/reports/school_trend_print.html",
        {
            "school": school,
            "rows": rows,
            "start": int(start_year),
            "end": int(end_year),
            "exam_type": exam_type.name,   # ✅ THIS LINE FIXES IT
            "exam_type_id": int(exam_type_id),
        }
    )





@login_required
def subject_district_rank_html(request):

    region = request.GET.get("region")
    exam_type_id = request.GET.get("exam_type_id")
    year = request.GET.get("year")
    subject = request.GET.get("subject")

    rows = compute_subject_district_rank(region, exam_type_id, year, subject)

    return render(request,"exams/reports/subject_district_rank_html.html",{
        "rows": rows,
        "region": Region.objects.get(id=region),
        "exam_type": ExamType.objects.get(id=exam_type_id),
        "year": year,
        "subject": ExamSubject.objects.get(subject_code=subject)
    })




@login_required
def subject_district_rank_excel(request):

    region_id = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")

    rows = compute_subject_district_rank(region_id, exam_type_id, year, subject_code)

    region = Region.objects.get(id=region_id)
    exam_type = ExamType.objects.get(id=exam_type_id)
    subject = ExamSubject.objects.get(subject_code=subject_code)

    wb = Workbook()
    ws = wb.active
    ws.title = "Subject District Rank"

    # ---------------- HEADER ----------------
    ws.merge_cells("A1:L1")
    ws["A1"] = f"SUBJECT DISTRICT RANKING – {subject.subject_name_eng} – {exam_type.name} – {region.name} – {year}"
    ws["A1"].font = Font(bold=True, size=14)
    ws["A1"].alignment = Alignment(horizontal="center")

    ws.append([])

    # ---------------- TABLE HEADER ----------------
    headers = [
        "#","DISTRICT","SAT",
        "A(F)","A(M)","A(T)",
        "B(F)","B(M)","B(T)",
        "C(F)","C(M)","C(T)",
        "D(F)","D(M)","D(T)",
        "F(F)","F(M)","F(T)",
        "PASS","PASS %","GPA","GRADE","STATUS"
    ]

    ws.append(headers)

    for c in range(1, len(headers)+1):
        ws.cell(row=3, column=c).font = Font(bold=True)
        ws.cell(row=3, column=c).alignment = Alignment(horizontal="center")

    # ---------------- DATA ----------------
    r = 4
    for row in rows:
        ws.append([
            row["rank"],
            row["district"],
            row["sat"],

            row["af"],row["am"],row["at"],
            row["bf"],row["bm"],row["bt"],
            row["cf"],row["cm"],row["ct"],
            row["df"],row["dm"],row["dt"],
            row["ff"],row["fm"],row["ft"],

            row["pass_no"],
            row["pass_pct"],
            row["gpa"],
            row["grade"],
            row["status"]
        ])

        for c in range(1, 24):
            ws.cell(row=r, column=c).alignment = Alignment(horizontal="center")

        r += 1

    # ---------------- AUTOSIZE ----------------
    for i, col in enumerate(ws.iter_cols(min_row=3, max_row=ws.max_row), start=1):
        ws.column_dimensions[get_column_letter(i)].width = 15


    # ---------------- RESPONSE ----------------
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )

    filename = f"Subject_District_Rank_{subject.subject_name_eng}_{region.name}_{year}.xlsx"
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response


@login_required
def subject_reg_school_rank_html(request):

    region_id = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")   # <-- MUST be subject_code

    region = Region.objects.get(id=region_id)
    exam_type = ExamType.objects.get(id=exam_type_id)

    # Resolve subject ONLY by subject_code
    subject = ExamSubject.objects.filter(subject_code=subject_code).first()
    if not subject:
        raise Http404("Subject not found")

    rows = compute_subject_school_rank(
        region_id,
        exam_type_id,
        year,
        subject_code      # <-- pass code directly
    )

    return render(request, "exams/reports/subject_reg_school_rank_html.html", {
        "rows": rows,
        "region": region,
        "exam_type": exam_type,
        "subject": subject,
        "year": year,
        "exam_type_id": exam_type_id,
    })




@login_required
def subject_reg_school_rank_ext_html(request):

    region_id = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")   # <-- MUST be subject_code

    region = Region.objects.get(id=region_id)
    exam_type = ExamType.objects.get(id=exam_type_id)

    # Resolve subject ONLY by subject_code
    subject = ExamSubject.objects.filter(subject_code=subject_code).first()
    if not subject:
        raise Http404("Subject not found")

    rows = compute_subject_school_rank_ext(
        region_id,
        exam_type_id,
        year,
        subject_code      # <-- pass code directly
    )

    return render(request, "exams/reports/subject_reg_school_rank_ext_html.html", {
        "rows": rows,
        "region": region,
        "exam_type": exam_type,
        "subject": subject,
        "year": year,
        "exam_type_id": exam_type_id,
    })





@login_required
def subject_reg_school_rank_excel(request):

    region_id = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")   # MUST be subject_code

    # --- Resolve subject ---
    subject = ExamSubject.objects.filter(subject_code=subject_code).first()
    if not subject:
        return HttpResponse("Subject not found")

    # --- Get rows using SAME logic as HTML ---
    rows = compute_subject_school_rank(
        region_id,
        exam_type_id,
        year,
        subject_code
    )

    # DEBUG (optional)
    # print("ROWS COUNT:", len(rows))

    from openpyxl import Workbook
    wb = Workbook()
    ws = wb.active
    ws.title = "Subject School Rank"

    # ---- HEADER ----
    headers = [
        "#","CODE","SCHOOL","DISTRICT",
        "A","B","C","D","F",
        "PASS","PASS %","GPA","GRADE","STATUS"
    ]
    ws.append(headers)

    # ---- DATA ----
    for r in rows:
        ws.append([
            r.get("rank"),
            r.get("code"),
            r.get("name"),
            r.get("district"),

            r.get("a"),
            r.get("b"),
            r.get("c"),
            r.get("d"),
            r.get("f"),

            r.get("pass"),
            r.get("pass_pct"),
            r.get("gpa"),
            r.get("grade"),
            r.get("status"),
        ])

    # ---- RESPONSE ----
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename=Subject_School_Rank_{subject.subject_code}_{year}.xlsx'
    )

    wb.save(response)
    return response





@login_required
def subject_reg_school_rank_ext_excel(request):

    region_id = request.GET.get("region")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")

    region = Region.objects.get(id=region_id)
    exam_type = ExamType.objects.get(id=exam_type_id)
    subject = ExamSubject.objects.get(subject_code=subject_code)

    rows = compute_subject_school_rank_ext(
        region_id, exam_type_id, year, subject_code
    )

    wb = Workbook()
    ws = wb.active
    ws.title = "Subject School Rank"

    # ---------- TITLE ----------
    ws.merge_cells("A1:AJ1")
    ws["A1"] = f"SUBJECT SCHOOL RANKING – {subject.subject_name_eng} | {exam_type.name} | {region.name} | {year}"
    ws["A1"].font = ws["A1"].font.copy(bold=True)

    # ---------- HEADERS ----------
    headers = [
        "#","CNO","SCHOOL","REGION","DISTRICT","OWNERSHIP",

        "REG F","REG M","REG T",
        "SAT F","SAT M","SAT T",

        "A F","A M","A T","A %",
        "B F","B M","B T","B %",
        "C F","C M","C T","C %",
        "D F","D M","D T","D %",
        "F F","F M","F T","F %",

        "PASS F","PASS M","PASS T","PASS %",

        "GPA","GRADE","STATUS","POS"
    ]

    ws.append(headers)

    for cell in ws[2]:
        cell.font = cell.font.copy(bold=True)

    # ---------- DATA ----------
    for r in rows:
        ws.append([
            r["rank"], r["code"], r["name"], r["region"], r["district"], r["ownership"],

            r["reg_f"], r["reg_m"], r["reg_t"],
            r["sat_f"], r["sat_m"], r["sat_t"],

            r["af"], r["am"], r["at"], r["a_pct"],
            r["bf"], r["bm"], r["bt"], r["b_pct"],
            r["cf"], r["cm"], r["ct"], r["c_pct"],
            r["df"], r["dm"], r["dt"], r["d_pct"],
            r["ff"], r["fm"], r["ft"], r["f_pct"],

            r["pass_f"], r["pass_m"], r["pass_t"], r["pass_pct"],

            r["gpa"], r["grade"], r["status"], r["rank"]
        ])

    # ---------- RESPONSE ----------
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    filename = f"Subject_School_Rank_{subject.subject_code}_{year}.xlsx"
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response

















#################################

#  SCHOOL RANKING - DISTRICT


##################################






@login_required
def district_school_ranking_html(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank(district, year, exam_type_id)

    exam_type = None
    if exam_type_id:
        exam_type = ExamType.objects.filter(id=exam_type_id).first()

    return render(request,"exams/reports/district_school_rank_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "exam_type": exam_type,
        "district": District.objects.get(id=district)
    })





@login_required
def district_school_ranking_print(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank(district, year, exam_type_id)

    return render(request,"exams/reports/district_school_rank_print.html",{
        "rows": rows,
        "year": year,
        "exam_type": ExamType.objects.get(id=exam_type_id),
        "district": District.objects.get(id=district),
    })


@login_required
def district_school_ranking_excel(request):

    from openpyxl import Workbook
    from django.http import HttpResponse

    rows = compute_district_school_rank(
        request.GET["district"],
        request.GET["year"],
        request.GET["exam_type_id"]
    )

    wb=Workbook()
    ws=wb.active

    ws.append(["#","CODE","NAME","DISTRICT","REGION",
               "I","II","III","IV","0","#I-IV","%I-IV","GPA"])

    for r in rows:
        ws.append([
            r["rank"],r["code"],r["name"],r["district"],r["district"],
            r["d1"],r["d2"],r["d3"],r["d4"],r["d0"],
            r["pass"],r["pass_pct"],r["gpa"]
        ])

    resp=HttpResponse(content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
    resp["Content-Disposition"]="attachment; filename=District_School_Rank.xlsx"
    wb.save(resp)
    return resp


@login_required
def district_school_ranking_pdf(request):

    rows = compute_district_school_rank(
        request.GET["district"],
        request.GET["year"],
        request.GET["exam_type_id"]
    )

    from reportlab.platypus import SimpleDocTemplate,Table,TableStyle
    from reportlab.lib.pagesizes import A4
    from reportlab.lib import colors
    from django.http import HttpResponse

    resp=HttpResponse(content_type="application/pdf")
    resp["Content-Disposition"]="attachment; filename=District_School_Rank.pdf"

    data=[["#","CODE","NAME","DISTRICT","REGION","I","II","III","IV","0","#I-IV","%I-IV","GPA"]]

    for r in rows:
        data.append([
            r["rank"],r["code"],r["name"],r["district"],r["district"],
            r["d1"],r["d2"],r["d3"],r["d4"],r["d0"],
            r["pass"],r["pass_pct"],r["gpa"]
        ])

    table=Table(data,repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID",(0,0),(-1,-1),1,colors.black),
        ("BACKGROUND",(0,0),(-1,0),colors.lightgrey),
        ("ALIGN",(0,0),(-1,-1),"CENTER")
    ]))

    doc=SimpleDocTemplate(resp,pagesize=A4)
    doc.build([table])
    return resp


@login_required
def district_school_ranking_ext_html(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank_ext(district, year, exam_type_id)

    exam_type = None
    if exam_type_id:
        exam_type = ExamType.objects.filter(id=exam_type_id).first()

    return render(request,"exams/reports/district_school_rank_ext_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "exam_type": exam_type,
        "district": District.objects.get(id=district)
    })






@login_required
def district_school_ranking_ext_html(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank_ext(district, year, exam_type_id)

    exam_type = None
    if exam_type_id:
        exam_type = ExamType.objects.filter(id=exam_type_id).first()

    return render(request,"exams/reports/district_school_rank_ext_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "exam_type": exam_type,
        "district": District.objects.get(id=district)
    })


    
from openpyxl import Workbook
from django.http import HttpResponse

@login_required
def district_school_ranking_ext_excel(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank_ext(district, year, exam_type_id)

    wb = Workbook()
    ws = wb.active
    ws.title = "District Ranking"

    # ---------------- HEADER ROW ----------------
    headers = [
        "#","CNO","SCHOOL NAME","REGION","DISTRICT","OWNERSHIP",

        "REG F","REG M","REG T",

        "SAT F","SAT M","SAT T",

        "I F","I M","I T","I %",

        "II F","II M","II T","II %",

        "III F","III M","III T","III %",

        "PASS I-III F","PASS I-III M","PASS I-III T","PASS I-III %",

        "IV F","IV M","IV T","IV %",

        "FAIL F","FAIL M","FAIL T","FAIL %",

        "PASS I-IV F","PASS I-IV M","PASS I-IV T","PASS I-IV %",

        "GPA","GRADE","STATUS","POSITION"
    ]

    ws.append(headers)

    # ---------------- DATA ROWS ----------------
    for r in rows:
        ws.append([
            r["no"], r["cno"], r["name"], r["region"], r["district"], r["ownership"],

            r["reg_f"], r["reg_m"], r["reg_t"],

            r["sat_f"], r["sat_m"], r["sat_t"],

            r["i_f"], r["i_m"], r["i_t"], r["i_pct"],

            r["ii_f"], r["ii_m"], r["ii_t"], r["ii_pct"],

            r["iii_f"], r["iii_m"], r["iii_t"], r["iii_pct"],

            r["pass_i_iii_f"], r["pass_i_iii_m"], r["pass_i_iii_t"], r["pass_i_iii_pct"],

            r["iv_f"], r["iv_m"], r["iv_t"], r["iv_pct"],

            r["f_f"], r["f_m"], r["f_t"], r["f_pct"],

            r["pass_i_iv_f"], r["pass_i_iv_m"], r["pass_i_iv_t"], r["pass_i_iv_pct"],

            r["gpa"], r["grade"], r["status"], r["position"]
        ])

    # ---------------- RESPONSE ----------------
    filename = f"DISTRICT_SCHOOL_RANK_EXT_{year}.xlsx"

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response


@login_required
def district_school_ranking_ext_print(request):

    district = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_school_rank_ext(district, year, exam_type_id)

    return render(request,"exams/reports/district_school_rank_ext_html.html",{
        "rows": rows,
        "year": year,
        "exam_type_id": exam_type_id,
        "district": District.objects.get(id=district)
    })






@login_required
def subject_dis_school_rank_html(request):

    district_id = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")   # <-- MUST be subject_code

    district = District.objects.get(id=district_id)
    exam_type = ExamType.objects.get(id=exam_type_id)

    # Resolve subject ONLY by subject_code
    subject = ExamSubject.objects.filter(subject_code=subject_code).first()
    if not subject:
        raise Http404("Subject not found")

    rows = compute_subject_dis_school_rank(
        district_id,
        exam_type_id,
        year,
        subject_code      # <-- pass code directly
    )

    return render(request, "exams/reports/subject_dis_school_rank_html.html", {
        "rows": rows,
        "district": district,
        "exam_type": exam_type,
        "subject": subject,
        "year": year,
        "exam_type_id": exam_type_id,
    })




@login_required
def subject_dis_school_rank_ext_html(request):

    district_id = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")   # <-- MUST be subject_code

    district = District.objects.get(id=district_id)
    exam_type = ExamType.objects.get(id=exam_type_id)

    # Resolve subject ONLY by subject_code
    subject = ExamSubject.objects.filter(subject_code=subject_code).first()
    if not subject:
        raise Http404("Subject not found")

    rows = compute_subject_dis_school_rank_ext(
        district_id,
        exam_type_id,
        year,
        subject_code      # <-- pass code directly
    )

    return render(request, "exams/reports/subject_dis_school_rank_ext_html.html", {
        "rows": rows,
        "district": district,
        "exam_type": exam_type,
        "subject": subject,
        "year": year,
        "exam_type_id": exam_type_id,
    })





@login_required
def subject_dis_school_rank_excel(request):

    district_id = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")   # MUST be subject_code

    # --- Resolve subject ---
    subject = ExamSubject.objects.filter(subject_code=subject_code).first()
    if not subject:
        return HttpResponse("Subject not found")

    # --- Get rows using SAME logic as HTML ---
    rows = compute_subject_dis_school_rank(
        district_id,
        exam_type_id,
        year,
        subject_code
    )

    # DEBUG (optional)
    # print("ROWS COUNT:", len(rows))

    from openpyxl import Workbook
    wb = Workbook()
    ws = wb.active
    ws.title = "Subject School Rank"

    # ---- HEADER ----
    headers = [
        "#","CODE","SCHOOL","DISTRICT",
        "A","B","C","D","F",
        "PASS","PASS %","GPA","GRADE","STATUS"
    ]
    ws.append(headers)

    # ---- DATA ----
    for r in rows:
        ws.append([
            r.get("rank"),
            r.get("code"),
            r.get("name"),
            r.get("district"),

            r.get("a"),
            r.get("b"),
            r.get("c"),
            r.get("d"),
            r.get("f"),

            r.get("pass"),
            r.get("pass_pct"),
            r.get("gpa"),
            r.get("grade"),
            r.get("status"),
        ])

    # ---- RESPONSE ----
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename=Subject_School_Rank_{subject.subject_code}_{year}.xlsx'
    )

    wb.save(response)
    return response





@login_required
def subject_dis_school_rank_ext_excel(request):

    district_id = request.GET.get("district")
    year = request.GET.get("year")
    exam_type_id = request.GET.get("exam_type_id")
    subject_code = request.GET.get("subject")

    district = District.objects.get(id=district_id)
    exam_type = ExamType.objects.get(id=exam_type_id)
    subject = ExamSubject.objects.get(subject_code=subject_code)

    rows = compute_subject_dis_school_rank_ext(
        district_id, exam_type_id, year, subject_code
    )

    wb = Workbook()
    ws = wb.active
    ws.title = "Subject School Rank"

    # ---------- TITLE ----------
    ws.merge_cells("A1:AJ1")
    ws["A1"] = f"SUBJECT SCHOOL RANKING – {subject.subject_name_eng} | {exam_type.name} | {district.name} | {year}"
    ws["A1"].font = ws["A1"].font.copy(bold=True)

    # ---------- HEADERS ----------
    headers = [
        "#","CNO","SCHOOL","REGION","DISTRICT","OWNERSHIP",

        "REG F","REG M","REG T",
        "SAT F","SAT M","SAT T",

        "A F","A M","A T","A %",
        "B F","B M","B T","B %",
        "C F","C M","C T","C %",
        "D F","D M","D T","D %",
        "F F","F M","F T","F %",

        "PASS F","PASS M","PASS T","PASS %",

        "GPA","GRADE","STATUS","POS"
    ]

    ws.append(headers)

    for cell in ws[2]:
        cell.font = cell.font.copy(bold=True)

    # ---------- DATA ----------
    for r in rows:
        ws.append([
            r["rank"], r["code"], r["name"], r["district"], r["district"], r["ownership"],

            r["reg_f"], r["reg_m"], r["reg_t"],
            r["sat_f"], r["sat_m"], r["sat_t"],

            r["af"], r["am"], r["at"], r["a_pct"],
            r["bf"], r["bm"], r["bt"], r["b_pct"],
            r["cf"], r["cm"], r["ct"], r["c_pct"],
            r["df"], r["dm"], r["dt"], r["d_pct"],
            r["ff"], r["fm"], r["ft"], r["f_pct"],

            r["pass_f"], r["pass_m"], r["pass_t"], r["pass_pct"],

            r["gpa"], r["grade"], r["status"], r["rank"]
        ])

    # ---------- RESPONSE ----------
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    filename = f"Subject_School_Rank_{subject.subject_code}_{year}.xlsx"
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response



@login_required
def school_subject_trend_html(request):

    school_code  = request.GET.get("school")
    subject_code = request.GET.get("subject")
    start_year   = request.GET.get("start")
    end_year     = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_school_subject_trend(
        school_code,
        subject_code,
        start_year,
        end_year,
        exam_type_id
    )

    school = School.objects.get(school_code=school_code)

    subject = ExamSubject.objects.get(subject_code=subject_code)

    return render(request, "exams/reports/school_subject_trend_html.html", {
        "school": school,
        "subject": subject,                 # ✅ FULL OBJECT
        "rows": rows,
        "start": int(start_year),
        "end": int(end_year),
        "exam_type_id": int(exam_type_id),
    })



@login_required
def region_subject_trend_html(request):

    region_id    = request.GET.get("region")
    subject_code = request.GET.get("subject")
    start_year   = request.GET.get("start")
    end_year     = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_region_subject_trend(
        region_id,
        subject_code,
        start_year,
        end_year,
        exam_type_id
    )

    region = Region.objects.get(id=region_id)
    subject = ExamSubject.objects.get(subject_code=subject_code)
    exam    = ExamType.objects.get(id=exam_type_id)   # ✅ ADD THIS
    return render(request, "exams/reports/region_subject_trend_html.html", {
        "region": region,
        "subject": subject,
        "rows": rows,
        "start": int(start_year),
        "end": int(end_year),
        "exam_type_id": int(exam_type_id),
        "exam": exam,   # ✅ PASS TO TEMPLATE
    })



@login_required
def district_subject_trend_html(request):

    district_id  = request.GET.get("district")
    subject_code = request.GET.get("subject")
    start_year   = request.GET.get("start")
    end_year     = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_district_subject_trend(
        district_id,
        subject_code,
        start_year,
        end_year,
        exam_type_id
    )

    district = District.objects.get(id=district_id)
    subject = ExamSubject.objects.get(subject_code=subject_code)
    exam    = ExamType.objects.get(id=exam_type_id)   # ✅ ADD THIS
    return render(request, "exams/reports/district_subject_trend_html.html", {
        "district": district,
        "subject": subject,
        "rows": rows,
        "start": int(start_year),
        "end": int(end_year),
        "exam_type_id": int(exam_type_id),
        "exam": exam,   # ✅ PASS TO TEMPLATE
    })



@login_required
def school_subject_trend_print(request):

    school_code  = request.GET.get("school")
    subject_code = request.GET.get("subject")
    start_year   = request.GET.get("start")
    end_year     = request.GET.get("end")
    exam_type_id = request.GET.get("exam_type_id")

    rows = compute_school_subject_trend(
        school_code,
        subject_code,
        start_year,
        end_year,
        exam_type_id
    )

    school = School.objects.get(school_code=school_code)
    subject = ExamSubject.objects.get(subject_code=subject_code)
    exam    = ExamType.objects.get(id=exam_type_id)   # ✅ ADD THIS

    return render(
        request,
        "exams/reports/school_subject_trend_print.html",
        {
            "school": school,
            "subject": subject,
            "rows": rows,
            "start": int(start_year),
            "end": int(end_year),
        }
    )


@login_required
def district_subject_trend_print(request):

    district_id  = int(request.GET.get("district"))
    subject_code = request.GET.get("subject")
    start_year   = int(request.GET.get("start"))
    end_year     = int(request.GET.get("end"))
    exam_type_id = int(request.GET.get("exam_type_id"))

    rows = compute_district_subject_trend(
        district_id, subject_code, start_year, end_year, exam_type_id
    )

    district = District.objects.get(id=district_id)
    subject = ExamSubject.objects.get(subject_code=subject_code)
    exam    = ExamType.objects.get(id=exam_type_id)   # ✅ ADD THIS

    return render(request,
        "exams/reports/_subject_trend_print_base.html",
        {
            "title": "District Subject Trend Print",
            "header_name": district.name,
            "subject": subject,
            "exam": exam,                 # ✅ ADD THIS
            "rows": rows,
            "start": start_year,
            "end": end_year,
            "location_line": (
                f"District: {district.name} | "
                f"Region: {district.region.name}"
            )
        }
    )


@login_required
def region_subject_trend_print(request):

    region_id    = int(request.GET.get("region"))
    subject_code = request.GET.get("subject")
    start_year   = int(request.GET.get("start"))
    end_year     = int(request.GET.get("end"))
    exam_type_id = int(request.GET.get("exam_type_id"))

    rows = compute_region_subject_trend(
        region_id, subject_code, start_year, end_year, exam_type_id
    )

    region  = Region.objects.get(id=region_id)
    subject = ExamSubject.objects.get(subject_code=subject_code)
    exam    = ExamType.objects.get(id=exam_type_id)

    return render(
        request,
        "exams/reports/_subject_trend_print_base.html",
        {
            "title": "Region Subject Trend Print",
            "header_name": region.name,
            "subject": subject,
            "exam": exam,                 # ✅ ADD THIS
            "rows": rows,
            "start": start_year,
            "end": end_year,
            "location_line": f"Region: {region.name}",
        }
    )



































































from django.urls import reverse_lazy
from django.views.generic import CreateView, ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from .models import (
    AcademicLevel,
    JointExamProject,
    GradingRule,
    DivisionRule,
    ExamScore
)
from .forms import (
    AcademicLevelForm,
    JointExamProjectForm,
    GradingRuleForm,
    DivisionRuleForm,
    ExamScoreForm
)





# views.py
from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.core.paginator import Paginator
from django.http import JsonResponse
from .models import SiteSetting, Gallery, News
from .forms import SiteSettingForm
import json

@login_required
def site_settings_management(request):
    """Combined site settings management view"""
    
    # Get or create settings
    setting, created = SiteSetting.objects.get_or_create(id=1)
    
    if request.method == "POST":
        action = request.POST.get("action")
        
        # Update site settings
        if action == "update_settings":
            form = SiteSettingForm(request.POST, request.FILES, instance=setting)
            if form.is_valid():
                form.save()
                messages.success(request, "Site settings saved successfully!")
            else:
                messages.error(request, "Error saving settings. Please check the form.")
            return redirect("exams:site_settings_management")
        
        # Gallery actions
        elif action == "add_gallery":
            if request.FILES.get("image"):
                gallery = Gallery.objects.create(
                    title=request.POST.get("title", ""),
                    description=request.POST.get("description", ""),
                    image=request.FILES["image"],
                    order_by=request.POST.get("order_by", 0)
                )
                messages.success(request, f"Gallery image '{gallery.title}' added!")
            else:
                messages.error(request, "Image is required for gallery")
            return redirect("exams:site_settings_management")
        
        elif action == "edit_gallery":
            gallery = get_object_or_404(Gallery, id=request.POST.get("id"))
            gallery.title = request.POST.get("title", gallery.title)
            gallery.description = request.POST.get("description", gallery.description)
            if request.FILES.get("image"):
                gallery.image = request.FILES["image"]
            gallery.order = request.POST.get("order_by", gallery.order_by)
            gallery.save()
            messages.success(request, f"Gallery '{gallery.title}' updated!")
            return redirect("exams:site_settings_management")
        
        elif action == "toggle_gallery":
            gallery = get_object_or_404(Gallery, id=request.POST.get("id"))
            gallery.is_active = not gallery.is_active
            gallery.save()
            status = "activated" if gallery.is_active else "deactivated"
            messages.success(request, f"Gallery '{gallery.title}' {status}!")
            return redirect("exams:site_settings_management")
        
        elif action == "delete_gallery":
            gallery = get_object_or_404(Gallery, id=request.POST.get("id"))
            title = gallery.title
            gallery.delete()
            messages.success(request, f"Gallery '{title}' deleted!")
            return redirect("exams:site_settings_management")
        
        # News actions
        elif action == "add_news":
            news = News.objects.create(
                title=request.POST.get("title", ""),
                content=request.POST.get("content", ""),
                summary=request.POST.get("summary", ""),
                is_featured=request.POST.get("is_featured") == "on",
                image=request.FILES.get("image")
            )
            messages.success(request, f"News '{news.title}' added!")
            return redirect("exams:site_settings_management")
        
        elif action == "edit_news":
            news = get_object_or_404(News, id=request.POST.get("id"))
            news.title = request.POST.get("title", news.title)
            news.content = request.POST.get("content", news.content)
            news.summary = request.POST.get("summary", news.summary)
            news.is_featured = request.POST.get("is_featured") == "on"
            if request.FILES.get("image"):
                news.image = request.FILES["image"]
            news.save()
            messages.success(request, f"News '{news.title}' updated!")
            return redirect("exams:site_settings_management")
        
        elif action == "toggle_news":
            news = get_object_or_404(News, id=request.POST.get("id"))
            news.is_active = not news.is_active
            news.save()
            status = "activated" if news.is_active else "deactivated"
            messages.success(request, f"News '{news.title}' {status}!")
            return redirect("exams:site_settings_management")
        
        elif action == "delete_news":
            news = get_object_or_404(News, id=request.POST.get("id"))
            title = news.title
            news.delete()
            messages.success(request, f"News '{title}' deleted!")
            return redirect("exams:site_settings_management")
        
        # Reset settings
        elif action == "reset_settings":
            setting.delete()
            SiteSetting.objects.create()
            messages.success(request, "Settings reset to default!")
            return redirect("exams:site_settings_management")
    
    # GET request - display forms and data
    form = SiteSettingForm(instance=setting)
    
    # Parse footer links for display
    footer_links = []
    try:
        if setting.footer_links:
            footer_links = json.loads(setting.footer_links) if isinstance(setting.footer_links, str) else setting.footer_links
    except:
        footer_links = []
    
    # Get gallery and news with pagination
    gallery_list = Gallery.objects.all().order_by('order_by', '-created_at')
    news_list = News.objects.all().order_by('-created_at')
    
    gallery_paginator = Paginator(gallery_list, 10)
    news_paginator = Paginator(news_list, 10)
    
    gallery_page = request.GET.get("gallery_page", 1)
    news_page = request.GET.get("news_page", 1)
    
    gallery_page_obj = gallery_paginator.get_page(gallery_page)
    news_page_obj = news_paginator.get_page(news_page)
    
    context = {
        "form": form,
        "setting": setting,
        "footer_links": footer_links,
        "gallery_page_obj": gallery_page_obj,
        "news_page_obj": news_page_obj,
        "gallery_count": gallery_list.count(),
        "news_count": news_list.count(),
    }
    
    return render(request, "exams/site_settings_management.html", context)


def site_settings_preview(request):
    """Preview site settings"""
    setting = SiteSetting.objects.first()
    return render(request, "exams/site_settings_preview.html", {"setting": setting})
    
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.http import JsonResponse
from .models import SiteSetting
from .forms import SiteSettingForm
import json

@login_required
@permission_required('exams.change_sitesetting', raise_exception=True)
def site_settings_edit(request):
    """Edit site settings"""
    setting = SiteSetting.get_active()
    
    if not setting:
        setting = SiteSetting.objects.create(
            site_name="KIGOMA | RESULTS",
            is_active=True
        )
    
    if request.method == 'POST':
        form = SiteSettingForm(request.POST, request.FILES, instance=setting)
        if form.is_valid():
            form.save()
            messages.success(request, 'Site settings updated successfully!')
            return redirect('exams:site_settings_management')
        else:
            messages.error(request, 'Please correct the errors below.')
    else:
        form = SiteSettingForm(instance=setting)
    
    # Parse footer links JSON for preview
    footer_links = []
    if setting.footer_links:
        try:
            footer_links = json.loads(setting.footer_links)
        except:
            footer_links = []
    
    context = {
        'form': form,
        'setting': setting,
        'footer_links': footer_links,
        'title': 'Site Settings',
    }
    
    return render(request, 'exams/site_settings.html', context)


# @login_required
# @permission_required('exams.view_sitesetting', raise_exception=True)
# def site_settings_preview(request):
#     """Preview site settings"""
#     setting = SiteSetting.get_active()
    
#     context = {
#         'setting': setting,
#         'title': 'Site Settings Preview',
#     }
    
#     return render(request, 'exams/site_settings_preview.html', context)


@login_required
@permission_required('exams.change_sitesetting', raise_exception=True)
def site_settings_reset(request):
    """Reset site settings to default"""
    if request.method == 'POST':
        # Delete all existing settings
        SiteSetting.objects.all().delete()
        
        # Create default settings
        default_setting = SiteSetting.objects.create(
            site_name="KIGOMA | RESULTS",
            site_title="Kigoma Regional Examination Results",
            site_tagline="Excellence in Education",
            header_bg_color="#ffffff",
            footer_copyright=f"© {datetime.now().year} Kigoma Region. All rights reserved.",
            is_active=True
        )
        
        messages.success(request, 'Site settings reset to default successfully!')
        return redirect('exams:site_settings_edit')
    
    return JsonResponse({'error': 'Invalid request'}, status=400)














    
# ===============================
# Academic Level
# ===============================

class AcademicLevelCreateView(LoginRequiredMixin, CreateView):
    model = AcademicLevel
    form_class = AcademicLevelForm
    template_name = 'exams/academic_level_form.html'
    success_url = reverse_lazy('academic_level_list')


class AcademicLevelListView(LoginRequiredMixin, ListView):
    model = AcademicLevel
    template_name = 'exams/academic_level_list.html'


# ===============================
# Joint Exam Project
# ===============================

class JointExamProjectCreateView(LoginRequiredMixin, CreateView):
    model = JointExamProject
    form_class = JointExamProjectForm
    template_name = 'exams/project_form.html'
    success_url = reverse_lazy('project_list')


class JointExamProjectListView(LoginRequiredMixin, ListView):
    model = JointExamProject
    template_name = 'exams/project_list.html'


# ===============================
# Grading Rule
# ===============================

class GradingRuleCreateView(LoginRequiredMixin, CreateView):
    model = GradingRule
    form_class = GradingRuleForm
    template_name = 'exams/grading_rule_form.html'
    success_url = reverse_lazy('grading_rule_list')


class GradingRuleListView(LoginRequiredMixin, ListView):
    model = GradingRule
    template_name = 'exams/grading_rule_list.html'


# ===============================
# Division Rule
# ===============================

class DivisionRuleCreateView(LoginRequiredMixin, CreateView):
    model = DivisionRule
    form_class = DivisionRuleForm
    template_name = 'exams/division_rule_form.html'
    success_url = reverse_lazy('division_rule_list')


class DivisionRuleListView(LoginRequiredMixin, ListView):
    model = DivisionRule
    template_name = 'exams/division_rule_list.html'


# ===============================
# Exam Score Entry
# ===============================

class ExamScoreCreateView(LoginRequiredMixin, CreateView):
    model = ExamScore
    form_class = ExamScoreForm
    template_name = 'exams/score_form.html'
    success_url = reverse_lazy('score_list')

    def form_valid(self, form):
        form.instance.entered_by = self.request.user
        return super().form_valid(form)


class ExamScoreListView(LoginRequiredMixin, ListView):
    model = ExamScore
    template_name = 'exams/score_list.html'





from django.shortcuts import render, redirect
from django.contrib.auth.decorators import login_required
from .forms import AcademicLevelForm
from .models import AcademicLevel


@login_required
def academic_level_add(request):
    if request.method == 'POST':
        form = AcademicLevelForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect('academic_level_list')
    else:
        form = AcademicLevelForm()

    return render(request, 'exams/academic_level_form.html', {
        'form': form
    })


@login_required
def academic_level_list(request):
    levels = AcademicLevel.objects.all()
    return render(request, 'exams/academic_level_list.html', {
        'levels': levels
    })



from django.contrib.auth.decorators import login_required
from django.shortcuts import render, redirect
from .models import JointExamProject
from .forms import JointExamProjectForm


@login_required
def project_list(request):
    projects = JointExamProject.objects.select_related("academic_level")
    return render(request, "exams/project_list.html", {
        "projects": projects
    })

 
@login_required
def project_add(request):
    if request.method == "POST":
        form = JointExamProjectForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("exams:project_list")
    else:
        form = JointExamProjectForm()

    return render(request, "exams/project_form.html", {
        "form": form
    })


from django.shortcuts import get_object_or_404, render
from django.contrib.auth.decorators import login_required

@login_required
def project_manage(request, project_code):
    """
    Comprehensive project management page
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    context = {
        'project': project,
    }
    
    return render(request, 'exams/project_manage.html', context)

@login_required
def academic_level_list(request):
    levels = AcademicLevel.objects.all()
    return render(request, "exams/academic_level_list.html", {"levels": levels})


@login_required
def academic_level_add(request):
    if request.method == "POST":
        form = AcademicLevelForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("exams:academic_level_list")
    else:
        form = AcademicLevelForm()

    return render(request, "exams/academic_level_form.html", {"form": form})


@login_required
def project_list(request):
    projects = JointExamProject.objects.select_related("academic_level")
    return render(request, "exams/project_list.html", {"projects": projects})


@login_required
def project_add(request):
    if request.method == "POST":
        form = JointExamProjectForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("exams:project_list")
    else:
        form = JointExamProjectForm()

    return render(request, "exams/project_form.html", {"form": form})


@login_required
def grading_rule_list(request):
    rules = GradingRule.objects.select_related("academic_level")
    return render(request, "exams/grading_rule_list.html", {"rules": rules})


@login_required
def grading_rule_add(request):
    if request.method == "POST":
        form = GradingRuleForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("exams:grading_rule_list")
    else:
        form = GradingRuleForm()

    return render(request, "exams/grading_rule_form.html", {"form": form})


@login_required
def division_rule_list(request):
    rules = DivisionRule.objects.select_related("academic_level")
    return render(request, "exams/division_rule_list.html", {"rules": rules})


@login_required
def division_rule_add(request):
    if request.method == "POST":
        form = DivisionRuleForm(request.POST)
        if form.is_valid():
            form.save()
            return redirect("exams:division_rule_list")
    else:
        form = DivisionRuleForm()

    return render(request, "exams/division_rule_form.html", {"form": form})


@login_required
def score_list(request):
    scores = ExamScore.objects.select_related(
        "project", "subject", "school"
    )
    return render(request, "exams/score_list.html", {"scores": scores})


@login_required
def score_add(request):
    if request.method == "POST":
        form = ExamScoreForm(request.POST)
        if form.is_valid():
            obj = form.save(commit=False)
            obj.entered_by = request.user
            obj.save()
            return redirect("exams:score_list")
    else:
        form = ExamScoreForm()

    return render(request, "exams/score_form.html", {"form": form})



@login_required
def project_process(request, pk):
    process_project_results(pk)
    messages.success(request, "Project processed successfully.")
    return redirect("exams:project_list")



from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse
from django.core.paginator import Paginator
from django.contrib.auth.decorators import login_required
from django.db import transaction
import openpyxl

@login_required
@transaction.atomic
def project_students_upload(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    students_qs = (
        ProjectStudent.objects
        .filter(project=project)
        .select_related("school")
        .only("id", "student_number", "full_name", "sex", "school__school_code")
    )

    paginator = Paginator(students_qs, 50)
    page_number = request.GET.get("page")
    students = paginator.get_page(page_number)

    # ======================================================
    # UPLOAD HANDLER
    # ======================================================
    if request.method == "POST" and request.FILES.get("file"):

        excel_file = request.FILES["file"]

        try:
            wb = openpyxl.load_workbook(excel_file, data_only=True)
            ws = wb.active
        except Exception:
            return JsonResponse({
                "status": "error",
                "message": "Invalid Excel file"
            })

        # --------------------------------------------------
        # READ HEADER
        # --------------------------------------------------
        header_row = [str(c.value).strip().lower() for c in ws[1]]

        required_columns = {
            "school_code",
            "student_number",
            "full_name",
            "sex",
            "combination",
        }

        if not required_columns.issubset(set(header_row)):
            return JsonResponse({
                "status": "error",
                "message": "Invalid Excel format"
            })

        col_index = {h: i for i, h in enumerate(header_row)}

        # --------------------------------------------------
        # FAST LOOKUP MAPS
        # --------------------------------------------------

        # 1️⃣ School map
        schools_map = {
            s.school_code: s.id
            for s in School.objects.only("id", "school_code")
        }

        # 2️⃣ Combination map (per academic level)
        combinations = (
            ExamCombination.objects
            .filter(academic_level=project.academic_level)
            .prefetch_related("papers")
        )

        combo_map = {
            c.code: {
                "id": c.id,
                "papers": list(c.papers.values_list("paper_id", flat=True))
            }
            for c in combinations
        }

        # 3️⃣ Existing students map
        existing_students = dict(
            ProjectStudent.objects
            .filter(project=project)
            .values_list("student_number", "id")
        )

        students_to_insert = []
        scores_to_insert = []

        # ======================================================
        # PASS 1: CREATE STUDENTS
        # ======================================================
        for row in ws.iter_rows(min_row=2, values_only=True):

            school_code = str(row[col_index["school_code"]]).strip()
            student_number = str(row[col_index["student_number"]]).strip()
            name = str(row[col_index["full_name"]]).strip()
            sex = str(row[col_index["sex"]]).strip()
            combo_code = str(row[col_index["combination"]]).strip()

            school_id = schools_map.get(school_code)
            combo_data = combo_map.get(combo_code)

            if not school_id or not combo_data:
                continue

            if student_number not in existing_students:
                students_to_insert.append(
                    ProjectStudent(
                        project=project,
                        school_id=school_id,
                        student_number=student_number,
                        full_name=name,
                        sex=sex,
                        combination_id=combo_data["id"],
                    )
                )

        with transaction.atomic():
            created_students = ProjectStudent.objects.bulk_create(
                students_to_insert,
                ignore_conflicts=True,
                batch_size=2000,
            )

        # --------------------------------------------------
        # REFRESH STUDENT MAP (INCLUDE NEW)
        # --------------------------------------------------
        all_students = dict(
            ProjectStudent.objects
            .filter(project=project)
            .values_list("student_number", "id")
        )

        # ======================================================
        # PASS 2: CREATE EXAM SCRIPTS
        # ======================================================
        for row in ws.iter_rows(min_row=2, values_only=True):

            student_number = str(row[col_index["student_number"]]).strip()
            combo_code = str(row[col_index["combination"]]).strip()

            student_id = all_students.get(student_number)
            combo_data = combo_map.get(combo_code)

            if not student_id or not combo_data:
                continue

            for paper_id in combo_data["papers"]:
                scores_to_insert.append(
                    ExamScore(
                        project_id=project_id,
                        student_id=student_id,
                        paper_id=paper_id,
                    )
                )

        with transaction.atomic():
            ExamScore.objects.bulk_create(
                scores_to_insert,
                ignore_conflicts=True,
                batch_size=5000,
            )

        return JsonResponse({
            "status": "success",
            "students_created": len(created_students),
            "scripts_created": len(scores_to_insert),
        })

    # ======================================================
    # PAGE LOAD
    # ======================================================
    return render(request, "exams/project_students_upload.html", {
        "project": project,
        "students": students,
    })







from django.contrib import messages
from django.shortcuts import get_object_or_404

@login_required
def project_process(request, pk):
    project = get_object_or_404(JointExamProject, pk=pk)

    # TODO: call processing engine here later
    messages.success(request, "Project processed successfully.")

    return redirect("exams:project_list")


from django.http import HttpResponse
from openpyxl import Workbook
from openpyxl.styles import Font


@login_required
def project_students_sample_excel(request, project_code):
    project = JointExamProject.objects.get(id=project_id)

    wb = Workbook()
    ws = wb.active
    ws.title = "Student Sample"


    # Headers
    headers = ["school_code", "student_number", "full_name", "sex"]

    # If A-Level include combination
    if project.academic_level.code.upper() == "A-LEVEL":
        headers.append("combination")

    ws.append(headers)

    # Make header bold
    for col in range(1, len(headers) + 1):
        ws.cell(row=1, column=col).font = Font(bold=True)

    # Example row
    example = ["S0001", "S0001-1001", "John Doe", "M"]

    if project.academic_level.code.upper() == "A-LEVEL":
        example.append("PCM")

    ws.append(example)

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="student_sample_{project.id}.xlsx"'

    wb.save(response)
    return response



@transaction.atomic
def bulk_register_student_papers(project, dataframe):

    # PRELOAD (FAST)
    students_map = {
        s.student_number: s.id
        for s in ProjectStudent.objects.filter(project=project)
        .only("id", "student_number")
    }

    papers_map = {
        p.paper_code: p.id
        for p in ExamSubjectPaper.objects.all()
        .only("id", "paper_code")
    }

    insert_list = []

    for _, row in dataframe.iterrows():

        student_id = students_map.get(str(row["student_number"]).strip())
        if not student_id:
            continue

        # Loop through all columns except first
        for col in dataframe.columns[1:]:

            paper_code = str(row[col]).strip()

            if paper_code and paper_code != "nan":

                paper_id = papers_map.get(paper_code)
                if not paper_id:
                    continue

                insert_list.append(
                    ExamScore(
                        project=project,
                        student_id=student_id,
                        paper_id=paper_id,
                        score=None
                    )
                )

    ExamScore.objects.bulk_create(
        insert_list,
        ignore_conflicts=True,
        batch_size=2000
    )





@login_required
def assign_student_subjects(request, project_code, student_id):
    project = JointExamProject.objects.get(id=project_id)
    student = ProjectStudent.objects.get(id=student_id)

    subjects = ExamSubject.objects.filter(
        academic_level=project.academic_level
    )

    if request.method == "POST":
        selected = request.POST.getlist("subjects")

        StudentSubject.objects.filter(student=student).delete()

        for sub_id in selected:
            StudentSubject.objects.create(
                project=project,
                student=student,
                subject_id=sub_id
            )

        return redirect("exams:project_students_upload", project_id=project.id)

    return render(request, "exams/assign_subjects.html", {
        "project": project,
        "student": student,
        "subjects": subjects
    })


@login_required
def delete_project_student(request, project_code, student_id):
    ProjectStudent.objects.filter(id=student_id).delete()
    return redirect("exams:project_students_upload", project_id=project_id)



@login_required
def get_student_papers_modal(request, project_code, student_id):

    project = JointExamProject.objects.get(id=project_id)
    student = ProjectStudent.objects.get(id=student_id)

    papers = ExamSubjectPaper.objects.select_related("subject").order_by(
        "subject__subject_code", "paper_code"
    )

    assigned = set(
        ExamScore.objects.filter(
            project=project,
            student=student
        ).values_list("paper_id", flat=True)
    )

    return render(request, "exams/ajax_student_papers_modal.html", {
        "student": student,
        "papers": papers,
        "assigned": assigned
    })



from django.db import transaction
from django.http import JsonResponse


@login_required
@transaction.atomic
def assign_student_papers_ajax(request, project_code, student_id):

    if request.method != "POST":
        return JsonResponse({"status": "error"}, status=400)

    project = JointExamProject.objects.get(id=project_id)
    student = ProjectStudent.objects.get(id=student_id)

    paper_ids = request.POST.getlist("papers[]")

    # Remove existing registrations
    ExamScore.objects.filter(
        project=project,
        student=student
    ).delete()

    insert_list = []

    for pid in paper_ids:
        insert_list.append(
            ExamScore(
                project=project,
                student=student,
                paper_id=pid,
                score=None
            )
        )

    ExamScore.objects.bulk_create(insert_list, batch_size=500)

    return JsonResponse({
        "status": "success",
        "count": len(insert_list)
    })




from openpyxl import Workbook
from django.http import HttpResponse

def subjectpaper_template_download(request):
    wb = Workbook()
    ws = wb.active
    ws.title = "Subject Papers"

    headers = ["subject_code", "paper_code", "paper_name", "max_marks"]
    ws.append(headers)

    ws.append(["031", "31_1", "Physics Theory", 100])
    ws.append(["031", "31_2", "Physics Practical", 50])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = "attachment; filename=subject_papers_template.xlsx"

    wb.save(response)
    return response


from django.contrib import messages
from django.shortcuts import redirect
from django.db import transaction
import openpyxl

@transaction.atomic
def subjectpaper_bulk_upload(request):

    if request.method != "POST":
        return redirect("exams:subjectpaper_list")

    file = request.FILES.get("excel_file")
    if not file:
        messages.error(request, "No file uploaded.")
        return redirect("exams:subjectpaper_list")

    # --------------------------------------------------
    # LOAD EXCEL
    # --------------------------------------------------
    try:
        wb = openpyxl.load_workbook(file, data_only=True)
        ws = wb.active
    except Exception:
        messages.error(request, "Invalid Excel file.")
        return redirect("exams:subjectpaper_list")

    # --------------------------------------------------
    # READ HEADER
    # --------------------------------------------------
    header = [str(c.value).strip().lower() for c in ws[1]]

    required_columns = {
        "subject_code",
        "paper_code",
        "paper_name",
        "max_marks",
    }

    if not required_columns.issubset(set(header)):
        messages.error(request, "Invalid Excel format.")
        return redirect("exams:subjectpaper_list")

    col = {h: i for i, h in enumerate(header)}

    # --------------------------------------------------
    # FAST SUBJECT MAP
    # --------------------------------------------------
    subjects_map = {
        s.subject_code: s.subject_id
        for s in ExamSubject.objects.only("subject_id", "subject_code")
    }

    insert_list = []
    skipped = 0

    # --------------------------------------------------
    # PROCESS ROWS
    # --------------------------------------------------
    for row in ws.iter_rows(min_row=2, values_only=True):

        subject_code = str(row[col["subject_code"]]).strip()
        subject_id = subjects_map.get(subject_code)

        if not subject_id:
            skipped += 1
            continue

        try:
            max_marks = float(row[col["max_marks"]])
        except (TypeError, ValueError):
            skipped += 1
            continue

        insert_list.append(
            ExamSubjectPaper(
                subject_id=subject_id,
                paper_code=str(row[col["paper_code"]]).strip(),
                paper_name=str(row[col["paper_name"]]).strip(),
                max_marks=max_marks,
            )
        )

    # --------------------------------------------------
    # BULK INSERT
    # --------------------------------------------------
    ExamSubjectPaper.objects.bulk_create(
        insert_list,
        ignore_conflicts=True,
        batch_size=1000,
    )

    messages.success(
        request,
        f"{len(insert_list)} papers uploaded successfully. {skipped} skipped."
    )

    return redirect("exams:subjectpaper_list")


@login_required
def subjectpaper_list(request):
    papers = ExamSubjectPaper.objects.select_related("subject").all()

    return render(request, "exams/subjectpaper_list.html", {
        "papers": papers
    })


@login_required
def get_bulk_papers_modal(request, project_code):

    project = JointExamProject.objects.get(id=project_id)

    papers = ExamSubjectPaper.objects.select_related("subject").order_by(
        "subject__subject_code", "paper_code"
    )

    return render(request, "exams/ajax_bulk_assign_modal.html", {
        "papers": papers
    })



from django.db import transaction
from django.http import JsonResponse


@login_required
@transaction.atomic
def bulk_assign_papers_ajax(request, project_code):

    if request.method != "POST":
        return JsonResponse({"status": "error"}, status=400)

    project = JointExamProject.objects.get(id=project_id)

    student_ids = request.POST.getlist("students[]")
    paper_ids = request.POST.getlist("papers[]")

    # Remove existing for selected students
    ExamScore.objects.filter(
        project=project,
        student_id__in=student_ids
    ).delete()

    insert_list = []

    for sid in student_ids:
        for pid in paper_ids:
            insert_list.append(
                ExamScore(
                    project=project,
                    student_id=sid,
                    paper_id=pid,
                    score=None
                )
            )

    ExamScore.objects.bulk_create(
        insert_list,
        batch_size=2000
    )

    return JsonResponse({
        "status": "success",
        "students": len(student_ids),
        "papers": len(paper_ids)
    })






from django.db import transaction
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404
import openpyxl

@login_required
@transaction.atomic
def bulk_student_paper_upload(request, project_code):

    if request.method != "POST":
        return JsonResponse({"status": "error"}, status=400)

    project = get_object_or_404(JointExamProject, project_code=project_code)

    file = request.FILES.get("file")
    if not file:
        return JsonResponse({
            "status": "error",
            "message": "No file uploaded"
        })

    # --------------------------------------------------
    # LOAD EXCEL
    # --------------------------------------------------
    try:
        wb = openpyxl.load_workbook(file, data_only=True)
        ws = wb.active
    except Exception:
        return JsonResponse({
            "status": "error",
            "message": "Invalid Excel file"
        })

    # --------------------------------------------------
    # READ HEADER
    # --------------------------------------------------
    header = [
        str(c.value).strip().lower()
        if c.value is not None else ""
        for c in ws[1]
    ]

    if "student_number" not in header:
        return JsonResponse({
            "status": "error",
            "message": "student_number column missing"
        })

    col = {h: i for i, h in enumerate(header)}

    # All columns except student_number are paper columns
    paper_columns = [h for h in header if h != "student_number"]

    # --------------------------------------------------
    # 1️⃣ BUILD FAST LOOKUP MAPS
    # --------------------------------------------------
    student_map = dict(
        ProjectStudent.objects
        .filter(project=project)
        .values_list("student_number", "id")
    )

    paper_map = dict(
        ExamSubjectPaper.objects
        .values_list("paper_code", "id")
    )

    # Existing (student, paper) pairs — VERY IMPORTANT
    existing_scores = set(
        ExamScore.objects
        .filter(project=project)
        .values_list("student_id", "paper_id")
    )

    # --------------------------------------------------
    # 2️⃣ PREPARE INSERT LIST
    # --------------------------------------------------
    insert_list = []
    skipped_students = 0
    skipped_papers = 0

    # --------------------------------------------------
    # 3️⃣ PROCESS EXCEL ROWS
    # --------------------------------------------------
    for row in ws.iter_rows(min_row=2, values_only=True):

        raw_student = row[col["student_number"]]

        if raw_student in (None, "", " "):
            skipped_students += 1
            continue

        student_number = str(raw_student).strip()
        student_id = student_map.get(student_number)

        if not student_id:
            skipped_students += 1
            continue

        for paper_col in paper_columns:

            raw_value = row[col[paper_col]]

            if raw_value in (None, "", " "):
                continue

            paper_code = str(raw_value).strip()
            paper_id = paper_map.get(paper_code)

            if not paper_id:
                skipped_papers += 1
                continue

            key = (student_id, paper_id)

            # ❌ Skip if already exists
            if key in existing_scores:
                continue

            # ✅ Insert only NEW subject
            insert_list.append(
                ExamScore(
                    project_id=project.id,
                    student_id=student_id,
                    paper_id=paper_id,
                    score=None
                )
            )

            # Prevent duplicates within same upload
            existing_scores.add(key)

    # --------------------------------------------------
    # 4️⃣ BULK INSERT (NO DELETES!)
    # --------------------------------------------------
    if insert_list:
        ExamScore.objects.bulk_create(
            insert_list,
            batch_size=5000
        )

    return JsonResponse({
        "status": "success",
        "registered": len(insert_list),
        "skipped_students": skipped_students,
        "skipped_papers": skipped_papers
    })







@login_required
def project_papers_upload(request, project_code):

    project = JointExamProject.objects.get(id=project_id)

    search_query = request.GET.get("q", "").strip()
    
    # Get students with related data
    students = ProjectStudent.objects.filter(
        project=project
    ).select_related("school").prefetch_related(
        "examscore_set__paper__subject"
    )
    
    # 🔍 Search filter
    if search_query:
        students = students.filter(
            Q(student_number__icontains=search_query) |
            Q(full_name__icontains=search_query) |
            Q(school__name__icontains=search_query) |
            Q(school__school_code__icontains=search_query)
        )
    
    # 📄 Pagination - 50 students per page
    paginator = Paginator(students, 50)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    
    return render(request, "exams/project_papers_upload.html", {
        "project": project,
        "page_obj": page_obj,
        "search_query": search_query
    })



from django.core.paginator import Paginator
from django.db.models import Count, Q, F, FloatField, ExpressionWrapper
from django.db.models.functions import Coalesce

@login_required
def project_marks_subjects(request, project_code):

    project = JointExamProject.objects.get(id=project_id)

    search_query = request.GET.get("q", "").strip()

    papers = (
        ExamSubjectPaper.objects
        .filter(examscore__project_id=project_id)   # 🔥 only papers with data
        .select_related("subject")
        .distinct()
        .annotate(
            total_scripts=Count(
                "examscore",
                filter=Q(examscore__project_id=project_id),
                distinct=True
            ),
            filled_scores=Count(
                "examscore",
                filter=Q(
                    examscore__project_id=project_id,
                    examscore__score__isnull=False
                ),
                distinct=True
            )
        )
    )

    # 🔍 Search filter
    if search_query:
        papers = papers.filter(
            Q(subject__subject_name_eng__icontains=search_query) |
            Q(subject__subject_code__icontains=search_query) |
            Q(paper_code__icontains=search_query)
        )

    # 📊 Completion %
    papers = papers.annotate(
        completion_percent=ExpressionWrapper(
            100.0 * F("filled_scores") /
            Coalesce(F("total_scripts"), 1),
            output_field=FloatField()
        )
    ).order_by("subject__subject_code", "paper_code")

    # 📄 Pagination
    paginator = Paginator(papers, 25)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "exams/project_marks_subjects.html", {
        "project": project,
        "page_obj": page_obj,
        "search_query": search_query
    })


 


from django.core.paginator import Paginator
from django.db.models import Count, Q, F, FloatField, ExpressionWrapper
from django.db.models.functions import Coalesce

@login_required
def project_marks_schools(request, project_code, paper_id):

    project = JointExamProject.objects.get(id=project_id)
    paper = ExamSubjectPaper.objects.select_related("subject").get(id=paper_id)

    search_query = request.GET.get("q", "").strip()

    schools = (
        School.objects
        .filter(
            projectstudent__project_id=project_id,
            projectstudent__examscore__paper_id=paper_id
        )
        .distinct()
        .annotate(
            total_scripts=Count(
                "projectstudent__examscore",
                filter=Q(
                    projectstudent__project_id=project_id,
                    projectstudent__examscore__paper_id=paper_id
                ),
                distinct=True
            ),
            entered_marks=Count(
                "projectstudent__examscore",
                filter=Q(
                    projectstudent__project_id=project_id,
                    projectstudent__examscore__paper_id=paper_id,
                    projectstudent__examscore__score__isnull=False
                ),
                distinct=True
            )
        )
    )

    # 🔍 SEARCH
    if search_query:
        schools = schools.filter(
            Q(name__icontains=search_query) |
            Q(school_code__icontains=search_query)
        )

    # 📊 Progress %
    schools = schools.annotate(
        progress_percent=ExpressionWrapper(
            100.0 * F("entered_marks") /
            Coalesce(F("total_scripts"), 1),
            output_field=FloatField()
        )
    ).order_by("name")

    # 📄 PAGINATION
    paginator = Paginator(schools, 25)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)

    return render(request, "exams/project_marks_schools.html", {
        "project": project,
        "paper": paper,
        "page_obj": page_obj,
        "search_query": search_query
    })




@login_required
def project_marks_capture_page(request, project_code, paper_id, school_id):

    project = JointExamProject.objects.get(id=project_id)
    paper = ExamSubjectPaper.objects.get(id=paper_id)

    scores = ExamScore.objects.filter(
        project=project,
        paper=paper,
        student__school_id=school_id
    ).select_related("student")

    return render(request, "exams/project_marks_capture.html", {
        "project": project,
        "paper": paper,
        "scores": scores
    })



import json
from django.utils.timezone import now

from django.db.models import F, Value, Case, When
from django.db import models  # Add this import

@login_required
@transaction.atomic
def save_marks_ajax(request, project_code):

    if request.method != "POST":
        return JsonResponse({"status": "error"}, status=400)

    data = json.loads(request.POST.get("data", "[]"))

    user = request.user

    update_ids = []

    for item in data:

        score_value = item.get("score", "").strip()

        if score_value == "":
            continue  # skip blank fields

        score_id = item["id"]
        update_ids.append(score_id)

        # Update using Case/When to conditionally set updated_by
        ExamScore.objects.filter(
            id=score_id,
            project_id=project_id
        ).update(
            score=score_value,
            score_status=1,
            updated_at=now(),
            updated_by=Case(
                When(updated_by__isnull=True, then=Value(user.id)),
                default=F('updated_by'),
                output_field=models.IntegerField()  # Now works with models import
            )
        )

    return JsonResponse({
        "status": "success",
        "updated_count": len(update_ids)
    })



@login_required
@transaction.atomic
def bulk_register_by_combination(request, project_code):

    if request.method != "POST":
        return JsonResponse({"status": "error"}, status=400)

    project = JointExamProject.objects.get(id=project_id)
    file = request.FILES.get("file")

    df = pd.read_excel(file)

    # -------------------------------------------------
    # 1️⃣ FAST LOOKUP MAPS
    # -------------------------------------------------

    # student_number → student_id
    student_map = dict(
        ProjectStudent.objects.filter(project=project)
        .values_list("student_number", "id")
    )

    # combination_code → combination_id
    combination_map = dict(
        ExamCombination.objects.values_list("code", "id")
    )

    # combination_id → list of paper_ids
    combo_papers = {}

    for combo_id, paper_id in CombinationPaper.objects.values_list(
        "combination_id", "paper_id"
    ):
        combo_papers.setdefault(combo_id, []).append(paper_id)

    insert_list = []
    skipped_students = 0
    skipped_combinations = 0

    for _, row in df.iterrows():

        student_number = str(row["student_number"]).strip()
        combo_code = str(row["combination"]).strip()

        student_id = student_map.get(student_number)
        if not student_id:
            skipped_students += 1
            continue

        combo_id = combination_map.get(combo_code)
        if not combo_id:
            skipped_combinations += 1
            continue

        paper_ids = combo_papers.get(combo_id, [])

        for paper_id in paper_ids:
            insert_list.append(
                ExamScore(
                    project_id=project.id,
                    student_id=student_id,
                    paper_id=paper_id,
                    score=None
                )
            )

    # Delete only affected students
    affected_student_ids = list(
        set([i.student_id for i in insert_list])
    )

    ExamScore.objects.filter(
        project=project,
        student_id__in=affected_student_ids
    ).delete()

    ExamScore.objects.bulk_create(
        insert_list,
        batch_size=5000
    )

    return JsonResponse({
        "status": "success",
        "registered": len(insert_list),
        "skipped_students": skipped_students,
        "skipped_combinations": skipped_combinations
    })


from django.db.models import Count


from django.db.models import Count, Prefetch

@login_required
def combination_list(request):

    combinations = (
        ExamCombination.objects
        .select_related("academic_level")
        .prefetch_related(
            Prefetch(
                "papers",
                queryset=CombinationPaper.objects
                .select_related("paper__subject")
                .order_by("paper__subject__subject_code")
            )
        )
        .annotate(
            papers_count=Count("papers", distinct=True)
        )
        .order_by("code")
    )

    levels = AcademicLevel.objects.all().order_by("name")

    return render(request, "exams/combination_list.html", {
        "combinations": combinations,
        "levels": levels,          # ✅ THIS WAS MISSING
    })




@login_required
@require_POST
def combination_add(request):
    code = request.POST.get("code", "").strip()
    name = request.POST.get("name", "").strip()
    category = request.POST.get("combination_category", "").strip()
    academic_level_id = request.POST.get("academic_level")

    # ------------------
    # VALIDATION
    # ------------------
    if not all([code, name, category, academic_level_id]):
        messages.error(request, "All fields are required.")
        return redirect("exams:combination_list")

    if ExamCombination.objects.filter(code=code).exists():
        messages.error(request, f"Combination {code} already exists.")
        return redirect("exams:combination_list")

    academic_level = AcademicLevel.objects.filter(id=academic_level_id).first()
    if not academic_level:
        messages.error(request, "Invalid academic level.")
        return redirect("exams:combination_list")

    # ------------------
    # SAVE
    # ------------------
    ExamCombination.objects.create(
        code=code,
        name=name,
        combination_category=category,
        academic_level=academic_level
    )

    messages.success(request, "Combination added successfully.")
    return redirect("exams:combination_list")




@login_required
@transaction.atomic
def combination_bulk_upload(request):

    if request.method != "POST":
        return redirect("exams:combination_list")

    file = request.FILES.get("excel_file")

    if not file:
        messages.error(request, "No file uploaded.")
        return redirect("exams:combination_list")

    df = pd.read_excel(file)

    required_columns = {"code","name", "combination_category", "academic_level_code"}

    if not required_columns.issubset(df.columns):
        messages.error(request, "Invalid Excel format.")
        return redirect("exams:combination_list")

    # Fast map academic level
    level_map = dict(
        AcademicLevel.objects.values_list("code", "id")
    )

    insert_list = []
    skipped = 0

    for _, row in df.iterrows():

        code = str(row["code"]).strip()
        name = str(row["name"]).strip()
        combination_category = str(row["combination_category"]).strip()
        level_code = str(row["academic_level_code"]).strip()

        level_id = level_map.get(level_code)

        if not level_id:
            skipped += 1
            continue

        insert_list.append(
            ExamCombination(
                code=code,
                name=name,
                combination_category=combination_category,
                academic_level_id=level_id
            )
        )

    ExamCombination.objects.bulk_create(
        insert_list,
        ignore_conflicts=True,
        batch_size=1000
    )

    messages.success(
        request,
        f"{len(insert_list)} combinations uploaded. {skipped} skipped."
    )

    return redirect("exams:combination_list")




@login_required
def combination_papers_manage(request, combination_id):

    combination = ExamCombination.objects.get(id=combination_id)

    all_papers = ExamSubjectPaper.objects.select_related(
        "subject"
    ).order_by("subject__subject_code", "paper_code")

    assigned = set(
        CombinationPaper.objects.filter(
            combination=combination
        ).values_list("paper_id", flat=True)
    )

    return render(request, "exams/combination_papers_manage.html", {
        "combination": combination,
        "papers": all_papers,
        "assigned": assigned
    })



@login_required
@transaction.atomic
def combination_papers_save(request, combination_id):

    combination = ExamCombination.objects.get(id=combination_id)

    selected_papers = request.POST.getlist("papers")

    # Clear existing
    CombinationPaper.objects.filter(
        combination=combination
    ).delete()

    insert_list = []

    for paper_id in selected_papers:
        insert_list.append(
            CombinationPaper(
                combination_id=combination_id,
                paper_id=paper_id
            )
        )

    CombinationPaper.objects.bulk_create(insert_list)

    messages.success(request, "Combination papers updated.")

    return redirect("exams:combination_list")



@login_required
def combination_papers_modal(request, combination_id):

    combination = ExamCombination.objects.get(id=combination_id)

    papers = ExamSubjectPaper.objects.select_related(
        "subject"
    ).order_by("subject__subject_code", "paper_code")

    assigned = set(
        CombinationPaper.objects.filter(
            combination=combination
        ).values_list("paper_id", flat=True)
    )

    return render(request, "exams/ajax_combination_papers_modal.html", {
        "combination": combination,
        "papers": papers,
        "assigned": assigned
    })


from django.http import JsonResponse
from django.db import transaction

@login_required
@transaction.atomic
def combination_papers_save_ajax(request, combination_id):

    if request.method != "POST":
        return JsonResponse({"status": "error"}, status=400)

    paper_ids = request.POST.getlist("papers[]")

    # Clear existing
    CombinationPaper.objects.filter(
        combination_id=combination_id
    ).delete()

    # Insert new
    insert_list = [
        CombinationPaper(
            combination_id=combination_id,
            paper_id=pid
        )
        for pid in paper_ids
    ]

    CombinationPaper.objects.bulk_create(insert_list)

    return JsonResponse({
        "status": "success",
        "count": len(insert_list)
    })





from django.db import transaction
from django.contrib import messages
from django.shortcuts import redirect, render, get_object_or_404
from django.contrib.auth.decorators import login_required
import openpyxl

@login_required
@transaction.atomic
def scripts_by_combination_upload(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    if request.method != "POST":
        return render(request, "exams/scripts_by_combination.html", {
            "project": project
        })

    file = request.FILES.get("excel_file")

    if not file:
        messages.error(request, "No file uploaded.")
        return redirect(
            "exams:scripts_by_combination_upload",
            project_id=project_id
        )

    # --------------------------------------------------
    # LOAD EXCEL
    # --------------------------------------------------
    try:
        wb = openpyxl.load_workbook(file, data_only=True)
        ws = wb.active
    except Exception:
        messages.error(request, "Invalid Excel file.")
        return redirect(
            "exams:scripts_by_combination_upload",
            project_id=project_id
        )

    # --------------------------------------------------
    # READ HEADER
    # --------------------------------------------------
    header = [str(c.value).strip().lower() for c in ws[1]]

    required_columns = {"student_number", "combination"}

    if not required_columns.issubset(set(header)):
        messages.error(request, "Invalid Excel format.")
        return redirect(
            "exams:scripts_by_combination_upload",
            project_id=project_id
        )

    col = {h: i for i, h in enumerate(header)}

    # --------------------------------------------------
    # FAST LOOKUP MAPS
    # --------------------------------------------------

    # 1️⃣ Students in this project
    students_map = dict(
        ProjectStudent.objects
        .filter(project=project)
        .values_list("student_number", "id")
    )

    # 2️⃣ Combination → Papers map
    combo_map = {
        c.code: list(
            c.papers.values_list("paper_id", flat=True)
        )
        for c in ExamCombination.objects.prefetch_related("papers")
    }

    insert_list = []
    skipped = 0

    # --------------------------------------------------
    # PROCESS ROWS
    # --------------------------------------------------
    for row in ws.iter_rows(min_row=2, values_only=True):

        student_number = str(row[col["student_number"]]).strip()
        combo_code = str(row[col["combination"]]).strip()

        student_id = students_map.get(student_number)
        paper_ids = combo_map.get(combo_code)

        if not student_id or not paper_ids:
            skipped += 1
            continue

        for pid in paper_ids:
            insert_list.append(
                ExamScore(
                    project_id=project_id,
                    student_id=student_id,
                    paper_id=pid
                )
            )

    # --------------------------------------------------
    # BULK INSERT
    # --------------------------------------------------
    ExamScore.objects.bulk_create(
        insert_list,
        ignore_conflicts=True,
        batch_size=5000
    )

    messages.success(
        request,
        f"{len(insert_list)} scripts registered. {skipped} skipped."
    )

    return redirect(
        "exams:scripts_by_combination_upload",
        project_id=project_id
    )




from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
from reportlab.lib.units import inch
from reportlab.lib.pagesizes import A4
from django.http import HttpResponse
from django.utils.timezone import now
import math

@login_required
def project_school_subject_print(request, project_code, paper_id, school_id):

    project = JointExamProject.objects.get(id=project_id)
    paper = ExamSubjectPaper.objects.select_related("subject").get(id=paper_id)
    school = School.objects.get(id=school_id)

    scores = list(
        ExamScore.objects
        .filter(
            project_id=project_id,
            paper_id=paper_id,
            student__school_id=school_id
        )
        .select_related("student", "updated_by")  # Added updated_by to select_related
        .order_by("student__student_number")
    )

    # Layout settings
    maxPerCol = 30
    cols = 4
    maxPerPage = maxPerCol * cols
    total = len(scores)
    pages = math.ceil(total / maxPerPage)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="{school.school_code}_{paper.paper_code}.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        topMargin=60,
        bottomMargin=30,
        leftMargin=25,
        rightMargin=25
    )

    elements = []

    styles = getSampleStyleSheet()

    for page in range(pages):

        start = page * maxPerPage
        end = start + maxPerPage
        page_scores = scores[start:end]

        # Get unique usernames for this page
        entered_by_users = set()
        for score in page_scores:
            if score.updated_by:
                entered_by_users.add(score.updated_by.username)
        
        entered_by_text = ", ".join(entered_by_users) if entered_by_users else "Not entered"

        # Header
        PAGE_WIDTH = A4[0] - doc.leftMargin - doc.rightMargin

        site = SiteSetting.get_active()

        import re

        title_text = (site.header_text or site.site_name or "")
        title_text = re.sub(r"<br\s*/?>", "\n", title_text)  # normalize breaks
        title_text = title_text.upper()

        elements.append(
            RoundedHeader(
                title=title_text,
                subtitle=f"{project.name.upper()} - {project.year}",
                width=PAGE_WIDTH
            )

        )

        elements.append(Spacer(1, 10))

        elements.append(
            Paragraph("<b>SUBJECT SCORES</b>", styles["Heading2"])
        )
        elements.append(Spacer(1, 6))
        elements.append(
            Paragraph(
                f"<b>SCHOOL:</b> {school.school_code} - {school.name}",
                styles["Normal"]
            )
        )
        elements.append(
            Paragraph(
                f"<b>SUBJECT:</b> {paper.paper_code} - {paper.paper_name}",
                styles["Normal"]
            )
        )
        elements.append(Spacer(1, 10))

        # Prepare empty grid
        data = []

        # Column header row
        header_row = []
        for c in range(cols):
            header_row.extend(["STUDENT NO", "SCORE"])
        data.append(header_row)

        # Fill rows downward (30 per column)
        for row_index in range(maxPerCol):

            row_data = []

            for col_index in range(cols):

                index = col_index * maxPerCol + row_index

                if index < len(page_scores):
                    s = page_scores[index]
                    cand = s.student.student_number
                    score = s.score if s.score is not None else ""
                else:
                    cand = ""
                    score = ""

                row_data.extend([cand, score])

            data.append(row_data)

        table = Table(data, colWidths=[1.2*inch, 0.6*inch]*cols)

        table.setStyle(TableStyle([
            ("GRID", (0,0), (-1,-1), 0.5, colors.black),
            ("BACKGROUND", (0,0), (-1,0), colors.lightgrey),
            ("ALIGN", (1,1), (-1,-1), "CENTER"),
            ("VALIGN", (0,0), (-1,-1), "MIDDLE"),
        ]))

        elements.append(table)
        elements.append(Spacer(1, 12))

        # Updated footer with actual usernames
        elements.append(
            Paragraph(
                f"<b>ENTERED BY:</b> {entered_by_text}",
                styles["Normal"]
            )
        )
        
        elements.append(Spacer(1, 4))
        
        elements.append(
            Paragraph(
                f"<i>Generated on {now().strftime('%Y-%m-%d %H:%M:%S')}</i>",
                styles["Italic"]
            )
        )

        elements.append(
            Paragraph(
                f"Page {page+1} of {pages}",
                styles["Normal"]
            )
        )

        if page < pages - 1:
            elements.append(PageBreak())

    doc.build(elements)

    return response










@login_required
def project_subjects_all_schools_print(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    papers = (
        ExamSubjectPaper.objects
        .select_related("subject")
        .filter(subject__academic_level=project.academic_level)
        .order_by("subject__subject_code", "paper_code")
    )

    schools = School.objects.order_by("school_code")

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="{project.name}_ALL_SUBJECTS.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        topMargin=60,
        bottomMargin=30,
        leftMargin=25,
        rightMargin=25
    )

    elements = []
    styles = getSampleStyleSheet()

    maxPerCol = 30
    cols = 4
    maxPerPage = maxPerCol * cols
    PAGE_WIDTH = A4[0] - doc.leftMargin - doc.rightMargin

    # ============================
    # LOOP SUBJECTS
    # ============================
    for paper in papers:

        # Subject title page (optional but clean)
        # elements.append(
        #     RoundedHeader(
        #         title="PRIME MINISTER'S OFFICE-RALG",
        #         subtitle=f"{project.name.upper()} — {paper.paper_name.upper()}",
        #         width=PAGE_WIDTH
        #     )
        # )
        # elements.append(Spacer(1, 20))

        # ============================
        # LOOP SCHOOLS
        # ============================
        for school in schools:

            scores = list(
                ExamScore.objects
                .filter(
                    project=project,
                    paper=paper,
                    student__school=school
                )
                .select_related("student")
                .order_by("student__student_number")
            )

            if not scores:
                continue

            total = len(scores)
            pages = math.ceil(total / maxPerPage)

            for page in range(pages):

                start = page * maxPerPage
                end = start + maxPerPage
                page_scores = scores[start:end]

                # ===== Header =====
                elements.append(
                    RoundedHeader(
                        title="PRIME MINISTER'S OFFICE-RALG",
                        subtitle=project.name.upper(),
                        width=PAGE_WIDTH
                    )
                )
                elements.append(Spacer(1, 8))

                elements.append(
                    Paragraph("<b>SUBJECT SCORES</b>", styles["Heading2"])
                )
                elements.append(
                    Paragraph(
                        f"<b>SCHOOL:</b> {school.school_code} - {school.name}",
                        styles["Normal"]
                    )
                )
                elements.append(
                    Paragraph(
                        f"<b>SUBJECT:</b> {paper.paper_code} - {paper.paper_name}",
                        styles["Normal"]
                    )
                )
                elements.append(Spacer(1, 8))

                # ===== Table =====
                data = []
                header_row = []
                for _ in range(cols):
                    header_row.extend(["STUDENT NO", "SCORE"])
                data.append(header_row)

                for row_index in range(maxPerCol):
                    row_data = []
                    for col_index in range(cols):
                        idx = col_index * maxPerCol + row_index
                        if idx < len(page_scores):
                            s = page_scores[idx]
                            row_data.extend([
                                s.student.student_number,
                                s.score if s.score is not None else ""
                            ])
                        else:
                            row_data.extend(["", ""])
                    data.append(row_data)

                table = Table(data, colWidths=[1.2 * inch, 0.6 * inch] * cols)
                table.setStyle(TableStyle([
                    ("GRID", (0, 0), (-1, -1), 0.5, colors.black),
                    ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
                    ("ALIGN", (1, 1), (-1, -1), "CENTER"),
                ]))

                elements.append(table)
                elements.append(Spacer(1, 10))

                # ===== Footer =====
                elements.append(
                    Paragraph(
                        f"ENTERED BY: ____________________    ",
                        # f"{now().strftime('%Y-%m-%d %H:%M:%S')}",
                        styles["Normal"]
                    )
                )

                elements.append(
                    Paragraph(
                        f"Page {page + 1} of {pages}",
                        styles["Normal"]
                    )
                )

                elements.append(PageBreak())

    doc.build(elements)
    return response


@login_required
def project_subject_all_schools_print(request, project_code, paper_id):

    project = get_object_or_404(JointExamProject, project_code=project_code)
    paper = get_object_or_404(
        ExamSubjectPaper.objects.select_related("subject"),
        id=paper_id
    )

    schools = School.objects.order_by("school_code")

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="{paper.paper_code}_ALL_SCHOOLS.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=A4,
        topMargin=60,
        bottomMargin=30,
        leftMargin=25,
        rightMargin=25
    )

    elements = []
    styles = getSampleStyleSheet()

    # Layout constants
    maxPerCol = 30
    cols = 4
    maxPerPage = maxPerCol * cols
    PAGE_WIDTH = A4[0] - doc.leftMargin - doc.rightMargin

    # ============================
    # LOOP SCHOOLS
    # ============================
    for school in schools:

        scores = list(
            ExamScore.objects
            .filter(
                project=project,
                paper=paper,
                student__school=school
            )
            .select_related("student", "updated_by")
            .order_by("student__student_number")
        )

        if not scores:
            continue

        total = len(scores)
        pages = math.ceil(total / maxPerPage)

        for page in range(pages):

            start = page * maxPerPage
            end = start + maxPerPage
            page_scores = scores[start:end]

            # ===== Header =====
            elements.append(
                RoundedHeader(
                    title="PRIME MINISTER'S OFFICE-RALG",
                    subtitle=f"{project.name.upper()}",
                    width=PAGE_WIDTH
                )
            )
            elements.append(Spacer(1, 8))

            elements.append(
                Paragraph("<b>SUBJECT SCORES</b>", styles["Heading2"])
            )
            elements.append(
                Paragraph(
                    f"<b>SCHOOL:</b> {school.school_code} - {school.name}",
                    styles["Normal"]
                )
            )
            elements.append(
                Paragraph(
                    f"<b>SUBJECT:</b> {paper.paper_code} - {paper.paper_name}",
                    styles["Normal"]
                )
            )
            elements.append(Spacer(1, 8))

            # ===== Table =====
            data = []
            header_row = []
            for _ in range(cols):
                header_row.extend(["STUDENT NO", "SCORE"])
            data.append(header_row)

            for row_index in range(maxPerCol):
                row_data = []
                for col_index in range(cols):
                    idx = col_index * maxPerCol + row_index
                    if idx < len(page_scores):
                        s = page_scores[idx]
                        row_data.extend([
                            s.student.student_number,
                            s.score if s.score is not None else ""
                        ])
                    else:
                        row_data.extend(["", ""])
                data.append(row_data)

            table = Table(data, colWidths=[1.2 * inch, 0.6 * inch] * cols)
            table.setStyle(TableStyle([
                ("GRID", (0, 0), (-1, -1), 0.5, colors.black),
                ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
                ("ALIGN", (1, 1), (-1, -1), "CENTER"),
            ]))

            elements.append(table)
            elements.append(Spacer(1, 10))

            # ===== Footer with ENTERED BY username =====
            # Get unique usernames who entered scores for this paper/school
            entered_by_users = set()
            for score in page_scores:
                if score.updated_by:
                    entered_by_users.add(score.updated_by.username)
            
            entered_by_text = ", ".join(entered_by_users) if entered_by_users else "________________"
            
            elements.append(
                Paragraph(
                    f"<b>ENTERED BY:</b> {entered_by_text}",
                    styles["Normal"]
                )
            )
            
            elements.append(Spacer(1, 4))
            
            elements.append(
                Paragraph(
                    f"<i>Generated on {now().strftime('%Y-%m-%d %H:%M:%S')}</i>",
                    styles["Italic"]
                )
            )

            elements.append(
                Paragraph(
                    f"Page {page + 1} of {pages}",
                    styles["Normal"]
                )
            )

            elements.append(PageBreak())

    doc.build(elements)
    return response







@login_required
def subject_paper_registration_summary_pdf(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    # All papers in this project
    papers = (
        ExamSubjectPaper.objects
        .filter(
            examscore__project_id=project_id   # ✅ at least one registration
        )
        .select_related("subject")
        .annotate(reg_count=Count("examscore"))
        .filter(reg_count__gt=0)
        .order_by("paper_code")
        .distinct()
    )

    schools = (
        School.objects
        .filter(
            projectstudent__examscore__project_id=project_id
        )
        .annotate(
            reg_count=Count("projectstudent__examscore")
        )
        .filter(reg_count__gt=0)
        .order_by("school_code")
        .distinct()
    )

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        'inline; filename="SUBJECT_PAPER_REGISTRATIONS.pdf"'
    )

    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A4),   # ✅ LANDSCAPE
        topMargin=40,
        bottomMargin=25,
        leftMargin=25,
        rightMargin=25
    )


    styles = getSampleStyleSheet()
    elements = []

    # =======================
    # TITLE BLOCK
    # =======================
    elements.append(Paragraph("<b>PMO-RALG</b>", styles["Title"]))
    elements.append(Paragraph("<b>FORM SIX EXAMS RESULTS</b>", styles["Title"]))
    elements.append(Paragraph("<b>SUBJECT PAPER REGISTRATIONS</b>", styles["Title"]))
    elements.append(Spacer(1, 15))

    # =======================
    # TABLE HEADER
    # =======================
    header = [
        Paragraph("<b>SCHOOL_CODE</b>", styles["Normal"]),
        Paragraph("<b>NAME</b>", styles["Normal"]),
    ]

    for p in papers:
        header.append(
            VerticalText(
                f"{p.paper_code}-{p.paper_name}",
                width=25,
                height=90
            )
        )

    data = [header]

    # =======================
    # SCHOOL ROWS
    # =======================
    totals = [0] * len(papers)

    for school in schools:

        row = [
            school.school_code,
            Paragraph(
                (school.name_short or school.name or "").strip(),
                wrap_style
            ),
        ]


        for i, paper in enumerate(papers):
            count = ExamScore.objects.filter(
                project=project,
                paper=paper,
                student__school=school
            ).count()

            row.append(count if count else "")
            totals[i] += count

        data.append(row)

    # =======================
    # TOTAL ROW
    # =======================
    total_row = ["Total", ""]
    total_row.extend(totals)
    data.append(total_row)

    # =======================
    # TABLE
    # =======================
    col_widths = [60, 110] + [25] * len(papers)

    table = Table(data, colWidths=col_widths, repeatRows=1)

    table.setStyle(TableStyle([
        ("GRID", (0, 0), (-1, -1), 0.75, colors.black),
        ("ALIGN", (2, 1), (-1, -1), "CENTER"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        ("BACKGROUND", (0, 0), (-1, 0), colors.whitesmoke),
        ("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
    ]))

    elements.append(table)

    doc.build(elements)
    return response
# @login_required
# @transaction.atomic
# def process_subject_scores(request, project_code, subject_id):

#     papers = ExamSubjectPaper.objects.filter(subject_id=subject_id)

#     students = ProjectStudent.objects.filter(
#         examscore__project_id=project_id,
#         examscore__paper__in=papers
#     ).distinct()

#     for student in students:

#         scores = ExamScore.objects.filter(
#             project_id=project_id,
#             student=student,
#             paper__in=papers,
#             score_status=1
#         )

#         total = 0
#         for s in scores:
#             if s.score.upper() == "X":
#                 total = None
#                 break
#             total += float(s.score)

#         if total is None:
#             continue

#         max_total = sum(p.max_marks for p in papers)
#         percentage = (total / max_total) * 100

#         ProcessedSubjectScore.objects.update_or_create(
#             project_id=project_id,
#             student=student,
#             subject_id=subject_id,
#             defaults={
#                 "raw_total": total,
#                 "max_total": max_total,
#                 "percentage_score": percentage,
#                 "processed_by": request.user
#             }
#         )




















from decimal import Decimal, InvalidOperation
from django.contrib.auth.decorators import login_required
from django.db import transaction
from django.http import JsonResponse
from django.shortcuts import get_object_or_404, render
from openpyxl import load_workbook
from django.core.paginator import Paginator
from django.db.models import Q


# exams/views.py
@login_required
def upload_final_processed_scores(request, project_code):
    """
    Unified upload handler that can:
    1. Upload students (mode='students') - simple student import
    2. Upload scores (mode='scores') - process subject scores with grading
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)

    # =====================================================
    # POST → UPLOAD & PROCESS
    # =====================================================
    if request.method == "POST":
        file = request.FILES.get("excel_file")
        if not file:
            return JsonResponse({"ok": False, "error": "No file uploaded"}, status=400)

        upload_mode = request.POST.get("upload_mode", "scores")  # 'students' or 'scores'
        
        try:
            wb = load_workbook(file, data_only=True)
            ws = wb.active
            
            # Route to appropriate handler
            if upload_mode == "students":
                return handle_student_upload(ws, project, request)
            else:
                return handle_score_upload(ws, project, request)
                
        except Exception as e:
            return JsonResponse(
                {"ok": False, "error": str(e)},
                status=500
            )

    # =====================================================
    # GET → DISPLAY PAGE
    # =====================================================
    return render_upload_page(request, project)
def handle_student_upload(ws, project, request):
    """
    Handle student data upload - fast batch import
    No score processing, just student records
    """
    try:
        # Read header
        header_row = [str(cell.value).strip().lower() if cell.value else '' for cell in ws[1]]
        
        # Simple required columns - no combination needed
        required_columns = {"school_code", "student_number", "full_name", "sex"}
        missing_columns = required_columns - set(header_row)
        
        if missing_columns:
            return JsonResponse({
                "ok": False,
                "error": f"Missing required columns: {', '.join(missing_columns)}"
            }, status=400)
        
        col_index = {h: i for i, h in enumerate(header_row)}
        
        # FIX: Don't use only() with select_related - either use only() with all fields
        # or remove only() completely for better performance with select_related
        schools_map = {
            s.school_code: s
            for s in School.objects.select_related('region', 'district', 'ward').all()
            # Removed .only() to avoid conflict with select_related
        }
        
        existing_students = dict(
            ProjectStudent.objects
            .filter(project=project)
            .values_list("student_number", "id")
        )
        
        # Process in batches
        students_to_create = []
        created_count = 0
        updated_count = 0
        skipped_count = 0
        errors = []
        uploaded_student_numbers = set()
        
        rows_list = list(ws.iter_rows(min_row=2, values_only=True))
        total_rows = len(rows_list)
        
        for row_idx, row in enumerate(rows_list, start=2):
            try:
                # Extract values
                school_code = str(row[col_index["school_code"]]).strip() if row[col_index["school_code"]] else ""
                student_number = str(row[col_index["student_number"]]).strip() if row[col_index["student_number"]] else ""
                full_name = str(row[col_index["full_name"]]).strip() if row[col_index["full_name"]] else ""
                sex = str(row[col_index["sex"]]).strip().upper() if row[col_index["sex"]] else ""
                
                # Validate
                if not school_code:
                    errors.append(f"Row {row_idx}: Missing school_code")
                    skipped_count += 1
                    continue
                    
                if not student_number:
                    errors.append(f"Row {row_idx}: Missing student_number")
                    skipped_count += 1
                    continue
                    
                if not full_name:
                    errors.append(f"Row {row_idx}: Missing full_name")
                    skipped_count += 1
                    continue
                    
                if sex not in ['M', 'F']:
                    errors.append(f"Row {row_idx}: Invalid sex '{sex}'")
                    skipped_count += 1
                    continue
                
                # Get school
                school = schools_map.get(school_code)
                if not school:
                    errors.append(f"Row {row_idx}: School '{school_code}' not found")
                    skipped_count += 1
                    continue
                
                # Check duplicate in same upload
                if student_number in uploaded_student_numbers:
                    errors.append(f"Row {row_idx}: Duplicate student_number in upload")
                    skipped_count += 1
                    continue
                
                # Update or create
                if student_number in existing_students:
                    # Update existing - using direct update for better performance
                    ProjectStudent.objects.filter(
                        id=existing_students[student_number]
                    ).update(
                        school=school,
                        full_name=full_name,
                        sex=sex
                    )
                    updated_count += 1
                else:
                    # Create new
                    students_to_create.append(
                        ProjectStudent(
                            project=project,
                            school=school,
                            student_number=student_number,
                            full_name=full_name,
                            sex=sex,
                        )
                    )
                    created_count += 1
                    uploaded_student_numbers.add(student_number)
                    
            except Exception as e:
                errors.append(f"Row {row_idx}: {str(e)}")
                skipped_count += 1
        
        # Bulk create new students
        if students_to_create:
            batch_size = 1000
            for i in range(0, len(students_to_create), batch_size):
                batch = students_to_create[i:i+batch_size]
                ProjectStudent.objects.bulk_create(batch, batch_size=len(batch))
        
        return JsonResponse({
            "ok": True,
            "mode": "students",
            "total_rows": total_rows,
            "created": created_count,
            "updated": updated_count,
            "skipped": skipped_count,
            "total_processed": created_count + updated_count,
            "errors": errors[:20]
        })
        
    except Exception as e:
        return JsonResponse({"ok": False, "error": str(e)}, status=500)







def handle_score_upload(ws, project, request):
    """
    Ultra-fast subject score upload with grading
    Handles 20,000+ students with 7+ subjects efficiently
    Stores 'X' marks and provides detailed error reporting
    """
    import time
    start_time = time.time()
    
    try:
        HEADER_ROW = 1
        DATA_START_ROW = 3
        STUDENT_COL = 2
        FIRST_SUBJECT_COL = 6
        
        # =====================================================
        # 1. FAST LOOKUPS WITH DICTIONARIES
        # =====================================================
        print("Loading data structures...")
        
        # Student map
        students_map = dict(
            ProjectStudent.objects
            .filter(project_id=project.id)
            .values_list("student_number", "id")
        )
        print(f"Loaded {len(students_map)} students")
        
        # Subject map - store both code and ID
        subjects_map = {}
        subjects_info = {}  # Store subject_code and subject_name
        for subj in ExamSubject.objects.values('subject_id', 'subject_code', 'subject_name_eng'):
            subjects_map[subj['subject_code']] = subj['subject_id']
            subjects_info[subj['subject_id']] = {
                'code': subj['subject_code'],
                'name': subj['subject_name_eng']
            }
        
        # Grading rules (cached)
        grading_rules = list(
            GradingRule.objects
            .filter(academic_level=project.academic_level)
            .order_by("-min_score")
        )
        
        # Pre-calculate grade lookup for speed (0-100)
        grade_lookup = {}
        points_lookup = {}
        for score in range(0, 101):
            for rule in grading_rules:
                if rule.min_score <= score <= rule.max_score:
                    grade_lookup[score] = rule.grade
                    points_lookup[score] = int(rule.points * rule.weight)
                    break
            else:
                grade_lookup[score] = None
                points_lookup[score] = None
        
        # =====================================================
        # 2. PARSE SUBJECT COLUMNS
        # =====================================================
        subject_columns = {}  # col -> subject_id
        subject_column_info = {}  # col -> (subject_code, subject_name)
        invalid_subject_cols = []
        
        for col in range(FIRST_SUBJECT_COL, ws.max_column + 1):
            raw = ws.cell(row=HEADER_ROW, column=col).value
            if not raw:
                continue
            
            subject_code = str(raw).split("/")[0].strip()
            subject_id = subjects_map.get(subject_code)
            
            if subject_id:
                subject_columns[col] = subject_id
                subject_column_info[col] = {
                    'code': subject_code,
                    'name': subjects_info.get(subject_id, {}).get('name', subject_code)
                }
            else:
                invalid_subject_cols.append(
                    f"Column {col}: '{subject_code}' - invalid subject"
                )
        
        if not subject_columns:
            return JsonResponse({
                "ok": False,
                "error": "No valid subject codes found"
            }, status=400)
        
        print(f"Found {len(subject_columns)} subject columns")
        
        # =====================================================
        # 3. GET ALL EXISTING SCORES IN ONE QUERY
        # =====================================================
        print("Fetching existing scores...")
        
        existing_scores_map = {}
        existing_scores_qs = ProcessedSubjectScore.objects.filter(
            project_id=project.id
        ).values_list('student_id', 'subject_id', 'id', 'percentage_score')
        
        for student_id, subject_id, score_id, percentage_score in existing_scores_qs.iterator(chunk_size=10000):
            existing_scores_map[(student_id, subject_id)] = {
                'id': score_id,
                'percentage_score': percentage_score
            }
        
        print(f"Loaded {len(existing_scores_map)} existing scores")
        
        # =====================================================
        # 4. PROCESS ROWS IN BATCHES
        # =====================================================
        total_rows = max(ws.max_row - DATA_START_ROW + 1, 0)
        print(f"Processing {total_rows} rows...")
        
        # Progress tracking
        progress, _ = UploadProgress.objects.update_or_create(
            project=project,
            user=request.user,
            defaults={
                "total_rows": total_rows,
                "processed_rows": 0,
                "status": "processing",
            }
        )
        
        # Batch processing
        BATCH_SIZE = 5000
        scores_to_create = []
        scores_to_update = []
        x_marks_to_create = []  # For X marks that don't exist yet
        x_marks_to_update = []  # For X marks that need to be updated
        
        skipped_rows = []
        processed_rows_count = 0
        created_count = 0
        updated_count = 0
        x_created_count = 0
        x_updated_count = 0
        deleted_count = 0
        x_count = 0
        
        # Process in batches for memory efficiency
        rows_list = list(ws.iter_rows(min_row=DATA_START_ROW, values_only=True))
        
        for batch_start in range(0, len(rows_list), BATCH_SIZE):
            batch_end = min(batch_start + BATCH_SIZE, len(rows_list))
            batch_rows = rows_list[batch_start:batch_end]
            
            batch_create = []
            batch_update = []
            batch_x_create = []
            batch_x_update = []
            
            for row_idx, row in enumerate(batch_rows, start=batch_start + DATA_START_ROW):
                student_number = row[STUDENT_COL - 1]  # Adjust for 0-based index
                
                # Skip empty rows
                if not student_number or str(student_number).strip() == "":
                    continue
                
                student_number = str(student_number).strip()
                student_id = students_map.get(student_number)
                
                if not student_id:
                    skipped_rows.append(f"Row {row_idx}: Student '{student_number}' not found")
                    continue
                
                # Process each subject column
                row_has_data = False
                row_errors = []
                
                for col, subject_id in subject_columns.items():
                    cell = row[col - 1]  # Adjust for 0-based index
                    
                    # Get subject info for error messages
                    subject_info = subject_column_info[col]
                    subject_display = f"{subject_info['code']}-{subject_info['name']}"
                    
                    # Skip empty cells
                    if cell is None or (isinstance(cell, str) and cell.strip() == ""):
                        continue
                    
                    row_has_data = True
                    cell_str = str(cell).strip().upper()
                    
                    score_key = (student_id, subject_id)
                    existing = existing_scores_map.get(score_key)
                    
                    # Handle X (Absent)
                    if cell_str == "X":
                        x_count += 1
                        
                        if existing:
                            # Update existing score to X
                            if existing['percentage_score'] != 'X':
                                batch_x_update.append({
                                    'id': existing['id'],
                                    'percentage_score': 'X',
                                    'grade': None,
                                    'points': None,
                                    'processed_by_id': request.user.id
                                })
                                x_updated_count += 1
                        else:
                            # Create new X mark
                            batch_x_create.append(
                                ProcessedSubjectScore(
                                    project_id=project.id,
                                    student_id=student_id,
                                    subject_id=subject_id,
                                    percentage_score='X',
                                    grade=None,
                                    points=None,
                                    processed_by=request.user
                                )
                            )
                            x_created_count += 1
                        continue
                    
                    # Handle numeric scores
                    try:
                        score_value = float(cell_str)
                        if score_value < 0 or score_value > 100:
                            row_errors.append(f"{subject_display}={score_value} (out of range)")
                            continue
                        
                        score_int = int(round(score_value))
                        grade = grade_lookup.get(score_int)
                        points = points_lookup.get(score_int)
                        
                        if grade is None:
                            row_errors.append(f"{subject_display}={score_value} (no grade rule)")
                            continue
                        
                        if existing:
                            # Update existing numeric score
                            if existing['percentage_score'] != str(score_value) or existing.get('grade') != grade:
                                batch_update.append({
                                    'id': existing['id'],
                                    'percentage_score': score_value,
                                    'grade': grade,
                                    'points': points,
                                    'processed_by_id': request.user.id
                                })
                                updated_count += 1
                        else:
                            # Create new numeric score
                            batch_create.append(
                                ProcessedSubjectScore(
                                    project_id=project.id,
                                    student_id=student_id,
                                    subject_id=subject_id,
                                    percentage_score=score_value,
                                    grade=grade,
                                    points=points,
                                    processed_by=request.user
                                )
                            )
                            created_count += 1
                            
                    except (ValueError, TypeError):
                        row_errors.append(f"{subject_display}='{cell_str}' (invalid format)")
                        continue
                
                # Track errors
                if row_errors:
                    skipped_rows.append(f"Row {row_idx} ({student_number}): {', '.join(row_errors)}")
                
                if row_has_data:
                    processed_rows_count += 1
                    
                    # Update progress every 1000 rows
                    if processed_rows_count % 1000 == 0:
                        UploadProgress.objects.filter(id=progress.id).update(
                            processed_rows=processed_rows_count
                        )
            
            # =====================================================
            # 5. BULK DATABASE OPERATIONS
            # =====================================================
            
            # Create new numeric scores
            if batch_create:
                ProcessedSubjectScore.objects.bulk_create(
                    batch_create,
                    batch_size=1000,
                    ignore_conflicts=False
                )
                batch_create.clear()
            
            # Update existing numeric scores
            if batch_update:
                update_objs = []
                for update_data in batch_update:
                    obj = ProcessedSubjectScore(id=update_data['id'])
                    obj.percentage_score = update_data['percentage_score']
                    obj.grade = update_data['grade']
                    obj.points = update_data['points']
                    obj.processed_by_id = update_data['processed_by_id']
                    update_objs.append(obj)
                
                ProcessedSubjectScore.objects.bulk_update(
                    update_objs,
                    ['percentage_score', 'grade', 'points', 'processed_by', 'processed_at'],
                    batch_size=1000
                )
                batch_update.clear()
            
            # Create new X marks
            if batch_x_create:
                ProcessedSubjectScore.objects.bulk_create(
                    batch_x_create,
                    batch_size=1000,
                    ignore_conflicts=False
                )
                batch_x_create.clear()
            
            # Update existing scores to X
            if batch_x_update:
                update_objs = []
                for update_data in batch_x_update:
                    obj = ProcessedSubjectScore(id=update_data['id'])
                    obj.percentage_score = update_data['percentage_score']
                    obj.grade = update_data['grade']
                    obj.points = update_data['points']
                    obj.processed_by_id = update_data['processed_by_id']
                    update_objs.append(obj)
                
                ProcessedSubjectScore.objects.bulk_update(
                    update_objs,
                    ['percentage_score', 'grade', 'points', 'processed_by', 'processed_at'],
                    batch_size=1000
                )
                batch_x_update.clear()
        
        # =====================================================
        # 6. UPDATE PROGRESS
        # =====================================================
        elapsed_time = time.time() - start_time
        
        total_scores_processed = created_count + updated_count + x_created_count + x_updated_count
        
        report_lines = []
        report_lines.append(f"⏱️ Processing time: {elapsed_time:.2f} seconds")
        report_lines.append(f"📊 Total rows processed: {processed_rows_count}")
        report_lines.append(f"✅ Numeric scores created: {created_count}")
        report_lines.append(f"🔄 Numeric scores updated: {updated_count}")
        report_lines.append(f"✖️ X marks created: {x_created_count}")
        report_lines.append(f"📝 X marks updated: {x_updated_count}")
        report_lines.append(f"📊 Total X marks processed: {x_count}")
        report_lines.append(f"⚠️ Skipped rows: {len(skipped_rows)}")
        
        if invalid_subject_cols:
            report_lines.append(f"\n📋 Invalid subject columns:")
            report_lines.extend([f"   - {col}" for col in invalid_subject_cols[:10]])
            if len(invalid_subject_cols) > 10:
                report_lines.append(f"   ... and {len(invalid_subject_cols) - 10} more")
        
        if skipped_rows:
            report_lines.append(f"\n⚠️ Detailed errors (first 20):")
            for err in skipped_rows[:20]:
                report_lines.append(f"   {err}")
            if len(skipped_rows) > 20:
                report_lines.append(f"   ... and {len(skipped_rows) - 20} more errors")
        
        UploadProgress.objects.filter(id=progress.id).update(
            status="done",
            report="\n".join(report_lines)
        )
        
        print(f"Upload completed in {elapsed_time:.2f} seconds")
        
        # Format errors with proper subject names for display
        formatted_errors = []
        for err in skipped_rows[:50]:
            formatted_errors.append(err)
        
        return JsonResponse({
            "ok": True,
            "mode": "scores",
            "created": created_count,
            "updated": updated_count,
            "x_created": x_created_count,
            "x_updated": x_updated_count,
            "x_count": x_count,
            "skipped": len(skipped_rows),
            "processed": processed_rows_count,
            "total_scores": total_scores_processed,
            "errors": formatted_errors,
            "invalid_columns": invalid_subject_cols[:20],
            "processing_time": f"{elapsed_time:.2f}",
            "message": f"Processed {processed_rows_count} rows in {elapsed_time:.2f}s. Created {created_count} scores, updated {updated_count} scores, processed {x_count} X marks."
        })
        
    except Exception as e:
        import traceback
        error_details = traceback.format_exc()
        print(error_details)
        
        UploadProgress.objects.filter(
            project=project,
            user=request.user
        ).update(
            status="error",
            report=f"Error: {str(e)}\n\n{error_details[:500]}"
        )
        
        return JsonResponse({
            "ok": False,
            "error": str(e),
            "details": error_details[:500] if request.user.is_superuser else None
        }, status=500)

        
def render_upload_page(request, project):
    """
    Render the upload page with appropriate data based on mode
    """
    # Get current mode from URL or default to 'students'
    mode = request.GET.get("mode", "students")
    
    if mode == "scores":
        # Score upload mode - show processed scores
        search_query = request.GET.get("q", "").strip()
        
        scores_qs = (
            ProcessedSubjectScore.objects
            .filter(project_id=project.id)
            .select_related("student", "subject")
        )
        
        if search_query:
            scores_qs = scores_qs.filter(
                Q(student__student_number__icontains=search_query) |
                Q(subject__subject_code__icontains=search_query)
            )
        
        scores_qs = scores_qs.order_by("id")
        paginator = Paginator(scores_qs, 100)
        page_obj = paginator.get_page(request.GET.get("page"))
        
        table = {}
        subjects = []
        
        for s in page_obj:
            subjects.append(s.subject.subject_code)
            table.setdefault(s.student, {})[s.subject.subject_code] = s.percentage_score
        
        progress = UploadProgress.objects.filter(
            project=project,
            user=request.user
        ).order_by("-id").first()
        
        return render(request, "exams/final_processed_scores_upload.html", {
            "project": project,
            "mode": "scores",
            "table": table,
            "subjects": sorted(set(subjects)),
            "page_obj": page_obj,
            "search_query": search_query,
            "report": progress.report if progress else ""
        })
    
    else:
        # Student upload mode - show student list
        students_qs = (
            ProjectStudent.objects
            .filter(project=project)
            .select_related("school")
            .only("id", "student_number", "full_name", "sex", "school__school_code", "school__name")
            .order_by("student_number")
        )
        
        paginator = Paginator(students_qs, 50)
        page_number = request.GET.get("page")
        students = paginator.get_page(page_number)
        
        # Statistics
        total_students = students_qs.count()
        schools_count = students_qs.values('school').distinct().count()
        male_count = students_qs.filter(sex='M').count()
        female_count = students_qs.filter(sex='F').count()
        
        return render(request, "exams/final_processed_scores_upload.html", {
            "project": project,
            "mode": "students",
            "students": students,
            "total_students": total_students,
            "schools_count": schools_count,
            "male_count": male_count,
            "female_count": female_count,
        })



from openpyxl import Workbook
from django.http import HttpResponse

@login_required
def final_scores_template_download(request, project_code):

    wb = Workbook()
    ws = wb.active
    ws.title = "Final Scores"

    # Row 1 → Subject Codes
    headers = [
        "Centre",
        "student_number",
        "student_name",
        "sex",
        "combination",
        "65", "63", "141",
        "112/1", "112/2",
        "131/1", "131/2", "131/3"
    ]

    for col, value in enumerate(headers, start=1):
        ws.cell(row=1, column=col, value=value)

    # Row 2 → Descriptions (ignored by system)
    ws.append([
        "", "", "", "", "",
        "HISTORIA TANZANIA NA MAADILI",
        "ACADEMIC COMMUNICATION",
        "BASIC APPLIED MATHS",
        "HISTORY 1", "HISTORY 2",
        "PHYSICS 1", "PHYSICS 2", "PHYSICS PRACTICAL"
    ])

    # Example row
    ws.append([
        "S0702", "S0702-0501", "STUDENT NAME", "F", "HGK",
        66, 62, "", 65, 61, "", "", ""
    ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        'attachment; filename="final_processed_scores_template.xlsx"'
    )

    wb.save(response)
    return response


# @login_required
# def upload_progress_status(request, project_code):

#     progress = UploadProgress.objects.filter(
#         project_id=project_id,
#         user=request.user
#     ).order_by("-id").first()

#     if not progress:
#         return JsonResponse({"percent": 0})

#     percent = int(
#         (progress.processed_rows / progress.total_rows) * 100
#     ) if progress.total_rows else 0

#     return JsonResponse({
#         "percent": percent,
#         "processed": progress.processed_rows,
#         "total": progress.total_rows,
#         "status": progress.status
#     })


@login_required
def upload_progress_status(request, project_code):
    """
    Get upload progress status using project_code
    """
    # First get the project by code to get the id
    from .models import JointExamProject
    
    try:
        project = JointExamProject.objects.get(project_code=project_code)
        project_id = project.id
    except JointExamProject.DoesNotExist:
        return JsonResponse({
            "processed": 0,
            "total": 0,
            "status": "error",
            "report": "Project not found"
        }, status=404)

    progress = (
        UploadProgress.objects
        .filter(
            project_id=project_id,
            user=request.user
        )
        .order_by("-id")
        .first()
    )

    if not progress:
        return JsonResponse({
            "processed": 0,
            "total": 0,
            "status": "idle",
            "report": ""
        })

    percent = int(
        (progress.processed_rows / progress.total_rows) * 100
    ) if progress.total_rows else 0

    return JsonResponse({
        "processed": progress.processed_rows,
        "total": progress.total_rows,
        "percent": percent,
        "status": progress.status,
        "report": progress.report or ""
    })



@login_required
def update_scores_page(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    qs = (
        ExamScore.objects
        .filter(project_id=project_id)
        .select_related("student", "paper", "paper__subject")
        .only(
            "id",
            "score",
            "student__student_number",
            "paper__paper_code",
            "paper__subject__subject_name_eng",
        )
        .order_by("student__student_number")
    )

    q = request.GET.get("q", "").strip()
    if q:
        qs = qs.filter(
            Q(student__student_number__icontains=q) |
            Q(paper__paper_code__icontains=q)
        )

    paginator = Paginator(qs, 50)   # 🔥 50 rows per page
    page_obj = paginator.get_page(request.GET.get("page"))

    return render(request, "exams/update_scores.html", {
        "project": project,
        "page_obj": page_obj,
        "search_query": q
    })





from decimal import Decimal, InvalidOperation
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required
from openpyxl import load_workbook

@login_required
def bulk_update_scores(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    if request.method != "POST":
        return JsonResponse({"error": "Invalid request"}, status=400)

    file = request.FILES.get("excel_file")
    if not file:
        return JsonResponse({"error": "No file uploaded"}, status=400)

    wb = load_workbook(file, data_only=True)
    ws = wb.active

    # ----------------------------------
    # READ HEADER (ROW 2)
    # ----------------------------------
    header_row = 2
    headers = {
        str(cell.value).strip().lower(): idx
        for idx, cell in enumerate(ws[header_row], start=1)
        if cell.value
    }

    required = {"student_number", "paper_code", "score"}
    if not required.issubset(headers):
        return JsonResponse({
            "error": "Template must contain student_number, paper_code, score"
        }, status=400)

    STUDENT_COL = headers["student_number"]
    PAPER_COL = headers["paper_code"]
    SCORE_COL = headers["score"]

    DATA_START_ROW = header_row + 1
    TOTAL_ROWS = ws.max_row - DATA_START_ROW + 1

    # ----------------------------------
    # INIT PROGRESS
    # ----------------------------------
    progress, _ = UploadProgress.objects.update_or_create(
        project=project,
        user=request.user,
        defaults={
            "total_rows": TOTAL_ROWS,
            "processed_rows": 0,
            "status": "processing"
        }
    )

    # ----------------------------------
    # FAST LOOKUPS
    # ----------------------------------
    students_map = dict(
        ProjectStudent.objects
        .filter(project_id=project_id)
        .values_list("student_number", "id")
    )

    papers_map = dict(
        ExamSubjectPaper.objects
        .values_list("paper_code", "id")
    )

    scores_map = {
        (s.student_id, s.paper_id): s
        for s in ExamScore.objects
        .filter(project_id=project_id)
        .only("id", "student_id", "paper_id", "score")
    }

    to_update = []
    skipped = 0
    processed = 0

    # ----------------------------------
    # PROCESS ROWS
    # ----------------------------------
    for row in range(DATA_START_ROW, ws.max_row + 1):

        student_number = ws.cell(row=row, column=STUDENT_COL).value
        paper_code = ws.cell(row=row, column=PAPER_COL).value
        raw_score = ws.cell(row=row, column=SCORE_COL).value

        if not student_number or not paper_code:
            skipped += 1
            continue

        student_id = students_map.get(str(student_number).strip())
        paper_id = papers_map.get(str(paper_code).strip())

        if not student_id or not paper_id:
            skipped += 1
            continue

        score_obj = scores_map.get((student_id, paper_id))
        if not score_obj:
            skipped += 1
            continue

        from decimal import Decimal, InvalidOperation

        # ---------------- SCORE LOGIC ----------------
        raw = str(raw_score).strip() if raw_score is not None else ""

        # ABSENT
        if raw.upper() == "X":
            score_obj.score = "X"
            score_obj.score_status = 2   # ABSENT

        # NUMERIC (INTEGER OR DECIMAL)
        else:
            try:
                # validate numeric (but DO NOT convert)
                value = Decimal(raw)
            except (InvalidOperation, TypeError):
                skipped += 1
                continue

            if value < 0 or value > 100:
                skipped += 1
                continue

            # store EXACT text as entered (e.g. 56.5)
            score_obj.score = raw
            score_obj.score_status = 1   # UPDATED

        # AUDIT
        score_obj.updated_by_id = request.user.id
        to_update.append(score_obj)

        # ---------------- PROGRESS ----------------
        processed += 1
        UploadProgress.objects.filter(id=progress.id).update(
            processed_rows=processed
        )

    # ----------------------------------
    # SAVE
    # ----------------------------------
    UploadProgress.objects.filter(id=progress.id).update(status="saving")

    ExamScore.objects.bulk_update(
        to_update,
        ["score", "updated_by", "score_status", "updated_at"],
        batch_size=5000
    )

    UploadProgress.objects.filter(id=progress.id).update(status="done")

    return JsonResponse({
        "ok": True,
        "updated": len(to_update),
        "skipped": skipped
    })




from decimal import Decimal, InvalidOperation
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST


@login_required
@require_POST
def update_single_score(request, score_id):
    """
    Update a single student's score (AJAX).
    Accepts: 0–100 (integer/decimal) OR X/x for ABSENT
    """

    score_obj = get_object_or_404(ExamScore, id=score_id)

    raw = request.POST.get("score", "").strip()

    if not raw:
        return JsonResponse(
            {"ok": False, "error": "Score is required"},
            status=400
        )

    # ---------------- ABSENT ----------------
    if raw.upper() == "X":
        score_obj.score = "X"
        score_obj.score_status = 2   # ABSENT

    # ---------------- NUMERIC ----------------
    else:
        try:
            value = Decimal(raw)
        except InvalidOperation:
            return JsonResponse(
                {"ok": False, "error": "Invalid score value"},
                status=400
            )

        if value < 0 or value > 100:
            return JsonResponse(
                {"ok": False, "error": "Score must be between 0 and 100"},
                status=400
            )

        # store EXACT text (no rounding)
        score_obj.score = raw
        score_obj.score_status = 1   # UPDATED

    # ---------------- AUDIT ----------------
    score_obj.updated_by = request.user
    score_obj.save(
        update_fields=[
            "score",
            "updated_by",
            "score_status",
            "updated_at",
        ]
    )

    return JsonResponse({
        "ok": True,
        "score": score_obj.score,
        "status": score_obj.score_status
    })



from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from openpyxl import Workbook

@login_required
def update_scores_template(request, project_code):
    """
    Download Excel template for updating existing scores.
    """

    wb = Workbook()
    ws = wb.active
    ws.title = "Update Scores"

    # Header row
    ws.append([
        "student_number",
        "paper_code",
        "score"
    ])

    # Optional note row
    ws.append([
        "e.g. S0123-0456",
        "e.g. 112/1",
        "0 - 100 (decimals allowed)"
    ])

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = (
        f'attachment; filename="update_scores_template.xlsx"'
    )

    wb.save(response)
    return response



from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
from django.shortcuts import get_object_or_404
from openpyxl import Workbook
from openpyxl.styles import Font, Border, Side, Alignment


@login_required
def download_paper_registrations(request, project_code, paper_id):

    project = get_object_or_404(JointExamProject, project_code=project_code)
    paper = get_object_or_404(ExamSubjectPaper, id=paper_id)

    registrations = (
        ExamScore.objects
        .filter(project_id=project_id, paper_id=paper_id)
        .select_related("student", "student__school")
        .only(
            "student__student_number",
            "student__school__school_code",
            "student__school__name",
        )
        .order_by("student__student_number")
    )


    # ---------------- EXCEL SETUP ----------------
    wb = Workbook()
    ws = wb.active
    ws.title = safe_sheet_title(paper.paper_code)

    # Fonts & styles
    title_font = Font(name="Arial", size=14, bold=True)
    header_font = Font(name="Arial", bold=True)
    normal_font = Font(name="Arial")

    thin = Side(style="thin")
    border = Border(left=thin, right=thin, top=thin, bottom=thin)

    center = Alignment(horizontal="center", vertical="center")

    # ---------------- TITLE ROW ----------------
    ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=6)
    title_cell = ws.cell(row=1, column=1)
    title_cell.value = project.name
    title_cell.font = title_font
    title_cell.alignment = center

    # ---------------- HEADER ROW ----------------
    headers = [
        "school_code",
        "name",
        "paper_code",
        "paper_name",
        "student_number",
        "score",
    ]

    for col, header in enumerate(headers, start=1):
        cell = ws.cell(row=2, column=col)
        cell.value = header
        cell.font = header_font
        cell.border = border
        cell.alignment = center

    # ---------------- DATA ROWS ----------------
    row_num = 3
    for reg in registrations.iterator(chunk_size=5000):

        row_data = [
            reg.student.school.school_code,
            reg.student.school.name,          # ✅ REAL SCHOOL NAME
            paper.paper_code,
            paper.paper_name,
            reg.student.student_number,
            "",
        ]

        for col, value in enumerate(row_data, start=1):
            cell = ws.cell(row=row_num, column=col)
            cell.value = value
            cell.font = normal_font
            cell.border = border

        row_num += 1

    # ---------------- COLUMN WIDTHS ----------------
    widths = [15, 20, 15, 30, 20, 10]
    for i, w in enumerate(widths, start=1):
        ws.column_dimensions[chr(64 + i)].width = w

    # ---------------- FILE NAME ----------------
    safe_code = paper.paper_code.replace("/", "_")
    filename = f"{safe_code}-{paper.paper_name}-Template.xlsx"

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response




from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.views.decorators.http import require_POST
from django.db import transaction

@login_required
@require_POST
@transaction.atomic
def delete_all_processed_scores(request, project_code):
    """
    Completely delete all processed scores for a project.
    """

    project = get_object_or_404(JointExamProject, project_code=project_code)

    qs = ProcessedSubjectScore.objects.filter(project_id=project_id)
    deleted_count = qs.count()

    qs.delete()

    return JsonResponse({
        "ok": True,
        "deleted": deleted_count,
        "message": f"{deleted_count} records deleted successfully."
    })



@login_required
def rules_management(request):

    academic_levels = AcademicLevel.objects.all()

    level_id = request.GET.get("level")
    selected_level = None

    grading_rules = []
    division_rules = []
    gpa_rules = []
    subject_rules = []


    if level_id:
        selected_level = AcademicLevel.objects.get(id=level_id)

        grading_rules = GradingRule.objects.filter(
            academic_level=selected_level
        )

        division_rules = DivisionRule.objects.filter(
            academic_level=selected_level
        )

        gpa_rules = GpaRule.objects.filter(
            academic_level=selected_level
        )

        subject_rules = SubjectProcessingRule.objects.filter(
            academic_level=selected_level
        ).select_related("subject")

    return render(request, "exams/rules_management.html", {
        "academic_levels": academic_levels,
        "selected_level": selected_level,
        "grading_rules": grading_rules,
        "division_rules": division_rules,
        "gpa_rules": gpa_rules,
        "subject_rules": subject_rules,
    })


@require_POST
@login_required
def save_grading_rule(request):
    rule_id = request.POST.get("id")

    obj = GradingRule.objects.get(id=rule_id) if rule_id else GradingRule()

    obj.academic_level_id = request.POST["academic_level"]
    obj.min_score = request.POST["min_score"]
    obj.max_score = request.POST["max_score"]
    obj.grade = request.POST["grade"]
    obj.points = request.POST["points"]
    obj.status = request.POST["status"]
    obj.weight = request.POST.get("weight", 1)

    obj.save()
    return JsonResponse({"ok": True})



@require_POST
@login_required
def save_division_rule(request):
    rule_id = request.POST.get("id")

    obj = DivisionRule.objects.get(id=rule_id) if rule_id else DivisionRule()

    obj.academic_level_id = request.POST["academic_level"]
    obj.min_points = request.POST["min_points"]
    obj.max_points = request.POST["max_points"]
    obj.division = request.POST["division"]

    obj.save()
    return JsonResponse({"ok": True})

@require_POST
@login_required
def save_gpa_rule(request):
    rule_id = request.POST.get("id")

    # EDIT or CREATE
    obj = GpaRule.objects.get(id=rule_id) if rule_id else GpaRule()

    obj.academic_level_id = request.POST["academic_level"]
    obj.min_gpa = request.POST["min_gpa"]
    obj.max_gpa = request.POST["max_gpa"]
    obj.gpa_grade = request.POST["gpa_grade"]
    obj.gpa_points = request.POST["gpa_points"]
    obj.gpa_status = request.POST["gpa_status"]
    obj.gpa_colour = request.POST["gpa_colour"]

    obj.save()
    return JsonResponse({"ok": True})

@require_POST
@login_required
def save_subject_rule(request):
    rule_id = request.POST.get("id")

    obj = SubjectProcessingRule.objects.get(id=rule_id) if rule_id else SubjectProcessingRule()

    obj.academic_level_id = request.POST["academic_level"]
    obj.subject_id = request.POST["subject"]
    obj.total_max_marks = request.POST["total_max_marks"]
    obj.formula = request.POST["formula"]

    obj.save()
    return JsonResponse({"ok": True})



import csv
from django.http import StreamingHttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404

@login_required
def export_all_subject_scores_csv(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    # 🔥 Query optimized for large data
    qs = (
        ProcessedSubjectScore.objects
        .filter(project_id=project_id)
        .select_related(
            "student",
            "student__school",
            "subject",
        )
        .order_by("student__student_number")
        .iterator(chunk_size=10000)
    )

    # ---------------- CSV STREAMER ----------------
    class Echo:
        def write(self, value):
            return value

    pseudo_buffer = Echo()
    writer = csv.writer(pseudo_buffer)

    def stream():
        # Project title (first row)
        yield writer.writerow([project.name])
        yield writer.writerow([])

        # Header
        yield writer.writerow([
            "school_code",
            "school_name",
            "subject_code",
            "subject_name",
            "student_number",
            "score",
        ])

        # Data rows
        for s in qs:
            yield writer.writerow([
                s.student.school.school_code if s.student.school else "",
                s.student.school.name if s.student.school else "",
                s.subject.subject_code,
                s.subject.subject_name_eng,
                s.student.student_number,
                s.percentage_score,
            ])

    filename = f"ALL-SUBJECT-SCORES-{project.name}.csv"

    response = StreamingHttpResponse(
        stream(),
        content_type="text/csv"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    return response


import csv
from django.http import StreamingHttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404

@login_required
def export_exam_scores_csv(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    # 🔥 Optimized queryset (streaming-safe)
    qs = (
        ExamScore.objects
        .filter(project_id=project_id)
        .select_related(
            "student",
            "student__school",
            "paper",
            "paper__subject",
        )
        .order_by(
            "student__student_number",
            "paper__paper_code"
        )
        .iterator(chunk_size=10000)
    )

    # ---------------- CSV STREAMER ----------------
    class Echo:
        def write(self, value):
            return value

    pseudo_buffer = Echo()
    writer = csv.writer(pseudo_buffer)

    def stream():
        # 🔹 Project title
        yield writer.writerow([project.name])
        yield writer.writerow([])

        # 🔹 Header
        yield writer.writerow([
            "school_code",
            "school_name",
            "paper_code",
            "paper_name",
            "student_number",
            "score",
            "score_status",
            "updated_at",
        ])

        # 🔹 Data rows
        for s in qs:
            yield writer.writerow([
                s.student.school.school_code if s.student.school else "",
                s.student.school.name if s.student.school else "",
                s.paper.paper_code,
                s.paper.paper_name,
                s.student.student_number,
                s.score,
                s.score_status,
                s.updated_at.strftime("%Y-%m-%d %H:%M:%S") if s.updated_at else "",
            ])

    filename = f"EXAM-SCORES-{project.name}.csv"

    response = StreamingHttpResponse(
        stream(),
        content_type="text/csv"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    return response



import csv
from django.http import StreamingHttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404

@login_required
def export_processed_scores_pivot_csv(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    # -------------------------------------------------
    # 1️⃣ GET ALL SUBJECTS (COLUMN ORDER IS IMPORTANT)
    # -------------------------------------------------
    subjects = list(
        ExamSubject.objects
        .filter(processedsubjectscore__project_id=project_id)
        .distinct()
        .order_by("subject_code")
        .values("subject_id", "subject_code", "subject_name_eng")
    )

    subject_ids = [s["subject_id"] for s in subjects]

    # -------------------------------------------------
    # 2️⃣ STREAM ALL SCORES (ORDERED BY STUDENT)
    # -------------------------------------------------
    scores_qs = (
        ProcessedSubjectScore.objects
        .filter(project_id=project_id)
        .select_related("student", "student__school", "subject", "student__combination")
        .order_by("student__student_number")
        .iterator(chunk_size=10000)
    )

    # -------------------------------------------------
    # 3️⃣ CSV STREAMER
    # -------------------------------------------------
    class Echo:
        def write(self, value):
            return value

    pseudo_buffer = Echo()
    writer = csv.writer(pseudo_buffer)

    def stream():

        # 🔹 ROW 1: STUDENT DETAILS + SUBJECT CODES
        yield writer.writerow(
            ["STUDENT DETAILS", "", "", "", ""] +
            [s["subject_code"] for s in subjects]
        )

        # 🔹 ROW 2: FULL HEADERS
        yield writer.writerow(
            ["C.REG", "C/NO", "STUDENTS' NAME", "SEX", "COMB"] +
            [s["subject_name_eng"] for s in subjects]
        )

        current_student = None
        row = None
        subject_index = {sid: i for i, sid in enumerate(subject_ids)}

        # -------------------------------------------------
        # 4️⃣ BUILD ROW PER STUDENT (STREAMING)
        # -------------------------------------------------
        for s in scores_qs:

            student = s.student

            # 🔄 New student → flush previous row
            if current_student != student.id:
                if row:
                    yield writer.writerow(row)

                row = [
                    student.school.school_code if student.school else "",
                    student.student_number,
                    student.full_name,
                    student.sex,
                    student.combination.code if student.combination else "",
                ] + [""] * len(subjects)

                current_student = student.id

            # 🔹 Place score in correct subject column
            idx = subject_index.get(s.subject_id)
            if idx is not None:
                row[5 + idx] = s.percentage_score

        # 🔚 LAST STUDENT
        if row:
            yield writer.writerow(row)

    filename = f"FINAL-PROCESSED-SCORES-{project.name}.csv"

    response = StreamingHttpResponse(stream(), content_type="text/csv")
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    return response



import csv
from django.http import StreamingHttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404

@login_required
def export_exam_scores_pivot_csv(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    # -------------------------------------------------
    # 1️⃣ GET ALL PAPERS USED IN THIS PROJECT (COLUMNS)
    # -------------------------------------------------
    papers = list(
        ExamSubjectPaper.objects
        .filter(examscore__project_id=project_id)
        .distinct()
        .order_by("paper_code")
        .values(
            "id",
            "paper_code",
            "paper_name",
            "subject__subject_name_eng",
        )
    )

    paper_ids = [p["id"] for p in papers]

    # -------------------------------------------------
    # 2️⃣ STREAM ALL EXAM SCORES (ORDERED BY STUDENT)
    # -------------------------------------------------
    scores_qs = (
        ExamScore.objects
        .filter(project_id=project_id)
        .select_related(
            "student",
            "student__school",
            "student__combination",
            "paper",
        )
        .order_by(
            "student__student_number",
            "paper__paper_code",
        )
        .iterator(chunk_size=10000)
    )

    # -------------------------------------------------
    # 3️⃣ CSV STREAMER
    # -------------------------------------------------
    class Echo:
        def write(self, value):
            return value

    pseudo_buffer = Echo()
    writer = csv.writer(pseudo_buffer)

    def stream():

        # 🔹 ROW 1: STUDENT DETAILS + PAPER CODES
        yield writer.writerow(
            ["STUDENT DETAILS", "", "", "", ""] +
            [p["paper_code"] for p in papers]
        )

        # 🔹 ROW 2: FULL HEADERS
        yield writer.writerow(
            ["C.REG", "C/NO", "STUDENTS' NAME", "SEX", "COMB"] +
            [
                f'{p["subject__subject_name_eng"]} ({p["paper_code"]})'
                for p in papers
            ]
        )

        current_student_id = None
        row = None

        paper_index = {
            pid: idx for idx, pid in enumerate(paper_ids)
        }

        # -------------------------------------------------
        # 4️⃣ BUILD ONE ROW PER STUDENT
        # -------------------------------------------------
        for s in scores_qs:

            student = s.student

            # 🔄 New student → flush previous row
            if current_student_id != student.id:
                if row:
                    yield writer.writerow(row)

                row = [
                    student.school.school_code if student.school else "",
                    student.student_number,
                    student.full_name,
                    student.sex,
                    student.combination.code if student.combination else "",
                ] + [""] * len(papers)

                current_student_id = student.id

            # 🔹 Place score in correct paper column
            idx = paper_index.get(s.paper_id)
            if idx is not None:
                row[5 + idx] = s.score

        # 🔚 LAST STUDENT
        if row:
            yield writer.writerow(row)

    filename = f"EXAM-SCORES-PIVOT-{project.name}.csv"

    response = StreamingHttpResponse(stream(), content_type="text/csv")
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    return response











def validate_student_processing(project):
    return ProcessedSubjectScore.objects.filter(project=project).exists()


def validate_school_processing(project):
    return StudentFinalResult.objects.filter(project=project).exists()


def validate_subject_processing(project):
    return ProcessedSubjectScore.objects.filter(project=project).exists()







@login_required
def generate_student_final_results(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)
    level_id = project.academic_level_id

    print("\n GENERATE STUDENT FINAL RESULTS STARTED")
    print("PROJECT:", project)
    print("PROJECT ID:", project_code)

    # --------------------------------------------------
    # DISABLE FOREIGN KEY CHECKS
    # --------------------------------------------------
    from django.db import connection
    
    with connection.cursor() as cursor:
        cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")

    try:
        # ---------------- LOAD RULES ONCE ----------------
        grading_rules = list(
            GradingRule.objects
            .filter(academic_level=project.academic_level)
            .order_by("-min_score")
        )
        print(f"Grading rules loaded: {len(grading_rules)}")

        division_rules = list(
            DivisionRule.objects
            .filter(academic_level=project.academic_level)
            .order_by("min_points")
        )
        print(f"Division rules loaded: {len(division_rules)}")

        # ---------------- STUDENTS ----------------
        students = list(
            ProjectStudent.objects
            .filter(project=project)
            .select_related("school", "combination")
        )
        total_students = len(students)
        print(f"Total students: {total_students}")

        progress, _ = UploadProgress.objects.update_or_create(
            project=project,
            user=request.user,
            defaults={
                "total_rows": total_students,
                "processed_rows": 0,
                "status": "processing"
            }
        )

        # ---------------- SCORES GROUPED ----------------
        scores_qs = (
            ProcessedSubjectScore.objects
            .filter(project=project)  # Use project object directly
            .select_related("subject")
        )

        print(f"Total processed subject scores: {scores_qs.count()}")

        scores_map = {}
        for s in scores_qs:
            scores_map.setdefault(s.student_id, []).append(s)
        print(f"Students with scores: {len(scores_map)}")

        results_bulk = []
        processed = 0

        with transaction.atomic():

            for student in students:

                subjects = scores_map.get(student.id, [])

                # ---------------- CHECK IF STUDENT HAS ANY SUBJECTS ----------------
                if not subjects:
                    # NO SUBJECTS AT ALL → ABS
                    results_bulk.append(
                        StudentFinalResult(
                            project=project,
                            student=student,
                            std_average='',
                            std_averagegrade='',
                            total_points=0,
                            division='ABS',
                            full_results=''
                        )
                    )
                    processed += 1
                    if processed % 100 == 0:
                        UploadProgress.objects.filter(id=progress.id).update(
                            processed_rows=processed
                        )
                    continue

                from decimal import Decimal, InvalidOperation

                # ---------------- FULL RESULTS ----------------
                full_parts = []
                total_score = Decimal("0")
                counted = 0
                points_list = []
                principal_subjects_sat = 0  # Counter for principal subjects with valid scores
                has_any_numeric_score = False  # Flag to check if student has ANY numeric score

                for s in sorted(subjects, key=lambda x: x.subject.subject_code):

                    raw_score = s.percentage_score

                    # ---------------- ABSENT ----------------
                    if raw_score in (None, "", 'X'):
                        full_parts.append(f"{s.subject.subject_shortname}-'X'")
                        continue

                    # ---------------- SAFE NUMERIC ----------------
                    try:
                        score = Decimal(str(raw_score))
                    except (InvalidOperation, TypeError):
                        full_parts.append(f"{s.subject.subject_shortname}-'X'")
                        continue

                    # ---------------- RANGE SAFETY ----------------
                    if score < 0 or score > 100:
                        full_parts.append(f"{s.subject.subject_shortname}-'X'")
                        continue

                    # ---------------- VALID NUMERIC SUBJECT ----------------
                    has_any_numeric_score = True
                    full_parts.append(f"{s.subject.subject_shortname}-'{s.grade or 'X'}'")

                    total_score += score
                    counted += 1

                    # Count principal subjects with valid scores
                    if s.subject.is_principal:
                        principal_subjects_sat += 1
                        if s.points is not None:
                            try:
                                points_list.append(int(s.points))
                            except (TypeError, ValueError):
                                pass

                # ---------------- CHECK STUDENT STATUS ----------------
                # Define minimum principal subjects required based on academic level
                min_principal_required = {
                    1: 6,  # ACSEE/Form 6 - 7 principal subjects
                    2: 3,  # CSEE/Form 4 - 3 principal subjects (or core)
                    3: 6,  # Other levels - 6 subjects
                }.get(level_id, 3)  # Default to 3 if level not found

                # CASE 1: Student has NO numeric scores at all (all X or invalid)
                if not has_any_numeric_score:
                    std_average = 'X'
                    std_averagegrade = 'X'
                    division = 'ABS'
                    total_points = None
                    
                # CASE 2: Student has some scores but not enough principal subjects
                elif principal_subjects_sat < min_principal_required:
                    # Has some scores but incomplete principal subjects → INC
                    if counted > 0:
                        avg = (total_score / counted).quantize(Decimal('0.01'))
                        std_average = str(avg)
                    else:
                        std_average = 'X'
                    
                    std_averagegrade = 'INC'
                    division = 'INC'
                    total_points = None
                
                # CASE 3: Student has enough principal subjects → normal processing
                else:
                    # ---------------- AVERAGE ----------------
                    if counted == 0:
                        std_average = 'X'
                        std_averagegrade = 'X'
                    else:
                        avg = (total_score / counted).quantize(Decimal('0.01'))
                        std_average = str(avg)
                        std_averagegrade = resolve_average_grade(avg, grading_rules)

                    # ---------------- POINTS & DIVISION ----------------
                    division = "-"
                    total_points = None

                    if level_id in (1, 2):
                        best_n = 6 if level_id == 1 else 3

                        numeric_points = [
                            int(p) for p in points_list
                            if isinstance(p, (int, float))
                        ]

                        if not numeric_points:
                            division = "ABS"
                            total_points = None
                        else:
                            best_points = sorted(numeric_points)[:best_n]
                            total_points = sum(best_points)

                            for r in division_rules:
                                if r.min_points <= total_points <= r.max_points:
                                    division = r.division
                                    break

                    elif level_id == 3:
                        division = "-"
                        total_points = None

                # ---------------- SAVE RESULT ----------------
                results_bulk.append(
                    StudentFinalResult(
                        project=project,
                        student=student,
                        std_average=std_average,
                        std_averagegrade=std_averagegrade,
                        total_points=total_points,
                        division=division,
                        full_results=", ".join(full_parts)
                    )
                )

                processed += 1
                if processed % 100 == 0:
                    UploadProgress.objects.filter(id=progress.id).update(
                        processed_rows=processed
                    )
                    print(f"Processed {processed}/{total_students} students")

            print(f"Deleting existing results for project {project_code}")
            StudentFinalResult.objects.filter(project=project).delete()
            
            print(f"Creating {len(results_bulk)} new results")
            if results_bulk:
                # Batch insert in smaller chunks to avoid memory issues
                batch_size = 1000
                for i in range(0, len(results_bulk), batch_size):
                    batch = results_bulk[i:i+batch_size]
                    StudentFinalResult.objects.bulk_create(batch, batch_size=len(batch))
                    print(f"Inserted batch {i//batch_size + 1}/{(len(results_bulk)-1)//batch_size + 1}")

            UploadProgress.objects.filter(id=progress.id).update(status="done")
            print(" Student final results generation completed")

        return JsonResponse({"ok": True, "processed": len(results_bulk)})

    finally:
        # Re-enable foreign key checks
        with connection.cursor() as cursor:
            cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")
        print("Foreign key checks re-enabled")












@login_required
def student_final_results_report(request, project_code):
    """
    View student final results report using project_code
    """
    from django.core.paginator import Paginator
    from django.db.models import Q
    from .models import JointExamProject, StudentFinalResult
    
    # First get the project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    project_id = project.id
    
    q = request.GET.get("q", "").strip()

    qs = (
        StudentFinalResult.objects
        .filter(project_id=project_id)
        .select_related("student", "student__school", "student__combination")
    )

    if q:
        qs = qs.filter(
            Q(student__student_number__icontains=q) |
            Q(student__full_name__icontains=q) |
            Q(division__icontains=q)
        )

    paginator = Paginator(qs.order_by("student__student_number"), 50)
    page_obj = paginator.get_page(request.GET.get("page"))

    return render(request, "exams/student_final_results_report.html", {
        "page_obj": page_obj,
        "search_query": q,
        "project": project,
        "project_code": project_code,
        "project_id": project_id
    })



@login_required
def process_student_results_page(request, project_code):
    project = get_object_or_404(JointExamProject, project_code=project_code)

    progress = UploadProgress.objects.filter(
        project=project,
        user=request.user
    ).order_by("-id").first()

    return render(request, "exams/process_student_results.html", {
        "project": project,
        "progress": progress
    })




@login_required
@require_POST
@transaction.atomic
def delete_student_final_results(request, project_code):
    """Delete all student final results for a project"""
    from .models import JointExamProject, StudentFinalResult
    from django.shortcuts import get_object_or_404, redirect
    from django.contrib import messages
    
    # Get project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Delete student final results
    deleted_count, _ = StudentFinalResult.objects.filter(project=project).delete()
    
    messages.warning(
        request, 
        f"Student final results deleted. {deleted_count} records removed."
    )
    
    return redirect("exams:process_student_results_page", project_code=project.project_code)


@login_required
@require_POST
@transaction.atomic
def delete_school_results(request, project_code):
    """Delete all school results for a project"""
    from .models import JointExamProject, ExamSchoolResult
    from django.shortcuts import get_object_or_404, redirect
    from django.contrib import messages
    
    # Get project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Delete school results
    deleted_count, _ = ExamSchoolResult.objects.filter(project=project).delete()
    
    messages.warning(
        request, 
        f"School results deleted. {deleted_count} records removed."
    )
    
    return redirect("exams:process_student_results_page", project_code=project.project_code)


@login_required
@require_POST
@transaction.atomic
def delete_subject_results(request, project_code):
    """Delete all subject results for a project"""
    from .models import JointExamProject, ExamSubjectResult
    from django.shortcuts import get_object_or_404, redirect
    from django.contrib import messages
    
    # Get project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Delete subject results
    deleted_count, _ = ExamSubjectResult.objects.filter(project=project).delete()
    
    messages.warning(
        request,
        f"Subject result records deleted: {deleted_count} records removed."
    )
    
    return redirect("exams:process_student_results_page", project_code=project.project_code)







from openpyxl import Workbook
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404

@login_required
def download_papers_upload_template(request, project_code):
    """
    Download Excel template:
    student_number | paper_code1 | ... | paper_code15
    """

    project = get_object_or_404(JointExamProject, project_code=project_code)

    # Fetch students only (fast)
    students = (
        ProjectStudent.objects
        .filter(project_id=project_id)
        .only("student_number")
        .order_by("student_number")
        .iterator(chunk_size=5000)
    )

    wb = Workbook()
    ws = wb.active
    ws.title = "Paper Upload Template"

    # ---------------- HEADER ----------------
    headers = ["student_number"]
    headers += [f"paper_code{i}" for i in range(1, 16)]

    ws.append(headers)

    # ---------------- DATA ----------------
    for s in students:
        row = [s.student_number] + [""] * 15
        ws.append(row)

    # ---------------- RESPONSE ----------------
    filename = f"{project.name}-Paper-Upload-Template.xlsx"

    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'

    wb.save(response)
    return response














@login_required
def school_results_report(request, project_code):
    """View school results report using project_code"""
    from django.core.paginator import Paginator
    from django.db.models import Q
    from .models import JointExamProject, ExamSchoolResult
    
    # Get the project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    project_id = project.id
    
    q = request.GET.get("q", "").strip()
    
    qs = ExamSchoolResult.objects.filter(
        project_id=project_id
    ).select_related('school', 'school__district', 'school__region')
    
    if q:
        qs = qs.filter(
            Q(school__school_code__icontains=q) |
            Q(school__name__icontains=q)
        )
    
    # Calculate ranks for all schools
    sorted_by_gpa = ExamSchoolResult.objects.filter(
        project_id=project_id,
        sch_gpa__isnull=False
    ).order_by('sch_gpa')
    
    gpa_list = list(sorted_by_gpa.values_list('id', flat=True))
    total_schools = len(gpa_list)
    
    # Paginate
    paginator = Paginator(qs, 50)
    page_obj = paginator.get_page(request.GET.get("page"))
    
    # Create ranked rows for the template (matches template variable name)
    ranked_rows = []
    for result in page_obj:
        rank = gpa_list.index(result.id) + 1 if result.id in gpa_list else None
        ranked_rows.append({
            'rank': rank,
            'obj': result,
        })
    
    return render(request, "exams/school_ranking.html", {
        "project": project,
        "page_obj": page_obj,
        "ranked_rows": ranked_rows,  # This matches your template
        "search_query": q,
        "total_schools": total_schools,
    })
































@login_required
@require_POST
def generate_subject_results(request, project_code):

    print(">>> SUBJECT PROCESS STARTED <<<")

    project = get_object_or_404(JointExamProject, project_code=project_code)
    level_id = project.academic_level_id

    # Disable foreign key checks temporarily
    from django.db import connection
    with connection.cursor() as cursor:
        cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")

    try:
        grading_rules = list(
            GradingRule.objects.filter(
                academic_level_id=level_id
            ).order_by("-min_score")
        )

        gpa_rules = list(
            GpaRule.objects.filter(
                academic_level_id=level_id
            ).order_by("-min_gpa")
        )

        # 🔥 SUBJECT DATA (FAST)
        qs = (
            ProcessedSubjectScore.objects
            .filter(project=project)
            .select_related("student__school", "subject")
            .only("percentage_score", "grade", "student__school", "subject")
        )

        subject_map = {}
        skipped_count = 0

        for r in qs:
            if not r.student or not r.student.school:
                continue

            # Check if school is private
            is_private = r.student.school.ownership == 'PRIVATE'

            key = (r.subject_id, r.student.school_id)
            data = subject_map.setdefault(key, {
                "subject": r.subject,
                "school": r.student.school,
                "scores": [],
                "grades": [],
                "is_private": is_private,  # Store private flag
            })

            # FIX: Handle empty strings and non-numeric values
            if r.percentage_score:
                score_str = str(r.percentage_score).strip()
                
                # Skip if it's "X" or empty
                if score_str.upper() == "X" or score_str == "":
                    skipped_count += 1
                    continue
                
                # Try to convert to float
                try:
                    score_float = float(score_str)
                    data["scores"].append(score_float)
                except (ValueError, TypeError):
                    print(f"Warning: Could not convert '{score_str}' to float for student {r.student_id}, subject {r.subject_id}")
                    skipped_count += 1
                    continue

            # Only add grade if it's valid (not None, not empty)
            if r.grade and r.grade.strip():
                data["grades"].append(r.grade.strip())

        print(f"Skipped {skipped_count} records with invalid scores")
        print(f"Processing {len(subject_map)} subject-school combinations")

        results = []
        gpa_weights = SUBJECT_GPA_RULES.get(level_id, {})
        private_skipped = 0

        for (subject_id, school_id), data in subject_map.items():

            scores = data["scores"]
            grades = data["grades"]
            is_private = data["is_private"]

            if not scores:
                print(f"  No valid scores for subject {subject_id}, school {school_id}")
                continue

            with_results = len(scores)
            subj_avg = round(sum(scores) / with_results, 2)

            # ---------- AVG GRADE (Always calculate for all schools) ----------
            subj_avg_grd = subj_avg_status = None
            for g in grading_rules:
                if g.min_score <= subj_avg <= g.max_score:
                    subj_avg_grd = g.grade
                    subj_avg_status = g.grade
                    break

            # ---------- SUBJECT GPA (Skip for private schools) ----------
            subj_gpa = None
            subj_gpa_grade = None
            subj_gpa_status = None
            
            if not is_private:  # Only calculate GPA for non-private schools
                grade_counts = {}
                for g in grades:
                    if g in gpa_weights:
                        grade_counts[g] = grade_counts.get(g, 0) + 1

                total_grades = sum(grade_counts.values())

                if total_grades > 0:
                    weighted_sum = sum(grade_counts[g] * gpa_weights[g] for g in grade_counts)
                    subj_gpa = round(weighted_sum / total_grades, 4)

                if subj_gpa is not None:
                    for r in gpa_rules:
                        if r.min_gpa <= subj_gpa <= r.max_gpa:
                            subj_gpa_grade = r.gpa_grade
                            subj_gpa_status = r.gpa_status
                            break
            else:
                private_skipped += 1

            results.append(
                ExamSubjectResult(
                    project=project,
                    subject=data["subject"],
                    school=data["school"],
                    with_results=with_results,
                    subj_avg=subj_avg,
                    subj_avg_grd=subj_avg_grd,
                    subj_avg_status=subj_avg_status,
                    subj_gpa=subj_gpa,
                    subj_gpa_grade=subj_gpa_grade,
                    subj_gpa_status=subj_gpa_status,
                )
            )

        print(f"Generated {len(results)} subject results")
        print(f"Skipped GPA calculation for {private_skipped} private school subject combinations")

        # 🔥 SAVE - Clear old and insert new
        ExamSubjectResult.objects.filter(project=project).delete()
        
        if results:
            # Batch insert in smaller chunks
            batch_size = 500
            for i in range(0, len(results), batch_size):
                batch = results[i:i+batch_size]
                ExamSubjectResult.objects.bulk_create(batch, batch_size=len(batch))
                print(f"  Inserted batch {i//batch_size + 1}/{(len(results)-1)//batch_size + 1}")

        return JsonResponse({
            "ok": True,
            "subjects_processed": len(results),
            "skipped_records": skipped_count,
            "private_schools_skipped_gpa": private_skipped
        })

    finally:
        # Re-enable foreign key checks
        with connection.cursor() as cursor:
            cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")

























@login_required
@require_POST
def generate_school_results(request, project_code):
    """
    Generate school-level results using project_code
    """
    from django.db import connection
    from .models import JointExamProject, GradingRule, GpaRule, StudentFinalResult, ExamSubject, ProcessedSubjectScore, ExamSchoolResult
    
    # Get project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    level_id = project.academic_level_id
    project_id = project.id
    
    # Define GPA rules constants INSIDE the function
    SUBJECT_GPA_RULES = {
        1: {"A":1, "B":2, "C":3, "D":4, "F":5},
        2: {"A":1, "B":2, "C":3, "D":4, "E":5, "S":6, "F":7},
        3: {"A":1, "B":2, "C":3, "D":4, "E":5},
    }
    
    DIVISION_GPA_RULES = {
        "I": 1,
        "II": 2,
        "III": 3,
        "IV": 4,
        "0": 5,
    }
    
    print(f"Processing project: {project.name} (ID: {project_id}, Level ID: {level_id})")

    # Disable foreign key checks
    with connection.cursor() as cursor:
        cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")

    try:
        grading_rules = list(
            GradingRule.objects.filter(
                academic_level_id=level_id
            ).order_by("-min_score")
        )
        print(f"Grading rules loaded: {len(grading_rules)}")

        gpa_rules = list(
            GpaRule.objects.filter(
                academic_level_id=level_id
            ).order_by("-min_gpa")
        )
        print(f"GPA rules loaded: {len(gpa_rules)}")

        # ================= STUDENT RESULTS =================
        students = (
            StudentFinalResult.objects
            .filter(project_id=project_id)
            .select_related("student__school")
        )
        print(f"Student results found: {students.count()}")

        if students.count() == 0:
            return JsonResponse({
                "ok": False,
                "error": "No student results found. Please process student results first."
            }, status=400)

        schools = {}

        for s in students:
            school = s.student.school
            if not school:
                continue

            data = schools.setdefault(school.id, {
                "school": school,
                "avg_sum": 0,
                "avg_count": 0,
                "grades": [],
                "divisions": [],
            })

            if s.std_average not in ("X", None) and s.std_average != 'X':
                try:
                    data["avg_sum"] += float(s.std_average)
                    data["avg_count"] += 1
                except (ValueError, TypeError):
                    pass

            if s.std_averagegrade and s.std_averagegrade not in ('X', 'INC'):
                data["grades"].append(s.std_averagegrade)

            if s.division not in ("-", "ABS", "INC", None):
                data["divisions"].append(s.division)

        print(f"Schools found: {len(schools)}")

        # ================= SUBJECT GRADES (PRINCIPAL SUBJECTS ONLY) =================
        # Get the list of principal subjects for this academic level
        principal_subject_ids = list(
            ExamSubject.objects.filter(
                academic_level_id=level_id,
                is_principal=True
            ).values_list('subject_id', flat=True)
        )
        print(f"Principal subjects: {len(principal_subject_ids)}")

        if principal_subject_ids:
            subject_qs = (
                ProcessedSubjectScore.objects
                .filter(
                    project_id=project_id,
                    subject_id__in=principal_subject_ids
                )
                .select_related("student__school")
                .values("student__school_id", "grade")
            )
            print(f"Subject scores found: {subject_qs.count()}")

            subject_map = {}
            for r in subject_qs:
                sid = r["student__school_id"]
                if r["grade"] and r["grade"] not in ('X', 'INC'):
                    subject_map.setdefault(sid, []).append(r["grade"])
            print(f"Schools with subject grades: {len(subject_map)}")
        else:
            subject_map = {}
            print("No principal subjects found")

        results = []

        for school_id, data in schools.items():
            school = data["school"]
            
            # Check if school is private
            is_private = school.ownership == 'PRIVATE'
            print(f"Processing school: {school.school_code} - {school.name} (Private: {is_private})")

            # ---------- SCHOOL AVERAGE ----------
            sch_avg = (
                round(data["avg_sum"] / data["avg_count"], 2)
                if data["avg_count"] else None
            )
            print(f"  Avg count: {data['avg_count']}, Avg: {sch_avg}")

            sch_avg_grd = sch_avg_status = None
            if sch_avg is not None:
                for g in grading_rules:
                    if g.min_score <= sch_avg <= g.max_score:
                        sch_avg_grd = g.grade
                        sch_avg_status = g.grade
                        break

            # ---------- SUBJECT GPA (PRINCIPAL SUBJECTS ONLY) ----------
            subj_gpa = None
            if not is_private and principal_subject_ids:
                subj_weights = SUBJECT_GPA_RULES.get(level_id, {})
                subj_counts = {}

                for g in subject_map.get(school_id, []):
                    if g and g in subj_weights:
                        subj_counts[g] = subj_counts.get(g, 0) + 1

                subj_total = sum(subj_counts.values())
                if subj_total:
                    subj_gpa = (
                        sum(subj_counts[g] * subj_weights[g] for g in subj_counts) / subj_total
                    )
                    print(f"  Subject GPA: {subj_gpa}")

            # ---------- DIVISION GPA ----------
            div_gpa = None
            if not is_private:
                div_counts = {}
                for d in data["divisions"]:
                    div_counts[d] = div_counts.get(d, 0) + 1

                div_total = sum(div_counts.values())
                
                valid_div_counts = {d: count for d, count in div_counts.items() 
                                  if d in DIVISION_GPA_RULES}
                
                if valid_div_counts:
                    div_gpa = (
                        sum(valid_div_counts[d] * DIVISION_GPA_RULES[d] for d in valid_div_counts) 
                        / sum(valid_div_counts.values())
                    )
                    print(f"  Division GPA: {div_gpa}")

            # ---------- FINAL GPA ----------
            sch_gpa = None
            sch_gpa_grade = None
            sch_gpa_status = None
            
            if not is_private:
                if level_id == 3:
                    sch_gpa = subj_gpa
                else:
                    if subj_gpa is not None and div_gpa is not None:
                        sch_gpa = round((subj_gpa + div_gpa) / 2, 4)

                if sch_gpa is not None:
                    for r in gpa_rules:
                        if r.min_gpa <= sch_gpa <= r.max_gpa:
                            sch_gpa_grade = r.gpa_grade
                            sch_gpa_status = r.gpa_status
                            break
                print(f"  Final GPA: {sch_gpa}, Grade: {sch_gpa_grade}")

            results.append(
                ExamSchoolResult(
                    project=project,
                    school=school,
                    with_results=data["avg_count"],
                    sch_avg=sch_avg,
                    sch_avg_grd=sch_avg_grd,
                    sch_avg_status=sch_avg_status,
                    sch_gpa=sch_gpa,
                    sch_gpa_grade=sch_gpa_grade,
                    sch_gpa_status=sch_gpa_status,
                )
            )

        # 🔥 FAST SAVE
        if not results:
            return JsonResponse({
                "ok": False,
                "error": "No school results generated. Check GPA/Division rules."
            }, status=400)

        # Delete old results and save new ones
        deleted_count = ExamSchoolResult.objects.filter(project=project).delete()[0]
        print(f"Deleted {deleted_count} old school results")
        
        created_count = ExamSchoolResult.objects.bulk_create(results, batch_size=500)
        print(f"Created {len(created_count)} new school results")

        # Calculate statistics
        private_schools = sum(1 for data in schools.values() if data["school"].ownership == 'PRIVATE')
        
        return JsonResponse({
            "ok": True,
            "schools_processed": len(results),
            "private_schools_excluded": private_schools,
            "total_schools": len(schools),
            "students_processed": students.count(),
            "message": f"Successfully processed {len(results)} schools"
        })

    except Exception as e:
        import traceback
        error_details = traceback.format_exc()
        print(f"Error in generate_school_results: {str(e)}")
        print(error_details)
        
        return JsonResponse({
            "ok": False,
            "error": str(e),
            "details": error_details[:500] if request.user.is_superuser else None
        }, status=500)

    finally:
        # Re-enable foreign key checks
        with connection.cursor() as cursor:
            cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")



@login_required
def subject_results_report(request, project_code):
    """View subject results report using project_code"""
    from django.core.paginator import Paginator
    from django.db.models import Q
    from .models import JointExamProject, ExamSubjectResult
    
    # Get the project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    project_id = project.id
    
    q = request.GET.get("q", "").strip()
    
    qs = ExamSubjectResult.objects.filter(
        project_id=project_id
    ).select_related('subject', 'school', 'school__district')
    
    if q:
        qs = qs.filter(
            Q(subject__subject_code__icontains=q) |
            Q(subject__subject_name_eng__icontains=q) |
            Q(school__school_code__icontains=q)
        )
    
    qs = qs.order_by('subject__subject_code', 'school__school_code')
    
    paginator = Paginator(qs, 50)
    page_obj = paginator.get_page(request.GET.get("page"))
    
    return render(request, "exams/subject_results_report.html", {
        "project": project,
        "page_obj": page_obj,
        "search_query": q,
        "total_subject_results": qs.count(),
    })















from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404
from django.db.models import Count, Q
from django.core.paginator import Paginator

from .models import (
    JointExamProject,
    ExamSubjectPaper,
    ExamScore,
)

@login_required
def project_process_subjects(request, project_code):
    """
    Display subject papers used in a project, with progress stats
    """

    project = get_object_or_404(JointExamProject, project_code=project_code)
    search_query = request.GET.get("q", "").strip()

    # --------------------------------------------------
    # PAPERS USED IN THIS PROJECT (VIA ExamScore)
    # --------------------------------------------------
    paper_ids = (
        ExamScore.objects
        .filter(project=project)
        .values_list("paper_id", flat=True)
        .distinct()
    )

    qs = (
        ExamSubjectPaper.objects
        .filter(id__in=paper_ids)
        .select_related("subject")
        .order_by("subject__subject_code", "paper_code")
    )

    # --------------------------------------------------
    # SEARCH
    # --------------------------------------------------
    if search_query:
        qs = qs.filter(
            Q(subject__subject_name_eng__icontains=search_query) |
            Q(subject__subject_code__icontains=search_query) |
            Q(paper_code__icontains=search_query)
        )

    # --------------------------------------------------
    # COUNTS (✔ CORRECT FK USAGE)
    # --------------------------------------------------
    qs = qs.annotate(
        total_scripts=Count(
            "examscore__student",
            filter=Q(examscore__project=project),
            distinct=True
        ),
        filled_scores=Count(
            "examscore__student",
            filter=Q(
                examscore__project=project,
                examscore__score__isnull=False
            ),
            distinct=True
        )
    )

    # --------------------------------------------------
    # COMPLETION %
    # --------------------------------------------------
    for p in qs:
        p.completion_percent = round(
            (p.filled_scores / p.total_scripts) * 100, 1
        ) if p.total_scripts else 0

    # --------------------------------------------------
    # PAGINATION
    # --------------------------------------------------
    paginator = Paginator(qs, 25)
    page_obj = paginator.get_page(request.GET.get("page"))

    return render(
        request,
        "exams/project_process_subjects.html",
        {
            "project": project,
            "page_obj": page_obj,
            "search_query": search_query,
        }
    )













from django.db import transaction
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404

from .models import (
    JointExamProject,
    ExamScore,
    ExamSubject,
    ProjectStudent,
    ProcessedSubjectScore,
    GradingRule,
)


@login_required
@require_POST
@transaction.atomic
def process_subject_scores(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)
    user = request.user

    print("\n PROCESS SUBJECT SCORES STARTED")
    print("PROJECT:", project)
    print("PROJECT ID:", project_id)

    # --------------------------------------------------
    # TEMPORARILY DISABLE FOREIGN KEY CHECKS
    # --------------------------------------------------
    from django.db import connection
    
    with connection.cursor() as cursor:
        cursor.execute("SET FOREIGN_KEY_CHECKS = 0;")

    try:
        # --------------------------------------------------
        # CHECK FOR MISSING STUDENTS
        # --------------------------------------------------
        print("\n=== CHECKING FOR MISSING STUDENTS ===")
        
        # Get all unique student_ids from ExamScore
        student_ids_from_scores = set(
            ExamScore.objects
            .filter(project=project)
            .values_list('student_id', flat=True)
            .distinct()
        )
        print(f"Unique student_ids in ExamScore: {len(student_ids_from_scores)}")
        
        # Get existing student_ids in ProjectStudent
        existing_student_ids = set(
            ProjectStudent.objects
            .filter(project=project)
            .values_list('id', flat=True)
        )
        print(f"Existing student_ids in ProjectStudent: {len(existing_student_ids)}")
        
        # Find missing student_ids
        missing_student_ids = student_ids_from_scores - existing_student_ids
        print(f"Missing student_ids: {len(missing_student_ids)}")
        
        if missing_student_ids:
            print(f"First 10 missing student_ids: {list(missing_student_ids)[:10]}")
            
            # Default school ID - UPDATE THIS TO YOUR ACTUAL SCHOOL ID
            DEFAULT_SCHOOL_ID = 1
            
            # Create placeholder ProjectStudent records
            project_students_to_create = []
            for student_id in missing_student_ids:
                project_students_to_create.append(
                    ProjectStudent(
                        id=student_id,
                        project=project,
                        school_id=DEFAULT_SCHOOL_ID,
                        student_number=f"TEMP_{student_id}",
                        full_name=f"Student {student_id}",
                        sex="M",
                    )
                )
            
            # Bulk create with ignore_conflicts
            created_count = 0
            if project_students_to_create:
                batch_size = 1000
                for i in range(0, len(project_students_to_create), batch_size):
                    batch = project_students_to_create[i:i+batch_size]
                    created = ProjectStudent.objects.bulk_create(
                        batch,
                        ignore_conflicts=True,
                        batch_size=batch_size
                    )
                    created_count += len(created)
                
                print(f"Created {created_count} placeholder ProjectStudent records")
        
        # --------------------------------------------------
        # SUBJECT MAP
        # --------------------------------------------------
        subjects = {
            s.subject_id: s
            for s in ExamSubject.objects.all()
        }
        print(f"Total subjects loaded: {len(subjects)}")

        # --------------------------------------------------
        # GRADING RULES
        # --------------------------------------------------
        grading_rules = list(
            GradingRule.objects
            .filter(academic_level=project.academic_level)
            .order_by("-min_score")
        )

        print(f"Grading rules count: {len(grading_rules)}")

        def get_grade_points(score_float):
            for r in grading_rules:
                if r.min_score <= score_float <= r.max_score:
                    return r.grade, r.points
            return "F", 1

        # --------------------------------------------------
        # LOAD RAW EXAM SCORES
        # --------------------------------------------------
        exam_rows = (
            ExamScore.objects
            .filter(project=project)
            .select_related('paper')
            .values(
                "student_id",
                "paper__subject_id",
                "score",
            )
        )

        # --------------------------------------------------
        # GROUP SCORES BY STUDENT AND SUBJECT
        # --------------------------------------------------
        grouped = {}
        for r in exam_rows.iterator(chunk_size=5000):
            key = (r["student_id"], r["paper__subject_id"])
            if key not in grouped:
                grouped[key] = []
            grouped[key].append(r["score"])

        print(f"GROUPED subject rows: {len(grouped)}")

        # --------------------------------------------------
        # CLEAR OLD DATA
        # --------------------------------------------------
        ProcessedSubjectScore.objects.filter(project=project).delete()
        print("Cleared existing ProcessedSubjectScore records")

        # --------------------------------------------------
        # PROCESS GROUPS
        # --------------------------------------------------
        batch = []
        BATCH_SIZE = 2000
        processed_count = 0  # ⚠️ THIS WAS MISSING

        for (student_id, subject_id), score_list in grouped.items():

            try:
                student = ProjectStudent.objects.get(id=student_id, project=project)
            except ProjectStudent.DoesNotExist:
                print(f" Student {student_id} still not found, skipping")
                continue

            subject = subjects.get(subject_id)
            if not subject:
                print(f" Subject {subject_id} not found, skipping")
                continue

            # ----------------------------------------------
            # PARSE SCORES
            # ----------------------------------------------
            numeric_scores = []
            has_x = False

            for s in score_list:
                if s is None:
                    continue

                s = str(s).strip().upper()

                if s == "X" or s == "":
                    has_x = True
                else:
                    try:
                        numeric_scores.append(float(s))
                    except ValueError:
                        has_x = True

            # ----------------------------------------------
            # DETERMINE MAX TOTAL
            # ----------------------------------------------
            papers = subject.number_of_papers

            if papers == 1:
                max_total = 100
            elif papers == 2:
                max_total = 200
            elif papers == 3:   # represents 2.5
                max_total = 250
            else:
                max_total = 100

            # ----------------------------------------------
            # CALCULATE RESULTS
            # ----------------------------------------------
            all_null = all(s is None for s in score_list)

            if all_null:
                raw_total = None
                percentage_score = None
                grade = None
                points = None

            elif has_x:
                raw_total = None
                percentage_score = "X"
                grade = None
                points = None

            else:
                raw_total = sum(numeric_scores)
                percentage_float = (raw_total / max_total) * 100
                percentage_int = int(round(percentage_float))
                percentage_score = str(percentage_int)
                grade, points = get_grade_points(percentage_int)

            # ----------------------------------------------
            # SAVE
            # ----------------------------------------------
            batch.append(
                ProcessedSubjectScore(
                    project=project,
                    student=student,
                    subject=subject,
                    raw_total=raw_total,
                    max_total=max_total,
                    percentage_score=percentage_score,
                    grade=grade,
                    points=points,
                    processed_by=user,
                )
            )

            processed_count += 1

            if len(batch) >= BATCH_SIZE:
                ProcessedSubjectScore.objects.bulk_create(batch)
                batch.clear()
                print(f"Batch saved, total processed so far: {processed_count}")

        if batch:
            ProcessedSubjectScore.objects.bulk_create(batch)
            print(f"Final batch saved, total processed: {processed_count}")

        print("\n PROCESS SUBJECT SCORES COMPLETED")
        print(f"TOTAL PROCESSED SUBJECTS: {processed_count}")

        return JsonResponse({
            "status": "success",
            "processed_subjects": processed_count
        })
        
    finally:
        # Re-enable foreign key checks
        with connection.cursor() as cursor:
            cursor.execute("SET FOREIGN_KEY_CHECKS = 1;")




from django.contrib import messages
from django.shortcuts import get_object_or_404, redirect
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required
from django.db import transaction

from .models import JointExamProject, ProcessedSubjectScore


@login_required
@require_POST
@transaction.atomic
def delete_processed_subject_scores(request, project_code):
    project = get_object_or_404(JointExamProject, project_code=project_code)

    deleted_count, _ = (
        ProcessedSubjectScore.objects
        .filter(project=project)
        .delete()
    )

    messages.warning(
        request,
        f"Deleted {deleted_count} processed subject score records."
    )

    return redirect(
        "exams:project_process_subjects",
        project_id=project.id
    )




from django.shortcuts import render, get_object_or_404
from django.core.paginator import Paginator
from django.db.models import Count, Q, Value, IntegerField, OuterRef, Subquery
from django.db.models.functions import Coalesce
from django.contrib.auth.decorators import login_required

from .models import (
    JointExamProject,
    ExamSchoolResult,
    StudentFinalResult,
    ProjectStudent,
)

 
@login_required
def manage_school_results(request, project_code):
    project = get_object_or_404(JointExamProject, project_code=project_code)
    q = request.GET.get("q", "").strip()

    # -------------------------------------------------
    # SUBQUERIES (SAFE, NO INNER JOIN DROPPING)
    # -------------------------------------------------
    def div_count(div):
        return Subquery(
            StudentFinalResult.objects
            .filter(
                project=project,
                division=div,
                student__school=OuterRef("school"),
            )
            .values("student__school")
            .annotate(c=Count("id"))
            .values("c")[:1],
            output_field=IntegerField(),
        )

    qs = (
        ExamSchoolResult.objects
        .select_related(
            "school",
            "school__region",
            "school__district",
        )
        .filter(project=project)
        .annotate(
            d1=Coalesce(div_count("I"), Value(0)),
            d2=Coalesce(div_count("II"), Value(0)),
            d3=Coalesce(div_count("III"), Value(0)),
            d4=Coalesce(div_count("IV"), Value(0)),
            d0=Coalesce(div_count("0"), Value(0)),
        )
        .order_by("school__school_code")
    )

    # -------------------------------------------------
    # SEARCH
    # -------------------------------------------------
    if q:
        qs = qs.filter(
            Q(school__school_code__icontains=q) |
            Q(school__name__icontains=q) |
            Q(school__district__name__icontains=q) |
            Q(school__region__name__icontains=q)
        )

    paginator = Paginator(qs, 25)
    page_obj = paginator.get_page(request.GET.get("page"))

    return render(
        request,
        "exams/manage_school_results.html",
        {
            "project": project,
            "page_obj": page_obj,
            "search_query": q,
        },
    )





@login_required
@require_POST
def toggle_project_live(request, project_code):
    """Toggle project live status"""
    from .models import JointExamProject
    from django.shortcuts import get_object_or_404
    from django.http import JsonResponse
    import json
    
    try:
        # Parse JSON data
        try:
            data = json.loads(request.body)
            is_active = data.get('is_active', False)
        except:
            # If no JSON body, toggle the status
            is_active = None
        
        # Get project by code
        project = get_object_or_404(JointExamProject, project_code=project_code)
        
        # Toggle or set status
        if is_active is None:
            project.is_live = not project.is_live
        else:
            project.is_live = is_active
        
        project.save(update_fields=['is_live'])
        
        return JsonResponse({
            'success': True,
            'message': f'Project "{project.name}" is now {"Live" if project.is_live else "Not Live"}',
            'is_active': project.is_live
        })
        
    except JointExamProject.DoesNotExist:
        return JsonResponse({
            'success': False,
            'error': f'Project with code "{project_code}" not found'
        }, status=404)
        
    except Exception as e:
        return JsonResponse({
            'success': False,
            'error': str(e)
        }, status=500)


@login_required
@require_POST
def toggle_school_results_status(request, project_code, school_id):
    """
    Toggle school results release status
    """
    from .models import JointExamProject, ExamSchoolResult
    from django.shortcuts import get_object_or_404
    from django.http import JsonResponse
    
    # Get project by code
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get the school result
    result = get_object_or_404(
        ExamSchoolResult,
        project=project,
        school_id=school_id
    )
    
    # Toggle status
    result.sch_release_status = not result.sch_release_status
    result.save(update_fields=["sch_release_status"])
    
    return JsonResponse({
        "ok": True,
        "status": result.sch_release_status,
        "message": f"School results {'released' if result.sch_release_status else 'hidden'} successfully"
    })






from django.contrib.auth.decorators import login_required
from django.shortcuts import render, get_object_or_404
from exams.models import JointExamProject
from .models import Region, District   # adjust import if needed

@login_required
def rank_summary_page(request, project_code):
    project = get_object_or_404(JointExamProject, project_code=project_code)

    regions = Region.objects.all().order_by("name")
    districts = District.objects.all().order_by("name")

    return render(
        request,
        "exams/reports/rank_summary_page.html",
        {
            "project": project,
            "regions": regions,     # ✅ now available in template
            "districts": districts  # ✅ now available in template
        }
    )
    
@login_required
def project_school_rank_router(request, project_code):

    scope = request.GET.get("scope", "overall")
    layout = request.GET.get("layout", "normal")
    output = request.GET.get("format", "html")
    category = request.GET.get("category", "all")

    # -----------------------------------------
    # SELECT DATA FUNCTION (YOU ALREADY HAVE THESE)
    # -----------------------------------------
    if scope == "district":
        data_fn = (
            compute_district_school_rank_ext
            if layout == "extended"
            else compute_district_school_rank
        )

    elif scope == "region":
        data_fn = (
            compute_region_school_rank_ext
            if layout == "extended"
            else compute_region_school_rank
        )

    else:  # overall
        data_fn = build_school_rank_data

    # -----------------------------------------
    # OUTPUT ROUTING
    # -----------------------------------------
    if output == "pdf":
        return render_school_rank_pdf(
            request, project_code, data_fn, category
        )

    if output == "excel":
        return render_school_rank_excel(
            request, project_code, data_fn, category
        )

    # DEFAULT: HTML
    return render_school_rank_html(
        request, project_code, data_fn, category
    )


def render_school_rank_html(request, project_code, data_fn, category):
    rows = data_fn(project_id=project_id, category=category)
    return render(
        request,
        "exams/reports/school_rank.html",
        {"rows": rows}
    )


def render_school_rank_excel(request, project_code, data_fn, category):
    rows = data_fn(project_id=project_id, category=category)
    return export_school_rank_excel(rows)


def render_school_rank_pdf(request, project_code, data_fn, category):
    rows = data_fn(project_id=project_id, category=category)
    return export_school_rank_pdf(rows)






























 

from django.utils import timezone
from datetime import timedelta
from django.db.models import Count
from .models import VisitorLog, Gallery, News, JointExamProject

def public_home(request):
    VisitorLog.objects.create(ip=request.META.get("REMOTE_ADDR"))

    now = timezone.now()
    today = now.date()

    stats = {
        "today": VisitorLog.objects.filter(visited_at__date=today).count(),
        "yesterday": VisitorLog.objects.filter(
            visited_at__date=today - timedelta(days=1)
        ).count(),
        "week": VisitorLog.objects.filter(
            visited_at__gte=now - timedelta(days=7)
        ).count(),
        "month": VisitorLog.objects.filter(
            visited_at__gte=now - timedelta(days=30)
        ).count(),
        "year": VisitorLog.objects.filter(
            visited_at__gte=now - timedelta(days=365)
        ).count(),
        "total": VisitorLog.objects.count(),
    }

    return render(request, "exams/home.html", {
        "gallery": Gallery.objects.filter(is_active=True),
        "news_list": News.objects.filter(is_active=True),
        "projects": JointExamProject.objects.filter(live_id=True),
        "visitor": stats,
    })



from django.shortcuts import render
from .models import Download, News, Gallery

def home(request):
    return render(request, "public/index.html", {
        "downloads": Download.objects.filter(is_active=True)[:5],
        "news_list": News.objects.filter(is_active=True)[:5],
        "gallery": Gallery.objects.all()[:8],
        "projects": JointExamProject.objects.filter(is_live=True).order_by("-created_at"),
    })

def downloads(request):
    return render(request, "public/downloads.html", {
        "downloads": Download.objects.filter(is_active=True)
    })

def news(request):
    return render(request, "public/news.html", {
        "news_list": News.objects.filter(is_active=True)
    })

def gallery(request):
    return render(request, "public/gallery.html", {
        "gallery": Gallery.objects.all()
    })


from django.shortcuts import render, get_object_or_404
from .models import News

def news_detail(request, pk):
    news = get_object_or_404(News, pk=pk, is_active=True)
    return render(request, "exams/public/news_detail.html", {
        "news": news
    })



@login_required
def project_school_ranking_html(request, project_code):
    """
    Display school ranking for a project
    """
    from .views_helpers import compute_project_school_rank
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Optional filters
    district_id = request.GET.get("district")
    region_id = request.GET.get("region")
    
    if district_id:
        rows = compute_project_district_school_rank(project_id, district_id)
        title = f"DISTRICT SCHOOL RANKING – {District.objects.get(id=district_id).name}"
    elif region_id:
        rows = compute_project_region_school_rank(project_id, region_id)
        title = f"REGION SCHOOL RANKING – {Region.objects.get(id=region_id).name}"
    else:
        rows, project = compute_project_school_rank(project_id)
        title = f"PROJECT SCHOOL RANKING – {project.name}"
    
    context = {
        "project": project,
        "rows": rows,
        "year": project.year,
        "title": title,
        "exam_type": project.name,
        "district_id": district_id,
        "region_id": region_id,
    }
    
    return render(request, "exams/reports/project_school_rank_html.html", context)


@login_required
def project_school_ranking_print(request, project_code):
    """
    Print-friendly version of school ranking - overall project ranking only
    """
    from .views_helpers import compute_project_school_rank
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get overall ranking (no district/region filtering)
    rows, project = compute_project_school_rank(project_id)
    title = f"{project.name} - SCHOOL RANKING {project.year}"
    
    return render(request, "exams/reports/project_school_rank_print.html", {
        "project": project,
        "rows": rows,
        "year": project.year,
        "title": title,
    })


    
@login_required
def project_school_ranking_excel(request, project_code):
    """
    Export school ranking to Excel
    """
    from openpyxl import Workbook
    from django.http import HttpResponse
    from .views_helpers import compute_project_school_rank
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    district_id = request.GET.get("district")
    region_id = request.GET.get("region")
    
    if district_id:
        rows, _ = compute_project_district_school_rank(project_id, district_id)
        district = District.objects.get(id=district_id)
        filename = f"{district.name}_School_Rank_{project.year}.xlsx"
    elif region_id:
        rows, _ = compute_project_region_school_rank(project_id, region_id)
        region = Region.objects.get(id=region_id)
        filename = f"{region.name}_School_Rank_{project.year}.xlsx"
    else:
        rows, project = compute_project_school_rank(project_id)
        filename = f"Project_{project.id}_School_Rank_{project.year}.xlsx"
    
    wb = Workbook()
    ws = wb.active
    ws.title = "School Ranking"
    
    # Headers
    headers = ["#", "CODE", "SCHOOL NAME", "DISTRICT", "REGION", "OWNERSHIP",
               "I", "II", "III", "IV", "0", "#I-IV", "%I-IV", "GPA", 
               "AVG SCORE", "GRADE", "STATUS"]
    ws.append(headers)
    
    # Data rows
    for r in rows:
        ws.append([
            r["rank"],
            r["code"],
            r["name"],
            r["district"],
            r["region"],
            r["ownership"],
            r["d1"],
            r["d2"],
            r["d3"],
            r["d4"],
            r["d0"],
            r["pass"],
            r["pass_pct"],
            r["gpa"],
            r["sch_avg"],
            r["sch_avg_grd"],
            r["sch_avg_status"],
        ])
    
    response = HttpResponse(
        content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
    )
    response["Content-Disposition"] = f'attachment; filename="{filename}"'
    
    wb.save(response)
    return response


@login_required
def project_school_ranking_pdf(request, project_code):
    """
    Export school ranking to PDF
    """
    from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
    from reportlab.lib.pagesizes import A4, landscape
    from reportlab.lib import colors
    from reportlab.lib.styles import getSampleStyleSheet
    from django.http import HttpResponse
    from .views_helpers import compute_project_school_rank
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    district_id = request.GET.get("district")
    region_id = request.GET.get("region")
    
    if district_id:
        rows, _ = compute_project_district_school_rank(project_id, district_id)
        district = District.objects.get(id=district_id)
        title = f"{district.name} – {project.name}"
    elif region_id:
        rows, _ = compute_project_region_school_rank(project_id, region_id)
        region = Region.objects.get(id=region_id)
        title = f"{region.name} – {project.name}"
    else:
        rows, project = compute_project_school_rank(project_id)
        title = f"{project.name}"
    
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = f'attachment; filename="School_Rank_{project.year}.pdf"'
    
    # Use landscape for more columns
    doc = SimpleDocTemplate(response, pagesize=landscape(A4))
    styles = getSampleStyleSheet()
    story = []
    
    # Title
    story.append(Paragraph(f"<b>SCHOOL RANKING – {title}</b>", styles["Title"]))
    story.append(Paragraph(f"Year: {project.year}", styles["Normal"]))
    story.append(Spacer(1, 12))
    
    # Table data
    data = [["#", "CODE", "SCHOOL", "DISTRICT", "REGION", 
             "I", "II", "III", "IV", "0", "#I-IV", "%", "GPA"]]
    
    for r in rows:
        data.append([
            r["rank"],
            r["code"],
            r["name_short"][:20] + "..." if len(r["name_short"]) > 20 else r["name_short"],
            r["district"],
            r["region"],
            r["d1"],
            r["d2"],
            r["d3"],
            r["d4"],
            r["d0"],
            r["pass"],
            f"{r['pass_pct']}%",
            f"{r['gpa']:.4f}",
        ])
    
    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([
        ("GRID", (0, 0), (-1, -1), 1, colors.black),
        ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
        ("ALIGN", (0, 0), (-1, -1), "CENTER"),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
        ("FONTSIZE", (0, 0), (-1, -1), 8),
    ]))
    
    story.append(table)
    doc.build(story)
    
    return response




@login_required
def school_results_list(request, project_code):
    """
    List schools that have results in ExamSchoolResult for a specific project
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    user = request.user
    profile = getattr(user, "profile", None)
    
    # Get school IDs that have results in ExamSchoolResult for this project
    school_ids_with_results = ExamSchoolResult.objects.filter(
        project=project
    ).values_list('school_id', flat=True).distinct()
    
    # Base queryset - only schools that have results in ExamSchoolResult
    schools_qs = School.objects.filter(
        id__in=school_ids_with_results
    ).select_related(
        "region", "district", "ward"
    ).order_by("school_code")
    
    # Apply role-based filtering
    if profile:
        if profile.role == "super":
            # Super admin sees all schools with results
            pass
        elif profile.role == "region":
            schools_qs = schools_qs.filter(region=profile.region)
        elif profile.role == "district":
            schools_qs = schools_qs.filter(district=profile.district)
        elif profile.role == "ward":
            schools_qs = schools_qs.filter(ward=profile.ward)
        elif profile.role == "school":
            if profile.school:
                schools_qs = schools_qs.filter(pk=profile.school.pk)
            else:
                schools_qs = School.objects.none()
        else:
            schools_qs = School.objects.none()
    else:
        schools_qs = School.objects.none()
    
    # Get filter parameters
    region_id = request.GET.get("region")
    district_id = request.GET.get("district")
    search = request.GET.get("q", "").strip()
    release_status = request.GET.get("release_status")
    rank_involvement = request.GET.get("rank_involvement")
    
    # Apply filters
    if region_id:
        schools_qs = schools_qs.filter(region_id=region_id)
    if district_id:
        schools_qs = schools_qs.filter(district_id=district_id)
    if search:
        schools_qs = schools_qs.filter(
            Q(school_code__icontains=search) |
            Q(name__icontains=search) |
            Q(name_short__icontains=search)
        )
    
    # Get ExamSchoolResult data for each school
    school_results = {}
    for result in ExamSchoolResult.objects.filter(
        project=project,
        school_id__in=schools_qs.values_list('id', flat=True)
    ).select_related('school'):
        school_results[result.school_id] = result
    
    # Annotate schools with result data
    for school in schools_qs:
        result = school_results.get(school.id)
        if result:
            school.with_results = result.with_results
            school.sch_avg = result.sch_avg
            school.sch_avg_grd = result.sch_avg_grd
            school.sch_avg_status = result.sch_avg_status
            school.sch_gpa = result.sch_gpa
            school.sch_gpa_grade = result.sch_gpa_grade
            school.sch_gpa_status = result.sch_gpa_status
            school.sch_release_status = result.sch_release_status
            school.sch_rank_involvement = result.sch_rank_involvement
            school.processed_at = result.processed_at
        else:
            # This shouldn't happen since we filtered by school_ids_with_results
            school.with_results = 0
            school.sch_avg = None
            school.sch_avg_grd = None
            school.sch_avg_status = None
            school.sch_gpa = None
            school.sch_gpa_grade = None
            school.sch_gpa_status = None
            school.sch_release_status = False
            school.sch_rank_involvement = False
    
    # Apply release status filter if provided
    if release_status == 'released':
        schools_qs = [s for s in schools_qs if s.sch_release_status]
    elif release_status == 'not_released':
        schools_qs = [s for s in schools_qs if not s.sch_release_status]
    
    # Apply rank involvement filter if provided
    if rank_involvement == 'included':
        schools_qs = [s for s in schools_qs if s.sch_rank_involvement]
    elif rank_involvement == 'excluded':
        schools_qs = [s for s in schools_qs if not s.sch_rank_involvement]
    
    # Pagination
    paginator = Paginator(schools_qs, 50)
    page_number = request.GET.get("page")
    page_obj = paginator.get_page(page_number)
    
    # Get regions and districts for filters
    regions = Region.objects.all().order_by("name")
    districts = District.objects.all().order_by("name")
    
    # Get summary stats
    total_schools = len(schools_qs)
    total_students_with_results = ExamSchoolResult.objects.filter(
        project=project,
        school_id__in=school_ids_with_results
    ).aggregate(total=Sum('with_results'))['total'] or 0
    
    released_count = ExamSchoolResult.objects.filter(
        project=project,
        school_id__in=school_ids_with_results,
        sch_release_status=True
    ).count()
    
    context = {
        "project": project,
        "page_obj": page_obj,
        "regions": regions,
        "districts": districts,
        "selected_region": region_id,
        "selected_district": district_id,
        "search_query": search,
        "release_status": release_status,
        "rank_involvement": rank_involvement,
        "user_profile": profile,
        "total_schools": total_schools,
        "total_students_with_results": total_students_with_results,
        "released_count": released_count,
    }
    
    return render(request, "exams/school_results_list.html", context)











from reportlab.lib.pagesizes import A4, portrait
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from decimal import Decimal
import math

def _on_page(canvas, doc):
    """Add page numbers to each page"""
    canvas.saveState()
    canvas.setFont("Helvetica", 9)
    page_num = canvas.getPageNumber()
    text = f"Page {page_num}"
    canvas.drawRightString(doc.pagesize[0] - 30, 20, text)
    canvas.restoreState()

@login_required
def project_subject_summary_pdf(request, project_code, paper_id):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    # 🔹 Identify subject via paper
    paper = get_object_or_404(
        ExamSubjectPaper.objects.select_related("subject"),
        id=paper_id
    )
    subject = paper.subject

    # 🔹 All papers under this subject
    subject_papers = (
        ExamSubjectPaper.objects
        .filter(subject=subject)
        .order_by("paper_code")
    )

    # 🔹 Only schools with at least one registration for this subject
    schools = (
        School.objects
        .filter(
            projectstudent__examscore__project=project,
            projectstudent__examscore__paper__in=subject_papers
        )
        .distinct()
        .order_by("school_code")
    )

    # ================= PDF SETUP =================
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="{subject.subject_code}_SUBJECT_SUMMARY.pdf"'
    )

    # Use PORTRAIT orientation
    doc = SimpleDocTemplate(
        response,
        pagesize=portrait(A4),
        topMargin=40,
        bottomMargin=50,  # Extra space for page numbers
        leftMargin=20,
        rightMargin=20
    )

    PAGE_WIDTH = portrait(A4)[0]
    USABLE_WIDTH = PAGE_WIDTH - doc.leftMargin - doc.rightMargin

    # Calculate column widths for portrait
    no_w = 25
    code_w = 50
    name_w = 130  # Reduced for portrait
    
    paper_cols = len(subject_papers) * 3  # Registered + SAT + Absent
    
    # Calculate paper column width
    fixed_width = no_w + code_w + name_w
    paper_w = (USABLE_WIDTH - fixed_width) / max(paper_cols, 1)
    
    # If paper columns become too narrow, we might need to split across pages
    # But for now, keep as is
    
    styles = getSampleStyleSheet()
    
    # Wrap style for long text
    wrap_style = ParagraphStyle(
        "wrap",
        fontSize=7,
        leading=9,
        wordWrap="CJK"
    )
    
    elements = []

    # ================= TITLE =================
    elements.append(Paragraph(
        f"<b>FORM SIX REGIONAL MOCK EXAMINATIONS RESULTS</b>",
        styles["Title"]
    ))
    elements.append(Spacer(1, 4))

    elements.append(Paragraph(
        f"<b>{project.name} - {project.year}</b>",
        styles["Heading2"]
    ))
    elements.append(Spacer(1, 4))

    elements.append(Paragraph(
        f"<b>{subject.subject_code} - {subject.subject_name_eng.upper()}</b>",
        styles["Heading3"]
    ))
    elements.append(Spacer(1, 12))

    # ================= TABLE HEADER =================
    # Row 1: Main categories
    header_row_1 = ["#", "CODE", "NAME"]
    
    # Add main category headers for each paper
    for i, p in enumerate(subject_papers):
        header_row_1.append("REGISTERED")
    for i, p in enumerate(subject_papers):
        header_row_1.append(f"SAT")
    for i, p in enumerate(subject_papers):
        header_row_1.append(f"ABS")

    # Row 2: Paper codes
    header_row_2 = ["", "", ""]
    
    for p in subject_papers:
        header_row_2.append(Paragraph(f"{p.paper_code}", wrap_style))
    for p in subject_papers:
        header_row_2.append(Paragraph(f"{p.paper_code}", wrap_style))
    for p in subject_papers:
        header_row_2.append(Paragraph(f"{p.paper_code}", wrap_style))

    data = [header_row_1, header_row_2]

    # ================= DATA ROWS =================
    totals_registered = [0] * len(subject_papers)
    totals_sat = [0] * len(subject_papers)
    totals_abs = [0] * len(subject_papers)

    for idx, school in enumerate(schools, start=1):

        row = [
            idx, 
            school.school_code or "", 
            Paragraph((school.name_short or school.name or "").strip(), wrap_style)
        ]

        # For each paper
        for i, p in enumerate(subject_papers):
            
            # REGISTERED: All students with any score (including NULL and X)
            reg = ExamScore.objects.filter(
                project=project,
                paper=p,
                student__school=school
            ).count()
            row.append(reg or "")
            totals_registered[i] += reg
            
        for i, p in enumerate(subject_papers):
            
            # SAT (0-100): Students with numeric scores between 0-100
            all_scores = ExamScore.objects.filter(
                project=project,
                paper=p,
                student__school=school
            )
            
            sat_count = 0
            for score_obj in all_scores:
                if score_obj.score and score_obj.score.strip():
                    score_val = score_obj.score.strip().upper()
                    if score_val != 'X':
                        try:
                            # Try to convert to float to check if it's numeric
                            float(score_val)
                            sat_count += 1
                        except (ValueError, TypeError):
                            # Not a valid number, skip
                            pass
            
            row.append(sat_count or "")
            totals_sat[i] += sat_count
            
        for i, p in enumerate(subject_papers):
            
            # ABSENT (X): Students with explicit 'X' mark
            abs_count = ExamScore.objects.filter(
                project=project,
                paper=p,
                student__school=school,
                score__iexact='X'
            ).count()
            
            row.append(abs_count or "")
            totals_abs[i] += abs_count

        data.append(row)

    # ================= TOTAL ROW =================
    total_row = ["", Paragraph("<b>TOTAL</b>", styles["Normal"]), ""]

    for v in totals_registered:
        total_row.append(Paragraph(f"<b>{v or ''}</b>", styles["Normal"]))
    for v in totals_sat:
        total_row.append(Paragraph(f"<b>{v or ''}</b>", styles["Normal"]))
    for v in totals_abs:
        total_row.append(Paragraph(f"<b>{v or ''}</b>", styles["Normal"]))

    data.append(total_row)

    # ================= TABLE =================
    col_widths = [no_w, code_w, name_w] + [paper_w] * paper_cols

    table = Table(data, colWidths=col_widths, repeatRows=2)
    
    # Build span indices
    reg_start = 3
    reg_end = 3 + len(subject_papers) - 1
    
    sat_start = reg_end + 1
    sat_end = sat_start + len(subject_papers) - 1
    
    abs_start = sat_end + 1
    abs_end = -1  # Last column
    
    table.setStyle(TableStyle([
        # Main header spans (Row 0)
        ("SPAN", (reg_start, 0), (reg_end, 0)),  # REGISTERED group
        ("SPAN", (sat_start, 0), (sat_end, 0)),  # SAT group
        ("SPAN", (abs_start, 0), (abs_end, 0)),  # ABSENT group
        
        # Basic grid
        ("GRID", (0, 0), (-1, -1), 0.5, colors.black),
        ("BOX", (0, 0), (-1, -1), 1, colors.black),
        
        # Alignment
        ("ALIGN", (3, 0), (-1, -1), "CENTER"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),
        
        # Font for headers
        ("FONTNAME", (0, 0), (-1, 1), "Helvetica-Bold"),
        ("FONTSIZE", (0, 0), (-1, 1), 8),
        
        # Font for total row
        ("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
        
        # Background colors
        ("BACKGROUND", (0, 0), (-1, 1), colors.lightgrey),
        ("BACKGROUND", (0, -1), (-1, -1), colors.lightblue),
        
        # Left align school name
        ("ALIGN", (2, 2), (2, -2), "LEFT"),
    ]))

    elements.append(table)
    
    # ================= FOOTER =================
    elements.append(Spacer(1, 15))
    
    from datetime import datetime
    current_date = datetime.now().strftime('%B %d, %Y')
    elements.append(Paragraph(
        f"<i>Generated on {current_date} | Total Schools: {len(schools)} | Total Papers: {len(subject_papers)}</i>",
        styles["Italic"]
    ))

    # Build document with page numbers
    doc.build(elements, onFirstPage=_on_page, onLaterPages=_on_page)
    return response































from django.db.models import Count, Sum, Q, F
from django.utils import timezone
from datetime import datetime, timedelta
import calendar

def get_marking_progress_data(project_id):
    """
    Get marking progress data for all subjects and papers in a project
    """
    project = JointExamProject.objects.get(id=project_id)
    
    # Get all subjects that have papers with exam scores in this project
    subjects = ExamSubject.objects.filter(
        papers__examscore__project_id=project_id  # Use 'papers' if that's the related_name
    ).distinct().order_by('subject_code')
    
    # If the above doesn't work, try this alternative:
    # subjects = ExamSubject.objects.filter(
    #     id__in=ExamSubjectPaper.objects.filter(
    #         examscore__project_id=project_id
    #     ).values_list('subject_id', flat=True)
    # ).distinct().order_by('subject_code')
    
    data = []
    total_registered = 0
    total_marked = 0
    date_columns = []
    
    # Generate last 7 days for columns
    today = timezone.now().date()
    for i in range(7):
        date_columns.append(today - timedelta(days=i))
    date_columns.reverse()  # Show oldest to newest
    
    for subject in subjects:
        # Get all papers for this subject
        # Try different related names based on your model
        papers = ExamSubjectPaper.objects.filter(
            subject=subject,
            examscore__project_id=project_id
        ).distinct().order_by('paper_code')
        
        if not papers:
            continue
            
        # Calculate registered count for subject (unique students)
        subject_registered = ProjectStudent.objects.filter(
            project_id=project_id,
            examscore__paper__in=papers
        ).distinct().count()
        
        subject_data = {
            'subject': subject,
            'papers': [],
            'subject_registered': subject_registered,
            'subject_marked': 0,
            'daily_counts': [0] * len(date_columns),
        }
        
        for paper in papers:
            # Get all scores for this paper
            scores = ExamScore.objects.filter(
                project_id=project_id,
                paper=paper
            )
            
            # Registered in this paper
            paper_registered = scores.count()
            
            # Marked (updated_by_id > 0 and not X)
            paper_marked = scores.filter(
                updated_by_id__gt=0
            ).exclude(
                score__iexact='X'
            ).exclude(
                score__isnull=True
            ).count()
            
            # Daily counts for this paper
            daily_counts = []
            for date in date_columns:
                count = scores.filter(
                    updated_by_id__gt=0,
                    updated_at__date=date
                ).exclude(
                    score__iexact='X'
                ).exclude(
                    score__isnull=True
                ).count()
                daily_counts.append(count)
            
            paper_data = {
                'paper': paper,
                'registered': paper_registered,
                'marked': paper_marked,
                'daily_counts': daily_counts,
                'remaining': paper_registered - paper_marked
            }
            
            subject_data['papers'].append(paper_data)
            subject_data['subject_marked'] += paper_marked
            
            # Add to daily counts for subject
            for i, count in enumerate(daily_counts):
                subject_data['daily_counts'][i] += count
        
        data.append(subject_data)
        total_registered += subject_data['subject_registered']
        total_marked += subject_data['subject_marked']
    
    return {
        'project': project,
        'data': data,
        'date_columns': date_columns,
        'total_registered': total_registered,
        'total_marked': total_marked,
        'remaining': total_registered - total_marked,
        'completion_percent': round((total_marked / total_registered * 100), 1) if total_registered else 0
    }



from reportlab.lib.pagesizes import A4, landscape
from reportlab.platypus import (
    SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer, PageBreak
)
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.units import cm
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from datetime import datetime
import math

def _on_page(canvas, doc):
    """Add page numbers to each page"""
    canvas.saveState()
    canvas.setFont("Helvetica", 8)
    page_num = canvas.getPageNumber()
    text = f"Page {page_num}"
    canvas.drawRightString(doc.pagesize[0] - 30, 20, text)
    
    # Add timestamp
    from datetime import datetime
    timestamp = datetime.now().strftime("%H:%M:%S")
    canvas.drawString(30, 20, f"{timestamp}")
    canvas.restoreState()

@login_required
def subject_paper_progress_summary_pdf(request, project_code):
    """
    Generate PDF report showing marking progress for all subjects and papers
    """
    from datetime import datetime
    
    # Get data
    context = get_marking_progress_data(project_id)
    project = context['project']
    data = context['data']
    date_columns = context['date_columns']
    total_registered = context['total_registered']
    total_marked = context['total_marked']
    remaining = context['remaining']
    completion_percent = context['completion_percent']
    
    # ================= PDF SETUP =================
    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="MARKING_PROGRESS_{project.name}_{project.year}.pdf"'
    )

    # Use LANDSCAPE for wider table
    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A4),
        topMargin=30,
        bottomMargin=40,
        leftMargin=20,
        rightMargin=20
    )

    PAGE_WIDTH = landscape(A4)[0]
    USABLE_WIDTH = PAGE_WIDTH - doc.leftMargin - doc.rightMargin

    styles = getSampleStyleSheet()
    
    # Custom styles
    title_style = ParagraphStyle(
        'CustomTitle',
        parent=styles['Heading1'],
        fontSize=14,
        alignment=1,  # Center
        spaceAfter=6
    )
    
    subtitle_style = ParagraphStyle(
        'CustomSubtitle',
        parent=styles['Heading2'],
        fontSize=12,
        alignment=1,  # Center
        spaceAfter=10
    )
    
    normal_style = ParagraphStyle(
        'CustomNormal',
        parent=styles['Normal'],
        fontSize=8,
        alignment=0  # Left
    )
    
    center_style = ParagraphStyle(
        'CustomCenter',
        parent=styles['Normal'],
        fontSize=8,
        alignment=1  # Center
    )
    
    bold_center_style = ParagraphStyle(
        'BoldCenter',
        parent=styles['Normal'],
        fontSize=8,
        alignment=1,
        fontName='Helvetica-Bold'
    )
    
    wrap_style = ParagraphStyle(
        "wrap",
        fontSize=7,
        leading=9,
        wordWrap="CJK",
        alignment=1
    )

    elements = []

    # ================= HEADER =================
    elements.append(Paragraph(
        f"<b>PRIME MINISTER'S OFFICE - REGIONAL ADMINISTRATION AND LOCAL GOVERNMENT (PO-RALG) - KIGOMA</b>",
        title_style
    ))
    elements.append(Spacer(1, 4))

    elements.append(Paragraph(
        f"<b>FORM SIX REGIONAL MOCK EXAMINATIONS {project.name.upper()} {project.year}</b>",
        subtitle_style
    ))
    elements.append(Spacer(1, 4))

    elements.append(Paragraph(
        f"<b>MARKING PROGRESS</b>",
        styles["Heading3"]
    ))
    elements.append(Spacer(1, 8))

    # ================= SUMMARY STATS =================
    # Create summary table
    current_time = datetime.now().strftime("%A, %B %d, %Y %I:%M:%S %p")
    completion_pct = f"{completion_percent}%"
    remaining_pct = f"{100 - completion_percent}%"
    
    summary_data = [
        ["GENERAL REPORT", "", "", ""],
        [f"DUE DATE:", current_time, "", ""],
        ["", "", "", ""],
        ["DONE WORK-LOAD:", f"{total_marked}", f"{completion_pct}", ""],
        ["REMAIN WORK-LOAD:", f"{remaining}", f"{remaining_pct}", ""],
    ]
    
    summary_table = Table(summary_data, colWidths=[120, 200, 80, 100])
    summary_table.setStyle(TableStyle([
        ('SPAN', (0, 0), (-1, 0)),
        ('ALIGN', (0, 0), (-1, 0), 'CENTER'),
        ('FONTNAME', (0, 0), (-1, 0), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, 0), 12),
        ('SPAN', (1, 1), (-1, 1)),
        ('ALIGN', (0, 3), (0, -1), 'RIGHT'),
        ('ALIGN', (1, 3), (1, -1), 'CENTER'),
        ('FONTNAME', (0, 3), (0, -1), 'Helvetica-Bold'),
        ('FONTNAME', (1, 3), (1, -1), 'Helvetica-Bold'),
        ('GRID', (0, 3), (2, -1), 0.5, colors.black),
        ('BACKGROUND', (0, 3), (2, -1), colors.lightgrey),
    ]))
    
    elements.append(summary_table)
    elements.append(Spacer(1, 15))

    # ================= PAPER SUBJECT MARKING PROGRESS TABLE =================
    elements.append(Paragraph(
        "<b>PAPER SUBJECT MARKING PROGRESS</b>",
        styles["Heading3"]
    ))
    elements.append(Spacer(1, 8))

    # Calculate column widths
    subject_col_width = 80
    reg_paper_col_width = 35
    reg_subject_col_width = 45
    date_col_width = 40
    remain_subject_col_width = 40
    done_subject_col_width = 35
    remain_paper_col_width = 40
    done_paper_col_width = 35
    percent_col_width = 35

    # Build headers
    header_1 = [
        "SUBJECTS",
        "REG IN PAPER",
        "REG IN SUBJECTS",
    ]
    
    # Add date headers
    for date in date_columns:
        header_1.append(date.strftime("%m/%d/%Y"))
    
    # Add remaining headers
    header_1.extend([
        "REMAIN PER SUBJECT",
        "",
        "DONE PER SUBJECT",
        "",
        "REMAIN PER PAPER",
        "",
        "DONE PER PAPER",
        "",
    ])

    header_2 = [""] * len(header_1)
    
    # Mark the percent columns
    percent_indices = [
        len(header_1) - 8,  # Remain per subject %
        len(header_1) - 6,  # Done per subject %
        len(header_1) - 4,  # Remain per paper %
        len(header_1) - 2,  # Done per paper %
    ]
    
    for idx in percent_indices:
        header_2[idx] = "%"
    
    table_data = [header_1, header_2]

    # ================= DATA ROWS =================
    for subject_data in data:
        subject = subject_data['subject']
        papers = subject_data['papers']
        
        subject_registered = subject_data['subject_registered']
        subject_marked = subject_data['subject_marked']
        subject_remaining = subject_registered - subject_marked
        subject_percent = round((subject_marked / subject_registered * 100), 1) if subject_registered else 0
        
        # First paper row (with subject name)
        first_paper = papers[0]
        
        row = [
            Paragraph(subject.subject_name_eng[:20], wrap_style),  # Subject
            first_paper['registered'],  # REG IN PAPER
            subject_registered,  # REG IN SUBJECTS
        ]
        
        # Daily counts
        for count in first_paper['daily_counts']:
            row.append(count or "")
        
        # Remaining per subject
        row.extend([
            subject_remaining,
            f"{100 - subject_percent}%",
            subject_marked,
            f"{subject_percent}%",
        ])
        
        # Remaining per paper (for first paper)
        row.extend([
            first_paper['remaining'],
            f"{round((first_paper['remaining'] / first_paper['registered'] * 100), 1) if first_paper['registered'] else 0}%",
            first_paper['marked'],
            f"{round((first_paper['marked'] / first_paper['registered'] * 100), 1) if first_paper['registered'] else 0}%",
        ])
        
        table_data.append(row)
        
        # Additional paper rows (if any)
        for i in range(1, len(papers)):
            paper = papers[i]
            
            row = [
                "",  # No subject name
                paper['registered'],  # REG IN PAPER
                "",  # No subject total
            ]
            
            # Daily counts
            for count in paper['daily_counts']:
                row.append(count or "")
            
            # Empty cells for subject totals
            row.extend(["", "", "", ""])
            
            # Paper specific data
            row.extend([
                paper['remaining'],
                f"{round((paper['remaining'] / paper['registered'] * 100), 1) if paper['registered'] else 0}%",
                paper['marked'],
                f"{round((paper['marked'] / paper['registered'] * 100), 1) if paper['registered'] else 0}%",
            ])
            
            table_data.append(row)

    # ================= TOTAL ROW =================
    total_row = ["TOTAL", "", ""]
    
    # Calculate totals for dates
    for i in range(len(date_columns)):
        total = 0
        for subject_data in data:
            for paper in subject_data['papers']:
                if i < len(paper['daily_counts']):
                    total += paper['daily_counts'][i]
        total_row.append(total or "")
    
    # Subject totals
    total_row.extend([
        remaining,
        f"{100 - completion_percent}%",
        total_marked,
        f"{completion_percent}%",
        remaining,  # Total remaining per paper
        f"{100 - completion_percent}%",
        total_marked,  # Total done per paper
        f"{completion_percent}%",
    ])
    
    table_data.append(total_row)

    # ================= CREATE TABLE =================
    # Build column widths
    col_widths = [
        subject_col_width,
        reg_paper_col_width,
        reg_subject_col_width,
    ]
    
    # Date columns
    for _ in date_columns:
        col_widths.append(date_col_width)
    
    # Subject remain/done columns (4 columns with percents)
    col_widths.extend([
        remain_subject_col_width,
        percent_col_width,
        done_subject_col_width,
        percent_col_width,
    ])
    
    # Paper remain/done columns (4 columns with percents)
    col_widths.extend([
        remain_paper_col_width,
        percent_col_width,
        done_paper_col_width,
        percent_col_width,
    ])

    table = Table(table_data, colWidths=col_widths, repeatRows=2)
    
    # Build style
    style = [
        ('GRID', (0, 0), (-1, -1), 0.5, colors.black),
        ('BOX', (0, 0), (-1, -1), 1, colors.black),
        ('ALIGN', (0, 0), (-1, -1), 'CENTER'),
        ('VALIGN', (0, 0), (-1, -1), 'MIDDLE'),
        ('FONTNAME', (0, 0), (-1, 1), 'Helvetica-Bold'),
        ('FONTSIZE', (0, 0), (-1, -1), 7),
        ('BACKGROUND', (0, 0), (-1, 1), colors.lightgrey),
        ('BACKGROUND', (0, -1), (-1, -1), colors.lightblue),
        ('FONTNAME', (0, -1), (-1, -1), 'Helvetica-Bold'),
    ]
    
    # Span headers for date columns
    if date_columns:
        date_start = 3
        date_end = 3 + len(date_columns) - 1
        style.append(('SPAN', (date_start, 0), (date_end, 0)))
    
    # Span for subject remain/done
    remain_subject_start = 3 + len(date_columns)
    remain_subject_end = remain_subject_start + 1
    style.append(('SPAN', (remain_subject_start, 0), (remain_subject_end, 0)))
    
    done_subject_start = remain_subject_end + 1
    done_subject_end = done_subject_start + 1
    style.append(('SPAN', (done_subject_start, 0), (done_subject_end, 0)))
    
    # Span for paper remain/done
    remain_paper_start = done_subject_end + 1
    remain_paper_end = remain_paper_start + 1
    style.append(('SPAN', (remain_paper_start, 0), (remain_paper_end, 0)))
    
    done_paper_start = remain_paper_end + 1
    done_paper_end = done_paper_start + 1
    style.append(('SPAN', (done_paper_start, 0), (done_paper_end, 0)))
    
    table.setStyle(TableStyle(style))
    
    elements.append(table)
    
    # ================= FOOTER =================
    elements.append(Spacer(1, 10))
    elements.append(Paragraph(
        f"<i>Generated on {datetime.now().strftime('%A, %B %d, %Y %I:%M:%S %p')}</i>",
        styles["Italic"]
    ))

    # Build document with page numbers
    doc.build(elements, onFirstPage=_on_page, onLaterPages=_on_page)
    return response










from reportlab.lib.pagesizes import A4, landscape
from reportlab.platypus import (
    SimpleDocTemplate, Table, TableStyle,
    Paragraph, Spacer
)
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from django.shortcuts import get_object_or_404
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required

from reportlab.lib.units import mm
from django.utils.timezone import now


@login_required
def project_marking_progress_pdf(request, project_code):

    project = get_object_or_404(JointExamProject, project_code=project_code)

    response = HttpResponse(content_type="application/pdf")
    response["Content-Disposition"] = (
        f'inline; filename="MARKING_PROGRESS_{project.id}.pdf"'
    )

    # ✅ Landscape is REQUIRED for this report
    doc = SimpleDocTemplate(
        response,
        pagesize=landscape(A4),
        topMargin=30,
        bottomMargin=25,
        leftMargin=25,
        rightMargin=25
    )

    styles = getSampleStyleSheet()

    # Smaller header styles (H4-like)
    title_style = ParagraphStyle(
        "titleStyle",
        parent=styles["Normal"],
        fontSize=11,
        leading=14,
        alignment=1,  # center
        spaceAfter=4,
        fontName="Helvetica-Bold"
    )

    subtitle_style = ParagraphStyle(
        "subtitleStyle",
        parent=styles["Normal"],
        fontSize=10,
        leading=13,
        alignment=1,
        fontName="Helvetica-Bold"
    )

    elements = []

    # ================= HEADER =================
    elements.append(Paragraph(
        "PRIME MINISTER'S OFFICE - REGIONAL ADMINISTRATION AND LOCAL GOVERNMENT (PO-RALG)",
        title_style
    ))
    elements.append(Paragraph(
        f"FORM SIX REGIONAL MOCK EXAMINATIONS {project.year}",
        title_style
    ))
    elements.append(Paragraph(
        "MARKING PROGRESS",
        subtitle_style
    ))
    elements.append(Spacer(1, 10))

    # ================= SUMMARY =================
    total_registered = ExamScore.objects.filter(project=project).count()
    total_done = ExamScore.objects.filter(
        project=project,
        updated_by_id__gt=0
    ).count()
    total_remain = total_registered - total_done

    done_pct = (total_done / total_registered * 100) if total_registered else 0
    remain_pct = 100 - done_pct

    elements.append(Paragraph(
        f"<b>DONE WORK-LOAD:</b> {total_done} ({done_pct:.1f}%)",
        styles["Normal"]
    ))
    elements.append(Paragraph(
        f"<b>REMAIN WORK-LOAD:</b> {total_remain} ({remain_pct:.1f}%)",
        styles["Normal"]
    ))
    elements.append(Spacer(1, 12))

    # ================= TABLE HEADER =================
    data = [[
        "SUBJECT",
        "REG IN PAPER",
        "REG IN SUBJECT",
        "DONE PER SUBJECT",
        "%",
        "REMAIN PER SUBJECT",
        "%",
        "DONE PER PAPER",
        "%",
        "REMAIN PER PAPER",
        "%"
    ]]

    # Grand totals
    grand_reg = 0
    grand_done = 0
    grand_remain = 0

    # ================= SUBJECT LOOP =================
    subjects = (
        ExamSubject.objects
        .filter(papers__examscore__project=project)
        .distinct()
        .order_by("subject_name_eng")
    )

    for subject in subjects:

        papers = (
            subject.papers
            .filter(examscore__project=project)
            .distinct()
            .order_by("paper_code")
        )

        # SUBJECT TOTALS
        reg_subject = ExamScore.objects.filter(
            project=project,
            paper__in=papers
        ).count()

        done_subject = ExamScore.objects.filter(
            project=project,
            paper__in=papers,
            updated_by_id__gt=0
        ).count()

        remain_subject = reg_subject - done_subject

        done_pct_s = (done_subject / reg_subject * 100) if reg_subject else 0
        rem_pct_s = 100 - done_pct_s

        # Accumulate GRAND totals (once per subject)
        grand_reg += reg_subject
        grand_done += done_subject
        grand_remain += remain_subject

        # PAPER ROWS
        for paper in papers:

            reg_paper = ExamScore.objects.filter(
                project=project,
                paper=paper
            ).count()

            done_paper = ExamScore.objects.filter(
                project=project,
                paper=paper,
                updated_by_id__gt=0
            ).count()

            remain_paper = reg_paper - done_paper

            done_pct_p = (done_paper / reg_paper * 100) if reg_paper else 0
            rem_pct_p = 100 - done_pct_p

            data.append([
                subject.subject_name_eng,
                reg_paper,
                reg_subject,
                done_subject,
                f"{done_pct_s:.0f}%",
                remain_subject,
                f"{rem_pct_s:.0f}%",
                done_paper,
                f"{done_pct_p:.1f}%",
                remain_paper,
                f"{rem_pct_p:.1f}%"
            ])

    # ================= TOTAL ROW =================
    if grand_reg > 0:
        done_pct = (grand_done / grand_reg) * 100
        remain_pct = 100 - done_pct
    else:
        done_pct = remain_pct = 0

    data.append([
        "TOTAL",
        "",
        grand_reg,
        grand_done,
        f"{done_pct:.1f}%",
        grand_remain,
        f"{remain_pct:.1f}%",
        grand_done,
        f"{done_pct:.1f}%",
        grand_remain,
        f"{remain_pct:.1f}%"
    ])

    # ================= TABLE =================
    table = Table(data, repeatRows=1)
    table.setStyle(TableStyle([

        # Grid
        ("GRID", (0, 0), (-1, -1), 0.6, colors.black),

        # Header styling
        ("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
        ("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),

        # Alignment
        ("ALIGN", (1, 1), (-1, -1), "CENTER"),
        ("VALIGN", (0, 0), (-1, -1), "MIDDLE"),

        # Font size
        ("FONTSIZE", (0, 0), (-1, -1), 8),

        # TOTAL row highlight
        ("FONTNAME", (0, -1), (-1, -1), "Helvetica-Bold"),
        ("BACKGROUND", (0, -1), (-1, -1), colors.whitesmoke),
    ]))

    elements.append(table)

    doc.build(
        elements,
        onFirstPage=draw_header_footer,
        onLaterPages=draw_header_footer
    )
    return response











from exams.views_helpers import build_project_school_summary_context

@login_required
def project_school_summary_html(request, project_code, school_code):
    """
    Display comprehensive school summary for joint exam projects
    """
    context = build_project_school_summary_context(project_id, school_code)
    
    # Add all projects for the modal
    context['projects'] = JointExamProject.objects.filter(is_live=True).order_by('-year')
    
    return render(
        request,
        "exams/reports/project_school_summary_html.html",
        context
    )

@login_required
def project_school_summary_print(request, project_code, school_code):
    """
    Print-friendly version of school summary
    """
    context = build_project_school_summary_context(project_id, school_code)
    
    return render(
        request,
        "exams/reports/project_school_summary_print.html",
        context
    )








from exams.views_helpers import build_project_school_detailed_results
@login_required
def project_school_detailed_results(request, project_code, school_code):
    """
    Display detailed school results with student-level data
    """
    context = build_project_school_detailed_results(project_id, school_code)
    
    return render(
        request,
        "exams/reports/project_school_detailed_results.html",
        context
    )

@login_required
def project_school_detailed_results_print(request, project_code, school_code):
    """
    Print-friendly version of detailed school results
    """
    context = build_project_school_detailed_results(project_id, school_code)
    
    return render(
        request,
        "exams/reports/project_school_detailed_results_print.html",  # Direct to print template
        context
    )



from exams.views_helpers import prepare_school_detailed_results_excel_data
import openpyxl
from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
from openpyxl.utils import get_column_letter
from django.http import HttpResponse
from django.contrib.auth.decorators import login_required
from django.shortcuts import get_object_or_404

@login_required
def export_school_detailed_results_excel(request, project_code, school_code):
    """
    Export school detailed results to Excel
    """
    data = prepare_school_detailed_results_excel_data(project_id, school_code)
    
    project = data['project']
    school = data['school']
    subjects = data['subjects']
    students = data['students']
    division_counts = data['division_counts']
    school_result = data['school_result']
    school_rank = data['school_rank']
    overall_total = data['overall_total']
    total_students = data['total_students']
    generated_at = data['generated_at']
    
    # Create workbook
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = f"{school.school_code}_Results"
    
    # Define styles
    bold_font = Font(bold=True, size=11)
    header_font = Font(bold=True, size=10)
    normal_font = Font(size=9)
    center_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
    left_alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
    right_alignment = Alignment(horizontal='right', vertical='center', wrap_text=True)
    
    thin_border = Border(
        left=Side(style='thin'), 
        right=Side(style='thin'), 
        top=Side(style='thin'), 
        bottom=Side(style='thin')
    )
    
    # Title row
    ws.merge_cells('A1:Z1')
    ws['A1'] = "PRIME MINISTER'S OFFICE - REGIONAL ADMINISTRATION AND LOCAL GOVERNMENT (PMO-RALG)"
    ws['A1'].font = Font(bold=True, size=14)
    ws['A1'].alignment = center_alignment
    
    ws.merge_cells('A2:Z2')
    ws['A2'] = f"{project.name.upper()} - {project.year}"
    ws['A2'].font = Font(bold=True, size=12)
    ws['A2'].alignment = center_alignment
    
    # School info
    ws.merge_cells('A4:C4')
    ws['A4'] = f"School Code: {school.school_code}"
    ws['A4'].font = bold_font
    
    ws.merge_cells('D4:G4')
    ws['D4'] = f"School Name: {school.name}"
    ws['D4'].font = bold_font
    
    ws.merge_cells('H4:J4')
    ws['H4'] = f"Location: {school.district.name} - {school.region.name}"
    ws['H4'].font = bold_font
    ws['H4'].alignment = right_alignment
    
    # Division summary
    start_row = 6
    
    # Division headers
    ws.cell(row=start_row, column=1, value="DIVISION").font = bold_font
    ws.cell(row=start_row, column=2, value="I").font = bold_font
    ws.cell(row=start_row, column=3, value="II").font = bold_font
    ws.cell(row=start_row, column=4, value="III").font = bold_font
    ws.cell(row=start_row, column=5, value="IV").font = bold_font
    ws.cell(row=start_row, column=6, value="0").font = bold_font
    ws.cell(row=start_row, column=8, value="GPA").font = bold_font
    ws.cell(row=start_row, column=10, value="RANK(OVERALL)").font = bold_font
    
    # Division values
    ws.cell(row=start_row+1, column=1, value="COUNT").font = bold_font
    ws.cell(row=start_row+1, column=2, value=division_counts['I'])
    ws.cell(row=start_row+1, column=3, value=division_counts['II'])
    ws.cell(row=start_row+1, column=4, value=division_counts['III'])
    ws.cell(row=start_row+1, column=5, value=division_counts['IV'])
    ws.cell(row=start_row+1, column=6, value=division_counts['0'])
    
    gpa_value = school_result.sch_gpa if school_result and school_result.sch_gpa else "-"
    ws.cell(row=start_row+1, column=8, value=float(gpa_value) if gpa_value != "-" else "-")
    
    rank_value = f"{school_rank}/{overall_total}" if school_rank else "-"
    ws.cell(row=start_row+1, column=10, value=rank_value)
    
    # Total row
    ws.cell(row=start_row+3, column=1, value="TOTAL").font = bold_font
    ws.cell(row=start_row+3, column=2, value=division_counts['I'])
    ws.cell(row=start_row+3, column=3, value=division_counts['II'])
    ws.cell(row=start_row+3, column=4, value=division_counts['III'])
    ws.cell(row=start_row+3, column=5, value=division_counts['IV'])
    ws.cell(row=start_row+3, column=6, value=division_counts['0'])
    
    # Students table header
    table_start_row = start_row + 6
    
    # First header row
    col = 1
    ws.cell(row=table_start_row, column=col, value="STUDENT DETAILS").font = header_font
    ws.merge_cells(start_row=table_start_row, start_column=col, end_row=table_start_row, end_column=col+3)
    
    col = 5
    subject_cols = len(subjects) * 2
    ws.cell(row=table_start_row, column=col, value="SUBJECT SCORES AND GRADES").font = header_font
    ws.merge_cells(start_row=table_start_row, start_column=col, end_row=table_start_row, end_column=col+subject_cols-1)
    
    col = 5 + subject_cols
    ws.cell(row=table_start_row, column=col, value="STATUS").font = header_font
    ws.merge_cells(start_row=table_start_row, start_column=col, end_row=table_start_row, end_column=col+3)
    
    # Second header row
    table_start_row += 1
    
    # Student details columns
    ws.cell(row=table_start_row, column=1, value="C.NO").font = header_font
    ws.cell(row=table_start_row, column=2, value="NAMES").font = header_font
    ws.cell(row=table_start_row, column=3, value="SEX").font = header_font
    ws.cell(row=table_start_row, column=4, value="COMB").font = header_font
    
    # Subject columns (2 per subject)
    col = 5
    for subject in subjects:
        ws.cell(row=table_start_row, column=col, value=f"{subject.subject_code}-{subject.subject_name_eng}\nSCORE").font = header_font
        ws.cell(row=table_start_row, column=col+1, value=f"{subject.subject_code}-{subject.subject_name_eng}\nGRADE").font = header_font
        col += 2
    
    # Status columns
    ws.cell(row=table_start_row, column=col, value="AVERAGE").font = header_font
    ws.cell(row=table_start_row, column=col+1, value="AV.GRADES").font = header_font
    ws.cell(row=table_start_row, column=col+2, value="POINTS").font = header_font
    ws.cell(row=table_start_row, column=col+3, value="DIVISION").font = header_font
    
    # Apply borders to header
    for row in range(table_start_row-1, table_start_row+1):
        for col in range(1, col+4):
            cell = ws.cell(row=row, column=col)
            cell.border = thin_border
            cell.alignment = center_alignment
    
    # Student data rows
    data_start_row = table_start_row + 1
    
    for idx, student in enumerate(students):
        row = data_start_row + idx
        
        # Student details
        ws.cell(row=row, column=1, value=student['cand_no']).alignment = left_alignment
        ws.cell(row=row, column=2, value=student['name']).alignment = left_alignment
        ws.cell(row=row, column=3, value=student['sex']).alignment = center_alignment
        ws.cell(row=row, column=4, value=student['combination']).alignment = center_alignment
        
        # Subject scores
        col = 5
        for score_data in student['subject_scores']:
            ws.cell(row=row, column=col, value=score_data['score']).alignment = right_alignment
            ws.cell(row=row, column=col+1, value=score_data['grade']).alignment = center_alignment
            col += 2
        
        # Status columns
        ws.cell(row=row, column=col, value=student['average']).alignment = right_alignment
        ws.cell(row=row, column=col+1, value=student['avg_grade']).alignment = center_alignment
        ws.cell(row=row, column=col+2, value=student['points']).alignment = center_alignment
        ws.cell(row=row, column=col+3, value=student['division']).alignment = center_alignment
        
        # Apply borders to data row
        for c in range(1, col+4):
            cell = ws.cell(row=row, column=c)
            cell.border = thin_border
            cell.font = normal_font
    
    # Set column widths
    ws.column_dimensions['A'].width = 15  # C.NO
    ws.column_dimensions['B'].width = 30  # NAMES
    ws.column_dimensions['C'].width = 6   # SEX
    ws.column_dimensions['D'].width = 8   # COMB
    
    # Subject columns
    col = 5
    for i, subject in enumerate(subjects):
        ws.column_dimensions[get_column_letter(col)].width = 8   # Score
        ws.column_dimensions[get_column_letter(col+1)].width = 7  # Grade
        col += 2
    
    # Status columns
    ws.column_dimensions[get_column_letter(col)].width = 8     # AVERAGE
    ws.column_dimensions[get_column_letter(col+1)].width = 8   # AV.GRADES
    ws.column_dimensions[get_column_letter(col+2)].width = 7   # POINTS
    ws.column_dimensions[get_column_letter(col+3)].width = 8   # DIVISION
    
    # Freeze panes
    ws.freeze_panes = ws.cell(row=table_start_row+1, column=5)
    
    # Add footer with generation time
    footer_row = data_start_row + len(students) + 2
    ws.cell(row=footer_row, column=1, value=f"Generated on: {generated_at}")
    ws.cell(row=footer_row, column=1).font = Font(italic=True, size=8)
    
    # Create response
    filename = f"{school.school_code}_{project.name}_{project.year}_Detailed_Results.xlsx"
    response = HttpResponse(
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    
    wb.save(response)
    return response

from django.shortcuts import render, get_object_or_404
from django.contrib.auth.decorators import login_required
from exams.models import (
    JointExamProject, School, ExamSchoolResult, 
    StudentFinalResult, ProjectStudent, ExamSubject,
    ProcessedSubjectScore
)

@login_required
def necta_format_results(request, project_code, school_code):
    """
    Simple NECTA format view showing only subjects with marks
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, school_code=school_code)
    
    # =====================================================
    # 1. SCHOOL SUMMARY DATA
    # =====================================================
    school_result = ExamSchoolResult.objects.filter(
        project_id=project_id,
        school=school
    ).first()
    
    # =====================================================
    # 2. DIVISION COUNTS
    # =====================================================
    final_results = StudentFinalResult.objects.filter(
        project_id=project_id,
        student__school=school
    )
    
    division_counts = {
        'I': final_results.filter(division='I').count(),
        'II': final_results.filter(division='II').count(),
        'III': final_results.filter(division='III').count(),
        'IV': final_results.filter(division='IV').count(),
        '0': final_results.filter(division='0').count(),
    }
    
    # =====================================================
    # 3. SCHOOL RANK
    # =====================================================
    all_school_results = ExamSchoolResult.objects.filter(
        project_id=project_id,
        sch_gpa__isnull=False
    ).order_by('sch_gpa')
    
    overall_total = all_school_results.count()
    school_rank = None
    if school_result:
        school_list = list(all_school_results.values_list('id', flat=True))
        if school_result.id in school_list:
            school_rank = school_list.index(school_result.id) + 1
    
    # =====================================================
    # 4. GET STUDENTS WITH THEIR MARKS
    # =====================================================
    students = ProjectStudent.objects.filter(
        project_id=project_id,
        school=school
    ).select_related('combination').order_by('student_number')
    
    # Get all processed scores for this school
    all_scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school
    ).select_related('subject', 'student')
    
    # Group scores by student
    scores_by_student = {}
    subjects_with_marks = set()
    
    for score in all_scores:
        student_id = score.student_id
        if student_id not in scores_by_student:
            scores_by_student[student_id] = []
        
        # Only add if there's a valid grade (not None, not empty)
        if score.grade and score.grade.strip() and score.grade != '-':
            scores_by_student[student_id].append({
                'subject_code': score.subject.subject_code,
                'subject_name': score.subject.subject_name_eng,
                'subject_short': score.subject.subject_shortname or score.subject.subject_code,
                'grade': score.grade,
                'score': score.percentage_score,
            })
            subjects_with_marks.add(score.subject.subject_code)
    
    # Get final results
    final_results_map = {
        r.student_id: r for r in StudentFinalResult.objects.filter(
            project_id=project_id,
            student__school=school
        )
    }
    
    # =====================================================
    # 5. BUILD STUDENT ROWS (ONLY SUBJECTS WITH MARKS)
    # =====================================================
    student_rows = []
    
    for student in students:
        student_scores = scores_by_student.get(student.id, [])
        final = final_results_map.get(student.id)
        
        # Sort subjects by code for consistent display
        student_scores.sort(key=lambda x: x['subject_code'])
        
        # Create subject pairs for display
        subject_pairs = []
        for score in student_scores:
            subject_pairs.append({
                'name': score['subject_short'],
                'grade': score['grade'],
                'score': score['score'],
            })
        
        student_rows.append({
            'cand_no': student.student_number,
            'name': student.full_name,
            'sex': student.sex,
            'combination': student.combination.code if student.combination else '',
            'subject_pairs': subject_pairs,
            'points': final.total_points if final else '',
            'division': final.division if final else '',
            'has_marks': len(subject_pairs) > 0,
        })
    
    # Filter out students with no marks (optional - comment out if you want to show all)
    student_rows = [s for s in student_rows if s['has_marks']]
    
    # =====================================================
    # 6. PREPARE CONTEXT
    # =====================================================
    context = {
        'project': project,
        'school': school,
        'school_result': school_result,
        'division_counts': division_counts,
        'school_rank': school_rank,
        'overall_total': overall_total,
        'students': student_rows,
        'total_students_with_marks': len(student_rows),
    }
    
    return render(request, 'exams/reports/necta_format_results.html', context)


@login_required
def ajax_get_school_subjects(request, project_code, school_code):
    """
    Get all subjects with papers for a specific school and project
    """
    try:
        # Get the school first to verify it exists
        school = School.objects.get(school_code=school_code)
        
        # Get subjects that have exam scores for this school and project
        subjects = ExamSubject.objects.filter(
            papers__examscore__project_id=project_id,
            papers__examscore__student__school=school
        ).distinct().order_by('subject_code')
        
        subject_list = []
        for subject in subjects:
            # Count papers for this subject that have exam scores
            paper_count = ExamSubjectPaper.objects.filter(
                subject=subject,
                examscore__project_id=project_id,
                examscore__student__school=school
            ).distinct().count()
            
            subject_list.append({
                'code': subject.subject_code,
                'name': subject.subject_name_eng,
                'short_name': subject.subject_shortname or subject.subject_code,
                'papers': paper_count,
            })
        
        # If no subjects found, try alternative query
        if not subject_list:
            # Get subjects from processed scores
            subjects = ExamSubject.objects.filter(
                processedsubjectscore__project_id=project_id,
                processedsubjectscore__student__school=school
            ).distinct().order_by('subject_code')
            
            for subject in subjects:
                subject_list.append({
                    'code': subject.subject_code,
                    'name': subject.subject_name_eng,
                    'short_name': subject.subject_shortname or subject.subject_code,
                    'papers': 1,  # Default to 1 paper
                })
        
        return JsonResponse({
            'success': True,
            'subjects': subject_list,
            'count': len(subject_list)
        })
        
    except School.DoesNotExist:
        return JsonResponse({
            'success': False,
            'error': 'School not found'
        }, status=404)
        
    except Exception as e:
        return JsonResponse({
            'success': False,
            'error': str(e)
        }, status=500)


@login_required
def subject_performance_report(request, project_code, school_code, subject_code, report_type):
    """
    Generate subject performance report with summary chart and student details
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, school_code=school_code)
    subject = get_object_or_404(ExamSubject, subject_code=subject_code)
    
    # Get papers for this subject
    papers = ExamSubjectPaper.objects.filter(
        subject=subject,
        examscore__project_id=project_id,
        examscore__student__school=school
    ).distinct().order_by('paper_code')
    
    # Get ONLY students who have scores for this subject
    student_ids_with_scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).values_list('student_id', flat=True).distinct()
    
    students = ProjectStudent.objects.filter(
        id__in=student_ids_with_scores,
        school=school
    ).order_by('student_number')
    
    # Get processed scores for this subject
    scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).select_related('student')
    
    # Build student performance data
    student_data = []
    grade_counts = {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0}
    gender_counts = {'M': {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0},
                     'F': {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0}}
    
    female_sat = 0
    male_sat = 0
    
    scores_map = {}
    for score in scores:
        student_id = score.student_id
        if student_id not in scores_map:
            scores_map[student_id] = {}
        
        # Store score data
        scores_map[student_id] = {
            'score': score.percentage_score,
            'grade': score.grade,
            'points': score.points,
        }
        
        # Update counts - only count students with valid grades
        if score.grade and score.grade in grade_counts:
            grade_counts[score.grade] += 1
            if score.student.sex == 'M':
                gender_counts['M'][score.grade] += 1
                male_sat += 1
            else:
                gender_counts['F'][score.grade] += 1
                female_sat += 1
    
    total_sat = female_sat + male_sat
    
    # Build paper-specific data (only for students who have scores)
    paper_scores = {}
    for paper in papers:
        paper_scores[paper.paper_code] = {}
        exam_scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper,
            student__school=school,
            student_id__in=student_ids_with_scores  # Only students who have scores
        ).select_related('student')
        
        for es in exam_scores:
            student_id = es.student_id
            if student_id not in paper_scores[paper.paper_code]:
                paper_scores[paper.paper_code][student_id] = {
                    'score': es.score,
                }
    
    # Build student rows - only for students with scores
    students_with_scores = []
    for student in students:
        score_data = scores_map.get(student.id, {})
        
        # Get paper scores for this student
        student_paper_scores = {}
        for paper_code, scores_dict in paper_scores.items():
            if student.id in scores_dict:
                student_paper_scores[paper_code] = scores_dict[student.id]['score']
        
        students_with_scores.append({
            'cand_no': student.student_number,
            'name': student.full_name,
            'sex': student.sex,
            'paper_scores': student_paper_scores,
            'final_score': score_data.get('score', ''),
            'final_grade': score_data.get('grade', ''),
            'points': score_data.get('points', ''),
        })
    
    context = {
        'project': project,
        'school': school,
        'subject': subject,
        'papers': papers,
        'students': students,  # All students (for reference)
        'students_with_scores': students_with_scores,  # Only those with scores
        'grade_counts': grade_counts,
        'gender_counts': gender_counts,
        'female_sat': female_sat,
        'male_sat': male_sat,
        'total_sat': total_sat,
        'total_students': len(students),  # Total with scores
        'report_type': report_type,
    }
    
    if report_type == 'summary':
        return render(request, 'exams/reports/subject_performance_summary.html', context)
    elif report_type == 'detailed':
        return render(request, 'exams/reports/subject_performance_detailed.html', context)
    else:
        return render(request, 'exams/reports/subject_performance_both.html', context)










@login_required
def subject_performance_excel(request, project_code, school_code, subject_code, report_type):
    """
    Export subject performance to Excel
    """
    from openpyxl import Workbook
    from openpyxl.styles import Font, Alignment, Border, Side
    
    # Get the data directly without going through the view
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, school_code=school_code)
    subject = get_object_or_404(ExamSubject, subject_code=subject_code)
    
    # Get papers for this subject
    papers = ExamSubjectPaper.objects.filter(
        subject=subject,
        examscore__project_id=project_id,
        examscore__student__school=school
    ).distinct().order_by('paper_code')
    
    # Get ONLY students who have scores for this subject
    student_ids_with_scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).values_list('student_id', flat=True).distinct()
    
    students = ProjectStudent.objects.filter(
        id__in=student_ids_with_scores,
        school=school
    ).order_by('student_number')
    
    # Get processed scores for this subject
    scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).select_related('student')
    
    # Build student performance data
    grade_counts = {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0}
    gender_counts = {'M': {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0},
                     'F': {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0}}
    
    female_sat = 0
    male_sat = 0
    
    scores_map = {}
    for score in scores:
        student_id = score.student_id
        if student_id not in scores_map:
            scores_map[student_id] = {}
        
        # Store score data
        scores_map[student_id] = {
            'score': score.percentage_score,
            'grade': score.grade,
            'points': score.points,
        }
        
        # Update counts - only count students with valid grades
        if score.grade and score.grade in grade_counts:
            grade_counts[score.grade] += 1
            if score.student.sex == 'M':
                gender_counts['M'][score.grade] += 1
                male_sat += 1
            else:
                gender_counts['F'][score.grade] += 1
                female_sat += 1
    
    total_sat = female_sat + male_sat
    
    # Build paper-specific data (only for students who have scores)
    paper_scores = {}
    for paper in papers:
        paper_scores[paper.paper_code] = {}
        exam_scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper,
            student__school=school,
            student_id__in=student_ids_with_scores
        ).select_related('student')
        
        for es in exam_scores:
            student_id = es.student_id
            if student_id not in paper_scores[paper.paper_code]:
                paper_scores[paper.paper_code][student_id] = {
                    'score': es.score,
                }
    
    # Build student rows - only for students with scores
    students_with_scores = []
    for student in students:
        score_data = scores_map.get(student.id, {})
        
        # Get paper scores for this student
        student_paper_scores = {}
        for paper_code, scores_dict in paper_scores.items():
            if student.id in scores_dict:
                student_paper_scores[paper_code] = scores_dict[student.id]['score']
        
        students_with_scores.append({
            'cand_no': student.student_number,
            'name': student.full_name,
            'sex': student.sex,
            'paper_scores': student_paper_scores,
            'final_score': score_data.get('score', ''),
            'final_grade': score_data.get('grade', ''),
            'points': score_data.get('points', ''),
        })
    
    # Create Excel file
    wb = Workbook()
    ws = wb.active
    ws.title = f"{subject.subject_code}_Performance"
    
    # Title
    ws.merge_cells('A1:H1')
    ws['A1'] = f"{project.name} - {school.name} - {subject.subject_name_eng} Performance"
    ws['A1'].font = Font(bold=True, size=14)
    ws['A1'].alignment = Alignment(horizontal='center')
    
    # Headers
    headers = ['Student No', 'Name', 'Sex']
    for paper in papers:
        headers.append(paper.paper_code)
    headers.extend(['Final Score', 'Grade', 'Points'])
    
    ws.append(headers)
    
    # Style headers
    for col in range(1, len(headers)+1):
        cell = ws.cell(row=2, column=col)
        cell.font = Font(bold=True)
        cell.alignment = Alignment(horizontal='center')
    
    # Data rows
    for student in students_with_scores:
        row = [
            student['cand_no'],
            student['name'],
            student['sex']
        ]
        for paper in papers:
            row.append(student['paper_scores'].get(paper.paper_code, '-'))
        row.extend([
            student['final_score'],
            student['final_grade'],
            student['points']
        ])
        ws.append(row)
    # Auto-fit columns - skip merged cells
    from openpyxl.cell import MergedCell
    from openpyxl.utils import get_column_letter

    for col_idx, column in enumerate(ws.columns, 1):
        max_length = 0
        for cell in column:
            if not isinstance(cell, MergedCell):  # Skip merged cells
                try:
                    if cell.value and len(str(cell.value)) > max_length:
                        max_length = len(str(cell.value))
                except:
                    pass
        if max_length > 0:
            adjusted_width = min(max_length + 2, 50)
            # Use get_column_letter with col_idx instead of trying to get from a cell
            column_letter = get_column_letter(col_idx)
            ws.column_dimensions[column_letter].width = adjusted_width
    # Response
    filename = f"{school.school_code}_{subject.subject_code}_performance.xlsx"
    response = HttpResponse(
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    wb.save(response)
    return response





@login_required
def subject_performance_print(request, project_code, school_code, subject_code, report_type):
    """
    Print-friendly version of subject performance
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, school_code=school_code)
    subject = get_object_or_404(ExamSubject, subject_code=subject_code)
    
    # Get papers for this subject
    papers = ExamSubjectPaper.objects.filter(
        subject=subject,
        examscore__project_id=project_id,
        examscore__student__school=school
    ).distinct().order_by('paper_code')
    
    # Get ONLY students who have scores for this subject
    student_ids_with_scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).values_list('student_id', flat=True).distinct()
    
    students = ProjectStudent.objects.filter(
        id__in=student_ids_with_scores,
        school=school
    ).order_by('student_number')
    
    # Get processed scores for this subject
    scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).select_related('student')
    
    # Build student performance data
    grade_counts = {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0}
    gender_counts = {'M': {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0},
                     'F': {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'E': 0, 'F': 0, 'S': 0}}
    
    female_sat = 0
    male_sat = 0
    
    scores_map = {}
    for score in scores:
        student_id = score.student_id
        if student_id not in scores_map:
            scores_map[student_id] = {}
        
        # Store score data
        scores_map[student_id] = {
            'score': score.percentage_score,
            'grade': score.grade,
            'points': score.points,
        }
        
        # Update counts - only count students with valid grades
        if score.grade and score.grade in grade_counts:
            grade_counts[score.grade] += 1
            if score.student.sex == 'M':
                gender_counts['M'][score.grade] += 1
                male_sat += 1
            else:
                gender_counts['F'][score.grade] += 1
                female_sat += 1
    
    total_sat = female_sat + male_sat
    
    # Build paper-specific data (only for students who have scores)
    paper_scores = {}
    for paper in papers:
        paper_scores[paper.paper_code] = {}
        exam_scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper,
            student__school=school,
            student_id__in=student_ids_with_scores
        ).select_related('student')
        
        for es in exam_scores:
            student_id = es.student_id
            if student_id not in paper_scores[paper.paper_code]:
                paper_scores[paper.paper_code][student_id] = {
                    'score': es.score,
                }
    
    # Build student rows - only for students with scores
    students_with_scores = []
    for student in students:
        score_data = scores_map.get(student.id, {})
        
        # Get paper scores for this student
        student_paper_scores = {}
        for paper_code, scores_dict in paper_scores.items():
            if student.id in scores_dict:
                student_paper_scores[paper_code] = scores_dict[student.id]['score']
        
        students_with_scores.append({
            'cand_no': student.student_number,
            'name': student.full_name,
            'sex': student.sex,
            'paper_scores': student_paper_scores,
            'final_score': score_data.get('score', ''),
            'final_grade': score_data.get('grade', ''),
            'points': score_data.get('points', ''),
        })
    
    context = {
        'project': project,
        'school': school,
        'subject': subject,
        'papers': papers,
        'students_with_scores': students_with_scores,
        'grade_counts': grade_counts,
        'gender_counts': gender_counts,
        'female_sat': female_sat,
        'male_sat': male_sat,
        'total_sat': total_sat,
        'report_type': report_type,
    }
    
    return render(request, 'exams/reports/subject_performance_print_standalone.html', context)
@login_required
def test_subject_data(request, project_code, school_code):
    """
    Test endpoint to check what subjects exist for a school
    """
    school = School.objects.get(school_code=school_code)
    
    # Check ExamScore data
    exam_subjects = ExamSubject.objects.filter(
        papers__examscore__project_id=project_id,
        papers__examscore__student__school=school
    ).distinct()
    
    # Check ProcessedSubjectScore data
    processed_subjects = ExamSubject.objects.filter(
        processedsubjectscore__project_id=project_id,
        processedsubjectscore__student__school=school
    ).distinct()
    
    result = {
        'school': school.school_code,
        'project_id': project_id,
        'exam_score_subjects': [
            {
                'code': s.subject_code,
                'name': s.subject_name_eng,
                'papers': ExamSubjectPaper.objects.filter(
                    subject=s,
                    examscore__project_id=project_id,
                    examscore__student__school=school
                ).distinct().count()
            }
            for s in exam_subjects
        ],
        'processed_score_subjects': [
            {
                'code': s.subject_code,
                'name': s.subject_name_eng,
            }
            for s in processed_subjects
        ],
        'exam_score_count': exam_subjects.count(),
        'processed_score_count': processed_subjects.count(),
    }
    
    return JsonResponse(result)
















@login_required
def project_school_paper_results(request, project_code, school_code, subject_code):
    """
    Display paper results for a specific subject in a school
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, school_code=school_code)
    subject = get_object_or_404(ExamSubject, subject_code=subject_code)
    
    # Get papers for this subject
    papers = ExamSubjectPaper.objects.filter(
        subject=subject,
        examscore__project_id=project_id,
        examscore__student__school=school
    ).distinct().order_by('paper_code')
    
    # Get students who have scores for this subject
    student_ids_with_scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).values_list('student_id', flat=True).distinct()
    
    students = ProjectStudent.objects.filter(
        id__in=student_ids_with_scores,
        school=school
    ).select_related('combination').order_by('student_number')
    
    # Get processed scores
    scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        student__school=school,
        subject=subject
    ).select_related('student')
    
    # Build data structures
    grade_counts = {'A': 0, 'B': 0, 'C': 0, 'D': 0, 'F': 0, 'S': 0}
    absent_count = 0
    
    scores_map = {}
    for score in scores:
        student_id = score.student_id
        if student_id not in scores_map:
            scores_map[student_id] = {}
        
        scores_map[student_id] = {
            'final_score': score.percentage_score,
            'final_grade': score.grade,
            'points': score.points,
        }
        
        if score.grade and score.grade in grade_counts:
            grade_counts[score.grade] += 1
        elif score.percentage_score == 'X':
            absent_count += 1
    
    # Get paper-specific scores
    paper_scores = {}
    for paper in papers:
        paper_scores[paper.paper_code] = {}
        exam_scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper,
            student__school=school,
            student_id__in=student_ids_with_scores
        ).select_related('student')
        
        for es in exam_scores:
            paper_scores[paper.paper_code][es.student_id] = es.score
    
    # Build student rows
    students_with_scores = []
    for student in students:
        score_data = scores_map.get(student.id, {})
        
        student_paper_scores = {}
        for paper_code, scores_dict in paper_scores.items():
            if student.id in scores_dict:
                student_paper_scores[paper_code] = scores_dict[student.id]
        
        students_with_scores.append({
            'cand_no': student.student_number,
            'name': student.full_name,
            'sex': student.sex,
            'combination': student.combination.code if student.combination else '',
            'paper_scores': student_paper_scores,
            'final_score': score_data.get('final_score', ''),
            'final_grade': score_data.get('final_grade', ''),
            'points': score_data.get('points', ''),
        })
    
    total_sat = len(students_with_scores)
    
    context = {
        'project': project,
        'school': school,
        'subject': subject,
        'papers': papers,
        'students_with_scores': students_with_scores,
        'grade_counts': grade_counts,
        'absent_count': absent_count,
        'total_sat': total_sat,
    }
    
    return render(request, 'exams/reports/project_school_paper_results.html', context)



@login_required
def project_school_all_papers_results(request, project_code, school_code):
    """
    Display results for all subject papers in a school using ExamScore data
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, school_code=school_code)
    
    # Get all papers that have scores for this school
    papers = ExamSubjectPaper.objects.filter(
        examscore__project_id=project_id,
        examscore__student__school=school
    ).select_related('subject').distinct().order_by('subject__subject_code', 'paper_code')
    
    # Get all students who have exam scores
    student_ids = ExamScore.objects.filter(
        project_id=project_id,
        student__school=school
    ).values_list('student_id', flat=True).distinct()
    
    students = ProjectStudent.objects.filter(
        id__in=student_ids,
        school=school
    ).select_related('combination').order_by('student_number')
    
    # Get all exam scores for this school
    exam_scores = ExamScore.objects.filter(
        project_id=project_id,
        student__school=school
    ).select_related('student', 'paper')
    
    # Organize scores by student and paper
    scores_map = {}
    for score in exam_scores:
        student_id = score.student_id
        paper_code = score.paper.paper_code
        if student_id not in scores_map:
            scores_map[student_id] = {}
        scores_map[student_id][paper_code] = score.score
    
    # Build student rows
    student_rows = []
    for student in students:
        student_scores = scores_map.get(student.id, {})
        
        # Create paper scores dictionary for this student
        paper_scores = {}
        for paper in papers:
            score = student_scores.get(paper.paper_code, '')
            paper_scores[paper.paper_code] = score if score else ''
        
        student_rows.append({
            'cand_no': student.student_number,
            'name': student.full_name,
            'sex': student.sex,
            'combination': student.combination.code if student.combination else '',
            'paper_scores': paper_scores,
        })
    
    # Group papers by subject for display
    subjects_with_papers = {}
    for paper in papers:
        subject_code = paper.subject.subject_code
        if subject_code not in subjects_with_papers:
            subjects_with_papers[subject_code] = {
                'code': subject_code,
                'name': paper.subject.subject_name_eng,
                'papers': []
            }
        subjects_with_papers[subject_code]['papers'].append({
            'code': paper.paper_code,
            'name': paper.paper_name,
            'full_code': paper.paper_code,
        })
    
    # Convert to sorted list
    subjects_list = sorted(subjects_with_papers.values(), key=lambda x: x['code'])
    
    context = {
        'project': project,
        'school': school,
        'subjects': subjects_list,
        'papers': papers,
        'students': student_rows,
        'student_count': len(student_rows),
    }
    
    return render(request, 'exams/reports/project_school_all_papers_results.html', context)


















@login_required
def export_school_all_papers_excel(request, project_code, school_code):
    """
    Export all papers results for a school to Excel
    """
    from openpyxl import Workbook
    from openpyxl.styles import Font, Alignment, Border, Side, PatternFill
    from openpyxl.utils import get_column_letter
    from datetime import datetime
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, school_code=school_code)
    
    # Get all papers that have scores for this school
    papers = ExamSubjectPaper.objects.filter(
        examscore__project_id=project_id,
        examscore__student__school=school
    ).select_related('subject').distinct().order_by('subject__subject_code', 'paper_code')
    
    # Get all students who have exam scores
    student_ids = ExamScore.objects.filter(
        project_id=project_id,
        student__school=school
    ).values_list('student_id', flat=True).distinct()
    
    students = ProjectStudent.objects.filter(
        id__in=student_ids,
        school=school
    ).select_related('combination').order_by('student_number')
    
    # Get all exam scores for this school
    exam_scores = ExamScore.objects.filter(
        project_id=project_id,
        student__school=school
    ).select_related('student', 'paper')
    
    # Organize scores by student and paper
    scores_map = {}
    for score in exam_scores:
        student_id = score.student_id
        paper_code = score.paper.paper_code
        if student_id not in scores_map:
            scores_map[student_id] = {}
        scores_map[student_id][paper_code] = score.score
    
    # Group papers by subject
    subjects_with_papers = {}
    for paper in papers:
        subject_code = paper.subject.subject_code
        if subject_code not in subjects_with_papers:
            subjects_with_papers[subject_code] = {
                'code': subject_code,
                'name': paper.subject.subject_name_eng,
                'papers': []
            }
        subjects_with_papers[subject_code]['papers'].append({
            'code': paper.paper_code,
            'name': paper.paper_name,
        })
    
    # Convert to sorted list
    subjects_list = sorted(subjects_with_papers.values(), key=lambda x: x['code'])
    
    # Create workbook
    wb = Workbook()
    ws = wb.active
    ws.title = f"{school.school_code}_All_Papers"
    
    # Define styles
    title_font = Font(bold=True, size=14)
    header_font = Font(bold=True, size=11)
    subject_font = Font(bold=True, size=10, color="FFFFFF")
    normal_font = Font(size=10)
    
    center_alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
    left_alignment = Alignment(horizontal='left', vertical='center')
    right_alignment = Alignment(horizontal='right', vertical='center')  # Added this line
    
    thin_border = Border(
        left=Side(style='thin'), 
        right=Side(style='thin'), 
        top=Side(style='thin'), 
        bottom=Side(style='thin')
    )
    
    # Title
    ws.merge_cells('A1:D1')
    ws['A1'] = "PRIME MINISTER'S OFFICE - REGIONAL ADMINISTRATION AND LOCAL GOVERNMENT"
    ws['A1'].font = title_font
    ws['A1'].alignment = center_alignment
    
    ws.merge_cells('A2:D2')
    ws['A2'] = f"{project.name}"
    ws['A2'].font = Font(bold=True, size=12)
    ws['A2'].alignment = center_alignment
    
    # School info
    ws['A4'] = f"School Code: {school.school_code}"
    ws['A4'].font = Font(bold=True)
    ws['B4'] = f"School Name: {school.name}"
    ws['B4'].font = Font(bold=True)
    ws['D4'] = f"Location: {school.district.name} - {school.region.name}"
    ws['D4'].font = Font(bold=True)
    ws['D4'].alignment = right_alignment
    
    # Calculate total columns
    current_col = 1
    
    # Student details columns
    student_cols = ['C.NO', 'NAMES', 'SEX', 'COMB']
    for col_name in student_cols:
        cell = ws.cell(row=6, column=current_col, value=col_name)
        cell.font = header_font
        cell.alignment = center_alignment
        cell.border = thin_border
        current_col += 1
    
    # Subject and paper headers
    paper_start_cols = {}
    for subject in subjects_list:
        subject_start = current_col
        subject_end = current_col + len(subject['papers']) - 1
        
        # Subject header (merged)
        if subject_start != subject_end:
            ws.merge_cells(start_row=6, start_column=subject_start, 
                          end_row=6, end_column=subject_end)
        subject_cell = ws.cell(row=6, column=subject_start, 
                               value=f"{subject['code']} - {subject['name']}")
        subject_cell.font = subject_font
        subject_cell.fill = PatternFill(start_color="343a40", end_color="343a40", fill_type="solid")
        subject_cell.alignment = center_alignment
        subject_cell.border = thin_border
        
        # Paper headers (row 7)
        for paper in subject['papers']:
            paper_cell = ws.cell(row=7, column=current_col, 
                                 value=f"{paper['code']}\n{paper['name']}")
            paper_cell.font = Font(bold=True, size=9)
            paper_cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
            paper_cell.border = thin_border
            current_col += 1
    
    # Student data rows
    row_num = 8
    for student in students:
        # Student details
        ws.cell(row=row_num, column=1, value=student.student_number).alignment = left_alignment
        ws.cell(row=row_num, column=2, value=student.full_name).alignment = left_alignment
        ws.cell(row=row_num, column=3, value=student.sex).alignment = center_alignment
        ws.cell(row=row_num, column=4, value=student.combination.code if student.combination else '').alignment = center_alignment
        
        # Get student scores
        student_scores = scores_map.get(student.id, {})
        
        # Paper scores
        col = 5
        for paper in papers:
            score = student_scores.get(paper.paper_code, '')
            cell = ws.cell(row=row_num, column=col, value=score if score else '')
            cell.alignment = center_alignment
            cell.font = Font(name='Courier New', size=9)
            cell.border = thin_border
            col += 1
        
        # Apply borders to all cells in this row
        for c in range(1, col):
            cell = ws.cell(row=row_num, column=c)
            cell.border = thin_border
        
        row_num += 1
    
    # If no students, add a message
    if not students:
        ws.merge_cells(start_row=8, start_column=1, end_row=8, end_column=current_col-1)
        ws.cell(row=8, column=1, value="No student data available for this school").alignment = center_alignment
    
    # Set column widths
    ws.column_dimensions['A'].width = 15  # C.NO
    ws.column_dimensions['B'].width = 30  # NAMES
    ws.column_dimensions['C'].width = 5   # SEX
    ws.column_dimensions['D'].width = 8   # COMB
    
    # Paper columns
    for i in range(5, current_col):
        ws.column_dimensions[get_column_letter(i)].width = 12
    
    # Freeze panes
    ws.freeze_panes = 'E8'
    
    # Add footer
    footer_row = row_num + 1
    ws.cell(row=footer_row, column=1, value=f"Generated on: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
    ws.cell(row=footer_row, column=1).font = Font(italic=True, size=8)
    
    # Response
    filename = f"{school.school_code}_{project.name}_{project.year}_All_Papers.xlsx"
    response = HttpResponse(
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    
    wb.save(response)
    return response




from django.db.models import Prefetch
from django.template.loader import render_to_string
from weasyprint import HTML
import tempfile

@login_required
def project_all_schools_detailed_results_print(request, project_code):
    """
    Print-friendly view showing all schools in a single document with page breaks
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get all schools that have results
    school_ids_with_results = ExamSchoolResult.objects.filter(
        project=project
    ).values_list('school_id', flat=True).distinct()
    
    schools = School.objects.filter(
        id__in=school_ids_with_results
    ).select_related(
        'district', 'district__region'
    ).order_by('school_code')
    
    # Get all school results in one query
    school_results = {
        r.school_id: r for r in ExamSchoolResult.objects.filter(
            project=project,
            school_id__in=school_ids_with_results
        )
    }
    
    # Get all final results for all schools
    all_final_results = StudentFinalResult.objects.filter(
        project=project,
        student__school_id__in=school_ids_with_results
    ).select_related('student', 'student__combination')
    
    # Group final results by school
    final_results_by_school = {}
    for result in all_final_results:
        school_id = result.student.school_id
        if school_id not in final_results_by_school:
            final_results_by_school[school_id] = []
        final_results_by_school[school_id].append(result)
    
    # Get all processed scores for all schools
    all_processed_scores = ProcessedSubjectScore.objects.filter(
        project=project,
        student__school_id__in=school_ids_with_results
    ).select_related('student', 'subject')
    
    # Group processed scores by student
    scores_by_student = {}
    for score in all_processed_scores:
        student_id = score.student_id
        if student_id not in scores_by_student:
            scores_by_student[student_id] = {}
        scores_by_student[student_id][score.subject.subject_code] = {
            'score': score.percentage_score,
            'grade': score.grade,
        }
    
    # Get all subjects
    all_subjects = ExamSubject.objects.filter(
        papers__examscore__project=project
    ).distinct().order_by('subject_code')
    
    # Build data for each school
    schools_data = []
    
    for school in schools:
        school_result = school_results.get(school.id)
        school_final_results = final_results_by_school.get(school.id, [])
        
        # Calculate division counts
        division_counts = {'I': 0, 'II': 0, 'III': 0, 'IV': 0, '0': 0}
        for result in school_final_results:
            if result.division in division_counts:
                division_counts[result.division] += 1
        
        total_students = len(school_final_results)
        
        # Calculate school rank
        all_school_results = ExamSchoolResult.objects.filter(
            project=project,
            sch_gpa__isnull=False
        ).order_by('sch_gpa')
        all_schools_list = list(all_school_results.values_list('id', flat=True))
        school_rank = None
        if school_result and school_result.id in all_schools_list:
            school_rank = all_schools_list.index(school_result.id) + 1
        overall_total = all_school_results.count()
        
        # Build student data
        student_rows = []
        sorted_subject_codes = [s.subject_code for s in all_subjects]
        
        for result in school_final_results:
            student = result.student
            
            # Build subject scores list (format: "score grade" for backward compatibility)
            subject_scores_list = []
            for subject_code in sorted_subject_codes:
                score_data = scores_by_student.get(student.id, {}).get(subject_code, {})
                score = score_data.get('score', '')
                grade = score_data.get('grade', '')
                
                # Format score
                score_display = ''
                if score and score != 'X':
                    try:
                        score_display = str(int(float(score)))
                    except (ValueError, TypeError):
                        score_display = str(score)
                elif score == 'X':
                    score_display = 'X'
                
                # Format combined string for backward compatibility
                if score_display and grade:
                    subject_scores_list.append(f"{score_display} {grade}")
                elif score_display:
                    subject_scores_list.append(score_display)
                elif grade:
                    subject_scores_list.append(f"X {grade}")
                else:
                    subject_scores_list.append("")
            
            student_rows.append({
                'cand_no': student.student_number,
                'name': student.full_name,
                'sex': student.sex,
                'combination': student.combination.code if student.combination else '',
                'subject_scores': subject_scores_list,
                'average': result.std_average,
                'avg_grade': result.std_averagegrade,
                'points': result.total_points,
                'division': result.division,
            })
        
        schools_data.append({
            'school': school,
            'school_result': school_result,
            'division_counts': division_counts,
            'total_students': total_students,
            'school_rank': school_rank,
            'overall_total': overall_total,
            'students': student_rows,
            'student_count': len(student_rows),
        })
    
    # Sort schools by code
    schools_data.sort(key=lambda x: x['school'].school_code)
    
    context = {
        'project': project,
        'schools_data': schools_data,
        'subjects': all_subjects,
        'total_schools': len(schools_data),
    }
    
    return render(request, 'exams/reports/project_all_schools_detailed_results_print.html', context)

@login_required
def project_all_schools_detailed_results_pdf(request, project_code):
    """
    Generate PDF for all schools
    """
    # Get the HTML content first
    response = project_all_schools_detailed_results_print(request, project_code)
    html_string = response.content.decode('utf-8')
    
    # Generate PDF
    pdf_file = HTML(string=html_string).write_pdf()
    
    # Create response
    response = HttpResponse(pdf_file, content_type='application/pdf')
    response['Content-Disposition'] = f'attachment; filename="{project.name}_All_Schools.pdf"'
    
    return response



import json
import uuid
from decimal import Decimal, InvalidOperation
from django.db import transaction
from django.core.paginator import Paginator
from django.db.models import Q, Count, Avg, Max, Min
from django.http import JsonResponse
from django.shortcuts import render, get_object_or_404, redirect
from django.contrib import messages
from django.views.decorators.http import require_POST
from django.core.cache import cache

@login_required
def subject_standardization_list(request, project_code):
    """
    List all subjects with their statistics for standardization
    """

    project = get_object_or_404(JointExamProject, project_code=project_code)
    print(f"DEBUG: Project found: {project.name}")

    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get all subjects that have processed scores
    subjects = ExamSubject.objects.filter(
        processedsubjectscore__project_id=project_id
    ).distinct().order_by('subject_code')
    
    subject_data = []
    
    for subject in subjects:
        # Get all scores for this subject
        scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject
        )
        
        total_students = scores.count()
        
        # Count grades
        grade_counts = {
            'A': scores.filter(grade='A').count(),
            'B': scores.filter(grade='B').count(),
            'C': scores.filter(grade='C').count(),
            'D': scores.filter(grade='D').count(),
            'E': scores.filter(grade='E').count(),
            'S': scores.filter(grade='S').count(),
            'F': scores.filter(grade='F').count(),
        }
        
        # Calculate A-E total
        a_e_total = sum(grade_counts[g] for g in ['A', 'B', 'C', 'D', 'E'])
        a_e_percent = round((a_e_total / total_students * 100), 2) if total_students > 0 else 0
        
        # Calculate GPA
        gpa_numerator = (
            grade_counts['A'] * 1 +
            grade_counts['B'] * 2 +
            grade_counts['C'] * 3 +
            grade_counts['D'] * 4 +
            grade_counts['E'] * 5 +
            grade_counts['S'] * 6 +
            grade_counts['F'] * 7
        )
        gpa = round(gpa_numerator / total_students, 4) if total_students > 0 else 0
        
        # Get min and max scores (converting percentage_score to float)
        min_score = None
        max_score = None
        valid_scores = []
        
        for score in scores:
            if score.percentage_score and score.percentage_score != 'X':
                try:
                    score_val = float(score.percentage_score)
                    valid_scores.append(score_val)
                except (ValueError, TypeError):
                    pass
        
        if valid_scores:
            min_score = min(valid_scores)
            max_score = max(valid_scores)
        
        subject_data.append({
            'id': subject.subject_id,
            'code': subject.subject_code,
            'name': subject.subject_name_eng,
            'total': total_students,
            'A': grade_counts['A'],
            'B': grade_counts['B'],
            'C': grade_counts['C'],
            'D': grade_counts['D'],
            'E': grade_counts['E'],
            'S': grade_counts['S'],
            'F': grade_counts['F'],
            'a_e_total': a_e_total,
            'a_e_percent': a_e_percent,
            'gpa': gpa,
            'min_score': min_score,
            'max_score': max_score,
        })
    
    context = {
        'project': project,
        'subjects': subject_data,
    }
    
    return render(request, 'exams/subject_standardization_list.html', context)


@login_required
def subject_standardization_data(request, project_code, subject_id):
    """
    Get subject data for standardization modal
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    subject = get_object_or_404(ExamSubject, id=subject_id)
    
    # Get all scores for this subject
    scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        subject=subject
    )
    
    # Calculate min and max scores (converting percentage_score to float)
    min_score = None
    max_score = None
    valid_scores = []
    
    for score in scores:
        if score.percentage_score and score.percentage_score != 'X':
            try:
                score_val = float(score.percentage_score)
                valid_scores.append(score_val)
            except (ValueError, TypeError):
                pass
    
    if valid_scores:
        min_score = min(valid_scores)
        max_score = max(valid_scores)
    
    # Get grade distribution
    grade_counts = {
        'A': scores.filter(grade='A').count(),
        'B': scores.filter(grade='B').count(),
        'C': scores.filter(grade='C').count(),
        'D': scores.filter(grade='D').count(),
        'E': scores.filter(grade='E').count(),
        'S': scores.filter(grade='S').count(),
        'F': scores.filter(grade='F').count(),
    }
    
    data = {
        'id': subject.id,
        'code': subject.subject_code,
        'name': subject.subject_name_eng,
        'total_students': scores.count(),
        'min_score': min_score,
        'max_score': max_score,
        'grade_counts': grade_counts,
    }
    
    return JsonResponse(data)





@login_required
def subject_schools_list(request, project_code, subject_id):
    """
    List all schools with their performance for a specific subject
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    subject = get_object_or_404(ExamSubject, id=subject_id)
    
    # Get all schools that have scores for this subject
    school_ids = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        subject=subject
    ).values_list('student__school_id', flat=True).distinct()
    
    schools = School.objects.filter(id__in=school_ids).order_by('school_code')
    
    school_data = []
    
    for school in schools:
        scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject,
            student__school=school
        )
        
        total_students = scores.count()
        
        # Count grades
        grade_counts = {
            'A': scores.filter(grade='A').count(),
            'B': scores.filter(grade='B').count(),
            'C': scores.filter(grade='C').count(),
            'D': scores.filter(grade='D').count(),
            'E': scores.filter(grade='E').count(),
            'S': scores.filter(grade='S').count(),
            'F': scores.filter(grade='F').count(),
        }
        
        # Calculate A-E total
        a_e_total = sum(grade_counts[g] for g in ['A', 'B', 'C', 'D', 'E'])
        a_e_percent = round((a_e_total / total_students * 100), 2) if total_students > 0 else 0
        
        # Calculate GPA
        gpa_numerator = (
            grade_counts['A'] * 1 +
            grade_counts['B'] * 2 +
            grade_counts['C'] * 3 +
            grade_counts['D'] * 4 +
            grade_counts['E'] * 5 +
            grade_counts['S'] * 6 +
            grade_counts['F'] * 7
        )
        gpa = round(gpa_numerator / total_students, 4) if total_students > 0 else 0
        
        # Get min and max scores
        min_score = None
        max_score = None
        valid_scores = []
        
        for score in scores:
            if score.percentage_score and score.percentage_score != 'X':
                try:
                    score_val = float(score.percentage_score)
                    valid_scores.append(score_val)
                except (ValueError, TypeError):
                    pass
        
        if valid_scores:
            min_score = min(valid_scores)
            max_score = max(valid_scores)
        
        school_data.append({
            'id': school.id,
            'code': school.school_code,
            'name': school.name,
            'total': total_students,
            'A': grade_counts['A'],
            'B': grade_counts['B'],
            'C': grade_counts['C'],
            'D': grade_counts['D'],
            'E': grade_counts['E'],
            'S': grade_counts['S'],
            'F': grade_counts['F'],
            'a_e_total': a_e_total,
            'a_e_percent': a_e_percent,
            'gpa': gpa,
            'min_score': min_score,
            'max_score': max_score,
        })
    
    # Sort by GPA (lower is better)
    school_data.sort(key=lambda x: x['gpa'])
    
    context = {
        'project': project,
        'subject': subject,
        'schools': school_data,
    }
    
    return render(request, 'exams/subject_schools_list.html', context)


@login_required
def school_subject_standardization(request, project_code, subject_id, school_id):
    """
    Page for standardizing a specific subject in a specific school
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    subject = get_object_or_404(ExamSubject, id=subject_id)
    school = get_object_or_404(School, id=school_id)
    
    # Get scores for this subject in this school
    scores = ProcessedSubjectScore.objects.filter(
        project_id=project_id,
        subject=subject,
        student__school=school
    ).select_related('student')
    
    # Calculate statistics
    total = scores.count()
    
    grade_counts = {
        'A': scores.filter(grade='A').count(),
        'B': scores.filter(grade='B').count(),
        'C': scores.filter(grade='C').count(),
        'D': scores.filter(grade='D').count(),
        'E': scores.filter(grade='E').count(),
        'S': scores.filter(grade='S').count(),
        'F': scores.filter(grade='F').count(),
    }
    
    a_e_total = sum(grade_counts[g] for g in ['A', 'B', 'C', 'D', 'E'])
    a_e_percent = round((a_e_total / total * 100), 2) if total > 0 else 0
    
    # Get min and max scores
    min_score = None
    max_score = None
    valid_scores = []
    
    for score in scores:
        if score.percentage_score and score.percentage_score != 'X':
            try:
                score_val = float(score.percentage_score)
                valid_scores.append(score_val)
            except (ValueError, TypeError):
                pass
    
    if valid_scores:
        min_score = min(valid_scores)
        max_score = max(valid_scores)
    
    context = {
        'project': project,
        'subject': subject,
        'school': school,
        'total': total,
        'grade_counts': grade_counts,
        'a_e_total': a_e_total,
        'a_e_percent': a_e_percent,
        'min_score': min_score,
        'max_score': max_score,
    }
    
    return render(request, 'exams/school_subject_standardization.html', context)




from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required

@login_required
@require_POST
def test_standardize_endpoint(request, project_code, subject_id):
    """
    Test endpoint for standardization
    """
    print(f"\n=== TEST STANDARDIZE ENDPOINT CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"Subject ID: {subject_id}")
    print(f"Request body: {request.body}")
    
    return JsonResponse({
        'success': True,
        'message': 'Test endpoint working',
        'project_id': project_id,
        'subject_id': subject_id
    })




import json
import uuid
from decimal import Decimal, InvalidOperation
from django.db import transaction
from django.core.cache import cache
from django.http import JsonResponse
from django.views.decorators.http import require_POST
from django.contrib.auth.decorators import login_required


@login_required
@require_POST
def process_subject_standardization(request, project_code, subject_id):
    """
    Process standardization for a subject
    Adds a fixed value to all scores, ensuring they don't exceed 99
    """
    print(f"\n=== PROCESS SUBJECT STANDARDIZATION CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"Subject ID: {subject_id}")
    
    try:
        project = get_object_or_404(JointExamProject, project_code=project_code)
        
        # FIX: Use subject_id instead of id for ExamSubject
        subject = get_object_or_404(ExamSubject, subject_id=subject_id)
        
        print(f"Subject found: {subject.subject_name_eng} (ID: {subject.subject_id})")
        
        data = json.loads(request.body)
        add_value = float(data.get('add_value', 0))
        
        print(f"Add value: {add_value}")
        
        if add_value <= 0:
            return JsonResponse({'error': 'Add value must be positive'}, status=400)
        
        # Get all scores for this subject - EXCLUDE 'X' and NULL values
        scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject
        ).exclude(
            percentage_score__isnull=True
        ).exclude(
            percentage_score__in=['X', '', 'None', 'null']
        )
        
        total_scores = scores.count()
        print(f"Total scores to process: {total_scores}")
        
        if total_scores == 0:
            return JsonResponse({'error': 'No valid scores found for this subject'}, status=400)
        
        # Check if any score would exceed 99
        would_exceed = []
        valid_scores_for_update = []
        
        for score in scores:
            try:
                # Convert string to float
                current = float(score.percentage_score)
                new_value = current + add_value
                
                if new_value > 99:
                    would_exceed.append({
                        'student': score.student.student_number,
                        'current': current,
                        'new': new_value
                    })
                else:
                    # Store for later update
                    valid_scores_for_update.append({
                        'score_obj': score,
                        'new_value': new_value
                    })
                    
            except (ValueError, TypeError) as e:
                print(f"Skipping score {score.id} - invalid value: {score.percentage_score}")
                continue
        
        if would_exceed:
            return JsonResponse({
                'error': f'{len(would_exceed)} scores would exceed 99',
                'exceeding': would_exceed[:10]
            }, status=400)
        
        # Generate a task ID for progress tracking
        import uuid
        task_id = str(uuid.uuid4())
        
        # Store total count in cache
        from django.core.cache import cache
        cache.set(f'std_task_{task_id}_total', len(valid_scores_for_update), 300)
        cache.set(f'std_task_{task_id}_processed', 0, 300)
        cache.set(f'std_task_{task_id}_status', 'processing', 300)
        
        # Get grading rules for recalculating grades
        grading_rules = list(GradingRule.objects.filter(
            academic_level=project.academic_level
        ).order_by('-min_score'))
        
        # Process in batches
        updated = 0
        batch_size = 100
        total_to_update = len(valid_scores_for_update)
        
        for i in range(0, total_to_update, batch_size):
            batch = valid_scores_for_update[i:i+batch_size]
            scores_to_update = []
            
            for item in batch:
                score_obj = item['score_obj']
                new_value = item['new_value']
                
                # Round to 2 decimal places
                new_value_rounded = round(new_value, 2)
                
                # Update the score - STORE AS STRING WITH 2 DECIMAL PLACES
                score_obj.percentage_score = f"{new_value_rounded:.2f}"
                
                # Recalculate grade based on grading rules
                for rule in grading_rules:
                    if rule.min_score <= new_value_rounded <= rule.max_score:
                        score_obj.grade = rule.grade
                        score_obj.points = int(rule.points * rule.weight)
                        break
                
                scores_to_update.append(score_obj)
            
            # Bulk update this batch
            if scores_to_update:
                ProcessedSubjectScore.objects.bulk_update(
                    scores_to_update,
                    ['percentage_score', 'grade', 'points'],
                    batch_size=len(scores_to_update)
                )
            
            updated += len(batch)
            
            # Update progress
            cache.set(f'std_task_{task_id}_processed', updated, 300)
            print(f"Processed {updated}/{total_to_update} scores")
        
        cache.set(f'std_task_{task_id}_status', 'completed', 300)
        
        print(f"Successfully updated {updated} scores")
        
        return JsonResponse({
            'success': True,
            'task_id': task_id,
            'updated': updated,
            'total': total_to_update
        })
        
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()
        return JsonResponse({'error': str(e)}, status=500)


        

@login_required
def standardization_progress(request, project_code, task_id):
    """
    Get progress of standardization task
    """
    total = cache.get(f'std_task_{task_id}_total', 0)
    processed = cache.get(f'std_task_{task_id}_processed', 0)
    status = cache.get(f'std_task_{task_id}_status', 'processing')
    
    percent = int((processed / total * 100)) if total > 0 else 0
    
    return JsonResponse({
        'processed': processed,
        'total': total,
        'percent': percent,
        'status': status
    })





from django.core.cache import cache
import uuid
from django.db import transaction
from decimal import Decimal, InvalidOperation

@login_required
def examscore_standardization_list(request, project_code):
    """
    List all papers with their statistics for ExamScore standardization
    """
    print(f"\n=== EXAMSCORE STANDARDIZATION LIST VIEW CALLED ===")
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get all subjects that have papers with exam scores
    subjects = ExamSubject.objects.filter(
        papers__examscore__project_id=project_id
    ).distinct().order_by('subject_code')
    
    subject_data = []
    
    for subject in subjects:
        # Get all papers for this subject
        papers = ExamSubjectPaper.objects.filter(
            subject=subject,
            examscore__project_id=project_id
        ).distinct().order_by('paper_code')
        
        paper_list = []
        for paper in papers:
            # Get all scores for this paper
            scores = ExamScore.objects.filter(
                project_id=project_id,
                paper=paper
            )
            
            total_students = scores.count()
            
            # Count valid scores (numeric, not X, not null)
            valid_scores = []
            x_count = 0
            null_count = 0
            
            for score in scores:
                if score.score is None or score.score == '':
                    null_count += 1
                elif str(score.score).upper() == 'X':
                    x_count += 1
                else:
                    try:
                        score_val = float(score.score)
                        valid_scores.append(score_val)
                    except (ValueError, TypeError):
                        pass
            
            # Calculate min and max
            min_score = min(valid_scores) if valid_scores else None
            max_score = max(valid_scores) if valid_scores else None
            
            paper_list.append({
                'id': paper.id,
                'code': paper.paper_code,
                'name': paper.paper_name,
                'total': total_students,
                'valid_count': len(valid_scores),
                'x_count': x_count,
                'null_count': null_count,
                'min_score': min_score,
                'max_score': max_score,
            })
        
        subject_data.append({
            'id': subject.subject_id,
            'code': subject.subject_code,
            'name': subject.subject_name_eng,
            'papers': paper_list,
        })
    
    context = {
        'project': project,
        'subjects': subject_data,
    }
    
    return render(request, 'exams/examscore_standardization_list.html', context)


@login_required
def subject_papers_list(request, project_code, subject_id):
    """
    Get papers for a specific subject (AJAX endpoint)
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    subject = get_object_or_404(ExamSubject, subject_id=subject_id)
    
    papers = ExamSubjectPaper.objects.filter(
        subject=subject,
        examscore__project_id=project_id
    ).distinct().order_by('paper_code')
    
    paper_data = []
    for paper in papers:
        scores = ExamScore.objects.filter(project_id=project_id, paper=paper)
        
        valid_scores = []
        for score in scores:
            if score.score and str(score.score).upper() != 'X':
                try:
                    valid_scores.append(float(score.score))
                except (ValueError, TypeError):
                    pass
        
        paper_data.append({
            'id': paper.id,
            'code': paper.paper_code,
            'name': paper.paper_name,
            'total': scores.count(),
            'valid_count': len(valid_scores),
            'min_score': min(valid_scores) if valid_scores else None,
            'max_score': max(valid_scores) if valid_scores else None,
        })
    
    return JsonResponse({'papers': paper_data})


@login_required
def examscore_standardization_data(request, project_code, paper_id):
    """
    Get paper data for standardization modal
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    paper = get_object_or_404(ExamSubjectPaper, id=paper_id)
    
    # Get all scores for this paper
    scores = ExamScore.objects.filter(
        project_id=project_id,
        paper=paper
    )
    
    # Calculate statistics
    total = scores.count()
    
    # Get valid numeric scores
    valid_scores = []
    for score in scores:
        if score.score and str(score.score).upper() != 'X':
            try:
                valid_scores.append(float(score.score))
            except (ValueError, TypeError):
                pass
    
    min_score = min(valid_scores) if valid_scores else None
    max_score = max(valid_scores) if valid_scores else None
    
    data = {
        'id': paper.id,
        'code': paper.paper_code,
        'name': paper.paper_name,
        'subject_code': paper.subject.subject_code,
        'subject_name': paper.subject.subject_name_eng,
        'total_students': total,
        'valid_count': len(valid_scores),
        'min_score': min_score,
        'max_score': max_score,
    }
    
    return JsonResponse(data)


@login_required
@require_POST
def process_examscore_standardization(request, project_code, paper_id):
    """
    Process standardization for ExamScore records
    Adds a fixed value to all scores, ensuring they don't exceed 99
    """
    print(f"\n=== PROCESS EXAMSCORE STANDARDIZATION CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"Paper ID: {paper_id}")
    
    try:
        project = get_object_or_404(JointExamProject, project_code=project_code)
        paper = get_object_or_404(ExamSubjectPaper, id=paper_id)
        
        data = json.loads(request.body)
        add_value = float(data.get('add_value', 0))
        
        print(f"Add value: {add_value}")
        
        if add_value <= 0:
            return JsonResponse({'error': 'Add value must be positive'}, status=400)
        
        # Get all scores for this paper - EXCLUDE 'X' and NULL values
        scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper
        ).exclude(
            score__isnull=True
        ).exclude(
            score__in=['', 'X', 'x', 'None', 'null']
        )
        
        total_scores = scores.count()
        print(f"Total scores to process: {total_scores}")
        
        if total_scores == 0:
            return JsonResponse({'error': 'No valid scores found for this paper'}, status=400)
        
        # Check if any score would exceed 99
        would_exceed = []
        valid_scores_for_update = []
        
        for score in scores:
            try:
                # Convert string to float
                current = float(score.score)
                new_value = current + add_value
                
                if new_value > 99:
                    would_exceed.append({
                        'student': score.student.student_number,
                        'current': current,
                        'new': new_value
                    })
                else:
                    # Store for later update
                    valid_scores_for_update.append({
                        'score_obj': score,
                        'new_value': new_value
                    })
                    
            except (ValueError, TypeError) as e:
                print(f"Skipping score {score.id} - invalid value: {score.score}")
                continue
        
        if would_exceed:
            return JsonResponse({
                'error': f'{len(would_exceed)} scores would exceed 99',
                'exceeding': would_exceed[:10]
            }, status=400)
        
        # Generate a task ID for progress tracking
        task_id = str(uuid.uuid4())
        
        # Store total count in cache
        cache.set(f'escore_task_{task_id}_total', len(valid_scores_for_update), 300)
        cache.set(f'escore_task_{task_id}_processed', 0, 300)
        cache.set(f'escore_task_{task_id}_status', 'processing', 300)
        
        # Process in batches
        updated = 0
        batch_size = 100
        total_to_update = len(valid_scores_for_update)
        
        for i in range(0, total_to_update, batch_size):
            batch = valid_scores_for_update[i:i+batch_size]
            scores_to_update = []
            
            for item in batch:
                score_obj = item['score_obj']
                new_value = item['new_value']
                
                # Round to 2 decimal places and store as string
                new_value_rounded = round(new_value, 2)
                score_obj.score = f"{new_value_rounded:.2f}"
                score_obj.score_status = 1  # Mark as updated
                score_obj.updated_by = request.user
                
                scores_to_update.append(score_obj)
            
            # Bulk update this batch
            if scores_to_update:
                ExamScore.objects.bulk_update(
                    scores_to_update,
                    ['score', 'score_status', 'updated_by', 'updated_at'],
                    batch_size=len(scores_to_update)
                )
            
            updated += len(batch)
            
            # Update progress
            cache.set(f'escore_task_{task_id}_processed', updated, 300)
            print(f"Processed {updated}/{total_to_update} scores")
        
        cache.set(f'escore_task_{task_id}_status', 'completed', 300)
        
        print(f"Successfully updated {updated} scores")
        
        return JsonResponse({
            'success': True,
            'task_id': task_id,
            'updated': updated,
            'total': total_to_update
        })
        
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()
        return JsonResponse({'error': str(e)}, status=500)


@login_required
def examscore_standardization_progress(request, project_code, task_id):
    """
    Get progress of ExamScore standardization task
    """
    total = cache.get(f'escore_task_{task_id}_total', 0)
    processed = cache.get(f'escore_task_{task_id}_processed', 0)
    status = cache.get(f'escore_task_{task_id}_status', 'processing')
    
    percent = int((processed / total * 100)) if total > 0 else 0
    
    return JsonResponse({
        'processed': processed,
        'total': total,
        'percent': percent,
        'status': status
    })




from django.core.cache import cache
import uuid
from django.db import transaction
from decimal import Decimal, InvalidOperation

@login_required
@require_POST
def process_examscore_destandardization(request, project_code, paper_id):
    """
    Process destandardization for ExamScore records
    Subtracts a fixed value from all scores, ensuring they don't go below 0
    """
    print(f"\n=== PROCESS EXAMSCORE DESTANDARDIZATION CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"Paper ID: {paper_id}")
    
    try:
        project = get_object_or_404(JointExamProject, project_code=project_code)
        paper = get_object_or_404(ExamSubjectPaper, id=paper_id)
        
        data = json.loads(request.body)
        subtract_value = float(data.get('subtract_value', 0))
        
        print(f"Subtract value: {subtract_value}")
        
        if subtract_value <= 0:
            return JsonResponse({'error': 'Subtract value must be positive'}, status=400)
        
        # Get all scores for this paper - EXCLUDE 'X' and NULL values
        scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper
        ).exclude(
            score__isnull=True
        ).exclude(
            score__in=['', 'X', 'x', 'None', 'null']
        )
        
        total_scores = scores.count()
        print(f"Total scores to process: {total_scores}")
        
        if total_scores == 0:
            return JsonResponse({'error': 'No valid scores found for this paper'}, status=400)
        
        # Check if any score would go below 0
        would_go_below = []
        valid_scores_for_update = []
        
        for score in scores:
            try:
                # Convert string to float
                current = float(score.score)
                new_value = current - subtract_value
                
                if new_value < 0:
                    would_go_below.append({
                        'student': score.student.student_number,
                        'current': current,
                        'new': new_value
                    })
                else:
                    # Store for later update
                    valid_scores_for_update.append({
                        'score_obj': score,
                        'new_value': new_value
                    })
                    
            except (ValueError, TypeError) as e:
                print(f"Skipping score {score.id} - invalid value: {score.score}")
                continue
        
        if would_go_below:
            return JsonResponse({
                'error': f'{len(would_go_below)} scores would go below 0',
                'exceeding': would_go_below[:10]
            }, status=400)
        
        # Generate a task ID for progress tracking
        task_id = str(uuid.uuid4())
        
        # Store total count in cache
        cache.set(f'escore_destandardize_task_{task_id}_total', len(valid_scores_for_update), 300)
        cache.set(f'escore_destandardize_task_{task_id}_processed', 0, 300)
        cache.set(f'escore_destandardize_task_{task_id}_status', 'processing', 300)
        
        # Process in batches
        updated = 0
        batch_size = 100
        total_to_update = len(valid_scores_for_update)
        
        for i in range(0, total_to_update, batch_size):
            batch = valid_scores_for_update[i:i+batch_size]
            scores_to_update = []
            
            for item in batch:
                score_obj = item['score_obj']
                new_value = item['new_value']
                
                # Round to 2 decimal places and store as string
                new_value_rounded = round(new_value, 2)
                score_obj.score = f"{new_value_rounded:.2f}"
                score_obj.score_status = 1  # Mark as updated
                score_obj.updated_by = request.user
                
                scores_to_update.append(score_obj)
            
            # Bulk update this batch
            if scores_to_update:
                ExamScore.objects.bulk_update(
                    scores_to_update,
                    ['score', 'score_status', 'updated_by', 'updated_at'],
                    batch_size=len(scores_to_update)
                )
            
            updated += len(batch)
            
            # Update progress
            cache.set(f'escore_destandardize_task_{task_id}_processed', updated, 300)
            print(f"Processed {updated}/{total_to_update} scores")
        
        cache.set(f'escore_destandardize_task_{task_id}_status', 'completed', 300)
        
        print(f"Successfully updated {updated} scores")
        
        return JsonResponse({
            'success': True,
            'task_id': task_id,
            'updated': updated,
            'total': total_to_update
        })
        
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON'}, status=400)
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()
        return JsonResponse({'error': str(e)}, status=500)


@login_required
def examscore_destandardization_progress(request, project_code, task_id):
    """
    Get progress of ExamScore destandardization task
    """
    total = cache.get(f'escore_destandardize_task_{task_id}_total', 0)
    processed = cache.get(f'escore_destandardize_task_{task_id}_processed', 0)
    status = cache.get(f'escore_destandardize_task_{task_id}_status', 'processing')
    
    percent = int((processed / total * 100)) if total > 0 else 0
    
    return JsonResponse({
        'processed': processed,
        'total': total,
        'percent': percent,
        'status': status
    })





from django.core.cache import cache
import uuid
from django.db import transaction
from decimal import Decimal, InvalidOperation
from django.db.models import Count, Q

@login_required
def school_subject_standardization_list(request, project_code, school_id):
    """
    List all subjects for a specific school with their statistics for standardization
    """
    print(f"\n=== SCHOOL SUBJECT STANDARDIZATION LIST VIEW CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"School ID: {school_id}")
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, id=school_id)
    
    # Get all subjects that have processed scores for this school
    subjects = ExamSubject.objects.filter(
        processedsubjectscore__project_id=project_id,
        processedsubjectscore__student__school_id=school_id
    ).distinct().order_by('subject_code')
    
    subject_data = []
    
    for subject in subjects:
        # Get all scores for this subject in this school
        scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject,
            student__school_id=school_id
        )
        
        total_students = scores.count()
        
        # Count grades
        grade_counts = {
            'A': scores.filter(grade='A').count(),
            'B': scores.filter(grade='B').count(),
            'C': scores.filter(grade='C').count(),
            'D': scores.filter(grade='D').count(),
            'E': scores.filter(grade='E').count(),
            'S': scores.filter(grade='S').count(),
            'F': scores.filter(grade='F').count(),
        }
        
        # Calculate A-E total
        a_e_total = sum(grade_counts[g] for g in ['A', 'B', 'C', 'D', 'E'])
        a_e_percent = round((a_e_total / total_students * 100), 2) if total_students > 0 else 0
        
        # Calculate GPA
        gpa_numerator = (
            grade_counts['A'] * 1 +
            grade_counts['B'] * 2 +
            grade_counts['C'] * 3 +
            grade_counts['D'] * 4 +
            grade_counts['E'] * 5 +
            grade_counts['S'] * 6 +
            grade_counts['F'] * 7
        )
        gpa = round(gpa_numerator / total_students, 4) if total_students > 0 else 0
        
        # Get min and max scores (converting percentage_score to float)
        min_score = None
        max_score = None
        valid_scores = []
        
        for score in scores:
            if score.percentage_score and score.percentage_score != 'X':
                try:
                    score_val = float(score.percentage_score)
                    valid_scores.append(score_val)
                except (ValueError, TypeError):
                    pass
        
        if valid_scores:
            min_score = min(valid_scores)
            max_score = max(valid_scores)
        
        subject_data.append({
            'id': subject.subject_id,
            'code': subject.subject_code,
            'name': subject.subject_name_eng,
            'total': total_students,
            'A': grade_counts['A'],
            'B': grade_counts['B'],
            'C': grade_counts['C'],
            'D': grade_counts['D'],
            'E': grade_counts['E'],
            'S': grade_counts['S'],
            'F': grade_counts['F'],
            'a_e_total': a_e_total,
            'a_e_percent': a_e_percent,
            'gpa': gpa,
            'min_score': min_score,
            'max_score': max_score,
        })
    
    context = {
        'project': project,
        'school': school,
        'subjects': subject_data,
    }
    
    return render(request, 'exams/school_subject_standardization_list.html', context)


@login_required
@require_POST
def process_school_subject_standardization(request, project_code, school_id, subject_id):
    """
    Process standardization for a subject in a specific school (add marks)
    """
    print(f"\n=== PROCESS SCHOOL SUBJECT STANDARDIZATION CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"School ID: {school_id}")
    print(f"Subject ID: {subject_id}")
    
    try:
        project = get_object_or_404(JointExamProject, project_code=project_code)
        school = get_object_or_404(School, id=school_id)
        subject = get_object_or_404(ExamSubject, subject_id=subject_id)
        
        data = json.loads(request.body)
        add_value = float(data.get('add_value', 0))
        
        print(f"Add value: {add_value}")
        
        if add_value <= 0:
            return JsonResponse({'error': 'Add value must be positive'}, status=400)
        
        # Get all scores for this subject in this school
        scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject,
            student__school_id=school_id
        ).exclude(
            percentage_score__isnull=True
        ).exclude(
            percentage_score__in=['X', '', 'None', 'null']
        )
        
        total_scores = scores.count()
        print(f"Total scores to process: {total_scores}")
        
        if total_scores == 0:
            return JsonResponse({'error': 'No valid scores found for this subject in this school'}, status=400)
        
        # Check if any score would exceed 99
        would_exceed = []
        valid_scores_for_update = []
        
        for score in scores:
            try:
                current = float(score.percentage_score)
                new_value = current + add_value
                
                if new_value > 99:
                    would_exceed.append({
                        'student': score.student.student_number,
                        'current': current,
                        'new': new_value
                    })
                else:
                    valid_scores_for_update.append({
                        'score_obj': score,
                        'new_value': new_value
                    })
                    
            except (ValueError, TypeError):
                continue
        
        if would_exceed:
            return JsonResponse({
                'error': f'{len(would_exceed)} scores would exceed 99',
                'exceeding': would_exceed[:10]
            }, status=400)
        
        # Generate task ID
        task_id = str(uuid.uuid4())
        
        # Store in cache
        cache.set(f'school_std_task_{task_id}_total', len(valid_scores_for_update), 300)
        cache.set(f'school_std_task_{task_id}_processed', 0, 300)
        cache.set(f'school_std_task_{task_id}_status', 'processing', 300)
        
        # Get grading rules
        grading_rules = list(GradingRule.objects.filter(
            academic_level=project.academic_level
        ).order_by('-min_score'))
        
        # Process in batches
        updated = 0
        batch_size = 100
        total_to_update = len(valid_scores_for_update)
        
        for i in range(0, total_to_update, batch_size):
            batch = valid_scores_for_update[i:i+batch_size]
            scores_to_update = []
            
            for item in batch:
                score_obj = item['score_obj']
                new_value = item['new_value']
                
                new_value_rounded = round(new_value, 2)
                score_obj.percentage_score = f"{new_value_rounded:.2f}"
                
                # Recalculate grade
                for rule in grading_rules:
                    if rule.min_score <= new_value_rounded <= rule.max_score:
                        score_obj.grade = rule.grade
                        score_obj.points = int(rule.points * rule.weight)
                        break
                
                scores_to_update.append(score_obj)
            
            if scores_to_update:
                ProcessedSubjectScore.objects.bulk_update(
                    scores_to_update,
                    ['percentage_score', 'grade', 'points'],
                    batch_size=len(scores_to_update)
                )
            
            updated += len(batch)
            cache.set(f'school_std_task_{task_id}_processed', updated, 300)
        
        cache.set(f'school_std_task_{task_id}_status', 'completed', 300)
        
        return JsonResponse({
            'success': True,
            'task_id': task_id,
            'updated': updated,
            'total': total_to_update
        })
        
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()
        return JsonResponse({'error': str(e)}, status=500)


@login_required
@require_POST
def process_school_subject_destandardization(request, project_code, school_id, subject_id):
    """
    Process destandardization for a subject in a specific school (subtract marks)
    """
    print(f"\n=== PROCESS SCHOOL SUBJECT DESTANDARDIZATION CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"School ID: {school_id}")
    print(f"Subject ID: {subject_id}")
    
    try:
        project = get_object_or_404(JointExamProject, project_code=project_code)
        school = get_object_or_404(School, id=school_id)
        subject = get_object_or_404(ExamSubject, subject_id=subject_id)
        
        data = json.loads(request.body)
        subtract_value = float(data.get('subtract_value', 0))
        
        print(f"Subtract value: {subtract_value}")
        
        if subtract_value <= 0:
            return JsonResponse({'error': 'Subtract value must be positive'}, status=400)
        
        # Get all scores for this subject in this school
        scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject,
            student__school_id=school_id
        ).exclude(
            percentage_score__isnull=True
        ).exclude(
            percentage_score__in=['X', '', 'None', 'null']
        )
        
        total_scores = scores.count()
        print(f"Total scores to process: {total_scores}")
        
        if total_scores == 0:
            return JsonResponse({'error': 'No valid scores found for this subject in this school'}, status=400)
        
        # Check if any score would go below 0
        would_go_below = []
        valid_scores_for_update = []
        
        for score in scores:
            try:
                current = float(score.percentage_score)
                new_value = current - subtract_value
                
                if new_value < 0:
                    would_go_below.append({
                        'student': score.student.student_number,
                        'current': current,
                        'new': new_value
                    })
                else:
                    valid_scores_for_update.append({
                        'score_obj': score,
                        'new_value': new_value
                    })
                    
            except (ValueError, TypeError):
                continue
        
        if would_go_below:
            return JsonResponse({
                'error': f'{len(would_go_below)} scores would go below 0',
                'exceeding': would_go_below[:10]
            }, status=400)
        
        # Generate task ID
        task_id = str(uuid.uuid4())
        
        # Store in cache
        cache.set(f'school_destd_task_{task_id}_total', len(valid_scores_for_update), 300)
        cache.set(f'school_destd_task_{task_id}_processed', 0, 300)
        cache.set(f'school_destd_task_{task_id}_status', 'processing', 300)
        
        # Get grading rules
        grading_rules = list(GradingRule.objects.filter(
            academic_level=project.academic_level
        ).order_by('-min_score'))
        
        # Process in batches
        updated = 0
        batch_size = 100
        total_to_update = len(valid_scores_for_update)
        
        for i in range(0, total_to_update, batch_size):
            batch = valid_scores_for_update[i:i+batch_size]
            scores_to_update = []
            
            for item in batch:
                score_obj = item['score_obj']
                new_value = item['new_value']
                
                new_value_rounded = round(new_value, 2)
                score_obj.percentage_score = f"{new_value_rounded:.2f}"
                
                # Recalculate grade
                for rule in grading_rules:
                    if rule.min_score <= new_value_rounded <= rule.max_score:
                        score_obj.grade = rule.grade
                        score_obj.points = int(rule.points * rule.weight)
                        break
                
                scores_to_update.append(score_obj)
            
            if scores_to_update:
                ProcessedSubjectScore.objects.bulk_update(
                    scores_to_update,
                    ['percentage_score', 'grade', 'points'],
                    batch_size=len(scores_to_update)
                )
            
            updated += len(batch)
            cache.set(f'school_destd_task_{task_id}_processed', updated, 300)
        
        cache.set(f'school_destd_task_{task_id}_status', 'completed', 300)
        
        return JsonResponse({
            'success': True,
            'task_id': task_id,
            'updated': updated,
            'total': total_to_update
        })
        
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()
        return JsonResponse({'error': str(e)}, status=500)


@login_required
def school_subject_standardization_progress(request, project_code, task_id):
    """
    Get progress of school subject standardization task
    """
    total = cache.get(f'school_std_task_{task_id}_total', 0)
    processed = cache.get(f'school_std_task_{task_id}_processed', 0)
    status = cache.get(f'school_std_task_{task_id}_status', 'processing')
    
    percent = int((processed / total * 100)) if total > 0 else 0
    
    return JsonResponse({
        'processed': processed,
        'total': total,
        'percent': percent,
        'status': status
    })


@login_required
def school_subject_destandardization_progress(request, project_code, task_id):
    """
    Get progress of school subject destandardization task
    """
    total = cache.get(f'school_destd_task_{task_id}_total', 0)
    processed = cache.get(f'school_destd_task_{task_id}_processed', 0)
    status = cache.get(f'school_destd_task_{task_id}_status', 'processing')
    
    percent = int((processed / total * 100)) if total > 0 else 0
    
    return JsonResponse({
        'processed': processed,
        'total': total,
        'percent': percent,
        'status': status
    })




from django.core.cache import cache
import uuid
from django.db import transaction
from decimal import Decimal, InvalidOperation
from django.db.models import Count, Q

@login_required
def school_paper_standardization_list(request, project_code, school_id):
    """
    List all papers for a specific school with their statistics for standardization
    """
    print(f"\n=== SCHOOL PAPER STANDARDIZATION LIST VIEW CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"School ID: {school_id}")
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    school = get_object_or_404(School, id=school_id)
    
    # Get all subjects that have papers with exam scores for this school
    subjects = ExamSubject.objects.filter(
        papers__examscore__project_id=project_id,
        papers__examscore__student__school_id=school_id
    ).distinct().order_by('subject_code')
    
    subject_data = []
    
    for subject in subjects:
        # Get all papers for this subject
        papers = ExamSubjectPaper.objects.filter(
            subject=subject,
            examscore__project_id=project_id,
            examscore__student__school_id=school_id
        ).distinct().order_by('paper_code')
        
        paper_list = []
        for paper in papers:
            # Get all scores for this paper in this school
            scores = ExamScore.objects.filter(
                project_id=project_id,
                paper=paper,
                student__school_id=school_id
            )
            
            total_students = scores.count()
            
            # Count valid scores (numeric, not X, not null)
            valid_scores = []
            x_count = 0
            null_count = 0
            
            for score in scores:
                if score.score is None or score.score == '':
                    null_count += 1
                elif str(score.score).upper() == 'X':
                    x_count += 1
                else:
                    try:
                        score_val = float(score.score)
                        valid_scores.append(score_val)
                    except (ValueError, TypeError):
                        pass
            
            # Calculate min and max
            min_score = min(valid_scores) if valid_scores else None
            max_score = max(valid_scores) if valid_scores else None
            
            paper_list.append({
                'id': paper.id,
                'code': paper.paper_code,
                'name': paper.paper_name,
                'total': total_students,
                'valid_count': len(valid_scores),
                'x_count': x_count,
                'null_count': null_count,
                'min_score': min_score,
                'max_score': max_score,
            })
        
        subject_data.append({
            'id': subject.subject_id,
            'code': subject.subject_code,
            'name': subject.subject_name_eng,
            'papers': paper_list,
        })
    
    context = {
        'project': project,
        'school': school,
        'subjects': subject_data,
    }
    
    return render(request, 'exams/school_paper_standardization_list.html', context)


@login_required
@require_POST
def process_school_paper_standardization(request, project_code, school_id, paper_id):
    """
    Process standardization for a paper in a specific school (add marks)
    """
    print(f"\n=== PROCESS SCHOOL PAPER STANDARDIZATION CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"School ID: {school_id}")
    print(f"Paper ID: {paper_id}")
    
    try:
        project = get_object_or_404(JointExamProject, project_code=project_code)
        school = get_object_or_404(School, id=school_id)
        paper = get_object_or_404(ExamSubjectPaper, id=paper_id)
        
        data = json.loads(request.body)
        add_value = float(data.get('add_value', 0))
        
        print(f"Add value: {add_value}")
        
        if add_value <= 0:
            return JsonResponse({'error': 'Add value must be positive'}, status=400)
        
        # Get all scores for this paper in this school
        scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper,
            student__school_id=school_id
        ).exclude(
            score__isnull=True
        ).exclude(
            score__in=['', 'X', 'x', 'None', 'null']
        )
        
        total_scores = scores.count()
        print(f"Total scores to process: {total_scores}")
        
        if total_scores == 0:
            return JsonResponse({'error': 'No valid scores found for this paper in this school'}, status=400)
        
        # Check if any score would exceed 99
        would_exceed = []
        valid_scores_for_update = []
        
        for score in scores:
            try:
                current = float(score.score)
                new_value = current + add_value
                
                if new_value > 99:
                    would_exceed.append({
                        'student': score.student.student_number,
                        'current': current,
                        'new': new_value
                    })
                else:
                    valid_scores_for_update.append({
                        'score_obj': score,
                        'new_value': new_value
                    })
                    
            except (ValueError, TypeError):
                continue
        
        if would_exceed:
            return JsonResponse({
                'error': f'{len(would_exceed)} scores would exceed 99',
                'exceeding': would_exceed[:10]
            }, status=400)
        
        # Generate task ID
        task_id = str(uuid.uuid4())
        
        # Store in cache
        cache.set(f'school_paper_std_task_{task_id}_total', len(valid_scores_for_update), 300)
        cache.set(f'school_paper_std_task_{task_id}_processed', 0, 300)
        cache.set(f'school_paper_std_task_{task_id}_status', 'processing', 300)
        
        # Process in batches
        updated = 0
        batch_size = 100
        total_to_update = len(valid_scores_for_update)
        
        for i in range(0, total_to_update, batch_size):
            batch = valid_scores_for_update[i:i+batch_size]
            scores_to_update = []
            
            for item in batch:
                score_obj = item['score_obj']
                new_value = item['new_value']
                
                new_value_rounded = round(new_value, 2)
                score_obj.score = f"{new_value_rounded:.2f}"
                score_obj.score_status = 1
                score_obj.updated_by = request.user
                
                scores_to_update.append(score_obj)
            
            if scores_to_update:
                ExamScore.objects.bulk_update(
                    scores_to_update,
                    ['score', 'score_status', 'updated_by', 'updated_at'],
                    batch_size=len(scores_to_update)
                )
            
            updated += len(batch)
            cache.set(f'school_paper_std_task_{task_id}_processed', updated, 300)
        
        cache.set(f'school_paper_std_task_{task_id}_status', 'completed', 300)
        
        return JsonResponse({
            'success': True,
            'task_id': task_id,
            'updated': updated,
            'total': total_to_update
        })
        
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()
        return JsonResponse({'error': str(e)}, status=500)


@login_required
@require_POST
def process_school_paper_destandardization(request, project_code, school_id, paper_id):
    """
    Process destandardization for a paper in a specific school (subtract marks)
    """
    print(f"\n=== PROCESS SCHOOL PAPER DESTANDARDIZATION CALLED ===")
    print(f"Project ID: {project_id}")
    print(f"School ID: {school_id}")
    print(f"Paper ID: {paper_id}")
    
    try:
        project = get_object_or_404(JointExamProject, project_code=project_code)
        school = get_object_or_404(School, id=school_id)
        paper = get_object_or_404(ExamSubjectPaper, id=paper_id)
        
        data = json.loads(request.body)
        subtract_value = float(data.get('subtract_value', 0))
        
        print(f"Subtract value: {subtract_value}")
        
        if subtract_value <= 0:
            return JsonResponse({'error': 'Subtract value must be positive'}, status=400)
        
        # Get all scores for this paper in this school
        scores = ExamScore.objects.filter(
            project_id=project_id,
            paper=paper,
            student__school_id=school_id
        ).exclude(
            score__isnull=True
        ).exclude(
            score__in=['', 'X', 'x', 'None', 'null']
        )
        
        total_scores = scores.count()
        print(f"Total scores to process: {total_scores}")
        
        if total_scores == 0:
            return JsonResponse({'error': 'No valid scores found for this paper in this school'}, status=400)
        
        # Check if any score would go below 0
        would_go_below = []
        valid_scores_for_update = []
        
        for score in scores:
            try:
                current = float(score.score)
                new_value = current - subtract_value
                
                if new_value < 0:
                    would_go_below.append({
                        'student': score.student.student_number,
                        'current': current,
                        'new': new_value
                    })
                else:
                    valid_scores_for_update.append({
                        'score_obj': score,
                        'new_value': new_value
                    })
                    
            except (ValueError, TypeError):
                continue
        
        if would_go_below:
            return JsonResponse({
                'error': f'{len(would_go_below)} scores would go below 0',
                'exceeding': would_go_below[:10]
            }, status=400)
        
        # Generate task ID
        task_id = str(uuid.uuid4())
        
        # Store in cache
        cache.set(f'school_paper_destd_task_{task_id}_total', len(valid_scores_for_update), 300)
        cache.set(f'school_paper_destd_task_{task_id}_processed', 0, 300)
        cache.set(f'school_paper_destd_task_{task_id}_status', 'processing', 300)
        
        # Process in batches
        updated = 0
        batch_size = 100
        total_to_update = len(valid_scores_for_update)
        
        for i in range(0, total_to_update, batch_size):
            batch = valid_scores_for_update[i:i+batch_size]
            scores_to_update = []
            
            for item in batch:
                score_obj = item['score_obj']
                new_value = item['new_value']
                
                new_value_rounded = round(new_value, 2)
                score_obj.score = f"{new_value_rounded:.2f}"
                score_obj.score_status = 1
                score_obj.updated_by = request.user
                
                scores_to_update.append(score_obj)
            
            if scores_to_update:
                ExamScore.objects.bulk_update(
                    scores_to_update,
                    ['score', 'score_status', 'updated_by', 'updated_at'],
                    batch_size=len(scores_to_update)
                )
            
            updated += len(batch)
            cache.set(f'school_paper_destd_task_{task_id}_processed', updated, 300)
        
        cache.set(f'school_paper_destd_task_{task_id}_status', 'completed', 300)
        
        return JsonResponse({
            'success': True,
            'task_id': task_id,
            'updated': updated,
            'total': total_to_update
        })
        
    except Exception as e:
        print(f"Error: {str(e)}")
        import traceback
        traceback.print_exc()
        return JsonResponse({'error': str(e)}, status=500)


@login_required
def school_paper_standardization_progress(request, project_code, task_id):
    """
    Get progress of school paper standardization task
    """
    total = cache.get(f'school_paper_std_task_{task_id}_total', 0)
    processed = cache.get(f'school_paper_std_task_{task_id}_processed', 0)
    status = cache.get(f'school_paper_std_task_{task_id}_status', 'processing')
    
    percent = int((processed / total * 100)) if total > 0 else 0
    
    return JsonResponse({
        'processed': processed,
        'total': total,
        'percent': percent,
        'status': status
    })


@login_required
def school_paper_destandardization_progress(request, project_code, task_id):
    """
    Get progress of school paper destandardization task
    """
    total = cache.get(f'school_paper_destd_task_{task_id}_total', 0)
    processed = cache.get(f'school_paper_destd_task_{task_id}_processed', 0)
    status = cache.get(f'school_paper_destd_task_{task_id}_status', 'processing')
    
    percent = int((processed / total * 100)) if total > 0 else 0
    
    return JsonResponse({
        'processed': processed,
        'total': total,
        'percent': percent,
        'status': status
    })



@login_required
def subject_school_ranking_optimized(request, project_code):
    """
    Optimized version with better performance
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get all subjects
    subjects = ExamSubject.objects.filter(
        processedsubjectscore__project_id=project_id
    ).distinct().order_by('subject_code')
    
    subject_data = []
    
    for subject in subjects:
        # Get all scores for this subject grouped by school
        scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject
        ).select_related(
            'student__school',
            'student__school__district'
        ).only(
            'student__school_id',
            'student__school__school_code',
            'student__school__name',
            'student__school__district__name',
            'grade',
            'points'
        )
        
        # Group by school manually for better control
        school_gpas = {}
        
        for score in scores:
            school_id = score.student.school_id
            
            if school_id not in school_gpas:
                school_gpas[school_id] = {
                    'school_code': score.student.school.school_code,
                    'school_name': score.student.school.name,
                    'district': score.student.school.district.name,
                    'total_points': 0,
                    'count': 0,
                    'points_list': []
                }
            
            # Calculate point value based on grade if points not available
            if score.points:
                points = score.points
            else:
                # Map grade to points
                grade_points = {
                    'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'S': 6, 'F': 7
                }
                points = grade_points.get(score.grade, 0)
            
            school_gpas[school_id]['total_points'] += points
            school_gpas[school_id]['count'] += 1
            school_gpas[school_id]['points_list'].append(points)
        
        # Calculate GPA for each school
        school_list = []
        for school_id, data in school_gpas.items():
            if data['count'] > 0:
                gpa = data['total_points'] / data['count']
            else:
                gpa = 0
            
            school_list.append({
                'school_code': data['school_code'],
                'school_name': data['school_name'],
                'district': data['district'],
                'gpa': round(gpa, 4),
                'student_count': data['count'],
            })
        
        # Sort by GPA (lower is better)
        school_list.sort(key=lambda x: x['gpa'])
        
        # Add rank and total schools
        total_schools = len(school_list)
        for rank, school in enumerate(school_list, 1):
            subject_data.append({
                'subject_code': subject.subject_code,
                'subject_name': subject.subject_name_sw or subject.subject_name_eng,
                'school_gpa': school['gpa'],
                'rank': rank,
                'total_schools': total_schools,
                'school_code': school['school_code'],
                'school_name': school['school_name'],
                'district': school['district'],
                'student_count': school['student_count'],
            })
    
    # Sort by subject code and rank
    subject_data.sort(key=lambda x: (x['subject_code'], x['rank']))
    
    # Pagination
    paginator = Paginator(subject_data, 100)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)
    
    context = {
        'project': project,
        'page_obj': page_obj,
        'subjects': subjects,
    }
    
    return render(request, 'exams/reports/subject_school_ranking.html', context)


@login_required
def subject_school_ranking_excel(request, project_code):
    """
    Export subject-school ranking to Excel
    """
    import openpyxl
    from openpyxl.styles import Font, Alignment, PatternFill
    from django.http import HttpResponse
    
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get data (same as main view)
    subjects = ExamSubject.objects.filter(
        processedsubjectscore__project_id=project_id
    ).distinct().order_by('subject_code')
    
    subject_data = []
    
    for subject in subjects:
        school_scores = ProcessedSubjectScore.objects.filter(
            project_id=project_id,
            subject=subject
        ).values(
            'student__school_id',
            'student__school__school_code',
            'student__school__name',
            'student__school__district__name'
        ).annotate(
            school_gpa=Avg('points'),
            student_count=Count('id')
        ).order_by('school_gpa')
        
        total_schools = school_scores.count()
        
        for idx, school in enumerate(school_scores, 1):
            subject_data.append({
                'subject_code': subject.subject_code,
                'subject_name': subject.subject_name_sw or subject.subject_name_eng,
                'school_gpa': round(school['school_gpa'], 4) if school['school_gpa'] else 0,
                'rank': idx,
                'total_schools': total_schools,
                'school_code': school['student__school__school_code'],
                'school_name': school['student__school__name'],
                'district': school['student__school__district__name'],
                'student_count': school['student_count'],
            })
    
    # Create Excel workbook
    wb = openpyxl.Workbook()
    ws = wb.active
    ws.title = "Subject School Ranking"
    
    # Headers
    headers = ['CODE', 'SUBJECT NAME', 'GPA', 'REG/RANK', 'SCHOOL', 'HALMASHAURI', '#Std']
    ws.append(headers)
    
    # Style headers
    header_fill = PatternFill(start_color="1a3e6f", end_color="1a3e6f", fill_type="solid")
    header_font = Font(color="FFFFFF", bold=True)
    
    for col in range(1, len(headers) + 1):
        cell = ws.cell(row=1, column=col)
        cell.fill = header_fill
        cell.font = header_font
        cell.alignment = Alignment(horizontal="center")
    
    # Add data
    for item in subject_data:
        ws.append([
            item['subject_code'],
            item['subject_name'],
            item['school_gpa'],
            f"{item['rank']:02d}/{item['total_schools']:03d}",
            item['school_name'],
            item['district'],
            item['student_count']
        ])
    
    # Adjust column widths
    for col in ws.columns:
        max_length = 0
        col_letter = col[0].column_letter
        for cell in col:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(str(cell.value))
            except:
                pass
        ws.column_dimensions[col_letter].width = min(max_length + 2, 50)
    
    response = HttpResponse(
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    response['Content-Disposition'] = f'attachment; filename="{project.name}_subject_school_ranking.xlsx"'
    
    wb.save(response)
    return response




from django.shortcuts import get_object_or_404, render, redirect
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from .models import JointExamProject
from .forms import JointExamProjectForm

@login_required
def project_edit(request, pk):
    """Edit an existing joint exam project"""
    
    # Check permissions
    if not (request.user.is_superuser or (hasattr(request.user, 'profile') and request.user.profile.role == "super")):
        messages.error(request, "You don't have permission to edit projects.")
        return redirect('exams:project_list')
    
    # Get the project
    project = get_object_or_404(JointExamProject, pk=pk)
    
    if request.method == 'POST':
        form = JointExamProjectForm(request.POST, request.FILES, instance=project)
        if form.is_valid():
            form.save()
            messages.success(request, f'Project "{project.name}" updated successfully!')
            return redirect('exams:project_list')
        else:
            messages.error(request, 'Please correct the errors below.')
    else:
        form = JointExamProjectForm(instance=project)
    
    context = {
        'form': form,
        'project': project,
        'title': f'Edit Project: {project.name}',
        'is_edit': True,
    }
    
    return render(request, 'exams/project_form.html', context)



from django.views.decorators.http import require_POST
from django.http import JsonResponse
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required
import json

@login_required
@require_POST
def toggle_project_status(request, project_code):
    """Toggle project active/inactive status"""
    
    # Check permissions
    if not (request.user.is_superuser or (hasattr(request.user, 'profile') and request.user.profile.role == "super")):
        return JsonResponse({
            'success': False,
            'error': 'You do not have permission to change project status.'
        }, status=403)
    
    try:
        # Parse JSON data
        data = json.loads(request.body)
        is_active = data.get('is_active', False)
        
        # Get project
        project = get_object_or_404(JointExamProject, project_code=project_code)
        
        # Update status
        project.is_active = is_active
        project.save(update_fields=['is_active'])
        
        return JsonResponse({
            'success': True,
            'message': f'Project "{project.name}" is now {"Active" if is_active else "Inactive"}',
            'is_active': is_active
        })
        
    except json.JSONDecodeError:
        return JsonResponse({
            'success': False,
            'error': 'Invalid request data'
        }, status=400)
    except Exception as e:
        return JsonResponse({
            'success': False,
            'error': str(e)
        }, status=500)


from django.db.models import Count, Sum, Q, Avg, F
from django.utils import timezone
from datetime import datetime, timedelta
from .models import JointExamProject, ProjectStudent, StudentFinalResult, ProcessedSubjectScore, ExamSchoolResult

@login_required
def smart_dashboard(request):
    """Enhanced dashboard with smart analytics"""
    
    # Get current year
    current_year = datetime.now().year
    
    # ============================================
    # CARD 1: Projects Overview
    # ============================================
    total_projects = JointExamProject.objects.count()
    active_projects = JointExamProject.objects.filter(is_active=True).count()
    inactive_projects = JointExamProject.objects.filter(is_active=False).count()
    ongoing_projects = JointExamProject.objects.filter(
        is_active=True,
        year=current_year
    ).count()
    
    # Projects by level
    projects_by_level = JointExamProject.objects.values(
        'academic_level__name'
    ).annotate(
        count=Count('id')
    ).order_by('-count')
    
    # ============================================
    # CARD 2: Student Statistics by Academic Level
    # ============================================
    # Get all projects with student counts
    student_stats = {
        'A-Level': 0,
        'O-Level': 0,
        'Primary': 0,
        'Other': 0,
        'total': 0
    }
    
    # Get projects with academic levels
    projects_with_students = JointExamProject.objects.filter(
        is_active=True
    ).prefetch_related('projectstudent_set')
    
    for project in projects_with_students:
        student_count = project.projectstudent_set.count()
        level_name = project.academic_level.name.lower() if project.academic_level else ''
        
        if 'a-level' in level_name or 'form six' in level_name or 'acsee' in level_name:
            student_stats['A-Level'] += student_count
        elif 'o-level' in level_name or 'form four' in level_name or 'csee' in level_name:
            student_stats['O-Level'] += student_count
        elif 'primary' in level_name or 'standard' in level_name:
            student_stats['Primary'] += student_count
        else:
            student_stats['Other'] += student_count
        
        student_stats['total'] += student_count
    
    # Calculate percentages
    if student_stats['total'] > 0:
        for key in ['A-Level', 'O-Level', 'Primary', 'Other']:
            student_stats[f'{key}_percent'] = round(
                (student_stats[key] / student_stats['total']) * 100, 1
            )
    else:
        for key in ['A-Level', 'O-Level', 'Primary', 'Other']:
            student_stats[f'{key}_percent'] = 0
    
    # ============================================
    # CARD 3 & 4: Project Status Overview
    # ============================================
    # Get projects for current and next months
    today = timezone.now().date()
    next_month = today + timedelta(days=30)
    
    upcoming_projects = JointExamProject.objects.filter(
        is_active=True,
        year__gte=current_year
    ).count()
    
    completed_projects = JointExamProject.objects.filter(
        is_active=False,
        year__lt=current_year
    ).count()
    
    # ============================================
    # CARD 5: Division Performance (I-IV)
    # ============================================
    # Get results from all active projects
    division_stats = {
        'I': 0,
        'II': 0,
        'III': 0,
        'IV': 0,
        '0': 0,
        'total': 0,
        'pass_total': 0,
        'pass_percent': 0,
        'fail_total': 0,
        'fail_percent': 0
    }
    
    # Get all student final results from active projects
    active_projects_ids = JointExamProject.objects.filter(
        is_active=True
    ).values_list('id', flat=True)
    
    student_results = StudentFinalResult.objects.filter(
        project_id__in=active_projects_ids
    )
    
    for result in student_results:
        division = result.division
        if division in division_stats:
            division_stats[division] += 1
            division_stats['total'] += 1
            if division in ['I', 'II', 'III', 'IV']:
                division_stats['pass_total'] += 1
    
    # Calculate percentages
    if division_stats['total'] > 0:
        division_stats['pass_percent'] = round(
            (division_stats['pass_total'] / division_stats['total']) * 100, 1
        )
        division_stats['fail_percent'] = round(
            (division_stats['0'] / division_stats['total']) * 100, 1
        )
        
        # Individual division percentages
        for div in ['I', 'II', 'III', 'IV', '0']:
            division_stats[f'{div}_percent'] = round(
                (division_stats[div] / division_stats['total']) * 100, 1
            )
    
    # ============================================
    # CARD 6: Division 0 (Failed) Performance
    # ============================================
    failed_stats = {
        'total': division_stats['0'],
        'percentage': division_stats['fail_percent'],
        'trend': 'improving' if division_stats['fail_percent'] < 30 else 'needs_attention',
        'color': 'success' if division_stats['fail_percent'] < 30 else 'danger'
    }
    
    # Calculate trend (compare with previous year)
    previous_year_results = StudentFinalResult.objects.filter(
        project__year=current_year - 1
    )
    previous_fail_count = previous_year_results.filter(division='0').count()
    previous_total = previous_year_results.count()
    previous_fail_percent = (previous_fail_count / previous_total * 100) if previous_total > 0 else 0
    
    if previous_fail_percent > 0:
        trend_change = division_stats['fail_percent'] - previous_fail_percent
        failed_stats['trend_direction'] = 'down' if trend_change < 0 else 'up'
        failed_stats['trend_value'] = abs(round(trend_change, 1))
    else:
        failed_stats['trend_direction'] = 'neutral'
        failed_stats['trend_value'] = 0
    
    # ============================================
    # Additional Smart Analytics
    # ============================================
    # Top performing schools
    top_schools = ExamSchoolResult.objects.filter(
        project__is_active=True,
        sch_gpa__isnull=False
    ).order_by('sch_gpa')[:5]
    
    # Recent activity
    recent_projects = JointExamProject.objects.filter(
        is_active=True
    ).order_by('-created_at')[:3]
    
    # Completion rate
    completed_subjects = ProcessedSubjectScore.objects.filter(
        project__is_active=True,
        percentage_score__isnull=False
    ).values('project').distinct().count()
    
    total_subjects = ProcessedSubjectScore.objects.filter(
        project__is_active=True
    ).values('project', 'subject').distinct().count()
    
    completion_rate = round(
        (completed_subjects / total_subjects * 100), 1
    ) if total_subjects > 0 else 0
    
    context = {
        # Card 1: Projects
        'total_projects': total_projects,
        'active_projects': active_projects,
        'inactive_projects': inactive_projects,
        'ongoing_projects': ongoing_projects,
        'projects_by_level': projects_by_level,
        
        # Card 2: Students by Level
        'student_stats': student_stats,
        
        # Card 3 & 4: Status
        'upcoming_projects': upcoming_projects,
        'completed_projects': completed_projects,
        
        # Card 5: Division Performance
        'division_stats': division_stats,
        
        # Card 6: Failed Students
        'failed_stats': failed_stats,
        
        # Additional Analytics
        'top_schools': top_schools,
        'recent_projects': recent_projects,
        'completion_rate': completion_rate,
        'current_year': current_year,
    }
    
    return render(request, 'exams/smart_dashboard.html', context)




@login_required
def data_management_dashboard(request):
    """Data Management Dashboard with statistics"""
    
    context = {
        'academic_levels_count': AcademicLevel.objects.count(),
        'subjects_count': ExamSubject.objects.count(),
        'papers_count': ExamSubjectPaper.objects.count(),
        'combinations_count': ExamCombination.objects.count(),
        'exam_types_count': ExamType.objects.count(),
        'recent_subjects': ExamSubject.objects.all().order_by('-subject_id')[:5],
        'recent_papers': ExamSubjectPaper.objects.all().order_by('-id')[:5],
    }
    
    return render(request, 'exams/data_management_dashboard.html', context)













# exams/views.py
from django.contrib.auth import get_user_model
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import redirect
from django.contrib import messages
from django.http import JsonResponse
from django.db import transaction

User = get_user_model()

@login_required
@permission_required("exams.add_user", raise_exception=True)
def generate_users_from_schools(request):
    """
    Generate user accounts for schools that don't have users yet
    Username = school_code
    Email = school_code@gmail.com
    Default password = Elimu@1234
    Role = school
    """
    if request.method != "POST":
        return JsonResponse({"error": "Method not allowed"}, status=405)
    
    try:
        # Get all schools without associated user profiles
        schools_without_users = []
        schools_processed = 0
        errors = []
        
        with transaction.atomic():
            # Get all schools
            all_schools = School.objects.select_related('region', 'district').all()
            
            for school in all_schools:
                # Check if user already exists for this school
                user_exists = UserProfile.objects.filter(school=school).exists()
                
                if not user_exists:
                    username = school.school_code
                    email = f"{school.school_code}@gmail.com"
                    default_password = "Elimu@1234"
                    
                    # Check if username already exists
                    if User.objects.filter(username=username).exists():
                        errors.append(f"School {school.school_code}: Username '{username}' already exists")
                        continue
                    
                    try:
                        # Create user
                        user = User.objects.create_user(
                            username=username,
                            email=email,
                            password=default_password,
                            first_name=school.name_short or school.name[:50],
                            last_name="School"
                        )
                        
                        # Create profile
                        UserProfile.objects.create(
                            user=user,
                            role="school",
                            school=school,
                            region=school.region,
                            district=school.district
                        )
                        
                        schools_processed += 1
                        schools_without_users.append({
                            'school_code': school.school_code,
                            'username': username,
                            'password': default_password
                        })
                        
                    except Exception as e:
                        errors.append(f"School {school.school_code}: {str(e)}")
        
        # Prepare response message
        if schools_processed > 0:
            message = f"Successfully created {schools_processed} user accounts from schools."
            if errors:
                message += f" {len(errors)} errors occurred."
            messages.success(request, message)
            
            # Add warning about default password
            messages.warning(
                request, 
                f"Default password for new users is 'Elimu@1234'. Users should change their password on first login."
            )
        else:
            messages.info(request, "No new user accounts needed. All schools already have user accounts.")
        
        if errors:
            for error in errors[:5]:  # Show first 5 errors
                messages.error(request, error)
        
        return redirect("exams:geo_import")
        
    except Exception as e:
        messages.error(request, f"Error generating users: {str(e)}")
        return redirect("exams:geo_import")



# exams/views.py
@login_required
def download_student_template(request, project_code):
    """Download student template"""
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    wb = Workbook()
    ws = wb.active
    ws.title = "Student Data"
    
    headers = ['school_code', 'student_number', 'full_name', 'sex']
    ws.append(headers)
    
    # Style headers
    for col in range(1, len(headers) + 1):
        cell = ws.cell(row=1, column=col)
        cell.font = Font(bold=True, color="FFFFFF")
        cell.fill = PatternFill(start_color="4472C4", end_color="4472C4", fill_type="solid")
    
    # Example data
    examples = [
        ['S0001', 'S0001-001', 'John Doe', 'M'],
        ['S0001', 'S0001-002', 'Jane Smith', 'F'],
    ]
    for example in examples:
        ws.append(example)
    
    # Auto-adjust columns
    for column in ws.columns:
        max_length = 0
        for cell in column:
            try:
                if len(str(cell.value)) > max_length:
                    max_length = len(str(cell.value))
            except:
                pass
        ws.column_dimensions[column[0].column_letter].width = min(max_length + 2, 30)
    
    response = HttpResponse(
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    filename = f"{project.name}_{project.year}_student_template.xlsx"
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    wb.save(response)
    return response


@login_required
def download_score_template(request, project_code):
    """Download score template with subject columns"""
    project = get_object_or_404(JointExamProject, project_code=project_code)
    
    # Get subjects for this academic level
    subjects = ExamSubject.objects.filter(
        academic_level=project.academic_level,
        is_active=True
    ).order_by('subject_code')
    
    wb = Workbook()
    ws = wb.active
    ws.title = "Score Data"
    
    # Row 1: Headers
    headers = ['', 'Student Number', '', '', '']  # First 5 columns
    for subject in subjects:
        headers.append(subject.subject_code)
    ws.append(headers)
    
    # Row 2: Descriptions
    desc_row = ['', 'Student Number', '', '', '']
    for subject in subjects:
        desc_row.append(subject.subject_name_eng)
    ws.append(desc_row)
    
    # Example student
    example = ['', 'S0001-001', '', '', '']
    for _ in subjects:
        example.append('75')
    ws.append(example)
    
    # Style headers
    for col in range(1, len(headers) + 1):
        cell = ws.cell(row=1, column=col)
        cell.font = Font(bold=True)
        if col > 5:
            cell.fill = PatternFill(start_color="FFD966", end_color="FFD966", fill_type="solid")
    
    response = HttpResponse(
        content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
    )
    filename = f"{project.name}_{project.year}_score_template.xlsx"
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    wb.save(response)
    return response























# exams/views.py
import os
from django.conf import settings
from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required

# exams/views.py
import os
from django.conf import settings
from django.http import FileResponse, Http404
from django.shortcuts import get_object_or_404
from django.contrib.auth.decorators import login_required

@login_required
def download_academic_template(request, project_code, template_type):
    """
    Download template based on academic level and template type
    template_type: 'student' or 'scores'
    """
    project = get_object_or_404(JointExamProject, project_code=project_code)
    academic_level = project.academic_level
    
    # Map academic level to template filename
    level_mapping = {
        'O-Level': {
            'student': 'O-Level-Template-Joint.xlsx',
            'scores': 'O-Level-Template-Joint.xlsx'
        },
        'A-Level': {
            'student': 'A-Level-Template-Joint.xlsx',
            'scores': 'A-Level-Template-Joint.xlsx'
        },
        'Primary-Level': {
            'student': 'Primary-Level-Template-Joint.xlsx',
            'scores': 'Primary-Level-Template-Joint.xlsx'
        },
        'Form One': {
            'student': 'Form_One_student_template.xlsx',
            'scores': 'Form_One_score_template.xlsx'
        },
        'Form Two': {
            'student': 'Form_Two_student_template.xlsx',
            'scores': 'Form_Two_score_template.xlsx'
        },
        'Form Three': {
            'student': 'Form_Three_student_template.xlsx',
            'scores': 'Form_Three_score_template.xlsx'
        },
        'Form Four': {
            'student': 'Form_Four_student_template.xlsx',
            'scores': 'Form_Four_score_template.xlsx'
        },
        'Form Five': {
            'student': 'Form_Five_student_template.xlsx',
            'scores': 'Form_Five_score_template.xlsx'
        },
        'Form Six': {
            'student': 'Form_Six_student_template.xlsx',
            'scores': 'Form_Six_score_template.xlsx'
        },
    }
    
    # Get the template filename based on academic level and type
    level_name = academic_level.name
    template_config = level_mapping.get(level_name)
    
    if not template_config:
        # Fallback to default template if level not found
        template_config = {
            'student': 'default_student_template.xlsx',
            'scores': 'default_score_template.xlsx'
        }
    
    filename = template_config.get(template_type)
    
    # Try multiple possible locations to find the template file
    possible_paths = [
        # Media directory
        os.path.join(settings.MEDIA_ROOT, 'templates', filename),
        os.path.join(settings.MEDIA_ROOT, filename),
        
        # Static directory
        os.path.join(settings.STATIC_ROOT, 'templates', filename),
        os.path.join(settings.STATIC_ROOT, filename),
        
        # App static directory
        os.path.join(settings.BASE_DIR, 'exams', 'static', 'exams', 'templates', filename),
        os.path.join(settings.BASE_DIR, 'exams', 'static', 'exams', filename),
        os.path.join(settings.BASE_DIR, 'exams', 'static', 'templates', filename),
        
        # Project static directory
        os.path.join(settings.BASE_DIR, 'static', 'templates', filename),
        os.path.join(settings.BASE_DIR, 'static', filename),
        
        # Root templates directory
        os.path.join(settings.BASE_DIR, 'templates', filename),
        
        # Current directory
        os.path.join(settings.BASE_DIR, filename),
    ]
    
    # Also try to find by searching for the filename pattern
    search_dirs = [
        settings.MEDIA_ROOT,
        settings.STATIC_ROOT,
        os.path.join(settings.BASE_DIR, 'exams', 'static'),
        os.path.join(settings.BASE_DIR, 'static'),
        os.path.join(settings.BASE_DIR, 'templates'),
        settings.BASE_DIR,
    ]
    
    # Add search for filename in subdirectories
    for search_dir in search_dirs:
        if search_dir and os.path.exists(search_dir):
            for root, dirs, files in os.walk(search_dir):
                if filename in files:
                    file_path = os.path.join(root, filename)
                    possible_paths.append(file_path)
    
    # Try each possible path
    file_path = None
    for path in possible_paths:
        if path and os.path.exists(path):
            file_path = path
            break
    
    # If still not found, try case-insensitive search
    if not file_path:
        for search_dir in search_dirs:
            if search_dir and os.path.exists(search_dir):
                for root, dirs, files in os.walk(search_dir):
                    for file in files:
                        if file.lower() == filename.lower():
                            file_path = os.path.join(root, file)
                            break
                    if file_path:
                        break
            if file_path:
                break
    
    if not file_path:
        # Log the paths that were searched for debugging
        searched_paths = "\n".join([str(p) for p in possible_paths if p])
        error_msg = f"Template file '{filename}' not found for {level_name}.\n\nSearched in:\n{searched_paths}"
        raise Http404(error_msg)
    
    # Return the file
    response = FileResponse(open(file_path, 'rb'), as_attachment=True)
    response['Content-Disposition'] = f'attachment; filename="{filename}"'
    return response