"""
Models for the courses app.
Includes: Category, CertificateTemplate, Course (exam track), Module, Lesson, UserCourseProgress

Part 1 taxonomy: each Course is an exam track under a Category, supporting
Learning Path and/or Direct Certification Path.
"""
from decimal import Decimal

from django.conf import settings
from django.core.validators import MaxValueValidator, MinValueValidator
from django.db import models
from django.utils import timezone
from django.utils.text import slugify
from utils.choices import COURSE_DRAFT, COURSE_STATUS_CHOICES


CATEGORY_COURSE = 'course'
CATEGORY_CERT = 'certification'
CATEGORY_BOTH = 'both'
CATEGORY_APPLIES_CHOICES = [
    (CATEGORY_COURSE, 'Courses only'),
    (CATEGORY_CERT, 'Certifications only'),
    (CATEGORY_BOTH, 'Courses & certifications'),
]

LEVEL_BEGINNER = 'beginner'
LEVEL_INTERMEDIATE = 'intermediate'
LEVEL_ADVANCED = 'advanced'
COURSE_LEVEL_CHOICES = [
    (LEVEL_BEGINNER, 'Beginner'),
    (LEVEL_INTERMEDIATE, 'Intermediate'),
    (LEVEL_ADVANCED, 'Advanced'),
]

MODULE_VIDEO = 'video'
MODULE_TEXT = 'text'
MODULE_PDF = 'pdf'
MODULE_MIXED = 'mixed'
MODULE_CONTENT_TYPE_CHOICES = [
    (MODULE_VIDEO, 'Video'),
    (MODULE_TEXT, 'Text'),
    (MODULE_PDF, 'PDF'),
    (MODULE_MIXED, 'Mixed'),
]

MODULE_TYPE_THEORY = 'theory'
MODULE_TYPE_PROJECT = 'project'
MODULE_TYPE_CHOICES = [
    (MODULE_TYPE_THEORY, 'Theory'),
    (MODULE_TYPE_PROJECT, 'Project / practice'),
]

PROJECT_SUBMIT_GITHUB = 'github_link'
PROJECT_SUBMIT_FILE = 'file_upload'
PROJECT_SUBMIT_FIGMA = 'figma_link'
PROJECT_SUBMIT_LIVE = 'live_url'
PROJECT_SUBMISSION_TYPE_CHOICES = [
    (PROJECT_SUBMIT_GITHUB, 'GitHub link'),
    (PROJECT_SUBMIT_FILE, 'File upload'),
    (PROJECT_SUBMIT_FIGMA, 'Figma link'),
    (PROJECT_SUBMIT_LIVE, 'Live URL'),
]

SUBMISSION_PENDING = 'pending'
SUBMISSION_APPROVED = 'approved'
SUBMISSION_NEEDS_REVISION = 'needs_revision'
SUBMISSION_REJECTED = 'rejected'
PROJECT_SUBMISSION_STATUS_CHOICES = [
    (SUBMISSION_PENDING, 'Pending review'),
    (SUBMISSION_APPROVED, 'Approved'),
    (SUBMISSION_NEEDS_REVISION, 'Needs revision'),
    (SUBMISSION_REJECTED, 'Rejected'),
]


class Category(models.Model):
    """Shared taxonomy for courses (exam tracks) and certification programs."""
    name = models.CharField(max_length=120)
    slug = models.SlugField(max_length=140, unique=True, blank=True)
    description = models.CharField(max_length=255, blank=True)
    applies_to = models.CharField(
        max_length=20,
        choices=CATEGORY_APPLIES_CHOICES,
        default=CATEGORY_BOTH,
        help_text='Where this category can be used.',
    )
    order = models.PositiveIntegerField(default=0)
    is_active = models.BooleanField(default=True)

    class Meta:
        ordering = ['order', 'name']
        verbose_name_plural = 'Categories'

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            base = slugify(self.name) or 'category'
            slug = base
            n = 1
            while Category.objects.filter(slug=slug).exclude(pk=self.pk).exists():
                slug = f'{base}-{n}'
                n += 1
            self.slug = slug
        super().save(*args, **kwargs)


class CertificateTemplate(models.Model):
    """
    Certificate layout reference for a course/exam track.
    PDF/badge assets are generated per CourseCertificate (Part 6).
    """
    name = models.CharField(max_length=120)
    slug = models.SlugField(max_length=140, unique=True, blank=True)
    description = models.CharField(max_length=255, blank=True)
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['name']

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            base = slugify(self.name) or 'certificate-template'
            slug = base
            n = 1
            while CertificateTemplate.objects.filter(slug=slug).exclude(pk=self.pk).exists():
                slug = f'{base}-{n}'
                n += 1
            self.slug = slug
        super().save(*args, **kwargs)


class CourseBundle(models.Model):
    """
    Multi-course package sold cheaper than buying each exam separately.
    Courses link via Course.bundle (bundle_id).
    """
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    description = models.TextField(blank=True)
    discount_percent = models.PositiveSmallIntegerField(
        default=10,
        validators=[MinValueValidator(0), MaxValueValidator(100)],
        help_text='Discount vs sum of member exam fees.',
    )
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['name']
        verbose_name = 'Course bundle'
        verbose_name_plural = 'Course bundles'

    def __str__(self):
        return self.name

    def save(self, *args, **kwargs):
        if not self.slug:
            base = slugify(self.name) or 'bundle'
            slug = base
            n = 1
            while CourseBundle.objects.filter(slug=slug).exclude(pk=self.pk).exists():
                slug = f'{base}-{n}'
                n += 1
            self.slug = slug
        super().save(*args, **kwargs)

    def member_exam_list_total(self):
        from decimal import Decimal
        total = Decimal('0.00')
        for c in self.courses.filter(is_active=True, status='published'):
            total += c.exam_fee or Decimal('0.00')
        return total

    def bundle_price(self):
        from decimal import Decimal
        total = self.member_exam_list_total()
        discount = Decimal(self.discount_percent or 0) / Decimal('100')
        return (total * (Decimal('1') - discount)).quantize(Decimal('0.01'))


class Course(models.Model):
    """
    Exam track / course under a category.
    Supports Learning Path (modules) and Direct Certification Path (exam only).
    Both paths share the same exam engine (wired in later parts).

    Note: `title` is the course name (maps to schema field `name`) — kept as
    `title` for compatibility with the existing LMS codebase.
    """
    title = models.CharField(max_length=200, help_text='Course / exam track name.')
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    category = models.ForeignKey(
        Category,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='courses',
        limit_choices_to={'applies_to__in': [CATEGORY_COURSE, CATEGORY_BOTH], 'is_active': True},
    )
    thumbnail = models.ImageField(upload_to='course_thumbnails/', blank=True, null=True)
    badge_icon = models.ImageField(
        upload_to='course_badges/',
        blank=True,
        null=True,
        help_text='Badge / icon shown on completion.',
    )
    short_description = models.CharField(max_length=300)
    full_description = models.TextField()
    level = models.CharField(
        max_length=20,
        choices=COURSE_LEVEL_CHOICES,
        default=LEVEL_BEGINNER,
    )

    # Legacy enrollment display price (kept for existing enrollments UI)
    price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        default=0.00,
        help_text='Legacy course card price. Prefer learning_price / exam fees below.',
    )

    # Configurable per-course pricing (Part 3) — editable in Django admin
    currency = models.CharField(
        max_length=3,
        default='PKR',
        help_text='ISO currency code for this course (default PKR; USD later for international).',
    )
    duration_weeks = models.PositiveIntegerField(
        default=8,
        help_text='Estimated self-paced or mentor-paced completion time in weeks.',
    )
    learning_price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text='Learning Path access (modules + projects). Null if no learning content.',
    )
    mentorship_price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text='Optional mentor add-on price. Null if mentorship not offered.',
    )
    exam_fee = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        default=Decimal('0.00'),
        help_text='Certification exam fee (both Learning and Direct paths).',
    )
    direct_exam_only_price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text='Direct Certification Path price if different from exam_fee.',
    )
    reattempt_fee = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        default=Decimal('0.00'),
        help_text='Fee for each exam reattempt after a fail.',
    )
    bundle_discount_percent = models.PositiveSmallIntegerField(
        default=0,
        validators=[MinValueValidator(0), MaxValueValidator(100)],
        help_text='Discount %% when buying learning_price + exam_fee together.',
    )
    # Growth / merchandising (Part 8 ASAP)
    is_featured = models.BooleanField(
        default=False,
        db_index=True,
        help_text='Highlight on home and course listings.',
    )
    enable_free_diagnostic = models.BooleanField(
        default=True,
        help_text=(
            'Show “Free skill check (no login)” on the public course page. '
            'Needs at least 12 active + verified questions in the bank.'
        ),
    )
    discount_price = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text='Sale / promo exam price. When set and lower than exam_fee, shown as the offer price.',
    )
    bundle = models.ForeignKey(
        'CourseBundle',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='courses',
        help_text='Optional multi-course bundle this track belongs to.',
    )

    # Monthly subscription alternative for Learning Path
    subscription_enabled = models.BooleanField(
        default=False,
        help_text='Offer monthly pay-as-you-go for Learning Path.',
    )
    subscription_price_monthly = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        null=True,
        blank=True,
        help_text='Monthly subscription price (PKR power-adjusted, independent of one-time fees).',
    )
    min_subscription_months = models.PositiveSmallIntegerField(
        default=1,
        help_text='Minimum commitment in months (e.g. 1).',
    )
    estimated_completion_months = models.PositiveSmallIntegerField(
        default=3,
        help_text='Shown so candidates can estimate total subscription cost.',
    )

    # Exam engine defaults (shared by both paths)
    pass_percentage = models.PositiveSmallIntegerField(
        default=70,
        validators=[MinValueValidator(1), MaxValueValidator(100)],
        help_text='Minimum score % to pass (default 70).',
    )
    question_count_per_attempt = models.PositiveIntegerField(
        default=100,
        help_text='MCQs drawn per exam attempt (default 100).',
    )
    total_bank_size = models.PositiveIntegerField(
        default=800,
        help_text='Target size of the active question bank (default 800).',
    )
    duration_minutes = models.PositiveIntegerField(
        default=120,
        help_text='Exam timer in minutes (default 120 for 100 MCQs).',
    )
    learning_completion_required_percent = models.PositiveSmallIntegerField(
        default=100,
        validators=[MinValueValidator(0), MaxValueValidator(100)],
        help_text='Learning Path must complete this %% of modules before Take Exam is unlocked.',
    )

    certificate_template = models.ForeignKey(
        CertificateTemplate,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='courses',
    )
    certificate_code = models.CharField(
        max_length=12,
        blank=True,
        help_text='Short code in certificate numbers, e.g. SEO → TSN-SEO-2026-00001.',
    )

    # Path flags
    is_active = models.BooleanField(
        default=True,
        help_text='Inactive tracks are hidden from public listings.',
    )
    requires_practical = models.BooleanField(
        default=False,
        help_text='Needs practical submission in addition to MCQs before certificate issuance.',
    )
    practical_instructions = models.TextField(
        blank=True,
        help_text='Shown to candidates after they pass the MCQ (GitHub/Figma/ZIP expectations).',
    )
    practical_rubric = models.TextField(
        blank=True,
        help_text='Reviewer checklist for the practical component.',
    )
    has_learning_content = models.BooleanField(
        default=True,
        help_text='If True, show Start Learning path. If False, Direct Exam only.',
    )
    mentorship_available = models.BooleanField(
        default=False,
        help_text='Learning Path only: optional paid mentorship add-on can be offered (pricing in Part 3).',
    )

    # Existing LMS schedule fields
    duration = models.CharField(
        max_length=50,
        blank=True,
        default='',
        help_text='Human-readable study duration, e.g. "4 weeks".',
    )
    start_date = models.DateField(
        null=True,
        blank=True,
        help_text='Date the course starts. Enrollment closes on this date.',
    )
    end_date = models.DateField(null=True, blank=True, help_text='Date the course ends.')
    status = models.CharField(max_length=20, choices=COURSE_STATUS_CHOICES, default=COURSE_DRAFT)
    teachers = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='teaching_courses',
        limit_choices_to={'role': 'teacher'},
        blank=True,
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']
        verbose_name = 'Course'
        verbose_name_plural = 'Courses'

    def __str__(self):
        return self.title

    @property
    def name(self):
        """Schema alias for title."""
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            self.slug = self._generate_unique_slug()
        if self.direct_exam_only_price is None and self.exam_fee is not None:
            self.direct_exam_only_price = self.exam_fee
        super().save(*args, **kwargs)

    def _generate_unique_slug(self):
        base_slug = slugify(self.title) or 'course'
        slug = base_slug
        counter = 1
        while Course.objects.filter(slug=slug).exclude(pk=self.pk).exists():
            slug = f'{base_slug}-{counter}'
            counter += 1
        return slug

    @property
    def is_published(self):
        return self.status == 'published' and self.is_active

    @property
    def is_enrollment_open(self):
        """Returns False once the course start date has been reached."""
        if self.start_date is None:
            return True
        from django.utils import timezone
        return timezone.localdate() < self.start_date

    @property
    def effective_direct_exam_price(self):
        if self.direct_exam_only_price is not None:
            return self.direct_exam_only_price
        return self.exam_fee

    @property
    def display_exam_price(self):
        """Public offer price (sale discount_price wins when lower than exam_fee)."""
        if self.discount_price is not None and self.exam_fee is not None:
            if self.discount_price < self.exam_fee:
                return self.discount_price
        return self.exam_fee

    @property
    def has_sale_price(self):
        return (
            self.discount_price is not None
            and self.exam_fee is not None
            and self.discount_price < self.exam_fee
        )

    @property
    def learning_plus_exam_list_price(self):
        learning = self.learning_price or Decimal('0.00')
        return learning + (self.exam_fee or Decimal('0.00'))

    @property
    def learning_exam_bundle_price(self):
        """One-time Learning + Exam with bundle discount applied."""
        total = self.learning_plus_exam_list_price
        discount = Decimal(self.bundle_discount_percent or 0) / Decimal('100')
        return (total * (Decimal('1') - discount)).quantize(Decimal('0.01'))

    @property
    def estimated_subscription_total(self):
        """Rough total if candidate finishes in estimated_completion_months."""
        if not self.subscription_enabled or not self.subscription_price_monthly:
            return None
        months = max(self.min_subscription_months or 1, self.estimated_completion_months or 1)
        return (self.subscription_price_monthly * months).quantize(Decimal('0.01'))

    def pricing_snapshot(self):
        """Dict of price fields for history/logging."""
        return {
            'learning_price': self.learning_price,
            'mentorship_price': self.mentorship_price,
            'exam_fee': self.exam_fee,
            'direct_exam_only_price': self.direct_exam_only_price,
            'reattempt_fee': self.reattempt_fee,
            'bundle_discount_percent': self.bundle_discount_percent,
            'subscription_price_monthly': self.subscription_price_monthly,
            'currency': self.currency,
        }

    def get_module_count(self):
        return self.modules.count()

    def get_lesson_count(self):
        return Lesson.objects.filter(module__course=self).count()

    def get_learning_progress_percent(self, user):
        """Percent of modules marked complete for this user (Learning Path)."""
        total = self.modules.count()
        if total == 0 or not user or not user.is_authenticated:
            return 0
        done = UserCourseProgress.objects.filter(
            user=user, course=self, completed_at__isnull=False,
        ).count()
        return int(round((done / total) * 100))

    def can_take_exam_via_learning_path(self, user):
        """Gate Take Exam for Learning Path behind module completion threshold."""
        if not self.has_learning_content:
            return False
        required = self.learning_completion_required_percent or 0
        return self.get_learning_progress_percent(user) >= required

    def get_theory_practice_ratio(self):
        """
        Return (theory_percent, practice_percent) based on module_type counts.
        Target curriculum mix is ~20% theory / 80% practice.
        """
        total = self.modules.count()
        if total == 0:
            return (0, 0)
        theory = self.modules.filter(module_type=MODULE_TYPE_THEORY).count()
        practice = total - theory
        theory_pct = int(round((theory / total) * 100))
        return (theory_pct, 100 - theory_pct)


class Module(models.Model):
    """
    Learning Path module (section) within a course.
    OpenClassrooms-style mix: ~20% theory, ~80% project/practice (module_type).
    """
    course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='modules')
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    module_type = models.CharField(
        max_length=20,
        choices=MODULE_TYPE_CHOICES,
        default=MODULE_TYPE_THEORY,
        help_text='Theory vs project/practice — used for 20/80 curriculum mix display.',
    )
    content_type = models.CharField(
        max_length=20,
        choices=MODULE_CONTENT_TYPE_CHOICES,
        default=MODULE_MIXED,
    )
    content_body = models.TextField(
        blank=True,
        help_text='Text / HTML body for text or mixed modules.',
    )
    content_url = models.URLField(
        blank=True,
        help_text='YouTube/Vimeo unlisted link, or PDF URL.',
    )
    pdf_file = models.FileField(
        upload_to='module_pdfs/',
        blank=True,
        null=True,
        help_text='Optional uploaded PDF for PDF/mixed modules.',
    )
    linked_project = models.ForeignKey(
        'Project',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='modules',
        help_text='Optional linked project when module_type is project.',
    )
    order = models.PositiveIntegerField(
        default=0,
        help_text='Display order (order_index).',
    )
    is_free_preview = models.BooleanField(
        default=False,
        help_text='Visible without Learning Path enrollment.',
    )

    class Meta:
        ordering = ['order']
        verbose_name = 'Module'
        verbose_name_plural = 'Modules'

    def __str__(self):
        return f'{self.course.title} - {self.title}'

    @property
    def order_index(self):
        return self.order

    @property
    def content_body_or_url(self):
        return self.content_body or self.content_url or ''

    @property
    def is_project_module(self):
        return self.module_type == MODULE_TYPE_PROJECT


class Lesson(models.Model):
    """
    Optional richer lesson within a module (existing LMS structure).
    """
    module = models.ForeignKey(Module, on_delete=models.CASCADE, related_name='lessons')
    title = models.CharField(max_length=200)
    content = models.TextField(blank=True)
    video_url = models.URLField(blank=True, null=True, help_text='YouTube or Vimeo URL')
    video_file = models.FileField(upload_to='lesson_files/', blank=True, null=True)
    attachment = models.FileField(upload_to='lesson_files/attachments/', blank=True, null=True)
    meeting_link = models.URLField(
        blank=True, null=True,
        help_text='Zoom, Google Meet, or Microsoft Teams meeting URL',
    )
    meeting_id = models.CharField(max_length=100, blank=True, help_text='Meeting ID or host user ID')
    meeting_password = models.CharField(
        max_length=100, blank=True, help_text='Meeting password or passcode',
    )
    order = models.PositiveIntegerField(default=0)
    is_preview = models.BooleanField(
        default=False,
        help_text='Allow non-enrolled users to preview this lesson',
    )

    class Meta:
        ordering = ['order']
        verbose_name = 'Lesson'
        verbose_name_plural = 'Lessons'

    def __str__(self):
        return f'{self.module.title} - {self.title}'


class UserCourseProgress(models.Model):
    """
    Tracks Learning Path module completion.
    Direct Certification Path does not require these rows.
    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='course_progress',
    )
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='user_progress',
    )
    module = models.ForeignKey(
        Module,
        on_delete=models.CASCADE,
        related_name='user_progress',
    )
    completed_at = models.DateTimeField(null=True, blank=True)

    class Meta:
        ordering = ['-completed_at']
        verbose_name = 'User course progress'
        verbose_name_plural = 'User course progress'
        constraints = [
            models.UniqueConstraint(
                fields=['user', 'course', 'module'],
                name='uniq_user_course_module_progress',
            ),
        ]

    def __str__(self):
        status = 'done' if self.completed_at else 'started'
        return f'{self.user_id} / {self.course_id} / {self.module_id} ({status})'

    @property
    def is_complete(self):
        return self.completed_at is not None


class Project(models.Model):
    """
    Real-world project for Learning Path (OpenClassrooms-style practice).
    Not used by Direct Certification Path.
    """
    course = models.ForeignKey(Course, on_delete=models.CASCADE, related_name='projects')
    title = models.CharField(max_length=200)
    description = models.TextField()
    brief_document_url = models.URLField(
        blank=True,
        help_text='Link to project brief (PDF/Drive/Notion).',
    )
    brief_file = models.FileField(
        upload_to='project_briefs/',
        blank=True,
        null=True,
    )
    submission_type = models.CharField(
        max_length=20,
        choices=PROJECT_SUBMISSION_TYPE_CHOICES,
        default=PROJECT_SUBMIT_GITHUB,
    )
    rubric = models.JSONField(
        blank=True,
        default=dict,
        help_text='Rubric as JSON (criteria → weight/description) or store prose under key "text".',
    )
    order = models.PositiveIntegerField(default=0, help_text='order_index')
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['order', 'id']
        verbose_name = 'Project'
        verbose_name_plural = 'Projects'

    def __str__(self):
        return f'{self.course.title} — {self.title}'

    @property
    def order_index(self):
        return self.order

    @property
    def rubric_text(self):
        if isinstance(self.rubric, dict) and self.rubric.get('text'):
            return self.rubric['text']
        return self.rubric


class ProjectSubmission(models.Model):
    """Candidate submission for a Learning Path project; mentor/admin reviews."""
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='project_submissions',
    )
    project = models.ForeignKey(
        Project,
        on_delete=models.CASCADE,
        related_name='submissions',
    )
    submission_url = models.URLField(
        blank=True,
        help_text='GitHub / Figma / live URL when submission_type is a link.',
    )
    submission_file = models.FileField(
        upload_to='project_submissions/',
        blank=True,
        null=True,
        help_text='File upload when required.',
    )
    screenshot = models.ImageField(
        upload_to='project_screenshots/',
        blank=True,
        null=True,
        help_text='Optional screenshot for portfolio.',
    )
    status = models.CharField(
        max_length=20,
        choices=PROJECT_SUBMISSION_STATUS_CHOICES,
        default=SUBMISSION_PENDING,
        db_index=True,
    )
    mentor_feedback = models.TextField(blank=True)
    submitted_at = models.DateTimeField(auto_now_add=True)
    reviewed_at = models.DateTimeField(null=True, blank=True)
    reviewed_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='reviewed_project_submissions',
    )

    class Meta:
        ordering = ['-submitted_at']
        verbose_name = 'Project submission'
        verbose_name_plural = 'Project submissions'
        constraints = [
            models.UniqueConstraint(
                fields=['user', 'project'],
                name='uniq_user_project_submission',
            ),
        ]

    def __str__(self):
        return f'{self.user} → {self.project} ({self.status})'

    @property
    def submission_url_or_file(self):
        if self.submission_url:
            return self.submission_url
        if self.submission_file:
            return self.submission_file.url
        return ''

    @property
    def is_approved(self):
        return self.status == SUBMISSION_APPROVED


# Fields tracked for course_pricing_history
COURSE_PRICE_FIELDS = (
    'learning_price',
    'mentorship_price',
    'exam_fee',
    'direct_exam_only_price',
    'reattempt_fee',
    'bundle_discount_percent',
    'subscription_price_monthly',
    'currency',
)


class CoursePricingHistory(models.Model):
    """
    Audit log of per-course price changes for experiments and grandfathering.
    """
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='pricing_history',
    )
    field_changed = models.CharField(max_length=64)
    old_price = models.CharField(
        max_length=64,
        blank=True,
        help_text='Previous value as string (supports decimals and currency codes).',
    )
    new_price = models.CharField(max_length=64, blank=True)
    changed_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='course_price_changes',
    )
    changed_at = models.DateTimeField(auto_now_add=True)
    note = models.CharField(max_length=255, blank=True)

    class Meta:
        ordering = ['-changed_at']
        verbose_name = 'Course pricing history'
        verbose_name_plural = 'Course pricing history'

    def __str__(self):
        return f'{self.course_id} {self.field_changed}: {self.old_price} → {self.new_price}'


SUB_ACTIVE = 'active'
SUB_PAUSED = 'paused'
SUB_CANCELLED = 'cancelled'
SUB_COMPLETED = 'completed'
SUBSCRIPTION_STATUS_CHOICES = [
    (SUB_ACTIVE, 'Active'),
    (SUB_PAUSED, 'Paused'),
    (SUB_CANCELLED, 'Cancelled'),
    (SUB_COMPLETED, 'Completed'),
]


class LearningSubscription(models.Model):
    """
    Monthly Learning Path subscription (manual renew by default).

    Platform payments today: Stripe Checkout (one-time) + manual proof.
    No JazzCash/Easypaisa recurring API is wired — renewals are manual
    with reminder notifications until a recurring gateway is added.
    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='learning_subscriptions',
    )
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='subscriptions',
    )
    status = models.CharField(
        max_length=20,
        choices=SUBSCRIPTION_STATUS_CHOICES,
        default=SUB_ACTIVE,
        db_index=True,
    )
    started_at = models.DateTimeField(auto_now_add=True)
    current_period_end = models.DateTimeField(
        help_text='End of the paid month; candidate renews before/at this date.',
    )
    months_paid = models.PositiveIntegerField(default=1)
    monthly_price_locked = models.DecimalField(
        max_digits=10,
        decimal_places=2,
        help_text='Price locked at subscribe time (honors old rates if fees rise).',
    )
    currency_locked = models.CharField(max_length=3, default='PKR')
    cancelled_at = models.DateTimeField(null=True, blank=True)
    renew_reminder_sent_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='Last manual-renew reminder (email/WhatsApp later).',
    )
    notes = models.TextField(blank=True)

    class Meta:
        ordering = ['-started_at']
        verbose_name = 'Learning subscription'
        verbose_name_plural = 'Learning subscriptions'
        constraints = [
            models.UniqueConstraint(
                fields=['user', 'course'],
                condition=models.Q(status='active'),
                name='uniq_active_learning_subscription',
            ),
        ]

    def __str__(self):
        return f'{self.user} / {self.course} ({self.status})'

    @property
    def is_active(self):
        return self.status == SUB_ACTIVE


# ── Question bank (Part 4) ───────────────────────────────────────────────────

DIFFICULTY_EASY = 'easy'
DIFFICULTY_MEDIUM = 'medium'
DIFFICULTY_HARD = 'hard'
QUESTION_DIFFICULTY_CHOICES = [
    (DIFFICULTY_EASY, 'Easy'),
    (DIFFICULTY_MEDIUM, 'Medium'),
    (DIFFICULTY_HARD, 'Hard'),
]

OPTION_A = 'A'
OPTION_B = 'B'
OPTION_C = 'C'
OPTION_D = 'D'
CORRECT_OPTION_CHOICES = [
    (OPTION_A, 'A'),
    (OPTION_B, 'B'),
    (OPTION_C, 'C'),
    (OPTION_D, 'D'),
]

ATTEMPT_STARTED = 'started'
ATTEMPT_SUBMITTED = 'submitted'
ATTEMPT_EXPIRED = 'expired'
ATTEMPT_CANCELLED = 'cancelled'
COURSE_ATTEMPT_STATUS_CHOICES = [
    (ATTEMPT_STARTED, 'Started'),
    (ATTEMPT_SUBMITTED, 'Submitted'),
    (ATTEMPT_EXPIRED, 'Expired'),
    (ATTEMPT_CANCELLED, 'Cancelled'),
]


class CourseQuestion(models.Model):
    """
    MCQ bank item for a course/exam track.
    Only is_active + is_verified questions are eligible for live attempts.
    Target bank size ≈ course.total_bank_size (default 800) — import content separately.
    """
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='bank_questions',
    )
    question_text = models.TextField()
    option_a = models.CharField(max_length=500)
    option_b = models.CharField(max_length=500)
    option_c = models.CharField(max_length=500)
    option_d = models.CharField(max_length=500)
    correct_option = models.CharField(max_length=1, choices=CORRECT_OPTION_CHOICES)
    difficulty = models.CharField(
        max_length=10,
        choices=QUESTION_DIFFICULTY_CHOICES,
        default=DIFFICULTY_MEDIUM,
        db_index=True,
        help_text='Target mix ~40% easy / 40% medium / 20% hard (not DB-enforced).',
    )
    topic_tag = models.CharField(
        max_length=80,
        blank=True,
        db_index=True,
        help_text='Subtopic slug, e.g. on-page, technical-seo — used for balanced draws.',
    )
    is_active = models.BooleanField(default=True, db_index=True)
    is_verified = models.BooleanField(
        default=False,
        db_index=True,
        help_text='SME must verify before question can appear in a live exam.',
    )
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='created_bank_questions',
    )
    last_reviewed_at = models.DateTimeField(null=True, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['course', 'topic_tag', 'id']
        verbose_name = 'Course question'
        verbose_name_plural = 'Course questions'
        indexes = [
            models.Index(
                fields=['course', 'is_active', 'is_verified'],
                name='cq_eligible_idx',
            ),
        ]

    def __str__(self):
        return f'{self.course_id}: {self.question_text[:60]}'

    @property
    def is_eligible(self):
        return self.is_active and self.is_verified

    def options_map(self):
        return {
            'A': self.option_a,
            'B': self.option_b,
            'C': self.option_c,
            'D': self.option_d,
        }


# Tab / window switches before an attempt is flagged for manual review (not auto-fail).
TAB_SWITCH_FLAG_THRESHOLD = 3

EVENT_TAB_SWITCH = 'tab_switch'
EVENT_WINDOW_BLUR = 'window_blur'
EVENT_FULLSCREEN_EXIT = 'fullscreen_exit'
EVENT_FULLSCREEN_ENTER = 'fullscreen_enter'
EVENT_COPY_ATTEMPT = 'copy_attempt'
EVENT_PASTE_ATTEMPT = 'paste_attempt'
EVENT_NAV_BACK = 'nav_back'
EVENT_WARNING_SHOWN = 'warning_shown'
EVENT_AUTO_SUBMIT = 'auto_submit_expiry'
EVENT_MANUAL_SUBMIT = 'manual_submit'
# TODO(later): webcam snapshot monitoring — needs consent flow + storage-cost decision first.
EVENT_WEBCAM_STUB = 'webcam_snapshot_stub'

ATTEMPT_EVENT_TYPE_CHOICES = [
    (EVENT_TAB_SWITCH, 'Tab / visibility switch'),
    (EVENT_WINDOW_BLUR, 'Window blur'),
    (EVENT_FULLSCREEN_EXIT, 'Exited full screen'),
    (EVENT_FULLSCREEN_ENTER, 'Entered full screen'),
    (EVENT_COPY_ATTEMPT, 'Copy attempt'),
    (EVENT_PASTE_ATTEMPT, 'Paste attempt'),
    (EVENT_NAV_BACK, 'Browser back/forward'),
    (EVENT_WARNING_SHOWN, 'On-screen warning shown'),
    (EVENT_AUTO_SUBMIT, 'Auto-submit on timer expiry'),
    (EVENT_MANUAL_SUBMIT, 'Manual submit'),
    (EVENT_WEBCAM_STUB, 'Webcam snapshot (stub — not implemented)'),
]

# Event types that count toward the “3 switches → flag” threshold
TAB_SWITCH_EVENT_TYPES = frozenset({EVENT_TAB_SWITCH})


class CourseExamAttempt(models.Model):
    """
    One exam sitting for a course track.
    Part 4: drawn question set. Part 5: timer, scoring, soft anti-cheat flags.
    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='course_exam_attempts',
    )
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='exam_attempts',
    )
    status = models.CharField(
        max_length=20,
        choices=COURSE_ATTEMPT_STATUS_CHOICES,
        default=ATTEMPT_STARTED,
        db_index=True,
    )
    started_at = models.DateTimeField(auto_now_add=True)
    timer_started_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='Set when the candidate finishes practice and the timed exam begins.',
    )
    submitted_at = models.DateTimeField(null=True, blank=True)
    score_percent = models.DecimalField(
        max_digits=5, decimal_places=2, null=True, blank=True,
    )
    correct_count = models.PositiveIntegerField(default=0)
    total_asked = models.PositiveIntegerField(default=0)
    passed = models.BooleanField(default=False)
    time_taken_seconds = models.PositiveIntegerField(
        null=True,
        blank=True,
        help_text='Seconds from timer start to submit (null if never timed).',
    )
    auto_submitted = models.BooleanField(
        default=False,
        help_text='True when the server/client submitted because the timer expired.',
    )
    is_flagged_for_review = models.BooleanField(
        default=False,
        db_index=True,
        help_text='Soft flag for staff review (e.g. ≥3 tab switches). Does not auto-fail.',
    )
    flag_reason = models.CharField(max_length=255, blank=True)
    tab_switch_count = models.PositiveIntegerField(
        default=0,
        help_text='Counted tab/window leave events during the timed exam.',
    )
    topic_breakdown = models.JSONField(
        default=list,
        blank=True,
        help_text='Per-topic results, e.g. [{"topic_tag","label","correct","total","percent"}, ...].',
    )

    class Meta:
        ordering = ['-started_at']
        verbose_name = 'Course exam attempt'
        verbose_name_plural = 'Course exam attempts'

    def __str__(self):
        return f'Attempt {self.pk} — {self.user} / {self.course}'

    @property
    def is_open(self):
        return self.submitted_at is None and self.status == ATTEMPT_STARTED

    @property
    def is_practice_phase(self):
        return self.is_open and self.timer_started_at is None

    @property
    def is_timed_exam(self):
        return self.is_open and self.timer_started_at is not None

    def _duration_minutes(self):
        return max(1, int(self.course.duration_minutes or 120))

    @property
    def deadline_at(self):
        start = self.timer_started_at or timezone.now()
        return start + timezone.timedelta(minutes=self._duration_minutes())

    def time_expired(self):
        if not self.timer_started_at:
            return False
        return timezone.now() > self.deadline_at

    @property
    def seconds_remaining(self):
        if not self.timer_started_at:
            return int(self._duration_minutes() * 60)
        return max(0, int((self.deadline_at - timezone.now()).total_seconds()))


class AttemptQuestion(models.Model):
    """
    Exact question + shuffled options used on an attempt (audit / disputes).
    option_order stores original letters in display order, e.g. ['C','A','D','B'].
    """
    attempt = models.ForeignKey(
        CourseExamAttempt,
        on_delete=models.CASCADE,
        related_name='attempt_questions',
    )
    question = models.ForeignKey(
        CourseQuestion,
        on_delete=models.PROTECT,
        related_name='attempt_appearances',
    )
    order_index = models.PositiveIntegerField()
    option_order = models.JSONField(
        help_text="Original options in display order, e.g. ['B','A','D','C'].",
    )
    # Snapshots at draw time (dispute-safe even if bank text changes later)
    presented_question_text = models.TextField()
    presented_option_a = models.CharField(max_length=500)
    presented_option_b = models.CharField(max_length=500)
    presented_option_c = models.CharField(max_length=500)
    presented_option_d = models.CharField(max_length=500)
    correct_display_option = models.CharField(
        max_length=1,
        choices=CORRECT_OPTION_CHOICES,
        help_text='Which displayed letter (A–D) is correct after shuffle.',
    )
    selected_display_option = models.CharField(
        max_length=1,
        choices=CORRECT_OPTION_CHOICES,
        blank=True,
    )
    is_correct = models.BooleanField(
        null=True,
        blank=True,
        help_text='Set on submit; null while the attempt is in progress.',
    )

    class Meta:
        ordering = ['attempt', 'order_index']
        verbose_name = 'Attempt question'
        verbose_name_plural = 'Attempt questions'
        constraints = [
            models.UniqueConstraint(
                fields=['attempt', 'question'],
                name='uniq_attempt_bank_question',
            ),
            models.UniqueConstraint(
                fields=['attempt', 'order_index'],
                name='uniq_attempt_question_order',
            ),
        ]

    def __str__(self):
        return f'Attempt {self.attempt_id} Q{self.order_index}'


class AttemptEvent(models.Model):
    """
    Anti-cheat / lockdown telemetry for a course exam attempt.
    Tab switches are logged; after TAB_SWITCH_FLAG_THRESHOLD the attempt is
    flagged for manual review (never auto-failed from these alone).
    """
    attempt = models.ForeignKey(
        CourseExamAttempt,
        on_delete=models.CASCADE,
        related_name='events',
    )
    event_type = models.CharField(
        max_length=40,
        choices=ATTEMPT_EVENT_TYPE_CHOICES,
        db_index=True,
    )
    detail = models.CharField(max_length=255, blank=True)
    metadata = models.JSONField(default=dict, blank=True)
    created_at = models.DateTimeField(auto_now_add=True, db_index=True)

    class Meta:
        ordering = ['attempt', 'created_at']
        verbose_name = 'Attempt event'
        verbose_name_plural = 'Attempt events'

    def __str__(self):
        return f'{self.event_type} @ attempt {self.attempt_id}'


# ── Course certificates (Part 6) ─────────────────────────────────────────────

CERT_STATUS_VALID = 'valid'
CERT_STATUS_REVOKED = 'revoked'
CERTIFICATE_STATUS_CHOICES = [
    (CERT_STATUS_VALID, 'Valid'),
    (CERT_STATUS_REVOKED, 'Revoked'),
]

SCORE_BAND_EXCELLENT = 'excellent'
SCORE_BAND_PROFICIENT = 'proficient'
SCORE_BAND_COMPETENT = 'competent'
SCORE_BAND_CHOICES = [
    (SCORE_BAND_EXCELLENT, 'Passed — Excellent'),
    (SCORE_BAND_PROFICIENT, 'Passed — Proficient'),
    (SCORE_BAND_COMPETENT, 'Passed — Competent'),
]

ASSESSMENT_KNOWLEDGE = 'knowledge'
ASSESSMENT_THEORY_PRACTICAL = 'theory_practical'
ASSESSMENT_TYPE_CHOICES = [
    (ASSESSMENT_KNOWLEDGE, 'Knowledge Assessment'),
    (ASSESSMENT_THEORY_PRACTICAL, 'Theory + Practical Assessment'),
]

PRACTICAL_GITHUB = 'github_link'
PRACTICAL_FIGMA = 'figma_link'
PRACTICAL_FILE = 'file_upload'
PRACTICAL_SUBMISSION_TYPE_CHOICES = [
    (PRACTICAL_GITHUB, 'GitHub repository link'),
    (PRACTICAL_FIGMA, 'Figma file link'),
    (PRACTICAL_FILE, 'File upload (ZIP)'),
]

PRACTICAL_PENDING = 'pending'
PRACTICAL_APPROVED = 'approved'
PRACTICAL_NEEDS_REVISION = 'needs_revision'
PRACTICAL_REJECTED = 'rejected'
PRACTICAL_STATUS_CHOICES = [
    (PRACTICAL_PENDING, 'Pending review'),
    (PRACTICAL_APPROVED, 'Approved'),
    (PRACTICAL_NEEDS_REVISION, 'Needs revision'),
    (PRACTICAL_REJECTED, 'Rejected'),
]


class CourseCertificate(models.Model):
    """
    Formal certificate + shareable skill badge issued on a passing course exam.
    Public verification: /verify/{certificate_number}/
    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='course_certificates',
    )
    attempt = models.OneToOneField(
        CourseExamAttempt,
        on_delete=models.PROTECT,
        related_name='certificate',
    )
    course = models.ForeignKey(
        Course,
        on_delete=models.PROTECT,
        related_name='certificates',
    )
    certificate_number = models.CharField(
        max_length=64,
        unique=True,
        db_index=True,
        help_text='Public ID, e.g. TSN-SEO-2026-00001',
    )
    issued_at = models.DateTimeField(auto_now_add=True)
    score = models.DecimalField(max_digits=5, decimal_places=2)
    score_band = models.CharField(max_length=20, choices=SCORE_BAND_CHOICES)
    status = models.CharField(
        max_length=20,
        choices=CERTIFICATE_STATUS_CHOICES,
        default=CERT_STATUS_VALID,
        db_index=True,
    )
    revoke_reason = models.TextField(blank=True)
    revoked_at = models.DateTimeField(null=True, blank=True)
    revoked_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='revoked_course_certificates',
    )
    # Snapshot for public page / PDF (privacy-aware)
    recipient_name = models.CharField(max_length=200)
    privacy_initials_only = models.BooleanField(
        default=False,
        help_text='If True, public verify page shows initials instead of full name.',
    )
    course_title_snapshot = models.CharField(max_length=200)
    assessment_type = models.CharField(
        max_length=32,
        choices=ASSESSMENT_TYPE_CHOICES,
        default=ASSESSMENT_KNOWLEDGE,
        help_text='Printed on the certificate: Knowledge vs Theory + Practical.',
    )
    pdf_file = models.FileField(
        upload_to='course_certificates/pdf/',
        blank=True,
        null=True,
    )
    badge_image = models.ImageField(
        upload_to='course_certificates/badges/',
        blank=True,
        null=True,
    )
    integrity_hash = models.CharField(
        max_length=64,
        blank=True,
        help_text='SHA-256 of certificate payload (digital integrity stamp).',
    )
    assets_ready = models.BooleanField(
        default=False,
        help_text='True once async PDF + badge generation has finished.',
    )

    class Meta:
        ordering = ['-issued_at']
        verbose_name = 'Course certificate'
        verbose_name_plural = 'Course certificates'

    def __str__(self):
        return self.certificate_number

    @property
    def is_valid(self):
        return self.status == CERT_STATUS_VALID

    @property
    def score_band_label(self):
        return dict(SCORE_BAND_CHOICES).get(self.score_band, self.score_band)

    @property
    def assessment_label(self):
        return dict(ASSESSMENT_TYPE_CHOICES).get(
            self.assessment_type, 'Knowledge Assessment',
        )

    @property
    def public_display_name(self):
        return self.recipient_name

    def get_verify_path(self):
        return f'/verify/{self.certificate_number}/'


class PracticalSubmission(models.Model):
    """
    Post-MCQ practical for courses with requires_practical=True.
    Certificate is issued only after status=approved.
    """
    user = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='practical_submissions',
    )
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='practical_submissions',
    )
    attempt = models.ForeignKey(
        CourseExamAttempt,
        on_delete=models.CASCADE,
        related_name='practical_submissions',
    )
    submission_type = models.CharField(
        max_length=20,
        choices=PRACTICAL_SUBMISSION_TYPE_CHOICES,
    )
    submission_url = models.URLField(
        blank=True,
        help_text='GitHub or Figma link when submission_type is a link.',
    )
    submission_file = models.FileField(
        upload_to='practical_submissions/',
        blank=True,
        null=True,
        help_text='ZIP / file upload when submission_type is file_upload.',
    )
    status = models.CharField(
        max_length=20,
        choices=PRACTICAL_STATUS_CHOICES,
        default=PRACTICAL_PENDING,
        db_index=True,
    )
    reviewer_notes = models.TextField(
        blank=True,
        help_text='Shown to the candidate on rejection or resubmission requests.',
    )
    reviewed_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='reviewed_practical_submissions',
    )
    submitted_at = models.DateTimeField(auto_now_add=True)
    reviewed_at = models.DateTimeField(null=True, blank=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-submitted_at']
        verbose_name = 'Practical submission'
        verbose_name_plural = 'Practical submissions'
        constraints = [
            models.UniqueConstraint(
                fields=['attempt'],
                name='uniq_practical_per_attempt',
            ),
        ]
        indexes = [
            models.Index(
                fields=['status', 'submitted_at'],
                name='practical_queue_idx',
            ),
        ]

    def __str__(self):
        return f'Practical {self.pk} — {self.user} / {self.course} ({self.status})'

    @property
    def submission_url_or_file(self):
        if self.submission_url:
            return self.submission_url
        if self.submission_file:
            return self.submission_file.url
        return ''

    @property
    def is_approved(self):
        return self.status == PRACTICAL_APPROVED

    @property
    def can_resubmit(self):
        return self.status in (PRACTICAL_NEEDS_REVISION, PRACTICAL_REJECTED)


# ── Growth: free diagnostic (Part 8 ASAP) ────────────────────────────────────

DIAGNOSTIC_QUESTION_COUNT = 12


class DiagnosticAttempt(models.Model):
    """
    Anonymous free mini-quiz lead capture.
    Email/WhatsApp required before the projected score is shown.
    """
    course = models.ForeignKey(
        Course,
        on_delete=models.CASCADE,
        related_name='diagnostic_attempts',
    )
    email = models.EmailField()
    whatsapp = models.CharField(max_length=30, blank=True)
    score = models.DecimalField(max_digits=5, decimal_places=2)
    correct_count = models.PositiveIntegerField(default=0)
    total_asked = models.PositiveIntegerField(default=0)
    question_ids = models.JSONField(default=list, blank=True)
    taken_at = models.DateTimeField(auto_now_add=True)
    follow_up_sent = models.BooleanField(
        default=False,
        help_text='Mark when a discount / nurture message has been sent.',
    )

    class Meta:
        ordering = ['-taken_at']
        verbose_name = 'Diagnostic attempt'
        verbose_name_plural = 'Diagnostic attempts'

    def __str__(self):
        return f'{self.email} · {self.course} · {self.score}%'
