# retoor <retoor@molodetz.nl>

from __future__ import annotations

import asyncio

from devplacepy.database import _now_iso, db, soft_delete
from devplacepy.utils import generate_uid

from .. import grading, scoring
from .common import (
    QuizError,
    _answers,
    _attempts,
    _iso,
    _now,
    born_live,
    conditional_update_row,
    dump_json,
    load_json,
)
from .questions import get_question, list_options
from .quizzes import increment_attempt_count, question_uids


def get_attempt(attempt_uid: str) -> dict | None:
    if not attempt_uid:
        return None
    return _attempts().find_one(uid=attempt_uid, deleted_at=None)


def live_attempts_for_quiz(quiz_uid: str) -> list[dict]:
    return list(
        _attempts().find(quiz_uid=quiz_uid, status="in_progress", deleted_at=None)
    )


def in_progress_attempt(user_uid: str, quiz_uid: str) -> dict | None:
    return _attempts().find_one(
        user_uid=user_uid, quiz_uid=quiz_uid, status="in_progress", deleted_at=None
    )


def answers_for(attempt_uid: str) -> list[dict]:
    return list(
        _answers().find(attempt_uid=attempt_uid, deleted_at=None, order_by=["position", "id"])
    )


def answer_for_question(attempt_uid: str, question_uid: str) -> dict | None:
    return _answers().find_one(
        attempt_uid=attempt_uid, question_uid=question_uid, deleted_at=None
    )


def expire_if_due(attempt: dict, now=None) -> dict:
    if not attempt or attempt.get("status") != "in_progress":
        return attempt
    if not scoring.is_expired(attempt.get("expires_at") or "", now or _now()):
        return attempt
    total = _answered_total(attempt["uid"])
    conditional_update_row(
        "quiz_attempts",
        attempt["uid"],
        "status = 'expired', completed_at = :completed_at, "
        "score_points = :total, score_percent = :percent",
        "status = 'in_progress'",
        {
            "completed_at": _iso(),
            "total": total,
            "percent": scoring.score_percent(
                total, int(attempt.get("max_points") or 0)
            ),
        },
    )
    return get_attempt(attempt["uid"]) or attempt


def start_attempt(user: dict, quiz: dict) -> dict:
    existing = in_progress_attempt(user["uid"], quiz["uid"])
    if existing:
        refreshed = expire_if_due(existing)
        if refreshed.get("status") == "in_progress":
            return refreshed
    uid = generate_uid()
    started_at = _iso()
    order = materialize_order(quiz, uid)
    if not order:
        raise QuizError("This quiz has no questions yet")
    attempt = born_live(
        {
            "uid": uid,
            "quiz_uid": quiz["uid"],
            "user_uid": user["uid"],
            "status": "in_progress",
            "question_order": dump_json(order),
            "started_at": started_at,
            "expires_at": scoring.attempt_expires_at(
                started_at, int(quiz.get("time_limit_seconds") or 0)
            ),
            "completed_at": "",
            "answered_count": 0,
            "score_points": 0.0,
            "max_points": int(quiz.get("total_points") or 0),
            "score_percent": 0.0,
            "passed": 0,
        }
    )
    _attempts().insert(attempt)
    for position, question_uid in enumerate(order):
        _answers().insert(
            born_live(
                {
                    "uid": generate_uid(),
                    "attempt_uid": uid,
                    "question_uid": question_uid,
                    "quiz_uid": quiz["uid"],
                    "position": position,
                    "answer_text": "",
                    "option_uids": "[]",
                    "answered_at": None,
                    "is_correct": 0,
                    "awarded_points": 0.0,
                    "feedback": "",
                    "graded_by": "",
                    "confidence": 0.0,
                }
            )
        )
    winner = _earliest_in_progress(user["uid"], quiz["uid"])
    if winner and winner["uid"] != uid:
        stamp = _now_iso()
        soft_delete("quiz_answers", user["uid"], stamp=stamp, attempt_uid=uid)
        soft_delete("quiz_attempts", user["uid"], stamp=stamp, uid=uid)
        return winner
    return attempt


def materialize_order(quiz: dict, seed: str) -> list[str]:
    return scoring.question_order(
        question_uids(quiz["uid"]), bool(quiz.get("shuffle_questions")), seed
    )


async def answer(user: dict, quiz: dict, attempt: dict, question_uid: str, submission: dict):
    attempt = expire_if_due(attempt)
    if attempt.get("status") != "in_progress":
        raise QuizError("This attempt is no longer open")
    question = get_question(quiz["uid"], question_uid)
    if not question:
        raise QuizError("Question not found")
    row = answer_for_question(attempt["uid"], question_uid)
    if not row:
        raise QuizError("Question not found in this attempt")
    if row.get("answered_at"):
        raise QuizError("This question was already answered")
    options = list_options(question_uid)
    try:
        result = scoring.grade_answer(question, options, submission)
    except scoring.NeedsAiGrading:
        result = await asyncio.to_thread(
            grading.grade_free_text,
            (user.get("api_key") or "").strip(),
            question,
            submission.get("answer_text") or "",
        )
    points = scoring.awarded_points(int(question.get("points") or 1), result.score)
    rows = conditional_update_row(
        "quiz_answers",
        row["uid"],
        "answer_text = :answer_text, option_uids = :option_uids, answered_at = :answered_at, "
        "is_correct = :is_correct, awarded_points = :awarded_points, feedback = :feedback, "
        "graded_by = :graded_by, confidence = :confidence",
        "answered_at IS NULL OR answered_at = ''",
        {
            "answer_text": submission.get("answer_text") or "",
            "option_uids": dump_json(submission.get("option_uids") or []),
            "answered_at": _iso(),
            "is_correct": int(bool(result.is_correct)),
            "awarded_points": points,
            "feedback": result.feedback,
            "graded_by": result.graded_by,
            "confidence": float(result.confidence),
        },
    )
    if not rows:
        raise QuizError("This question was already answered")
    conditional_update_row(
        "quiz_attempts",
        attempt["uid"],
        "score_points = COALESCE(score_points, 0) + :points, "
        "answered_count = COALESCE(answered_count, 0) + 1",
        "status = 'in_progress'",
        {"points": points},
    )
    graded = _answers().find_one(uid=row["uid"])
    return graded, get_attempt(attempt["uid"]) or attempt, result


def finish(quiz: dict, attempt: dict) -> tuple[dict, bool]:
    attempt = expire_if_due(attempt)
    total = _answered_total(attempt["uid"])
    max_points = int(attempt.get("max_points") or 0)
    percent = scoring.score_percent(total, max_points)
    rows = conditional_update_row(
        "quiz_attempts",
        attempt["uid"],
        "status = 'completed', completed_at = :completed_at, score_points = :total, "
        "score_percent = :percent, passed = :passed",
        "status = 'in_progress'",
        {
            "completed_at": _iso(),
            "total": total,
            "percent": percent,
            "passed": int(scoring.passed(percent, int(quiz.get("pass_percent") or 0))),
        },
    )
    if rows:
        increment_attempt_count(quiz["uid"])
    return get_attempt(attempt["uid"]) or attempt, bool(rows)


def prune_attempts(older_than_iso: str) -> int:
    if "quiz_attempts" not in db.tables:
        return 0
    stale = list(
        _attempts().find(
            _attempts().table.columns.status.in_(["in_progress", "expired"]),
            _attempts().table.columns.created_at < older_than_iso,
        )
    )
    for attempt in stale:
        _answers().delete(attempt_uid=attempt["uid"])
        _attempts().delete(uid=attempt["uid"])
    return len(stale)


def attempt_order(attempt: dict) -> list[str]:
    return load_json(attempt.get("question_order"), [])


def _earliest_in_progress(user_uid: str, quiz_uid: str) -> dict | None:
    rows = list(
        _attempts().find(
            user_uid=user_uid,
            quiz_uid=quiz_uid,
            status="in_progress",
            deleted_at=None,
            order_by=["created_at", "id"],
            _limit=1,
        )
    )
    return rows[0] if rows else None


def _answered_total(attempt_uid: str) -> float:
    total = 0.0
    for row in db.query(
        "SELECT COALESCE(SUM(awarded_points), 0) AS total FROM quiz_answers "
        "WHERE attempt_uid = :attempt AND deleted_at IS NULL",
        attempt=attempt_uid,
    ):
        total = float(row.get("total") or 0.0)
        break
    return round(total, 4)
