"""
Models for certification programs, MCQ exams, proctoring, and verifiable certificates.
"""
import secrets
import string
from decimal import Decimal, ROUND_HALF_UP

from django.conf import settings
from django.db import models
from django.utils import timezone
from django.utils.text import slugify


def generate_verification_code():
    """Unique human-friendly code e.g. CERT-A7K2-9MQX."""
    alphabet = string.ascii_uppercase + string.digits
    part = lambda: ''.join(secrets.choice(alphabet) for _ in range(4))
    return f'CERT-{part()}-{part()}'


def generate_proctor_code():
    """Short code teacher shares in the live meeting, e.g. EXAM-7K2M."""
    alphabet = string.ascii_uppercase + string.digits
    # Avoid ambiguous chars
    alphabet = alphabet.replace('0', '').replace('O', '').replace('1', '').replace('I', '')
    body = ''.join(secrets.choice(alphabet) for _ in range(4))
    return f'EXAM-{body}'


class CertificationProgram(models.Model):
    """
    A skill certification track — independent of course completion.
    Candidates pay a fee, sit a proctored MCQ exam, and receive a verifiable certificate.
    """
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    category = models.ForeignKey(
        'courses.Category',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='certification_programs',
    )
    short_description = models.CharField(max_length=300)
    full_description = models.TextField(blank=True)
    thumbnail = models.ImageField(upload_to='certification_thumbnails/', blank=True, null=True)
    fee = models.DecimalField(
        max_digits=8,
        decimal_places=2,
        default=0,
        help_text='Initial certification fee (set by admin).',
    )
    reattempt_fee = models.DecimalField(
        max_digits=8,
        decimal_places=2,
        null=True,
        blank=True,
        help_text='Fee for each reattempt after a fail. Leave blank to use half of the certification fee.',
    )
    related_course = models.ForeignKey(
        'courses.Course',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='certification_programs',
        help_text='Optional link to a course this cert maps to.',
    )
    teachers = models.ManyToManyField(
        settings.AUTH_USER_MODEL,
        related_name='teaching_certification_programs',
        limit_choices_to={'role': 'teacher'},
        blank=True,
        help_text='Teachers/proctors assigned to this certification. Students of this program are handled by these teachers.',
    )
    questions_per_exam = models.PositiveIntegerField(
        default=100,
        help_text='How many MCQs are randomly drawn for each candidate attempt (default 100).',
    )
    min_question_bank = models.PositiveIntegerField(
        default=800,
        help_text='Minimum active MCQs required in the bank before exams can run (default 800).',
    )
    passing_score = models.PositiveIntegerField(
        default=70,
        help_text='Minimum percentage required to pass (default 70%).',
    )
    time_limit_minutes = models.PositiveIntegerField(
        default=120,
        help_text=(
            'Exam duration in minutes. Default 120 for 100 MCQs (~1.2 min each). '
            'Minimum is 1 minute per question (e.g. 100 minutes for 100 MCQs).'
        ),
    )
    results_delay_minutes = models.PositiveIntegerField(
        default=5,
        help_text=(
            'Minutes after submission before the score is shown to the candidate '
            '(default 5). Use 0 for immediate results. Cancelled attempts show immediately.'
        ),
    )
    max_attempts = models.PositiveIntegerField(
        default=3,
        help_text='Total exam attempts allowed (1 initial + 2 reattempts if failed). 0 = unlimited.',
    )
    require_proctor = models.BooleanField(
        default=True,
        help_text='Exams must be unlocked with a live teacher/consultant code (remote meeting or at the center).',
    )
    allow_center_exam = models.BooleanField(
        default=True,
        help_text='Candidates may book an in-person slot at a TSN Digital testing center.',
    )
    allow_remote_exam = models.BooleanField(
        default=True,
        help_text='Candidates may take the exam online with a live remote proctor meeting.',
    )
    meeting_instructions = models.TextField(
        blank=True,
        help_text='How candidates join the live proctor meeting (Zoom/Meet link, schedule notes, etc.).',
    )
    is_published = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return self.title

    def save(self, *args, **kwargs):
        if not self.slug:
            base = slugify(self.title) or 'certification'
            slug = base
            n = 1
            while CertificationProgram.objects.filter(slug=slug).exclude(pk=self.pk).exists():
                slug = f'{base}-{n}'
                n += 1
            self.slug = slug
        if self.passing_score is None or self.passing_score < 1:
            self.passing_score = 70
        # Always enforce a timer: at least 1 minute per exam question
        min_time = self.minimum_time_limit_minutes
        if not self.time_limit_minutes or self.time_limit_minutes < min_time:
            self.time_limit_minutes = self.recommended_time_limit_minutes
        super().save(*args, **kwargs)

    @property
    def minimum_time_limit_minutes(self):
        """Floor: 1 minute per MCQ (100 minutes for a 100-question exam)."""
        return max(60, int(self.questions_per_exam or 100))

    @property
    def recommended_time_limit_minutes(self):
        """Recommended: ~1.2 minutes per MCQ (120 minutes for 100 questions)."""
        q = int(self.questions_per_exam or 100)
        return max(self.minimum_time_limit_minutes, int(round(q * 1.2)))

    @property
    def active_question_count(self):
        return self.questions.filter(is_active=True).count()

    @property
    def is_exam_ready(self):
        """Bank must have at least min_question_bank (800) active MCQs."""
        return self.active_question_count >= (self.min_question_bank or 800)

    @property
    def question_bank_deficit(self):
        need = self.min_question_bank or 800
        return max(0, need - self.active_question_count)

    @property
    def effective_reattempt_fee(self):
        """Admin-set reattempt fee, or half of certification fee."""
        if self.reattempt_fee is not None:
            return self.reattempt_fee
        half = (Decimal(self.fee or 0) / Decimal('2')).quantize(
            Decimal('0.01'), rounding=ROUND_HALF_UP
        )
        return half

    @property
    def max_reattempts_after_fail(self):
        """How many retries after the first attempt (default 2 when max_attempts=3)."""
        if self.max_attempts == 0:
            return 999
        return max(0, self.max_attempts - 1)


class ExamQuestion(models.Model):
    program = models.ForeignKey(
        CertificationProgram,
        on_delete=models.CASCADE,
        related_name='questions',
    )
    text = models.TextField()
    order = models.PositiveIntegerField(default=0)
    is_active = models.BooleanField(default=True)

    class Meta:
        ordering = ['order', 'id']

    def __str__(self):
        return f'Q{self.pk}: {self.text[:60]}'


class ExamChoice(models.Model):
    question = models.ForeignKey(
        ExamQuestion,
        on_delete=models.CASCADE,
        related_name='choices',
    )
    text = models.CharField(max_length=500)
    is_correct = models.BooleanField(default=False)

    def __str__(self):
        return self.text[:60]


APP_PENDING_PAYMENT = 'pending_payment'
APP_PAID = 'paid'
APP_AWAITING_PROCTOR = 'awaiting_proctor'
APP_IN_EXAM = 'in_exam'
APP_PASSED = 'passed'
APP_FAILED = 'failed'
APP_PENDING_REATTEMPT = 'pending_reattempt_payment'
APP_CERTIFIED = 'certified'

APPLICATION_STATUS_CHOICES = [
    (APP_PENDING_PAYMENT, 'Pending certification fee'),
    (APP_PAID, 'Paid — schedule your test'),
    (APP_AWAITING_PROCTOR, 'Proctor unlocked — start exam'),
    (APP_IN_EXAM, 'Exam in progress'),
    (APP_PASSED, 'Passed'),
    (APP_FAILED, 'Failed'),
    (APP_PENDING_REATTEMPT, 'Pending reattempt fee'),
    (APP_CERTIFIED, 'Certificate issued'),
]

EXAM_MODE_CENTER = 'center'
EXAM_MODE_REMOTE = 'remote'
EXAM_MODE_CHOICES = [
    (EXAM_MODE_CENTER, 'At testing center (in person)'),
    (EXAM_MODE_REMOTE, 'Online remote (live proctor meeting)'),
]


class TestingCenter(models.Model):
    """Physical location where candidates can sit certification exams (e.g. TSN Digital Center)."""
    name = models.CharField(max_length=200)
    slug = models.SlugField(max_length=220, unique=True, blank=True)
    address = models.TextField()
    city = models.CharField(max_length=100)
    phone = models.CharField(max_length=40, blank=True)
    email = models.EmailField(blank=True)
    map_url = models.URLField(blank=True, help_text='Google Maps or similar link.')
    directions = models.TextField(blank=True, help_text='How to find the center, parking, what to bring.')
    is_active = models.BooleanField(default=True)
    sort_order = models.PositiveIntegerField(default=0)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['sort_order', 'city', 'name']

    def __str__(self):
        return f'{self.name} ({self.city})'

    def save(self, *args, **kwargs):
        if not self.slug:
            base = slugify(self.name) or 'center'
            slug = base
            n = 1
            while TestingCenter.objects.filter(slug=slug).exclude(pk=self.pk).exists():
                slug = f'{base}-{n}'
                n += 1
            self.slug = slug
        super().save(*args, **kwargs)


class TestScheduleSlot(models.Model):
    """Bookable exam window at a testing center."""
    center = models.ForeignKey(
        TestingCenter,
        on_delete=models.CASCADE,
        related_name='slots',
    )
    program = models.ForeignKey(
        CertificationProgram,
        on_delete=models.CASCADE,
        null=True,
        blank=True,
        related_name='schedule_slots',
        help_text='Leave blank to allow any certification program.',
    )
    date = models.DateField()
    start_time = models.TimeField()
    end_time = models.TimeField()
    capacity = models.PositiveIntegerField(default=20)
    is_active = models.BooleanField(default=True)
    notes = models.CharField(max_length=255, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ['date', 'start_time']
        indexes = [
            models.Index(fields=['date', 'center']),
        ]

    def __str__(self):
        return f'{self.center.name} — {self.date} {self.start_time}'

    @property
    def booked_count(self):
        if not self.pk:
            return 0
        return self.bookings.count()

    @property
    def seats_available(self):
        return max(0, self.capacity - self.booked_count)

    @property
    def is_upcoming(self):
        from datetime import datetime
        start = datetime.combine(self.date, self.start_time)
        if timezone.is_aware(timezone.now()):
            start = timezone.make_aware(start)
        return start >= timezone.now() - timezone.timedelta(hours=1)

    @property
    def is_bookable(self):
        return self.is_active and self.is_upcoming and self.seats_available > 0

    @property
    def label(self):
        return f'{self.date:%d %b %Y} · {self.start_time:%H:%M}–{self.end_time:%H:%M}'


class CertificationApplication(models.Model):
    """Candidate application + payment state for a certification program."""
    candidate = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.CASCADE,
        related_name='certification_applications',
    )
    program = models.ForeignKey(
        CertificationProgram,
        on_delete=models.CASCADE,
        related_name='applications',
    )
    status = models.CharField(
        max_length=40,
        choices=APPLICATION_STATUS_CHOICES,
        default=APP_PENDING_PAYMENT,
    )
    payment_method = models.CharField(max_length=20, blank=True, default='')
    payment_proof = models.ImageField(upload_to='cert_payment_proofs/', blank=True, null=True)
    payment_note = models.TextField(blank=True)
    payment_rejection_reason = models.TextField(
        blank=True,
        help_text='Set when admin rejects a manual payment proof; candidate can re-upload.',
    )
    payment_kind = models.CharField(
        max_length=20,
        default='initial',
        help_text='initial or reattempt — which fee the current proof/Stripe session is for.',
    )
    exam_mode = models.CharField(
        max_length=20,
        choices=EXAM_MODE_CHOICES,
        blank=True,
        default='',
        help_text='center = in-person at a testing center; remote = online with live proctor.',
    )
    testing_center = models.ForeignKey(
        TestingCenter,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='applications',
    )
    schedule_slot = models.ForeignKey(
        TestScheduleSlot,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='bookings',
    )
    scheduled_at = models.DateTimeField(null=True, blank=True)
    promo_code = models.ForeignKey(
        'promotions.PromoCode',
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='cert_applications',
    )
    original_amount = models.DecimalField(max_digits=8, decimal_places=2, null=True, blank=True)
    discount_amount = models.DecimalField(max_digits=8, decimal_places=2, default=0)
    payable_amount = models.DecimalField(
        max_digits=8,
        decimal_places=2,
        null=True,
        blank=True,
        help_text='Amount due after promo. Null = use full fee.',
    )
    stripe_session_id = models.CharField(max_length=255, blank=True)
    stripe_payment_intent = models.CharField(max_length=255, blank=True)
    paid_at = models.DateTimeField(null=True, blank=True)
    proctor_unlocked_at = models.DateTimeField(null=True, blank=True)
    approved_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='approved_cert_applications',
    )
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    class Meta:
        ordering = ['-created_at']
        unique_together = ('candidate', 'program')

    def __str__(self):
        return f'{self.candidate} → {self.program} [{self.status}]'

    @property
    def needs_schedule(self):
        """Paid (or reattempt-paid) but has not chosen center/remote yet."""
        if self.status != APP_PAID:
            return False
        if self.exam_mode == EXAM_MODE_REMOTE:
            return False
        if self.exam_mode == EXAM_MODE_CENTER and self.schedule_slot_id:
            return False
        return True

    @property
    def can_enter_proctor_code(self):
        """Paid, scheduled, and waiting for teacher unlock code."""
        if self.status == APP_CERTIFIED:
            return False
        if self.attempts_remaining <= 0:
            return False
        if self.needs_schedule:
            return False
        return self.status in (APP_PAID, APP_AWAITING_PROCTOR)

    @property
    def can_take_exam(self):
        if self.status == APP_CERTIFIED:
            return False
        if self.attempts_remaining <= 0:
            return False
        if self.status == APP_IN_EXAM:
            return True
        if self.needs_schedule:
            return False
        if self.program.require_proctor:
            return self.status == APP_AWAITING_PROCTOR and self.proctor_unlocked_at is not None
        return self.status in (APP_PAID, APP_AWAITING_PROCTOR)

    @property
    def needs_reattempt_payment(self):
        return self.status == APP_PENDING_REATTEMPT and self.attempts_remaining > 0

    @property
    def is_paid(self):
        return self.status not in (APP_PENDING_PAYMENT, APP_PENDING_REATTEMPT)

    def attempts_used(self):
        return self.attempts.filter(submitted_at__isnull=False).count()

    @property
    def attempts_remaining(self):
        max_a = self.program.max_attempts
        if max_a == 0:
            return 999
        return max(0, max_a - self.attempts_used())

    def current_fee_due(self):
        if self.status == APP_PENDING_REATTEMPT:
            return self.program.effective_reattempt_fee
        return self.program.fee


class ProctorSession(models.Model):
    """
    Live proctor unlock for a remote or on-site certification exam.
    Teacher/consultant generates a code during the session; student enters it to start.
    """
    application = models.ForeignKey(
        CertificationApplication,
        on_delete=models.CASCADE,
        related_name='proctor_sessions',
    )
    code = models.CharField(max_length=16, unique=True, db_index=True)
    created_by = models.ForeignKey(
        settings.AUTH_USER_MODEL,
        on_delete=models.SET_NULL,
        null=True,
        related_name='created_proctor_sessions',
    )
    meeting_url = models.URLField(blank=True, help_text='Optional Zoom/Meet/Teams link for this session.')
    notes = models.CharField(max_length=255, blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    expires_at = models.DateTimeField()
    unlocked_at = models.DateTimeField(null=True, blank=True)
    is_revoked = models.BooleanField(default=False)

    class Meta:
        ordering = ['-created_at']

    def __str__(self):
        return f'{self.code} → app {self.application_id}'

    def save(self, *args, **kwargs):
        if not self.code:
            code = generate_proctor_code()
            while ProctorSession.objects.filter(code=code).exists():
                code = generate_proctor_code()
            self.code = code
        if not self.expires_at:
            self.expires_at = timezone.now() + timezone.timedelta(hours=2)
        super().save(*args, **kwargs)

    @property
    def is_valid(self):
        if self.is_revoked or self.unlocked_at:
            return False
        return timezone.now() <= self.expires_at


class ExamAttempt(models.Model):
    application = models.ForeignKey(
        CertificationApplication,
        on_delete=models.CASCADE,
        related_name='attempts',
    )
    proctor_session = models.ForeignKey(
        ProctorSession,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
        related_name='attempts',
    )
    started_at = models.DateTimeField(auto_now_add=True)
    timer_started_at = models.DateTimeField(
        null=True,
        blank=True,
        help_text='Set when the candidate finishes the practice question and the real exam begins.',
    )
    submitted_at = models.DateTimeField(null=True, blank=True)
    cancelled_at = models.DateTimeField(null=True, blank=True)
    cancel_reason = models.CharField(
        max_length=64,
        blank=True,
        help_text='e.g. tab_leave, browser_leave',
    )
    question_ids = models.JSONField(default=list, 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)

    class Meta:
        ordering = ['-started_at']

    def __str__(self):
        return f'Attempt #{self.pk} for app {self.application_id}'

    @property
    def is_open(self):
        return self.submitted_at is None

    @property
    def is_practice_phase(self):
        """True until the candidate completes the sample question and starts the timer."""
        return self.submitted_at is None and self.timer_started_at is None

    @property
    def is_timed_exam(self):
        return self.submitted_at is None and self.timer_started_at is not None

    @property
    def was_cancelled(self):
        return self.cancelled_at is not None

    @property
    def results_available_at(self):
        """When the candidate may see their score. Cancelled = immediate."""
        if not self.submitted_at:
            return None
        if self.cancelled_at:
            return self.submitted_at
        delay = self.application.program.results_delay_minutes
        if delay is None:
            delay = 5
        return self.submitted_at + timezone.timedelta(minutes=max(0, int(delay)))

    @property
    def results_ready(self):
        at = self.results_available_at
        if not at:
            return False
        return timezone.now() >= at

    @property
    def results_seconds_remaining(self):
        at = self.results_available_at
        if not at:
            return 0
        return max(0, int((at - timezone.now()).total_seconds()))

    def _time_limit_minutes(self):
        limit = self.application.program.time_limit_minutes
        if not limit:
            limit = self.application.program.recommended_time_limit_minutes
        return limit

    def time_expired(self):
        if not self.timer_started_at:
            return False
        deadline = self.timer_started_at + timezone.timedelta(minutes=self._time_limit_minutes())
        return timezone.now() > deadline

    @property
    def deadline_at(self):
        start = self.timer_started_at or timezone.now()
        return start + timezone.timedelta(minutes=self._time_limit_minutes())

    @property
    def seconds_remaining(self):
        if not self.timer_started_at:
            return int(self._time_limit_minutes() * 60)
        left = (self.deadline_at - timezone.now()).total_seconds()
        return max(0, int(left))


class ExamAnswer(models.Model):
    attempt = models.ForeignKey(ExamAttempt, on_delete=models.CASCADE, related_name='answers')
    question = models.ForeignKey(ExamQuestion, on_delete=models.CASCADE)
    selected_choice = models.ForeignKey(
        ExamChoice,
        on_delete=models.SET_NULL,
        null=True,
        blank=True,
    )
    is_correct = models.BooleanField(default=False)

    class Meta:
        unique_together = ('attempt', 'question')


class Certificate(models.Model):
    """Issued certificate with public verification code."""
    application = models.OneToOneField(
        CertificationApplication,
        on_delete=models.CASCADE,
        related_name='certificate',
    )
    verification_code = models.CharField(max_length=32, unique=True, db_index=True)
    recipient_name = models.CharField(max_length=200)
    program_title = models.CharField(max_length=200)
    score_percent = models.DecimalField(max_digits=5, decimal_places=2)
    issued_at = models.DateTimeField(auto_now_add=True)
    is_revoked = models.BooleanField(default=False)
    revoke_reason = models.CharField(max_length=255, blank=True)

    class Meta:
        ordering = ['-issued_at']

    def __str__(self):
        return f'{self.verification_code} — {self.recipient_name}'

    def save(self, *args, **kwargs):
        if not self.verification_code:
            code = generate_verification_code()
            while Certificate.objects.filter(verification_code=code).exists():
                code = generate_verification_code()
            self.verification_code = code
        super().save(*args, **kwargs)

    @property
    def is_valid(self):
        return not self.is_revoked
