"""Núcleo do GAP Learning Engine.

Contém a estimativa de domínio por tópico e o motor de recomendação
baseado em regras simples (ver CONTEXT.md, secções 15 e 16).

A lógica é modular para que, no futuro, algoritmos como Bayesian
Knowledge Tracing possam ser adicionados sem reescrever a API.
"""

from django.db.models import Count, Q
from django.utils import timezone

from learning.llm import LLMUnavailableError, llm_service
from learning.models import Attempt, LearningProfile, Question, Recommendation, TopicMastery

# Taxas de atualização de domínio (0–1). Um acerto "move" o domínio para
# cima; um erro move para baixo. A dificuldade pondera o movimento:
#   - acertar algo difícil conta mais do que acertar algo fácil;
#   - errar algo fácil penaliza mais do que errar algo difícil.
MASTERY_STEP = 0.15
MASTERY_MIN = 0.0
MASTERY_MAX = 1.0

# Limiares usados pelo motor de recomendação (CONTEXT.md, secção 15).
THRESHOLD_REINFORCE = 0.40
THRESHOLD_INTERMEDIATE = 0.70

# Número de erros consecutivos que ativa a recomendação de pré-requisito.
REPEATED_ERRORS = 2


def get_or_create_profile(student) -> LearningProfile:
    profile, _ = LearningProfile.objects.get_or_create(student=student)
    return profile


def _difficulty_weight(difficulty: int) -> float:
    """Peso da dificuldade entre 0.5 (fácil) e 1.0 (difícil)."""
    return 0.5 + 0.125 * max(1, min(5, difficulty))


def estimate_mastery(profile: LearningProfile, topic) -> TopicMastery:
    """Devolve (criando se necessário) o domínio atual do tópico."""
    mastery, _ = TopicMastery.objects.get_or_create(profile=profile, topic=topic)
    return mastery


def update_mastery(student, question, is_correct: bool) -> TopicMastery:
    """Atualiza o domínio do tópico após uma tentativa e devolve o novo valor."""
    if not question.topic:
        return None

    profile = get_or_create_profile(student)
    mastery = estimate_mastery(profile, question.topic)

    w = _difficulty_weight(question.difficulty)
    if is_correct:
        delta = MASTERY_STEP * w
    else:
        delta = -MASTERY_STEP * (2 - w)

    mastery.mastery = min(MASTERY_MAX, max(MASTERY_MIN, mastery.mastery + delta))
    mastery.last_attempt_at = timezone.now()
    mastery.save()

    return mastery


def record_attempt(student, question, answer: str, is_correct: bool, time_spent: int = 0) -> Attempt:
    """Regista a tentativa, atualiza o domínio e gera recomendações."""
    attempt_number = (
        Attempt.objects.filter(student=student, question=question).count() + 1
    )
    attempt = Attempt.objects.create(
        student=student,
        question=question,
        answer=answer,
        is_correct=is_correct,
        time_spent=time_spent,
        attempt_number=attempt_number,
    )

    update_mastery(student, question, is_correct)
    generate_recommendations(student, topic=question.topic)
    return attempt


def _mastery_for_topics(student, topic_ids):
    """Mapa {topic_id: mastery} para os tópicos indicados."""
    profile = get_or_create_profile(student)
    rows = TopicMastery.objects.filter(profile=profile, topic_id__in=topic_ids)
    return {r.topic_id: r.mastery for r in rows}


def recommend_next_question(student, topic=None):
    """
    Seleciona a próxima questão para o estudante (CONTEXT.md, secção 15).

    Regras simples:
      - sem domínio registado → questão fácil do tópico;
      - domínio < 40% → questão de reforço (dificuldade 1–2);
      - domínio entre 40% e 70% → questão intermédia (2–4);
      - domínio > 70% → questão avançada (4–5);
      - evita questões já respondidas recentemente.
    """
    from operations.models import Topic

    enrolled_topic_ids = (
        Topic.objects.filter(course__classes__enrollments__student=student)
        .values_list("id", flat=True)
    )
    if topic:
        candidate_topics = [topic]
    else:
        candidate_topics = list(Topic.objects.filter(pk__in=enrolled_topic_ids))

    if not candidate_topics:
        candidate_topics = list(Topic.objects.all())

    if not candidate_topics:
        return None

    # Apenas tópicos que têm questões publicadas (evita categorias sem questões).
    published_topic_ids = set(
        Question.objects.filter(is_published=True)
        .exclude(topic__isnull=True)
        .values_list("topic_id", flat=True)
    )
    candidate_topics = [t for t in candidate_topics if t.id in published_topic_ids]
    if not candidate_topics:
        return None

    mastery_map = _mastery_for_topics(student, [t.id for t in candidate_topics])
    for t in candidate_topics:
        t._mastery = mastery_map.get(t.id, 0.0)

    candidate_topics.sort(key=lambda t: (t._mastery, t.order))
    target = candidate_topics[0]

    mastery = target._mastery
    if mastery < THRESHOLD_REINFORCE:
        difficulty_range = (1, 2)
    elif mastery < THRESHOLD_INTERMEDIATE:
        difficulty_range = (2, 4)
    else:
        difficulty_range = (4, 5)

    recent_ids = (
        Attempt.objects.filter(
            student=student, question__topic=target
        )
        .order_by("-created_at")
        .values_list("question_id", flat=True)[:10]
    )

    question = (
        target.questions.filter(
            is_published=True,
            difficulty__gte=difficulty_range[0],
            difficulty__lte=difficulty_range[1],
        )
        .exclude(pk__in=list(recent_ids))
        .order_by("?")
        .first()
    )
    if question is None:
        question = (
            target.questions.filter(is_published=True)
            .exclude(pk__in=list(recent_ids))
            .order_by("?")
            .first()
        )

    return question


def generate_recommendations(student, topic=None, limit: int = 3):
    """Gera recomendações de reforço, incluindo pré-requisitos com erros repetidos."""
    from operations.models import Topic

    profile = get_or_create_profile(student)
    candidate_topics = [topic] if topic else list(Topic.objects.all())
    created = 0

    for t in candidate_topics:
        mastery, _ = TopicMastery.objects.get_or_create(profile=profile, topic=t)
        if mastery.mastery < THRESHOLD_REINFORCE:
            recent_wrong = (
                Attempt.objects.filter(student=student, question__topic=t, is_correct=False)
                .order_by("-created_at")[:REPEATED_ERRORS]
            )
            if len(recent_wrong) >= REPEATED_ERRORS:
                for prereq in t.prerequisites.all():
                    prereq_mastery = estimate_mastery(profile, prereq)
                    if prereq_mastery.mastery < THRESHOLD_INTERMEDIATE:
                        Recommendation.objects.get_or_create(
                            student=student,
                            topic=prereq,
                            reason="Erros repetidos: reforçar pré-requisito.",
                            defaults={
                                "priority": 1,
                                "status": Recommendation.Status.SUGGESTED,
                            },
                        )
                        created += 1
            else:
                Recommendation.objects.get_or_create(
                    student=student,
                    topic=t,
                    reason=f"Domínio de {mastery.mastery:.0%}: recomenda-se reforço.",
                    defaults={"priority": 2, "status": Recommendation.Status.SUGGESTED},
                )
                created += 1
        if created >= limit:
            break

    return created


def recommend_and_store(student, topic=None):
    """Gera a recomendação diária/preferida e persiste como a próxima ação."""
    question = recommend_next_question(student, topic=topic)
    if question is None:
        return None

    topic = question.topic
    mastery = None
    if topic:
        profile = get_or_create_profile(student)
        mastery = estimate_mastery(profile, topic)

    reason = _llm_recommendation_reason(student, topic, mastery, question) or _build_reason(
        mastery, question
    )
    recommendation, _ = Recommendation.objects.update_or_create(
        student=student,
        question=question,
        status=Recommendation.Status.SUGGESTED,
        defaults={"topic": topic, "reason": reason, "priority": 1},
    )
    return recommendation


def _llm_recommendation_reason(student, topic, mastery, question):
    """
    Justificação personalizada da recomendação gerada pela Groq.

    A seleção da questão continua a ser feita por regras (previsível e
    auditável); o LLM apenas explica a recomendação. Devolve None quando
    o LLM está indisponível (sem chave ou falha), permitindo fallback.
    """
    if topic is None or not llm_service.is_available():
        return None

    stats = Attempt.objects.filter(student=student, question__topic=topic).aggregate(
        total=Count("id"), errors=Count("id", filter=Q(is_correct=False))
    )
    mastery_pct = round(mastery.mastery * 100) if mastery else 0

    system_prompt = (
        "És o assistente de recomendação pedagógica da GAP, uma plataforma de "
        "aprendizagem personalizada. Respondes em português de Moçambique, num tom "
        "curto, claro e encorajador, em 2 a 3 frases. Usas apenas os dados fornecidos; "
        "não inventas informações nem revelas que és uma IA."
    )
    user_prompt = (
        f"Dados do estudante {student.first_name or student.username}:\n"
        f"- Disciplina: {topic.course.name}\n"
        f"- Tópico recomendado: {topic.name}\n"
        f"- Domínio atual no tópico: {mastery_pct}%\n"
        f"- Dificuldade do exercício recomendado: {question.difficulty}/5\n"
        f"- Tentativas no tópico: {stats['total']}\n"
        f"- Erros no tópico: {stats['errors']}\n"
        "Escreve uma recomendação personalizada explicando ao estudante por que deve "
        "fazer este exercício agora e o que deve focar."
    )
    try:
        return llm_service.chat(system_prompt, user_prompt, temperature=0.6, max_tokens=200)
    except LLMUnavailableError:
        return None


def _build_reason(mastery, question) -> str:
    if mastery is None:
        return "Primeira prática neste tópico."
    if mastery.mastery < THRESHOLD_REINFORCE:
        return f"Domínio de {mastery.mastery:.0%} no tópico: exercício de reforço."
    if mastery.mastery < THRESHOLD_INTERMEDIATE:
        return f"Domínio de {mastery.mastery:.0%} no tópico: exercício intermédio."
    return f"Domínio de {mastery.mastery:.0%} no tópico: exercício avançado."


def next_step_summary(student):
    """Resposta curta para o dashboard: 'O que devo fazer agora?'."""
    recommendation = (
        Recommendation.objects.filter(
            student=student, status=Recommendation.Status.SUGGESTED
        )
        .order_by("priority", "-created_at")
        .first()
    )
    if recommendation is None:
        recommendation = recommend_and_store(student)

    if recommendation is None:
        return {"has_recommendation": False, "message": "Sem recomendações por agora."}

    return {
        "has_recommendation": True,
        "topic": recommendation.topic.name if recommendation.topic else None,
        "reason": recommendation.reason,
        "question": recommendation.question.id if recommendation.question else None,
    }
