# retoor <retoor@molodetz.nl>

from __future__ import annotations

from devplacepy.config import QUIZ_LIST_PER_PAGE
from devplacepy.database import (
    _now_iso,
    build_pagination,
    get_blocked_uids,
    interleave_by_author,
    text_search_clause,
)

from .common import (
    QuizError,
    _iso,
    _questions,
    _quizzes,
    conditional_update_row,
    get_quiz,
    guard_editable,
)
from .questions import list_questions, options_by_question

FILTERS = ("all", "todo", "done", "mine", "drafts")

SETTING_FIELDS = (
    "shuffle_questions",
    "shuffle_options",
    "reveal_answers",
    "allow_review",
    "time_limit_seconds",
    "pass_percent",
)


def quiz_fields(data) -> dict:
    return {
        "title": data.title.strip(),
        "description": data.description.strip(),
        "status": "draft",
        "published_at": "",
        "shuffle_questions": int(bool(data.shuffle_questions)),
        "shuffle_options": int(bool(data.shuffle_options)),
        "reveal_answers": int(bool(data.reveal_answers)),
        "allow_review": int(bool(data.allow_review)),
        "time_limit_seconds": max(0, int(data.time_limit_seconds or 0)),
        "pass_percent": max(0, min(100, int(data.pass_percent or 0))),
        "question_count": 0,
        "total_points": 0,
        "attempt_count": 0,
        "updated_at": _iso(),
    }


def settings_update(data) -> dict:
    fields = quiz_fields(data)
    return {key: fields[key] for key in ("title", "description", *SETTING_FIELDS)}


def recompute_quiz_totals(quiz_uid: str) -> dict:
    questions = list_questions(quiz_uid)
    totals = {
        "question_count": len(questions),
        "total_points": sum(max(1, int(q.get("points") or 1)) for q in questions),
    }
    _quizzes().update({"uid": quiz_uid, **totals, "updated_at": _now_iso()}, ["uid"])
    return totals


def validation_errors(quiz_uid: str) -> list[str]:
    questions = list_questions(quiz_uid)
    if not questions:
        return ["Add at least one question before publishing."]
    grouped = options_by_question(quiz_uid)
    errors: list[str] = []
    for question in questions:
        label = f"Question {int(question.get('position') or 0) + 1}"
        if not (question.get("prompt") or "").strip():
            errors.append(f"{label} has no prompt.")
        errors.extend(_kind_errors(label, question, grouped.get(question["uid"], [])))
    return errors


def publish_quiz(quiz_uid: str) -> dict:
    quiz = get_quiz(quiz_uid)
    if not quiz:
        raise QuizError("Quiz not found")
    if quiz.get("status") == "published":
        return quiz
    seen_version = int(quiz.get("content_version") or 0)
    errors = validation_errors(quiz_uid)
    if errors:
        raise QuizError("; ".join(errors))
    rows = conditional_update_row(
        "quizzes",
        quiz_uid,
        "status = 'published', published_at = :published_at",
        "status = 'draft' AND COALESCE(content_version, 0) = :seen_version",
        {"published_at": _iso(), "seen_version": seen_version},
    )
    published = get_quiz(quiz_uid) or quiz
    if not rows and published.get("status") != "published":
        raise QuizError("This quiz changed while publishing. Review it and try again")
    published["publish_won"] = bool(rows)
    return published


def edit_quiz(quiz_uid: str, update: dict) -> dict:
    guard_editable(quiz_uid)
    _quizzes().update({"uid": quiz_uid, **update, "updated_at": _now_iso()}, ["uid"])
    return get_quiz(quiz_uid) or {}


def increment_attempt_count(quiz_uid: str) -> None:
    conditional_update_row(
        "quizzes",
        quiz_uid,
        "attempt_count = COALESCE(attempt_count, 0) + 1",
        "1 = 1",
        {},
    )


def list_quizzes(
    *,
    viewer: dict | None = None,
    quiz_filter: str = "all",
    search: str = "",
    page: int = 1,
    per_page: int = QUIZ_LIST_PER_PAGE,
) -> tuple[list[dict], dict]:
    table = _quizzes()
    clauses, filters = _list_clauses(table, viewer, quiz_filter, search)
    if clauses is None:
        return [], build_pagination(1, 0, per_page)
    total = table.count(*clauses, deleted_at=None, **filters)
    pagination = build_pagination(page, total, per_page)
    offset = (pagination["page"] - 1) * pagination["per_page"]
    rows = list(
        table.find(
            *clauses,
            deleted_at=None,
            order_by=["-created_at"],
            _limit=pagination["per_page"],
            _offset=offset,
            **filters,
        )
    )
    return interleave_by_author(rows), pagination


def filter_counts(viewer: dict | None, search: str = "") -> dict[str, int]:
    table = _quizzes()
    counts: dict[str, int] = {}
    for name in FILTERS:
        clauses, filters = _list_clauses(table, viewer, name, search)
        counts[name] = 0 if clauses is None else table.count(
            *clauses, deleted_at=None, **filters
        )
    return counts


def _list_clauses(table, viewer, quiz_filter, search):
    if not table.exists:
        return None, {}
    viewer_uid = (viewer or {}).get("uid", "")
    columns = table.table.columns
    clauses = []
    filters: dict = {}
    if quiz_filter == "drafts":
        if not viewer_uid:
            return None, {}
        filters["status"] = "draft"
        filters["user_uid"] = viewer_uid
    else:
        filters["status"] = "published"
    if quiz_filter == "mine":
        if not viewer_uid:
            return None, {}
        filters["user_uid"] = viewer_uid
    if quiz_filter in ("todo", "done"):
        if not viewer_uid:
            return None, {}
        from .scoreboard import completed_quiz_uids

        completed = completed_quiz_uids(viewer_uid)
        if quiz_filter == "done":
            if not completed:
                return None, {}
            clauses.append(columns.uid.in_(completed))
        elif completed:
            clauses.append(columns.uid.notin_(completed))
    if viewer_uid and quiz_filter not in ("mine", "drafts"):
        blocked = get_blocked_uids(viewer_uid)
        if blocked:
            clauses.append(columns.user_uid.notin_(blocked))
    match = text_search_clause(
        table, search, ("title", "description"), author_field="user_uid"
    )
    if match is not None:
        clauses.append(match)
    return clauses, filters


def _kind_errors(label: str, question: dict, options: list[dict]) -> list[str]:
    kind = question.get("kind", "")
    correct = [option for option in options if int(option.get("is_correct") or 0)]
    if kind == "single_choice":
        if len(options) < 2:
            return [f"{label} needs at least two options."]
        if len(correct) != 1:
            return [f"{label} needs exactly one correct option."]
    elif kind == "multiple_choice":
        if len(options) < 2:
            return [f"{label} needs at least two options."]
        if not correct:
            return [f"{label} needs at least one correct option."]
    elif kind == "free_text":
        if not (question.get("expected_answer") or "").strip() and not (
            question.get("grading_criteria") or ""
        ).strip():
            return [f"{label} needs a reference answer or grading criteria."]
    elif kind == "fill_blank":
        if not options:
            return [f"{label} needs at least one blank."]
        if any(not (option.get("match_value") or "").strip() for option in options):
            return [f"{label} has a blank with no accepted answer."]
    elif kind == "ordering":
        if len(options) < 2:
            return [f"{label} needs at least two items to order."]
    elif kind == "matching":
        if len(options) < 2:
            return [f"{label} needs at least two pairs."]
        if any(not (option.get("match_value") or "").strip() for option in options):
            return [f"{label} has a pair with no right-hand value."]
    return []


def comment_counts(quiz_uids: list[str]) -> dict[str, int]:
    from devplacepy.database import _in_clause, db

    uids = [uid for uid in (quiz_uids or []) if uid]
    if not uids or "comments" not in db.tables:
        return {}
    placeholders, params = _in_clause(uids)
    rows = db.query(
        f"SELECT target_uid, COUNT(*) AS total FROM comments "
        f"WHERE target_type = 'quiz' AND deleted_at IS NULL "
        f"AND target_uid IN ({placeholders}) GROUP BY target_uid",
        **params,
    )
    return {row["target_uid"]: int(row["total"] or 0) for row in rows}


def question_uids(quiz_uid: str) -> list[str]:
    return [
        row["uid"]
        for row in _questions().find(
            quiz_uid=quiz_uid, deleted_at=None, order_by=["position", "id"]
        )
    ]
