"""
Issue course certificates on passing attempts (Part 6).

PDF + badge images are generated asynchronously via Django-Q
(see certificate_tasks.generate_certificate_assets).
"""
from __future__ import annotations

import hashlib
import logging
import re

from django.conf import settings
from django.db import transaction
from django.utils import timezone

from apps.courses.models import (
    ASSESSMENT_KNOWLEDGE,
    ASSESSMENT_THEORY_PRACTICAL,
    CERT_STATUS_REVOKED,
    CERT_STATUS_VALID,
    CourseCertificate,
    CourseExamAttempt,
    SCORE_BAND_COMPETENT,
    SCORE_BAND_EXCELLENT,
    SCORE_BAND_PROFICIENT,
)

logger = logging.getLogger(__name__)


def score_band_for_percent(percent) -> str:
    p = float(percent or 0)
    if p >= 90:
        return SCORE_BAND_EXCELLENT
    if p >= 80:
        return SCORE_BAND_PROFICIENT
    return SCORE_BAND_COMPETENT


def initials_from_name(name: str) -> str:
    parts = [p for p in re.split(r'\s+', (name or '').strip()) if p]
    if not parts:
        return 'A. Candidate'
    if len(parts) == 1:
        return f'{parts[0][0].upper()}.'
    return ' '.join(f'{p[0].upper()}.' for p in parts)


def course_cert_code(course) -> str:
    raw = (getattr(course, 'certificate_code', None) or '').strip().upper()
    if raw:
        return re.sub(r'[^A-Z0-9]', '', raw)[:12] or 'COURSE'
    # Derive from slug: seo-expert → SEO, digital-marketing → DM
    slug = (course.slug or 'COURSE').upper()
    bits = [b for b in re.split(r'[^A-Z0-9]+', slug) if b]
    if not bits:
        return 'COURSE'
    if len(bits) == 1:
        return bits[0][:8]
    return ''.join(b[0] for b in bits)[:8]


def org_cert_code() -> str:
    return re.sub(
        r'[^A-Z0-9]',
        '',
        (getattr(settings, 'CERTIFICATE_ORG_CODE', 'TSN') or 'TSN').upper(),
    )[:8] or 'TSN'


def allocate_certificate_number(course) -> str:
    """Generate unique number like TSN-SEO-2026-00001 (race-safe via retry)."""
    org = org_cert_code()
    code = course_cert_code(course)
    year = timezone.now().year
    prefix = f'{org}-{code}-{year}-'
    for _ in range(20):
        last = (
            CourseCertificate.objects.filter(certificate_number__startswith=prefix)
            .order_by('-certificate_number')
            .values_list('certificate_number', flat=True)
            .first()
        )
        seq = 1
        if last:
            try:
                seq = int(last.rsplit('-', 1)[-1]) + 1
            except ValueError:
                seq = CourseCertificate.objects.filter(
                    certificate_number__startswith=prefix,
                ).count() + 1
        candidate = f'{prefix}{seq:05d}'
        if not CourseCertificate.objects.filter(certificate_number=candidate).exists():
            return candidate
    raise RuntimeError('Could not allocate a unique certificate number')


def compute_integrity_hash(cert: CourseCertificate) -> str:
    payload = '|'.join([
        cert.certificate_number,
        str(cert.user_id),
        str(cert.course_id),
        str(cert.attempt_id),
        str(cert.score),
        cert.score_band,
        cert.issued_at.isoformat() if cert.issued_at else '',
        cert.recipient_name,
    ])
    return hashlib.sha256(payload.encode('utf-8')).hexdigest()


@transaction.atomic
def create_certificate_record(attempt: CourseExamAttempt) -> CourseCertificate | None:
    """
    Create the DB row for a passing attempt (idempotent).
    Returns None if attempt did not pass.
    """
    attempt = (
        CourseExamAttempt.objects.select_related('user', 'course')
        .select_for_update()
        .get(pk=attempt.pk)
    )
    if not attempt.passed:
        return None
    existing = CourseCertificate.objects.filter(attempt=attempt).first()
    if existing:
        return existing

    user = attempt.user
    full = (user.get_display_name() or user.username or 'Candidate').strip()
    privacy = bool(getattr(user, 'prefer_certificate_privacy', False))
    display = initials_from_name(full) if privacy else full

    cert = CourseCertificate(
        user=user,
        attempt=attempt,
        course=attempt.course,
        certificate_number=allocate_certificate_number(attempt.course),
        score=attempt.score_percent or 0,
        score_band=score_band_for_percent(attempt.score_percent),
        status=CERT_STATUS_VALID,
        recipient_name=display[:200],
        privacy_initials_only=privacy,
        course_title_snapshot=(attempt.course.title or '')[:200],
        assessment_type=(
            ASSESSMENT_THEORY_PRACTICAL
            if attempt.course.requires_practical
            else ASSESSMENT_KNOWLEDGE
        ),
    )
    cert.save()
    cert.integrity_hash = compute_integrity_hash(cert)
    cert.save(update_fields=['integrity_hash'])
    return cert


def enqueue_certificate_assets(certificate_id: int) -> None:
    """Queue PDF/badge generation on Django-Q; sync fallback if Q is unavailable."""
    try:
        from django_q.tasks import async_task

        async_task(
            'apps.courses.certificate_tasks.generate_certificate_assets',
            certificate_id,
            task_name=f'cert-assets-{certificate_id}',
        )
        logger.info('Queued certificate asset generation for %s', certificate_id)
    except Exception:
        logger.exception(
            'Django-Q unavailable — generating certificate assets synchronously for %s',
            certificate_id,
        )
        from apps.courses.certificate_tasks import generate_certificate_assets

        generate_certificate_assets(certificate_id)


def enqueue_certificate_for_attempt(attempt_id: int) -> CourseCertificate | None:
    """
    Called after a passing submit (or after practical approval).
    For requires_practical courses, blocked until practical is approved.
    """
    attempt = CourseExamAttempt.objects.select_related('user', 'course').filter(
        pk=attempt_id, passed=True,
    ).first()
    if not attempt:
        return None

    if attempt.course.requires_practical:
        from apps.courses.practical_service import practical_approved_for_attempt

        if not practical_approved_for_attempt(attempt):
            logger.info(
                'Deferring certificate for attempt %s until practical is approved',
                attempt_id,
            )
            return None

    cert = create_certificate_record(attempt)
    if cert and not cert.assets_ready:
        enqueue_certificate_assets(cert.pk)
    return cert


def revoke_certificate(cert: CourseCertificate, *, reason: str = '', revoked_by=None) -> CourseCertificate:
    cert.status = CERT_STATUS_REVOKED
    cert.revoke_reason = (reason or '')[:2000]
    cert.revoked_at = timezone.now()
    cert.revoked_by = revoked_by
    cert.save(update_fields=['status', 'revoke_reason', 'revoked_at', 'revoked_by'])
    return cert
