import datetime
import re
from django.core.paginator import Paginator
from django.shortcuts import render, redirect, get_object_or_404
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q
from django.views import View
from django.views.generic import TemplateView

from accounts.mixins import StudentRequiredMixin, TeacherRequiredMixin, AdminRequiredMixin
from portal.permissions import ModulePermissionMixin
from admissions.models import Enrollment, Program
from courses.models import Course
from .models import Result, TERM_CHOICES
from .grading import group_results_by_course, COMPONENT_WEIGHTS

# Marks band for each letter grade, used to validate marks ↔ grade consistency.
GRADE_BANDS = {'A': (80, 100), 'B': (65, 79), 'C': (50, 64), 'D': (40, 49), 'F': (0, 39)}
_AY_RE = re.compile(r'^\d{4}/\d{4}$')


def _valid_academic_year(ay):
    """True if `ay` is YYYY/YYYY with the second year exactly one after the first."""
    if not ay or not _AY_RE.match(ay):
        return False
    first, second = ay.split('/')
    return int(second) == int(first) + 1


class TeacherGradeEntryView(TeacherRequiredMixin, View):
    template_name = 'grades/teacher_entry.html'

    def _teacher_programs(self):
        profile = getattr(self.request.user, 'teacher_profile', None)
        from admissions.models import Program
        return profile.programs.filter(is_active=True) if profile else Program.objects.none()

    def _get_course(self, course_pk):
        programs = self._teacher_programs()
        return get_object_or_404(Course, pk=course_pk, program__in=programs, is_active=True)

    def get(self, request):
        programs = self._teacher_programs()
        courses = Course.objects.filter(program__in=programs, is_active=True).select_related('program').order_by('program__name', 'year', 'name')
        course_pk = request.GET.get('course')
        term = request.GET.get('term', '')
        grade_type = request.GET.get('grade_type', Result.THEORY)

        # Academic years: suggest the ones that already have grades for this
        # teacher's programs, defaulting to the most recent so previously
        # entered grades pre-populate instead of a blank current-year guess.
        today = datetime.date.today()
        current_ay_guess = f'{today.year}/{today.year + 1}'
        result_years = sorted(
            set(Result.objects.filter(course__program__in=programs)
                .values_list('academic_year', flat=True)),
            reverse=True,
        )
        academic_year_options = sorted(set(result_years) | {current_ay_guess}, reverse=True)
        default_year = result_years[0] if result_years else current_ay_guess
        academic_year = request.GET.get('academic_year') or default_year

        selected_course = None
        student_rows = []

        # Validate the academic-year format before doing anything with it.
        if request.GET.get('academic_year') and not _valid_academic_year(academic_year):
            messages.warning(
                request,
                f'"{academic_year}" is not a valid academic year. '
                'Use the format YYYY/YYYY (e.g. 2025/2026).'
            )

        if course_pk and term and grade_type and _valid_academic_year(academic_year):
            selected_course = get_object_or_404(Course, pk=course_pk, program__in=programs, is_active=True)
            enrollments = Enrollment.objects.filter(
                program=selected_course.program, is_active=True
            ).select_related('student').order_by('student__first_name', 'student__last_name')

            existing = {
                r.enrollment_id: r
                for r in Result.objects.filter(
                    course=selected_course, term=term,
                    academic_year=academic_year, grade_type=grade_type,
                    enrollment__in=enrollments,
                )
            }
            for enr in enrollments:
                student_rows.append({
                    'enrollment': enr,
                    'result': existing.get(enr.pk),
                })

            # Heads-up: this year has no grades yet, but other years do.
            if not existing:
                other_years = sorted(set(
                    Result.objects.filter(
                        course=selected_course, term=term, grade_type=grade_type,
                    ).exclude(academic_year=academic_year)
                    .values_list('academic_year', flat=True)
                ), reverse=True)
                if other_years:
                    gt_label = dict(Result.GRADE_TYPE_CHOICES).get(grade_type, '')
                    messages.info(
                        request,
                        f'No {gt_label} grades recorded for {academic_year} yet. '
                        f'This course/term already has grades under: {", ".join(other_years)}. '
                        'Switch the Academic Year if you meant to edit those.'
                    )

        return render(request, self.template_name, {
            'courses': courses,
            'term_choices': TERM_CHOICES,
            'grade_type_choices': Result.GRADE_TYPE_CHOICES,
            'grade_choices': Result.GRADE_CHOICES,
            'selected_course': selected_course,
            'student_rows': student_rows,
            'current_course': course_pk or '',
            'current_term': term,
            'current_academic_year': academic_year,
            'current_grade_type': grade_type,
            'academic_year_options': academic_year_options,
        })

    def post(self, request):
        programs = self._teacher_programs()
        course_pk = request.POST.get('course')
        term = request.POST.get('term', '').strip()
        academic_year = request.POST.get('academic_year', '').strip()
        grade_type = request.POST.get('grade_type', '').strip()

        selected_course = get_object_or_404(Course, pk=course_pk, program__in=programs, is_active=True)

        valid_terms = dict(TERM_CHOICES)
        valid_grade_types = dict(Result.GRADE_TYPE_CHOICES)
        valid_grades = dict(Result.GRADE_CHOICES)

        if term not in valid_terms or grade_type not in valid_grade_types or not academic_year:
            messages.error(request, 'Invalid submission. Please check your selections.')
            return redirect(f'{request.path}?course={course_pk}&term={term}&academic_year={academic_year}&grade_type={grade_type}')

        if not _valid_academic_year(academic_year):
            messages.error(
                request,
                f'"{academic_year}" is not a valid academic year. '
                'Use the format YYYY/YYYY (e.g. 2025/2026). No grades were saved.'
            )
            return redirect(f'{request.path}?course={course_pk}&term={term}&grade_type={grade_type}')

        enrollments = Enrollment.objects.filter(program=selected_course.program, is_active=True)
        saved = 0
        cleared = 0
        errors = []

        for enr in enrollments:
            field_key = f'grade_{enr.pk}'
            marks_key = f'marks_{enr.pk}'
            notes_key = f'notes_{enr.pk}'
            grade_val = request.POST.get(field_key, '').strip()
            marks_val = request.POST.get(marks_key, '').strip()
            notes_val = request.POST.get(notes_key, '').strip()
            name = enr.student.get_full_name()

            if grade_val:
                if grade_val not in valid_grades:
                    errors.append(f'{name}: "{grade_val}" is not a valid grade.')
                    continue
                marks = None
                if marks_val:
                    if not marks_val.isdigit() or not (0 <= int(marks_val) <= 100):
                        errors.append(f'{name}: marks must be a whole number between 0 and 100.')
                        continue
                    marks = int(marks_val)
                    lo, hi = GRADE_BANDS.get(grade_val, (0, 100))
                    if not (lo <= marks <= hi):
                        errors.append(
                            f'{name}: marks {marks} don’t match grade {grade_val} '
                            f'(expected {lo}–{hi}). Adjust the grade or the marks.'
                        )
                        continue
                Result.objects.update_or_create(
                    enrollment=enr,
                    course=selected_course,
                    grade_type=grade_type,
                    term=term,
                    academic_year=academic_year,
                    defaults={
                        'grade': grade_val,
                        'marks': marks,
                        'notes': notes_val,
                        'recorded_by': request.user,
                    },
                )
                saved += 1
            else:
                # No grade selected.
                if marks_val or notes_val:
                    errors.append(f'{name}: select a grade before entering marks or a note.')
                    continue
                deleted, _ = Result.objects.filter(
                    enrollment=enr, course=selected_course,
                    grade_type=grade_type, term=term, academic_year=academic_year,
                ).delete()
                if deleted:
                    cleared += 1

        msg_parts = []
        if saved:
            msg_parts.append(f'{saved} grade{"s" if saved != 1 else ""} saved')
        if cleared:
            msg_parts.append(f'{cleared} cleared')
        if msg_parts:
            messages.success(request, f'Grades updated: {", ".join(msg_parts)}.')
        elif not errors:
            messages.info(request, 'No changes made.')

        if errors:
            messages.warning(
                request,
                f'{len(errors)} entr{"y was" if len(errors) == 1 else "ies were"} not saved — '
                + ' '.join(errors)
            )

        return redirect(
            f'{request.path}?course={course_pk}&term={term}'
            f'&academic_year={academic_year}&grade_type={grade_type}'
        )


class StudentResultsView(StudentRequiredMixin, TemplateView):
    template_name = 'grades/student_results.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        if enrollment:
            # Newest academic year first so the most recent grades show
            # (and expand) at the top of the page.
            results = Result.objects.filter(enrollment=enrollment).select_related('course').order_by(
                '-academic_year', 'term', 'course__name', 'grade_type'
            )
            ctx['grouped_results'] = group_results_by_course(results)
            ctx['enrollment'] = enrollment
            ctx['total_results'] = results.count()
            ctx['weights'] = COMPONENT_WEIGHTS
        return ctx


class StudentTranscriptView(StudentRequiredMixin, TemplateView):
    template_name = 'grades/student_transcript.html'

    def get_context_data(self, **kwargs):
        ctx = super().get_context_data(**kwargs)
        enrollment = getattr(self.request.user, 'enrollment', None)
        if not enrollment:
            return ctx
        results = Result.objects.filter(enrollment=enrollment).select_related('course').order_by(
            'academic_year', 'term', 'course__name', 'grade_type'
        )
        ctx['enrollment'] = enrollment
        ctx['grouped_results'] = group_results_by_course(results)
        ctx['total_results'] = results.count()
        ctx['student'] = self.request.user
        import datetime
        ctx['printed_date'] = datetime.date.today()
        return ctx


class AdminResultsView(AdminRequiredMixin, ModulePermissionMixin, View):
    module = 'reports'
    template_name = 'grades/admin_results.html'

    def _build_queryset(self, request):
        qs = Result.objects.select_related(
            'enrollment__student', 'enrollment__program', 'course', 'recorded_by'
        )
        q = request.GET.get('q', '').strip()
        term = request.GET.get('term', '')
        year = request.GET.get('year', '')
        program = request.GET.get('program', '')
        grade_type = request.GET.get('grade_type', '')

        if q:
            qs = qs.filter(
                Q(enrollment__student__first_name__icontains=q) |
                Q(enrollment__student__last_name__icontains=q) |
                Q(enrollment__student_number__icontains=q) |
                Q(course__name__icontains=q)
            )
        if term:
            qs = qs.filter(term=term)
        if year:
            qs = qs.filter(academic_year=year)
        if program:
            qs = qs.filter(enrollment__program_id=program)
        if grade_type:
            qs = qs.filter(grade_type=grade_type)

        return qs, q, term, year, program, grade_type

    def get(self, request):
        qs, q, term, year, program, grade_type = self._build_queryset(request)

        if request.GET.get('export') == '1':
            from portal.exports import excel_response
            headers = ['Student No.', 'Student Name', 'Program', 'Course',
                       'Grade Type', 'Grade', 'Marks', 'Term', 'Academic Year']
            rows = [
                (
                    r.enrollment.student_number,
                    r.enrollment.student.get_full_name(),
                    r.enrollment.program.name,
                    r.course.name,
                    r.get_grade_type_display(),
                    r.grade,
                    r.marks if r.marks is not None else '',
                    r.get_term_display(),
                    r.academic_year,
                )
                for r in qs
            ]
            return excel_response('Results', 'MLS_Results', headers, rows)

        total_count = qs.count()
        paginator = Paginator(qs, 50)
        page_number = request.GET.get('page')
        page_obj = paginator.get_page(page_number)

        return render(request, self.template_name, {
            'results': page_obj,
            'page_obj': page_obj,
            'is_paginated': paginator.num_pages > 1,
            'programs': Program.objects.filter(is_active=True),
            'term_choices': TERM_CHOICES,
            'grade_type_choices': Result.GRADE_TYPE_CHOICES,
            'search_query': q,
            'current_term': term,
            'current_year': year,
            'current_program': program,
            'current_grade_type': grade_type,
            'total_count': total_count,
        })
