
from __future__ import annotations

import json
import logging
import uuid
from datetime import date, timedelta
from decimal import Decimal
from urllib.parse import urlencode

import requests
from django.apps import apps
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required, user_passes_test
from django.core.mail import EmailMessage, send_mail
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.db.models import Q, Prefetch, Count, Case, When, IntegerField, Subquery, OuterRef
from django.http import HttpResponse, JsonResponse, HttpResponseBadRequest, Http404
from django.shortcuts import get_object_or_404, render, redirect
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils import timezone
from django.utils.dateparse import parse_date
from django.utils.html import strip_tags
from django.views.decorators.cache import cache_page
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods, require_GET

# Third Party
from sslcommerz_python_api.payment import SSLCSession

# Local Imports
from content.forms import ContactForm
from content.models import (
    Banner, Notice, TimelineEvent, GalleryItem, AboutSection,
    AcademicCalendarItem, Course, FunctionHighlight, CollegeFestival, Contact, FooterSettings, GalleryPost,
    ClassResultSummary, ClassTopper, ExamTerm, AcademicClass, ClassResultSubjectAvg, AttendanceSession, Member,
    ExamRoutine, BusRoute, StudentMarksheet, AdmissionApplication, StudentMarksheetItem, StudentProfile, 
    AttendanceStatus, StudentAttendance, CourseCategory, TuitionInvoice, TuitionPayment
)

# Try to import the item-row model name you use; tolerate either name
try:
    from content.models import StudentMarksheetItem as RowModel
except Exception:
    from reportcards.models import MarkRow as RowModel

logger = logging.getLogger(__name__)










# -------------------------------------------------------------------
# Helpers
# -------------------------------------------------------------------

def _staff(user):
    return user.is_authenticated and user.is_staff


def _paginate(request, queryset, param_name: str, per_page: int):
    """Small helper to DRY pagination."""
    paginator = Paginator(queryset, per_page)
    page_num = request.GET.get(param_name) or 1
    try:
        page = paginator.page(page_num)
    except (PageNotAnInteger, EmptyPage):
        page = paginator.page(1)

    cur = page.number
    total = paginator.num_pages
    # compact window like: current-1 .. current .. current+1
    start = max(cur - 1, 1)
    end = min(cur + 1, total)
    page_range = range(start, end + 1)
    return page, page_range, paginator


def summary_toppers_qs():
    """Keep toppers ordered by rank everywhere."""
    return ClassTopper.objects.order_by("rank", "id")




# -------------------------------------------------------------------
# Public pages
# -------------------------------------------------------------------


def home(request):
    # ------------ Banners ------------
    banners_qs = (
        Banner.objects
        .filter(is_active=True)
        .filter(Q(image__isnull=False) | ~Q(image_url=""))
        .order_by("order", "-created_at")
    )

    # ------------ Notices (4 per page) ------------
    notices_qs = (
        Notice.objects
        .filter(is_active=True)
        .order_by("-published_at", "-created_at")
    )
    notices_paginator = Paginator(notices_qs, 3)
    try:
        npage = int(request.GET.get("npage", 1))
    except (TypeError, ValueError):
        npage = 1
    try:
        notices_page = notices_paginator.get_page(npage)
    except EmptyPage:
        notices_page = notices_paginator.get_page(notices_paginator.num_pages)

    q_notices = request.GET.copy()
    q_notices.pop("npage", None)
    base_qs_notices = q_notices.urlencode()

    # ------------ Timeline (4 per page) ------------
    tl_qs = (
        TimelineEvent.objects
        .filter(is_active=True)
        .order_by("-date", "-id")
    )
    tl_paginator = Paginator(tl_qs, 4)
    try:
        tl_page_num = int(request.GET.get("tpage", 1))
    except (TypeError, ValueError):
        tl_page_num = 1
    try:
        tl_page = tl_paginator.get_page(tl_page_num)
    except EmptyPage:
        tl_page = tl_paginator.get_page(tl_paginator.num_pages)

    q_tl = request.GET.copy()
    q_tl.pop("tpage", None)
    base_qs_tl = q_tl.urlencode()

    # ------------ Gallery (6 per page) ------------
    gallery_qs = (
        GalleryItem.objects
        .filter(is_active=True)
        .order_by("order", "-id")
    )
    gallery_paginator = Paginator(gallery_qs, 6)
    try:
        gpage_num = int(request.GET.get("gpage", 1))
    except (TypeError, ValueError):
        gpage_num = 1

    # --- ADD THIS LINE ---
    # --- REPLACE print() WITH logger.debug() ---
    logger.debug(f"!!!!!!!!!!!!!! Gallery page number requested: {gpage_num} !!!!!!!!!!!!!!")
    # --- END OF ADDED LINE ---

    try:
        gallery_page = gallery_paginator.get_page(gpage_num)
    except EmptyPage:
        gallery_page = gallery_paginator.get_page(gallery_paginator.num_pages)

    # --- THESE LINES WERE MISSING BEFORE ---
    q_gal = request.GET.copy()
    q_gal.pop("gpage", None)
    base_qs_gal = q_gal.urlencode()
    # --- END OF MISSING LINES ---

    # ------------ About ------------
    about = (
        AboutSection.objects
        .filter(is_active=True)
        .order_by("order")
        .first()
    )

    # ------------ Courses (paginated + filtering) ------------
    # Build initial queryset
    courses_qs = Course.objects.filter(is_active=True).order_by("order", "title")

    # read & sanitize category param
    category_param = request.GET.get("category")
    try:
        category_id = int(category_param) if category_param else None
    except (TypeError, ValueError):
        category_id = None

    # If category_id present, filter before creating paginator
    if category_id:
        courses_qs = courses_qs.filter(category_id=category_id)

    # categories for filter buttons
    categories = CourseCategory.objects.all().order_by("name")

    # paginator
    try:
        cpage_num = int(request.GET.get("cpage", 1))
    except (TypeError, ValueError):
        cpage_num = 1

    courses_paginator = Paginator(courses_qs, 3)  # show 6 per page (change to 3 if you prefer)
    try:
        courses_page = courses_paginator.get_page(cpage_num)
    except EmptyPage:
        courses_page = courses_paginator.get_page(courses_paginator.num_pages)

    # base_qs_courses: remove cpage and category so links can append them
    q_courses = request.GET.copy()
    q_courses.pop("cpage", None)
    q_courses.pop("category", None)
    base_qs_courses = q_courses.urlencode()

    # ------------ Academic Calendar (paginated: 6 per page) ------------
    cal_qs = (
        AcademicCalendarItem.objects
        .filter(is_active=True)
        .order_by("order", "-created_at", "-id")
    )
    cal_paginator = Paginator(cal_qs, 3)
    try:
        capage_num = int(request.GET.get("capage", 1))
    except (TypeError, ValueError):
        capage_num = 1
    try:
        calendar_page = cal_paginator.get_page(capage_num)
    except EmptyPage:
        calendar_page = cal_paginator.get_page(cal_paginator.num_pages)

    q_cal = request.GET.copy()
    q_cal.pop("capage", None)
    base_qs_cal = q_cal.urlencode()

    # ------------ Function Highlights (3 per page) ------------
    functions_qs = (
        FunctionHighlight.objects
        .filter(is_active=True)
        .order_by("order", "-id")
    )
    functions_page, functions_page_range, _functions_pag = _paginate(
        request, functions_qs, "fpage", per_page=3
    )
    q_fun = request.GET.copy()
    q_fun.pop("fpage", None)
    base_qs_functions = q_fun.urlencode()

    # ------------ Festivals (parametrized) ------------
    try:
        fest_per_page = int(request.GET.get("fest_per_page", 3))
    except (TypeError, ValueError):
        fest_per_page = 2

    festivals_qs = (
        CollegeFestival.objects
        .filter(is_active=True)
        .prefetch_related("media_items")
        .order_by("order", "-created_at")
    )
    festivals_page, festivals_page_range, _fest_pag = _paginate(
        request, festivals_qs, "festpage", per_page=fest_per_page
    )
    q_fest = request.GET.copy()
    q_fest.pop("festpage", None)
    base_qs_festivals = q_fest.urlencode()

    # ------------ Members ------------
    member_sections = [
        {"key": "hod", "label": "Head of Department", "items": []},
        {"key": "teacher", "label": "Teachers", "items": []},
        {"key": "student", "label": "Students", "items": []},
        {"key": "staff", "label": "Staff", "items": []},
    ]
    members_counts = {"hod": 0, "teacher": 0, "student": 0, "staff": 0}
    for sec in member_sections:
        qs = (
            Member.objects
            .filter(role=sec["key"], is_active=True)
            .order_by("order", "name")
        )
        sec["items"] = list(qs)
        members_counts[sec["key"]] = qs.count()

    # ------------ Footer / Contact ------------
    footer = (
        FooterSettings.objects
        .filter(is_active=True)
        .order_by("-updated_at", "-id")
        .first()
    )
    from content.forms import ContactForm
    
    contact_form = ContactForm()
    contact_info = Contact.objects.filter(is_active=True).order_by('-updated_at', '-id').first()

    # ------------ Context ------------
    context = {
        # Banners
        "banners": banners_qs,
        "banners_flat": [{
            "title": b.title,
            "subtitle": b.subtitle,
            "image": getattr(b, "image_src", None),
            "button_text": b.button_text,
            "button_link": b.button_link,
            "order": b.order,
        } for b in banners_qs],

        # Notices
        "notices": notices_qs,
        "notices_page": notices_page,
        "notices_flat": [{
            "title": n.title,
            "subtitle": n.subtitle,
            "image": getattr(n, "image_src", None),
            "published_at": n.published_at,
            "url": reverse("notice_detail", args=[n.pk]),
        } for n in notices_qs],
        "base_qs_notices": base_qs_notices,

        # Timeline
        "timeline_events": tl_page.object_list,
        "tl_page": tl_page,
        "tl_paginator": tl_paginator,
        "base_qs_tl": base_qs_tl,

        # Gallery
        "gallery_page": gallery_page,
        "base_qs_gal": base_qs_gal,

        # Academic Calendar (paginated)
        "calendar_page": calendar_page,
        "base_qs_cal": base_qs_cal,

        # About / Courses
        "about": about,
        "categories": categories,
        "category_id": category_id,
        "courses_page": courses_page,
        "base_qs_courses": base_qs_courses,

        # Functions
        "functions": functions_qs,
        "functions_page": functions_page,
        "functions_page_range": functions_page_range,
        "base_qs_functions": base_qs_functions,

        # Festivals
        "festivals": festivals_qs,
        "festivals_page": festivals_page,
        "festivals_page_range": festivals_page_range,
        "base_qs_festivals": base_qs_festivals,
        "fest_per_page": fest_per_page,

        # Members / Contact / Footer
        "member_sections": member_sections,
        "members_counts": members_counts,
        "contact_info": contact_info,
        "contact_form": contact_form,
        "footer": footer,
    }

    return render(request, "index.html", context)


def contact_submit(request):
    """
    Saves the contact message and sends:
      1) Notification email to site inbox (DEFAULT_CONTACT_EMAIL or DEFAULT_FROM_EMAIL)
      2) Auto-acknowledgement email to the sender
    Then redirects back to #contact with a flash message.
    """
    if request.method != "POST":
        return redirect(reverse("home") + "#contact")

    form = ContactForm(request.POST)
    if not form.is_valid():
        messages.error(request, "Please fix the errors below.")
        contact_info = Contact.objects.filter(is_active=True).order_by("-updated_at").first()
        return render(request, "index.html", {
            "contact_info": contact_info,
            "contact_form": form,
        })

    msg = form.save()  # ContactMessage row

    # Email addresses
    site_inbox = getattr(settings, "DEFAULT_CONTACT_EMAIL", None) or getattr(settings, "DEFAULT_FROM_EMAIL", None)
    from_addr = getattr(settings, "DEFAULT_FROM_EMAIL", getattr(settings, "EMAIL_HOST_USER", None))

    # Admin notification
    admin_ok = False
    if site_inbox and from_addr:
        subject_admin = f"[Website] New Contact: {msg.subject}"
        body_admin = (
            f"New contact message received:\n\n"
            f"Name: {msg.name}\n"
            f"Email: {msg.email}\n"
            f"Phone: {msg.phone or '-'}\n"
            f"Sent: {msg.created_at:%Y-%m-%d %H:%M}\n\n"
            f"Message:\n{msg.message}\n"
        )
        try:
            email_admin = EmailMessage(
                subject_admin, body_admin, from_addr, [site_inbox],
                headers={"Reply-To": msg.email}
            )
            email_admin.send(fail_silently=False)
            admin_ok = True
        except Exception:
            admin_ok = False

    # Auto-acknowledgement to the sender
    user_ok = False
    if from_addr and msg.email:
        subject_user = "Thanks for contacting us"
        body_user = (
            f"Hi {msg.name},\n\n"
            f"Thanks for reaching out. We received your message:\n\n"
            f"Subject: {msg.subject}\n"
            f"Message:\n{msg.message}\n\n"
            f"We'll get back to you soon.\n\n"
            f"Best regards,\n"
            f"{getattr(settings, 'SITE_NAME', 'Our College')}"
        )
        try:
            send_mail(
                subject_user,
                body_user,
                from_addr,
                [msg.email],  # send to user
                fail_silently=False,
            )
            user_ok = True
        except Exception:
            user_ok = False

    if admin_ok:
        messages.success(request, "Thanks! Your message has been sent.")
    else:
        messages.warning(request, "Message saved. Email delivery appears unavailable right now.")

    return redirect(reverse("home") + "#contact")


@cache_page(60 * 5)
def notices_list(request):
    """Paginated list of active notices."""
    qs = Notice.objects.filter(is_active=True).order_by("-published_at", "-created_at")
    page_obj = Paginator(qs, 12).get_page(request.GET.get("page") or 1)
    return render(request, "notices/notice_list.html", {
        "page_obj": page_obj,  # what your template expects
        "notices": page_obj,  # optional: backwards-compat if elsewhere you used 'notices'
    })


@cache_page(60 * 5)
def notice_detail(request, pk: int):
    notice = get_object_or_404(Notice.objects.filter(is_active=True), pk=pk)

    more_notices = (
        Notice.objects.filter(is_active=True)
        .exclude(pk=notice.pk)
        .order_by("-published_at", "-created_at")[:8]
    )
    prev_notice = (
        Notice.objects.filter(is_active=True, published_at__gt=notice.published_at)
        .order_by("published_at", "created_at")
        .first()
    )
    next_notice = (
        Notice.objects.filter(is_active=True, published_at__lt=notice.published_at)
        .order_by("-published_at", "-created_at")
        .first()
    )

    return render(request, "notice_detail.html", {  # <-- correct folder
        "notice": notice,
        "more_notices": more_notices,
        "prev_notice": prev_notice,
        "next_notice": next_notice,
    })


def gallery_page(request):
    qs = GalleryPost.objects.filter(is_active=True).order_by("order", "-created_at")
    paginator = Paginator(qs, 12)  # 12 items per page
    page_number = request.GET.get("page") or 1
    page = paginator.get_page(page_number)

    footer = FooterSettings.objects.filter(is_active=True).order_by("-updated_at").first()

    return render(request, "gallery_list.html", {
        "page": page,
        "footer": footer,
    })


# -------------------------------------------------------------------
# Results pages
# -------------------------------------------------------------------

# -------------------------------------------------------------------
# Results pages (Corrected & Cleaned)
# -------------------------------------------------------------------

from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.shortcuts import render, get_object_or_404
from urllib.parse import urlencode

# Consolidate the filter logic into ONE robust function
def _apply_result_filters(qs, request):
    """
    Applies filters based on GET parameters for StudentMarksheet.
    """
    # 1. Get Params
    year = request.GET.get("year")
    term_id = request.GET.get("term_id")
    class_id = request.GET.get("class_id")
    section = request.GET.get("section")
    
    # Search Params
    search_query = request.GET.get("q")
    roll_number = request.GET.get("roll")

    # 2. Apply Filters
    if year:
        qs = qs.filter(term__year=year)
    if term_id:
        qs = qs.filter(term_id=term_id)
    if class_id:
        qs = qs.filter(school_class_id=class_id)
    if section:
        qs = qs.filter(school_class__section__iexact=section)

    if search_query:
        qs = qs.filter(student_full_name__icontains=search_query.strip())
    if roll_number:
        qs = qs.filter(roll_number__iexact=roll_number.strip())

    return qs


def results_index(request):
    """
    Results page — filtering + pagination behaving EXACTLY like home() courses section.
    """

    # ---------------------------
    # STEP 1: Base queryset
    # ---------------------------
    qs = (
        StudentMarksheet.objects
        .select_related("school_class", "term")
        .order_by(
            "-term__year",
            "term__name",
            "school_class__name",
            "school_class__section",
            "student_full_name",
            "-id",
        )
    )

    # ---------------------------
    # STEP 2: Apply filters (inline, NOT from external function)
    # ---------------------------

    q = request.GET.get("q")
    roll = request.GET.get("roll")
    class_id = request.GET.get("class_id")
    section = request.GET.get("section")

    if q:
        qs = qs.filter(student_full_name__icontains=q.strip())

    if roll:
        qs = qs.filter(roll_number__iexact=roll.strip())

    if class_id:
        qs = qs.filter(school_class_id=class_id)

    if section:
        qs = qs.filter(school_class__section__iexact=section.strip())

    # ---------------------------
    # STEP 3: Pagination
    # ---------------------------
    # Same pattern as home() → courses pagination
    try:
        page_num = int(request.GET.get("page", 1))
    except (TypeError, ValueError):
        page_num = 1

    paginator = Paginator(qs, 12)     # change per page if needed

    try:
        page = paginator.get_page(page_num)
    except EmptyPage:
        page = paginator.get_page(paginator.num_pages)

    # ---------------------------
    # STEP 4: Build base_qs (for pagination URLs)
    # ---------------------------
    qs_copy = request.GET.copy()
    qs_copy.pop("page", None)
    base_qs = qs_copy.urlencode()

    # ---------------------------
    # STEP 5: Dropdown data
    # ---------------------------
    years = list(
        ExamTerm.objects
        .order_by("-year")
        .values_list("year", flat=True)
        .distinct()
    )
    terms = ExamTerm.objects.order_by("name", "-year").values("id", "name", "year")
    classes = AcademicClass.objects.order_by("-year", "name", "section").values(
        "id", "name", "section", "year"
    )

    # ---------------------------
    # STEP 6: Context
    # ---------------------------
    ctx = {
        "page": page,
        "summaries": page.object_list,

        # filters back to template
        "filters": {
            "q": q or "",
            "roll": roll or "",
            "class_id": class_id or "",
            "section": section or "",
        },

        "years": years,
        "terms": terms,
        "classes": classes,

        # EXACT same pattern as home courses
        "base_qs": base_qs,
    }

    return render(request, "results/results_index.html", ctx)
    
    
def results_filter(request):
    """Alias to results_index (keeps URL patterns working)."""
    return results_index(request)

def results_detail(request, summary_id: int):
    """
    Detail page for a specific StudentMarksheet.
    Loads the StudentMarksheet and its related StudentMarksheetItem rows (using '.items').
    """
    
    # 1. Load the StudentMarksheet object
    # Import StudentMarksheet model if it's not already available in this scope
    from content.models import StudentMarksheet  # or wherever your model is located
    
    marksheet = get_object_or_404(
        StudentMarksheet.objects.select_related("school_class", "term"),
        pk=summary_id,
    )
    
    # 2. CORRECTED: Access related subject rows using the correct 'items' related_name
    # The related manager is named 'items' on the StudentMarksheet model.
    items_qs = marksheet.items.select_related('subject').order_by('subject__name')
    
    # 3. Build the context using the 'ms' key (as required by your template)
    ctx = {
        "ms": marksheet,       # The primary object (StudentMarksheet)
        "items": items_qs,     # The related subject rows (StudentMarksheetItem objects)
        # Note: If your template requires 'klass' and 'term' separately, they are available
        # via ms.school_class and ms.term, so no need to add them here unless necessary.
    }
    
    # 4. Render the template
    return render(request, "results/marksheet_detail.html", ctx)
def results_debug(_):
    c = ClassResultSummary.objects.count()
    s = ClassResultSummary.objects.select_related("klass", "term").first()
    return HttpResponse(f"Summaries={c} | First={s}")


def attendance_dashboard(request):
    classes_qs = AcademicClass.objects.all().order_by('-year', 'name', 'section')

    # FIX N+1: Use Subquery to find the ID of the LATEST AttendanceSession for each class
    latest_session_ids_subquery = (
        AttendanceSession.objects
        .filter(school_class=OuterRef('pk'))
        .order_by('-date', '-id')
        .values('id')[:1]
    )

    classes_with_latest_id = classes_qs.annotate(
        latest_session_id=Subquery(latest_session_ids_subquery)
    )

    # Fetch all latest sessions in a single query based on the calculated IDs
    latest_sessions_map = {
        s.school_class_id: s 
        for s in AttendanceSession.objects.filter(
            id__in=[c.latest_session_id for c in classes_with_latest_id if c.latest_session_id]
        )
    }

    class_attendance = []
    # Iterate over the annotated queryset
    for klass in classes_with_latest_id: 
        latest = latest_sessions_map.get(klass.id)
        # The rest of your logic using 'latest' attributes is now fast.
        if latest:
            total = latest.total_count
            rate = latest.attendance_rate_pct
            present = latest.present_count
            absent = latest.absent_count
            late = latest.late_count
            excused = latest.excused_count
            date_taken = latest.date
        else:
            total = present = absent = late = excused = 0
            rate = 0.0
            date_taken = None

        class_attendance.append({
             'klass': klass,
             'date': date_taken,
             'total': total,
             'present': present,
             'absent': absent,
             'late': late,
             'excused': excused,
             'rate_pct': rate,
        })
    
    paginator = Paginator(class_attendance, 9)
    page_number = request.GET.get('page')
    page_obj = paginator.get_page(page_number)

    return render(request, "attendance/dashboard_all_classes.html", {
        'page_obj': page_obj
    })


@require_http_methods(["GET"])
def attendance_class_overview_json(request, class_id: int):
    """
    Overview for a class between start/end (inclusive), aggregated from per-day counts.
    Optional GET: start=YYYY-MM-DD, end=YYYY-MM-DD (defaults to last 30 days)
    """
    try:
        klass = AcademicClass.objects.get(pk=class_id)
    except AcademicClass.DoesNotExist:
        return JsonResponse({"error": "Class not found"}, status=404)

    end = parse_date(request.GET.get("end") or "") or date.today()
    start = parse_date(request.GET.get("start") or "") or (end - timedelta(days=30))

    days = (AttendanceSession.objects
            .filter(school_class=klass, date__gte=start, date__lte=end)
            .order_by("date")
            .values("date", "present_count", "absent_count", "late_count", "excused_count"))

    by_day = []
    totals = {"present": 0, "absent": 0, "late": 0, "excused": 0, "total": 0}

    for d in days:
        present = int(d["present_count"] or 0)
        absent = int(d["absent_count"] or 0)
        late = int(d["late_count"] or 0)
        excused = int(d["excused_count"] or 0)
        total = present + absent + late + excused
        # Note: Check if AttendanceSession model has these properties or methods.
        # For safety, using manual calculation here:
        rate = round(100.0 * (present + excused) / total, 1) if total else 0.0

        by_day.append({
            "date": d["date"].isoformat(),
            "present": present, "absent": absent, "late": late, "excused": excused,
            "total": total, "rate_pct": rate,
        })

        totals["present"] += present
        totals["absent"] += absent
        totals["late"] += late
        totals["excused"] += excused
        totals["total"] += total

    return JsonResponse({
        "klass": {"id": klass.id, "name": klass.name},
        "range": {"start": start.isoformat(), "end": end.isoformat()},
        "totals": totals,
        "by_day": by_day,
    })


@require_http_methods(["GET"])
def attendance_classday_get(request, class_id: int):
    """
    Get counts for a single class+date. (Existing Logic)
    """
    klass = get_object_or_404(AcademicClass, pk=class_id)

    date_param = (request.GET.get("date") or "").strip().lower()
    if date_param in ("", "today", "now"):
        the_date = date.today()
    else:
        parsed = parse_date(date_param)
        the_date = parsed or date.today()

    use_fallback = (request.GET.get("fallback", "1") != "0")

    # Try exact day first
    day = AttendanceSession.objects.filter(school_class=klass, date=the_date).first()
    source = "exact"
    effective_date = the_date

    # If none and fallback allowed, use latest available
    if not day and use_fallback:
        latest = (AttendanceSession.objects
                  .filter(school_class=klass)
                  .order_by("-date")
                  .first())
        if latest:
            day = latest
            source = "latest"
            effective_date = latest.date

    if not day:
        # Nothing at all; return zeros
        return JsonResponse({
            "class_id": klass.id,
            "date": the_date.isoformat(),
            "effective_date": the_date.isoformat(),
            "source": "none",
            "present": 0, "absent": 0, "late": 0, "excused": 0,
            "total": 0, "rate_pct": 0.0,
        })

    # Use AttendanceSession properties for consistent data
    return JsonResponse({
        "class_id": klass.id,
        "date": the_date.isoformat(),  # what was requested
        "effective_date": effective_date.isoformat(),  # where counts came from
        "source": source,
        "present": day.present_count,
        "absent": day.absent_count,
        "late": day.late_count,
        "excused": day.excused_count,
        "total": day.total_count,
        "rate_pct": day.attendance_rate_pct,
    })


@user_passes_test(_staff)
@require_http_methods(["POST"])
def attendance_classday_upsert(request):
    """
    Create/update a class-day with counts (no per-student rows). (Existing Logic)
    NOTE: This view is superseded by the new save_attendance view
    if you are using the individual student method.
    """

    # ... (body remains the same as your provided code) ...
    def _to_int(name):
        v = request.POST.get(name)
        if v in (None, ""): return 0
        try:
            return max(0, int(v))
        except ValueError:
            raise ValueError(f"{name} must be an integer â‰¥ 0")

    class_id = request.POST.get("class_id") or request.POST.get("klass_id")
    if not class_id: return HttpResponseBadRequest("class_id (or klass_id) required")
    try:
        klass = AcademicClass.objects.get(pk=class_id)
    except AcademicClass.DoesNotExist:
        return JsonResponse({"error": "Class not found"}, status=404)

    the_date = parse_date(request.POST.get("date") or "") or date.today()

    try:
        present = _to_int("present");
        absent = _to_int("absent")
        late = _to_int("late");
        excused = _to_int("excused")
    except ValueError as e:
        return HttpResponseBadRequest(str(e))

    day, _created = AttendanceSession.objects.get_or_create(
        school_class=klass, date=the_date, defaults={"created_by": request.user}
    )
    day.present_count = present;
    day.absent_count = absent
    day.late_count = late;
    day.excused_count = excused
    day.save(update_fields=["present_count", "absent_count", "late_count", "excused_count"])

    return JsonResponse({
        "class_id": klass.id, "date": the_date.isoformat(),
        "present": present, "absent": absent, "late": late, "excused": excused,
        "total": day.total_count, "rate_pct": day.attendance_rate_pct,
    }, status=200)


def attendance_class_page(request, class_id: int):
    klass = get_object_or_404(AcademicClass, pk=class_id)
    return render(request, "attendance/class_attendance.html", {"klass": klass})


# -------------------------------------------------------------------
# NEW: Student-Level Attendance Endpoints (for the teacher UI)
# -------------------------------------------------------------------

@login_required
@user_passes_test(_staff)
@require_http_methods(["GET"])
def get_classes_and_sections(request):
    """
    Returns unique class-section combinations for search/selection.
    """
    class_sections = StudentProfile.objects.filter(is_active=True).values(
        'school_class__id',
        'school_class__name',
        'section'
    ).annotate(
        student_count=Count('id')
    ).order_by('school_class__name', 'section')

    data = [
        {
            'class_id': item['school_class__id'],
            'class_name': item['school_class__name'],
            'section': item['section'] if item['section'] else "",  # Empty string for no section
            'id_slug': f"{item['school_class__id']}-{item['section']}",
            'student_count': item['student_count']
        }
        for item in class_sections
    ]
    return JsonResponse({'class_sections': data})


@login_required
@user_passes_test(_staff)
@require_http_methods(["GET"])
def get_section_students(request, class_id, section_name=""):
    """
    Lists students for a specific class/section and their attendance status for today.
    """
    klass = get_object_or_404(AcademicClass, pk=class_id)

    # 1. Determine the date
    try:
        attendance_date_str = request.GET.get('date', date.today().isoformat())
        attendance_date = date.fromisoformat(attendance_date_str)
    except ValueError:
        return JsonResponse({'error': 'Invalid date format.'}, status=400)

    # 2. Get the AttendanceSession (creating one if it doesn't exist)
    session, created = AttendanceSession.objects.get_or_create(
        school_class=klass,
        date=attendance_date,
        defaults={'created_by': request.user}
    )

    # 3. Filter Students
    students_qs = StudentProfile.objects.filter(
        school_class=klass,
        section__iexact=section_name,
        is_active=True
    ).select_related('user').order_by('roll_number')

    # 4. Get Existing Attendance Records
    # Fetch records only for the students in this specific section/class
    student_ids = list(students_qs.values_list('id', flat=True))
    existing_attendance = {
        record.student_id: record.status
        # Use .only() to pull back only the two columns needed for the Python dict
        for record in session.student_records.filter(student_id__in=student_ids).only('student_id', 'status') 
    }

    # 5. Prepare Students Data
    students_data = [
        {
            'id': student.id,
            'name': str(student.user),
            'roll_number': student.roll_number,
            # Current status defaults to 'P' (Present) if no record exists
            'status': existing_attendance.get(student.id, AttendanceStatus.PRESENT),
        }
        for student in students_qs
    ]

    return JsonResponse({
        'class_id': class_id,
        'section': section_name,
        'attendance_date': attendance_date.isoformat(),
        'session_id': session.id,
        'students': students_data,
        'session_counts': {
            'present': session.present_count,
            'absent': session.absent_count,
            'late': session.late_count,
            'excused': session.excused_count,
            'total': session.total_count,
        }
    })


@login_required
@user_passes_test(_staff)
@require_http_methods(["POST"])
def save_attendance(request, session_id):
    """
    Saves/updates individual student attendance and recalculates session counts.
    """
    try:
        data = json.loads(request.body)
        student_statuses = data.get('student_statuses', {})  # {student_id: 'P'/'A'/'L'/'E', ...}
    except json.JSONDecodeError:
        return JsonResponse({'error': 'Invalid JSON format.'}, status=400)

    session = get_object_or_404(AttendanceSession, id=session_id)

    # Get all student IDs being processed
    student_ids = [int(sid) for sid in student_statuses.keys()]

    # 1. Update/Create StudentAttendance Records
    records_to_create = []
    records_to_update = []

    # Fetch existing records for only the students sent in the POST data
    existing_records = {
        record.student_id: record
        for record in StudentAttendance.objects.filter(session=session, student_id__in=student_ids)
    }

    # Identify which students belong to the session's class/section (safety check)
    valid_student_ids = StudentProfile.objects.filter(
        id__in=student_ids,
        school_class=session.school_class
    ).values_list('id', flat=True)

    # Prepare for bulk operations
    for student_id in valid_student_ids:
        student_id_str = str(student_id)
        status = student_statuses.get(student_id_str)

        if status and status in AttendanceStatus.values:
            if student_id in existing_records:
                record = existing_records[student_id]
                if record.status != status:
                    record.status = status
                    records_to_update.append(record)
            else:
                records_to_create.append(StudentAttendance(
                    session=session,
                    student_id=student_id,
                    status=status
                ))

    # Perform bulk operations
    StudentAttendance.objects.bulk_create(records_to_create)
    StudentAttendance.objects.bulk_update(records_to_update, ['status'])

    # 2. Auto-Calculate and Update AttendanceSession Counts
    # Count the new status from ALL StudentAttendance records linked to this session
    counts = StudentAttendance.objects.filter(session=session).aggregate(
        present=Count('id', filter=Q(status=AttendanceStatus.PRESENT)),
        absent=Count('id', filter=Q(status=AttendanceStatus.ABSENT)),
        late=Count('id', filter=Q(status=AttendanceStatus.LATE)),
        excused=Count('id', filter=Q(status=AttendanceStatus.EXCUSED)),
    )

    # Update the AttendanceSession fields
    session.present_count = counts.get('present') or 0
    session.absent_count = counts.get('absent') or 0
    session.late_count = counts.get('late') or 0
    session.excused_count = counts.get('excused') or 0
    session.save(update_fields=["present_count", "absent_count", "late_count", "excused_count", "modified"])

    # Return the updated counts and rate
    return JsonResponse({
        'message': 'Attendance saved successfully.',
        'session_id': session.id,
        'present': session.present_count,
        'absent': session.absent_count,
        'late': session.late_count,
        'excused': session.excused_count,
        'total': session.total_count,
        'rate_pct': session.attendance_rate_pct,
    })


@cache_page(60)
@login_required
@require_GET
def exam_routines_json(request):
    """
    List exam routines. Optional filters:
      ?class_id=   (AcademicClass pk)
      ?term_id=    (ExamTerm pk)
      ?year=       (ExamTerm.year)
      ?active=0/1  (default 1)
    """
    qs = ExamRoutine.objects.select_related("school_class", "term")

    active = request.GET.get("active")
    if active is None or active == "1":
        qs = qs.filter(is_active=True)

    class_id = request.GET.get("class_id")
    term_id = request.GET.get("term_id")
    year = request.GET.get("year")

    if class_id:
        qs = qs.filter(school_class_id=class_id)
    if term_id:
        qs = qs.filter(term_id=term_id)
    if year:
        qs = qs.filter(term__year=year)

    qs = qs.order_by("-exam_start_date", "-id")

    data = []
    for r in qs:
        data.append({
            "id": r.id,
            "title": r.title or "",
            "image": r.image_src,
            "class": {
                "id": r.school_class.id,
                "name": r.school_class.name,
                "section": r.school_class.section,
                "year": r.school_class.year,
            },
            "term": {
                "id": r.term.id,
                "name": r.term.name,
                "year": r.term.year,
            },
            "exam_start_date": r.exam_start_date.isoformat(),
            "exam_end_date": r.exam_end_date.isoformat() if r.exam_end_date else None,
            "notes": r.notes,
            "is_active": r.is_active,
            "updated_at": r.updated_at.isoformat(),
        })
    return JsonResponse({"items": data})


@login_required
@require_GET
def exam_routine_detail_json(request, pk: int):
    """
    Single routine by id.
    """
    r = get_object_or_404(
        ExamRoutine.objects.select_related("school_class", "term"),
        pk=pk,
    )
    return JsonResponse({
        "id": r.id,
        "title": r.title or "",
        "image": r.image_src,
        "class": {
            "id": r.school_class.id,
            "name": r.school_class.name,
            "section": r.school_class.section,
            "year": r.school_class.year,
        },
        "term": {
            "id": r.term.id,
            "name": r.term.name,
            "year": r.term.year,
        },
        "exam_start_date": r.exam_start_date.isoformat(),
        "exam_end_date": r.exam_end_date.isoformat() if r.exam_end_date else None,
        "notes": r.notes,
        "is_active": r.is_active,
        "updated_at": r.updated_at.isoformat(),
    })


@require_GET
def exam_routines_page(request):
    """
    Grid of routines with simple filters (class / term / year).
    """
    qs = (ExamRoutine.objects
          .filter(is_active=True)
          .select_related("school_class", "term")
          .order_by("-exam_start_date", "-id"))

    class_id = request.GET.get("class_id") or ""
    term_id = request.GET.get("term_id") or ""
    year = request.GET.get("year") or ""

    if class_id:
        qs = qs.filter(school_class_id=class_id)
    if term_id:
        qs = qs.filter(term_id=term_id)
    if year:
        qs = qs.filter(term__year=year)

    page = Paginator(qs, 24).get_page(request.GET.get("page") or 1)

    classes = AcademicClass.objects.order_by("-year", "name", "section").values("id", "name", "section", "year")
    terms = ExamTerm.objects.order_by("-year", "name").values("id", "name", "year")
    years = list(ExamTerm.objects.order_by("-year").values_list("year", flat=True).distinct())

    ctx = {
        "page": page,
        "routines": page.object_list,
        "classes": classes,
        "terms": terms,
        "years": years,
        "filters": {"class_id": class_id, "term_id": term_id, "year": year},
    }
    return render(request, "exams/routines_list.html", ctx)


@require_GET
def exam_routine_detail_page(request, pk: int):
    """
    Big view of one routine (image + metadata).
    """
    r = get_object_or_404(
        ExamRoutine.objects.select_related("school_class", "term").filter(is_active=True),
        pk=pk,
    )
    return render(request, "exams/routine_detail.html", {"r": r})


def exam_routine_detail(request, pk: int):
    """
    Public detail page for a single routine.
    """
    routine = get_object_or_404(
        ExamRoutine.objects.select_related("school_class", "term").filter(is_active=True),
        pk=pk,
    )
    return render(request, "exams/routine_detail.html", {"routine": routine})


################################
# bus routine
################################


@require_GET
def bus_routes_json(request):
    """
    Public list of active routes.
    Filters:
      - q=search
      - active=1/0 (default 1)
      - include_stops=1 to embed stops
    """
    qs = BusRoute.objects.all().order_by("order", "name")

    active = request.GET.get("active", "1")
    if active == "1":
        qs = qs.filter(is_active=True)

    q = (request.GET.get("q") or "").strip()
    if q:
        qs = qs.filter(
            Q(name__icontains=q) |
            Q(code__icontains=q) |
            Q(driver_name__icontains=q) |
            Q(driver_phone__icontains=q) |
            Q(assistant_name__icontains=q) |
            Q(assistant_phone__icontains=q)
        )

    include_stops = request.GET.get("include_stops") == "1"

    items = []
    for r in qs.select_related().prefetch_related("stops"):
        obj = {
            "id": r.id,
            "name": r.name,
            "code": r.code,
            "is_active": r.is_active,
            "start_point": r.start_point,
            "end_point": r.end_point,
            "operating_days": r.operating_days_text,
            "driver": {"name": r.driver_name, "phone": r.driver_phone},
            "assistant": {"name": r.assistant_name, "phone": r.assistant_phone},
            "vehicle": {"plate": r.vehicle_plate, "capacity": r.vehicle_capacity},
            "fare_info": r.fare_info,
            "image": r.image_src,
            "map_embed_src": r.map_embed_src,
            "updated_at": r.updated_at.isoformat(),
        }
        if include_stops:
            obj["stops"] = [
                {
                    "id": s.id,
                    "name": s.name,
                    "landmark": s.landmark,
                    "time_morning": s.time_text_morning,
                    "time_evening": s.time_text_evening,
                    "lat": float(s.lat) if s.lat is not None else None,
                    "lng": float(s.lng) if s.lng is not None else None,
                    "order": s.order,
                }
                for s in r.stops.filter(is_active=True).order_by("order", "id")
            ]
        items.append(obj)

    return JsonResponse({"items": items})


@require_GET
def bus_route_detail_json(request, pk: int):
    """
    Public detail for a single route (always includes stops).
    """
    from django.shortcuts import get_object_or_404
    r = get_object_or_404(BusRoute.objects.prefetch_related("stops"), pk=pk, is_active=True)

    return JsonResponse({
        "id": r.id,
        "name": r.name,
        "code": r.code,
        "is_active": r.is_active,
        "start_point": r.start_point,
        "end_point": r.end_point,
        "operating_days": r.operating_days_text,
        "driver": {"name": r.driver_name, "phone": r.driver_phone},
        "assistant": {"name": r.assistant_name, "phone": r.assistant_phone},
        "vehicle": {"plate": r.vehicle_plate, "capacity": r.vehicle_capacity},
        "fare_info": r.fare_info,
        "image": r.image_src,
        "map_embed_src": r.map_embed_src,
        "notes": r.notes,
        "updated_at": r.updated_at.isoformat(),
        "stops": [
            {
                "id": s.id,
                "name": s.name,
                "landmark": s.landmark,
                "time_morning": s.time_text_morning,
                "time_evening": s.time_text_evening,
                "lat": float(s.lat) if s.lat is not None else None,
                "lng": float(s.lng) if s.lng is not None else None,
                "order": s.order,
            }
            for s in r.stops.filter(is_active=True).order_by("order", "id")
        ],
    })


def bus_routes_page(request):
    """
    Public list of active routes with simple search.
    GET: q=
    """
    q = (request.GET.get("q") or "").strip()
    qs = BusRoute.objects.filter(is_active=True).order_by("order", "name")
    if q:
        qs = qs.filter(
            Q(name__icontains=q) | Q(code__icontains=q) |
            Q(driver_name__icontains=q) | Q(driver_phone__icontains=q) |
            Q(assistant_name__icontains=q) | Q(assistant_phone__icontains=q) |
            Q(start_point__icontains=q) | Q(end_point__icontains=q)
        )

    page = Paginator(qs, 12).get_page(request.GET.get("page") or 1)
    return render(request, "bus/routes_list.html", {
        "page": page,
        "routes": page.object_list,
        "q": q,
    })


def bus_route_detail_page(request, pk: int):
    """
    Public detail page for a single route (shows stops + optional map).
    """
    r = get_object_or_404(
        BusRoute.objects.prefetch_related("stops"),
        pk=pk, is_active=True
    )
    stops = r.stops.filter(is_active=True).order_by("order", "id")
    return render(request, "bus/route_detail.html", {"r": r, "stops": stops})


# ---------- SEARCH ----------

# ---------- search view ----------
@require_GET
def marksheet_search(request):
    q = (request.GET.get("q") or "").strip()
    class_id = (request.GET.get("grade") or request.GET.get("class") or "").strip()
    section = (request.GET.get("section") or "").strip()
    roll = (request.GET.get("roll") or "").strip()
    page_number = request.GET.get("page", 1)

    qs = (StudentMarksheet.objects
          .filter(is_published=True)
          .select_related("school_class", "term"))

    if q:
        qs = qs.filter(student_full_name__icontains=q)
    if class_id:
        try:
            qs = qs.filter(school_class_id=int(class_id))
        except (TypeError, ValueError):
            pass
    if section:
        qs = qs.filter(section__iexact=section)
    if roll:
        qs = qs.filter(roll_number__icontains=roll)

    qs = qs.order_by("-updated_at")  # remove [:200] to allow full pagination

    # Pagination
    paginator = Paginator(qs, 16)
    results = paginator.get_page(page_number)

    classes = AcademicClass.objects.order_by("year", "name").only("id", "name", "section")

    ctx = {
        "q": q,
        "grade_id": str(class_id or ""),
        "section": section,
        "roll": roll,
        "results": results,
        "grades": classes,
    }
    return render(request, "results/marksheet_search.html", ctx)


# ---------- detail view ----------
def marksheet_detail(request, pk: int):
    ms = get_object_or_404(
        StudentMarksheet.objects.select_related("school_class", "term"),
        pk=pk, is_published=True
    )

    # ensure totals are fresh
    try:
        ms.recalc_totals()
        # ms.save(update_fields=["total_marks", "total_grade", "is_pass", "updated_at"]) # REMOVE THIS LINE
    except Exception:
        pass

    # prefer the related manager 'items' if present
    if hasattr(ms, "items"):
        items = ms.items.select_related("subject").order_by("order", "id")
    else:
        items = RowModel.objects.select_related("subject").filter(marksheet=ms).order_by("order", "id")

    # provide template-friendly keys used by your template
    ctx = {
        "ms": ms,
        "items": items,
        "student_name": ms.student_full_name,
        "total_obtained": ms.total_marks,
        "total_out_of": ms.max_marks_total(),
        "grade_letter": ms.total_grade,
    }
    return render(request, "results/marksheet_detail.html", ctx)


# ---------- PDF (HTML â†’ PDF) ----------

@require_GET
def marksheet_pdf(request, pk: int):
    ms = (StudentMarksheet.objects
          .filter(is_published=True, pk=pk)
          .select_related("school_class", "term")
          .first())
    if not ms:
        raise Http404("Marksheet not found")

    # make totals consistent

    try:
        ms.recalc_totals()
        # ms.save(update_fields=["total_marks", "total_grade", "is_pass", "updated_at"]) # REMOVE THIS LINE
    except Exception:
        pass

    if hasattr(ms, "items"):
        rows = ms.items.select_related("subject").order_by("order", "id")
    else:
        rows = RowModel.objects.select_related("subject").filter(marksheet=ms).order_by("order", "id")

    # render template expects 'items' and ms fields
    html = render_to_string("results/marksheet_pdf.html", {
        "ms": ms,
        "items": rows,
        "student_name": ms.student_full_name,
        "total_obtained": ms.total_marks,
        "total_out_of": ms.max_marks_total(),
        "grade_letter": ms.total_grade,
    })

    try:
        from weasyprint import HTML, CSS
        css = CSS(string=""" ... """)  # your CSS
        pdf = HTML(string=html, base_url=request.build_absolute_uri("/")).write_pdf(stylesheets=[css])
        resp = HttpResponse(pdf, content_type="application/pdf")
        safe_name = (ms.student_full_name or "marksheet").replace(" ", "_")
        resp["Content-Disposition"] = f'inline; filename="{safe_name}_{ms.school_class}_{ms.term}.pdf"'
        return resp
    except Exception:
        return HttpResponse(html)


# ===================================================================
# SSLCOMMERZ ADMISSION PAYMENT
# ===================================================================



import requests

def sslc_validate(val_id):
    url = "https://securepay.sslcommerz.com/validator/api/validationserverAPI.php"
    params = {
        "val_id": val_id,
        "store_id": settings.SSLCOMMERZ_STORE_ID,
        "store_passwd": settings.SSLCOMMERZ_STORE_PASSWORD,
        "v": 1,
        "format": "json"
    }

    response = requests.get(url, params=params, timeout=10)
    return response.json()



@login_required
def initiate_invoice_payment(request, invoice_id):
    """
    Start SSLCommerz payment for a Tuition/Invoice (called from 'My Invoices' page).
    """
    # Replace TuitionInvoice with the actual model name if different
    from content.models import TuitionInvoice

    # 1. Load invoice and security check
    invoice = get_object_or_404(TuitionInvoice, pk=invoice_id)

    # Ensure the invoice belongs to the logged-in user (adjust if your user relation differs)
    try:
        # If invoice.student is a User or a Student model referencing user:
        if hasattr(invoice, "student") and invoice.student != request.user:
            messages.error(request, "You do not have permission to pay this invoice.")
            return redirect("content:my-invoices")
    except Exception:
        # fail-safe: if you don't have student relation, skip check
        pass

    amount = Decimal(invoice.balance or invoice.balance_due or 0)
    if amount <= 0:
        messages.error(request, "This invoice has no outstanding balance.")
        return redirect("content:my-invoices")

    # 2. Create unique txn id and (optionally) save on invoice
    txn_id = f"inv_{invoice.id}_{uuid.uuid4().hex[:8]}"
    try:
        # If your model has a field to save this, update it; otherwise skip
        invoice.payment_txn_id = txn_id
        invoice.save(update_fields=["payment_txn_id"])
    except Exception:
        # not fatal if field doesn't exist
        pass

    # 3. Ensure gateway configured
    if not all([settings.SSLCOMMERZ_STORE_ID, settings.SSLCOMMERZ_STORE_PASSWORD]):
        messages.error(request, "Payment gateway is not configured.")
        return redirect("content:my-invoices")

    # 4. Create SSLCSession (use same constructor as other code)
    try:
        payment_session = SSLCSession(
            sslc_is_sandbox=settings.SSLCOMMERZ_SANDBOX,
            sslc_store_id=settings.SSLCOMMERZ_STORE_ID,
            sslc_store_pass=settings.SSLCOMMERZ_STORE_PASSWORD,
        )
    except Exception as e:
        logger.exception("Failed to create SSLCSession: %s", e)
        messages.error(request, "Payment gateway configuration error.")
        return redirect("content:my-invoices")

    # 5. Callback URLs (point to the ones registered under content namespace)
    success_url = request.build_absolute_uri(
        reverse("invoice_payment_complete", args=[invoice_id])
    )
    fail_url = request.build_absolute_uri(reverse("content:payment_fail"))
    cancel_url = request.build_absolute_uri(reverse("content:payment_cancel"))
    ipn_url = request.build_absolute_uri(reverse("content:payment_ipn"))

    payment_session.set_urls(
        success_url=success_url,
        fail_url=fail_url,
        cancel_url=cancel_url,
        ipn_url=ipn_url,
    )

    # 6. Product details
    product_name = getattr(invoice, "title", f"Invoice #{invoice.id}") or f"Invoice #{invoice.id}"
    payment_session.set_product_integration(
        total_amount=str(amount),
        currency="BDT",
        product_category="Tuition",
        product_name=product_name,
        num_of_item=1,
        shipping_method="NO",
        product_profile="general",
    )

    # 7. Customer info (best-effort)
    cus_name = request.user.get_full_name() or request.user.username
    cus_email = request.user.email or "customer@example.com"
    phone = getattr(request.user, "phone", "") or getattr(request.user, "username", "")

    payment_session.set_customer_info(
        name=cus_name,
        email=cus_email,
        address1="N/A",
        city="N/A",
        postcode="N/A",
        country="Bangladesh",
        phone=phone,
    )

    # 8. Additional values (we use value_a..d to carry txn and invoice id)
    payment_session.set_additional_values(
        value_a=txn_id,
        value_b=str(invoice.id),
        value_c="tuition_fee",
        value_d=str(request.user.id),
    )

    # 9. Initiate payment and redirect to GatewayPageURL on success
    try:
        response_data = payment_session.init_payment()
        logger.debug("SSLC init response: %s", response_data)

        if response_data.get("status") == "SUCCESS":
            return redirect(response_data["GatewayPageURL"])
        else:
            # Save/mark failed if your model supports it
            try:
                invoice.payment_status = "failed"
                invoice.save(update_fields=["payment_status"])
            except Exception:
                pass
            reason = response_data.get("failedreason", "Unknown gateway error")
            logger.error("SSLCommerz initiation failed: %s", reason)
            messages.error(request, f"Gateway error: {reason}")
            return redirect("content:my-invoices")
    except Exception as e:
        logger.exception("Exception initiating SSLCommerz payment: %s", e)
        messages.error(request, "Could not connect to payment gateway. Please try again.")
        return redirect("content:my-invoices")



@csrf_exempt
def invoice_payment_complete(request, invoice_id):
    """
    SSLCommerz returns user here after validating the payment.
    Records payment, updates invoice, sends email, and renders the payment confirmed page.
    """
    data = request.POST if request.method == "POST" else request.GET
    val_id = data.get("val_id") or data.get("valId")
    tran_id = data.get("value_a") or data.get("tran_id") or data.get("tranId")
    amount = data.get("amount")

    if not val_id or not tran_id:
        messages.error(request, "Payment validation data missing.")
        return redirect("content:my-invoices")

    if not settings.SSLCOMMERZ_SANDBOX:
        validation = sslc_validate(val_id)
        if not validation or validation.get("status") != "VALID":
            messages.error(request, "Payment validation failed.")
            return redirect("content:my-invoices")
        if validation.get("risk_level") not in ["0", 0, None]:
            messages.error(request, "Payment flagged unsafe. Contact support.")
            return redirect("content:my-invoices")

    invoice = get_object_or_404(TuitionInvoice, pk=invoice_id)

    try:
        amount_decimal = Decimal(amount)
    except (TypeError, ValueError):
        amount_decimal = invoice.balance

    if amount_decimal > 0 and invoice.balance > 0:
        payment = TuitionPayment.objects.create(
            invoice=invoice,
            amount=amount_decimal,
            provider="SSLCommerz",
            txn_id=tran_id,
            gateway="sslcommerz",
            gateway_ref=val_id,
            paid_at=timezone.now()
        )

        invoice.paid_amount = (invoice.paid_amount or Decimal("0")) + amount_decimal
        invoice.maybe_mark_paid()

        # send email
        if invoice.student.email:
            subject = f"Payment Successful for Invoice #{invoice.id}"
            context = {
                "student": invoice.student,
                "invoice": invoice,
                "amount": amount_decimal,
                "transaction_id": tran_id,
                "paid_at": payment.paid_at,
            }
            html_message = render_to_string("emails/invoice_paid.html", context)
            plain_message = strip_tags(html_message)
            send_mail(subject, plain_message, settings.DEFAULT_FROM_EMAIL, [invoice.student.email], html_message=html_message)

    # Render the Payment Confirmed page using your template
    context = {
        "invoice": invoice,
        "transaction_id": tran_id,
        "amount": amount_decimal,
    }
    return render(request, "payment_complete_invoice.html", context)


@login_required
def initiate_admission_payment(request, application_id):
    """
    This view starts the payment process for a *specific* admission application.
    """
    # 1. Get the application object
    try:
        app = AdmissionApplication.objects.get(pk=application_id)
    except AdmissionApplication.DoesNotExist:
        messages.error(request, "Admission application not found.")
        return redirect('home')  # Or a dashboard page

    # 2. Check if it's already paid
    if app.payment_status == 'paid':
        messages.info(request, "This application fee has already been paid.")
        return redirect('home')  # Or a "my application" page

    # 3. Get the amount from the model
    payment_amount = app.fee_total
    if not payment_amount or payment_amount <= 0:
        messages.error(request, "Payment amount is invalid.")
        return redirect('home')

    # 4. Create a unique transaction ID and save it to the application
    # This is the most important step
    transaction_id = f"app_{app.id}_{uuid.uuid4().hex[:6]}"
    app.payment_txn_id = transaction_id
    app.payment_status = 'pending'  # Ensure it's marked as pending
    app.save(update_fields=["payment_txn_id", "payment_status"])

    # 5. Get Store ID and Password from settings
    if not all([settings.SSLCOMMERZ_STORE_ID, settings.SSLCOMMERZ_STORE_PASSWORD]):
        messages.error(request, "Payment gateway is not configured.")
        return redirect('home')

    # 6. Instantiate SSLCSession
    payment_session = SSLCSession(
        sslc_is_sandbox=settings.SSLCOMMERZ_SANDBOX,
        sslc_store_id=settings.SSLCOMMERZ_STORE_ID,
        sslc_store_pass=settings.SSLCOMMERZ_STORE_PASSWORD
    )

    # 7. Build Full Callback URLs
    # --- IMPORTANT: These now point to the 'admissions' namespace ---
    success_url = request.build_absolute_uri(reverse('admissions:payment_success'))
    fail_url = request.build_absolute_uri(reverse('admissions:payment_fail'))
    cancel_url = request.build_absolute_uri(reverse('admissions:payment_cancel'))
    ipn_url = request.build_absolute_uri(reverse('admissions:payment_ipn'))

    payment_session.set_urls(
        success_url=success_url,
        fail_url=fail_url,
        cancel_url=cancel_url,
        ipn_url=ipn_url
    )

    # 8. Set Product and Transaction Details
    payment_session.set_product_integration(
        total_amount=payment_amount,
        currency='BDT',
        product_category='Admission',
        product_name=f"Admission Fee for {app.desired_course.title}",
        num_of_item=1,
        shipping_method='NO',
        product_profile='general'
    )

    # 9. Set Customer Information (from the application model)
    payment_session.set_customer_info(
        name=app.full_name,
        email=app.email or 'customer@example.com',  # SSLCommerz requires an email
        address1=app.address or 'N/A',
        city='N/A',
        postcode='N/A',
        country='Bangladesh',
        phone=app.phone
    )

    # 10. Set Custom Transaction Values (VERY IMPORTANT)
    payment_session.set_additional_values(
        value_a=transaction_id,  # Your Internal Transaction ID (payment_txn_id)
        value_b=str(app.id),  # Your AdmissionApplication ID
        value_c='admission_fee',  # Payment Type
        value_d=''
    )

    # 11. Initiate Payment
    try:
        response_data = payment_session.init_payment()

        if response_data.get('status') == 'SUCCESS':
            # Redirect user to the SSLCommerz payment gateway
            return redirect(response_data['GatewayPageURL'])
        else:
            # Mark the payment as failed in our database
            app.payment_status = 'failed'
            app.save(update_fields=["payment_status"])
            logger.error(f"SSLCommerz initiation failed: {response_data.get('failedreason', 'Unknown error')}")
            messages.error(request, f"Gateway error: {response_data.get('failedreason', 'Please try again.')}")
            return redirect('home')  # Or a 'payment_failed' page

    except Exception as e:
        app.payment_status = 'failed'
        app.save(update_fields=["payment_status"])
        logger.error(f"SSLCommerz initiation exception: {e}")
        messages.error(request, "Could not connect to payment gateway. Please try again.")
        return redirect('home')


@csrf_exempt
def payment_success(request):
    """
    Handles the 'Success' callback from SSLCommerz.
    Handles both GET and POST data.
    """
    data = request.POST if request.method == 'POST' else request.GET
    print("Payment Success Data:", data)

    val_id = data.get('val_id') or data.get('valId')
    tran_id = data.get('value_a') or data.get('tran_id') or data.get('tranId')

    if not val_id or not tran_id:
        logger.error(f"Payment validation data missing. Received data: {dict(data)}")
        messages.error(request, "Payment validation data missing.")
        return redirect('admissions:payment_fail_page')

    # SANDBOX SAFE â€” skip API validation
    if settings.SSLCOMMERZ_SANDBOX:
        if data.get("status") != "VALID":
            messages.error(request, "Payment invalid in sandbox mode.")
            return redirect('admissions:payment_fail_page')
    else:
        # LIVE mode â€” validate via API
        from .utils import sslc_validate  # your requests function
        validation = sslc_validate(val_id)
        print("VALIDATION RESPONSE:", validation)
        if not validation or validation.get("status") != "VALID":
            AdmissionApplication.objects.filter(payment_txn_id=tran_id).update(payment_status='failed')
            messages.error(request, "Payment validation failed by gateway.")
            return redirect('admissions:payment_fail_page')

        if validation.get("risk_level") not in ["0", 0, None]:
            messages.error(request, "Payment flagged unsafe by gateway. Please contact support.")
            return redirect('admissions:payment_fail_page')

    # PROCESS PAYMENT
    try:
        app = AdmissionApplication.objects.get(payment_txn_id=tran_id)

        if app.payment_status == 'pending':
            app.payment_status = 'paid'
            app.payment_provider = 'SSLCommerz'
            app.paid_at = timezone.now()
            app.save(update_fields=['payment_status', 'payment_provider', 'paid_at'])

        messages.success(request, "Payment successful! Your application is processing.")
        return redirect('admissions:payment_complete_page')

    except AdmissionApplication.DoesNotExist:
        logger.error(f"No application found for tran_id={tran_id}")
        messages.error(request, "Payment confirmed but application not found.")
        return redirect('admissions:payment_fail_page')


@csrf_exempt
def payment_fail(request):
    """
    Handles the 'Fail' callback from SSLCommerz.
    """
    data = request.POST if request.method == 'POST' else request.GET
    tran_id = data.get('value_a') or data.get('tran_id')
    error_reason = data.get('error', 'Unknown error')

    if tran_id:
        AdmissionApplication.objects.filter(payment_txn_id=tran_id).update(payment_status='failed')
        logger.warning(f"Payment FAILED for tran_id: {tran_id}. Reason: {error_reason}")

    messages.error(request, f"Your payment has failed. Reason: {error_reason}")
    return redirect('admissions:payment_fail_page')


@csrf_exempt
def payment_cancel(request):
    """
    Handles the 'Cancel' callback from SSLCommerz.
    """
    data = request.POST if request.method == 'POST' else request.GET
    tran_id = data.get('value_a') or data.get('tran_id')

    if tran_id:
        AdmissionApplication.objects.filter(payment_txn_id=tran_id).update(payment_status='failed')
        logger.info(f"Payment CANCELLED by user for tran_id: {tran_id}")

    messages.warning(request, "Your payment was cancelled.")
    return redirect('admissions:payment_fail_page')


@csrf_exempt
@require_http_methods(["POST"])  # This MUST be a POST
def payment_ipn(request):
    """
    Handles the Instant Payment Notification (IPN) from SSLCommerz.
    This is the most reliable server-to-server confirmation.
    """
    post_data = request.POST

    # 1. Get the key data
    val_id = post_data.get('val_id')
    tran_id = post_data.get('value_a')  # Your custom payment_txn_id
    status = post_data.get('status')

    if not all([val_id, tran_id, status]):
        logger.error("[IPN] Received with missing data.")
        return HttpResponse("IPN Error: Missing data", status=400)

    # 2. Instantiate the session
    payment_session = SSLCSession(
        sslc_is_sandbox=settings.SSLCOMMERZ_SANDBOX,
        sslc_store_id=settings.SSLCOMMERZ_STORE_ID,
        sslc_store_pass=settings.SSLCOMMERZ_STORE_PASSWORD
    )

    try:
        # 3. Validate the transaction
        is_valid = payment_session.validate_transaction(val_id)

        if is_valid and status == 'VALID':
            # 4. PAYMENT IS VALID - Update your database
            try:
                app = AdmissionApplication.objects.get(payment_txn_id=tran_id)

                # 5. Check if it's already processed (by success view)
                if app.payment_status == 'pending':
                    # Manually update the fields
                    app.payment_status = 'paid'
                    app.payment_provider = 'SSLCommerz-IPN'  # Mark it as IPN
                    app.paid_at = timezone.now()

                    app.save(update_fields=['payment_status', 'payment_provider', 'paid_at'])

                    logger.info(f"[IPN] Payment VALIDATED and recorded for tran_id: {tran_id}")

                return HttpResponse("IPN Processed", status=200)

            except AdmissionApplication.DoesNotExist:
                logger.error(f"[IPN] Validation success, but no application found for tran_id: {tran_id}")
                return HttpResponse("IPN Error: Application not found", status=200)  # Return 200 so SSLCommerz stops

        elif status == 'FAILED':
            AdmissionApplication.objects.filter(payment_txn_id=tran_id).update(payment_status='failed')
            logger.warning(f"[IPN] Payment FAILED for tran_id: {tran_id}")
            return HttpResponse("IPN Processed as Failed", status=200)

        else:
            # 6. PAYMENT IS INVALID
            logger.warning(f"[IPN] Payment INVALID for tran_id: {tran_id}")
            return HttpResponse("IPN Invalid", status=200)

    except Exception as e:
        logger.error(f"[IPN] Error during IPN processing: {e}")
        return HttpResponse(f"IPN Exception: {e}", status=500)


