"""
renderer.py
Renderer pour générer le HTML des quiz
"""

import json
import base64
import re
import random
from typing import Any, Dict, Optional

from .database import QuizDatabase
from .config import normalize_compare_answers


def _coerce_float(v: Any) -> Optional[float]:
    try:
        if v is None:
            return None
        return float(v)
    except Exception:
        return None

def _parse_bracket_list_key(key: str):
    """Parse '[Paris, Londres]' → ['Paris', 'Londres'], ou None si pas bracket."""
    s = str(key).strip()
    if not (s.startswith('[') and s.endswith(']')):
        return None
    inner = s[1:-1].strip()
    if not inner:
        return []
    return [x.strip() for x in inner.split(',') if x.strip()]


def _flatten_score_block(cfg: dict) -> dict:
    """
    Aplatit le sous-dict 'score:' dans cfg si présent.
    Gère les deux formes : avant et après normalisation i18n.
    { 'score': { 'mode'/'scoring_mode': ..., 'min'/'min_score': ..., 'errors'/'errors_mode': {...} } }
    → { 'scoring_mode': ..., 'min': ..., 'errors_mode': {...} }
    Priorité : clés déjà présentes au niveau cfg > clés issues de score:
    """
    if not isinstance(cfg, dict):
        return cfg
    score_block = cfg.get('score')
    if not isinstance(score_block, dict):
        return cfg
    out = dict(cfg)
    del out['score']
    # mode / scoring_mode → scoring_mode
    if 'mode' in score_block and 'scoring_mode' not in out:
        out['scoring_mode'] = score_block['mode']
    if 'scoring_mode' in score_block and 'scoring_mode' not in out:
        out['scoring_mode'] = score_block['scoring_mode']
    # min / min_score → min
    if 'min' in score_block and 'min' not in out:
        out['min'] = score_block['min']
    if 'min_score' in score_block and 'min' not in out:
        out['min'] = score_block['min_score']
    # errors / errors_mode → errors_mode
    if 'errors' in score_block and 'errors_mode' not in out:
        out['errors_mode'] = score_block['errors']
    if 'errors_mode' in score_block and 'errors_mode' not in out:
        out['errors_mode'] = score_block['errors_mode']
    return out


class QuizRenderer:
    def __init__(self, config: dict, page_uuid: str, teacher_code_hash: str, i18n, language: str = 'en'):
        self.config = config
        self.page_uuid = page_uuid
        self.teacher_code_hash = teacher_code_hash
        self.i18n = i18n
        self.language = language
        self.db = None

        try:
            from pathlib import Path
            db_path = Path.cwd() / 'docs' / 'quizzes.db'
            if db_path.exists():
                self.db = QuizDatabase(str(db_path))
        except Exception as e:
            print(f"Warning: Could not connect to database: {e}")

    # ─────────────────────────────────────────────────────────────────────────
    # Main entry point
    # ─────────────────────────────────────────────────────────────────────────

    def render_quiz(self, quiz: dict) -> str:
        quiz_id = f"mkq-{quiz['number']}"
        quiz_type = quiz['type']
        title = quiz['title']

        questions_html = ''
        for q in quiz.get('questions', []):
            questions_html += self.render_question(quiz, q, quiz_id)

        preamble_html = self._wrap_rich_content(quiz.get('preamble', ''))

        correction_data = self._prepare_correction_data(quiz, quiz_id)
        encrypted_corrections = self._encrypt_corrections(correction_data)

        quiz_type_config = self.config.get(quiz_type, {})
        _quiz_fm = quiz.get('config') or {}
        _sc = _quiz_fm.get('show_correction')
        show_correction = _sc if _sc is not None else quiz_type_config.get('show_correction', 'yes')
        granularity = self.config.get('correction_granularity', 'exercise')

        randomize_answers   = bool(quiz_type_config.get('randomize_answers', False))
        randomize_questions = bool(quiz_type_config.get('randomize_questions', False))

        icon_locked   = self.config.get('correction_locked_icon',   '❌')
        icon_unlock   = self.config.get('correction_unlock_icon',   '🔐')
        icon_unlocked = self.config.get('correction_unlocked_icon', '✅')

        if show_correction == 'no':
            unlock_html = f"""
<span class="mkq-lock-indicator locked-permanent"
      title="{self.i18n.t('ui.lock_tooltip_permanent', 'Corrections disabled')}"
      data-lock-state="locked-permanent">
    {icon_locked}
</span>"""
        elif show_correction == 'on_demand':
            if granularity == 'page':
                if quiz['number'] == 1:
                    unlock_html = f"""
<button class="mkq-unlock-btn locked"
        onclick="showUnlockModal('page')"
        title="{self.i18n.t('ui.lock_tooltip_page', 'Click to unlock all quizzes on this page')}"
        data-lock-state="locked">
    {icon_unlock}
</button>"""
                else:
                    unlock_html = f"""
<span class="mkq-lock-indicator locked-permanent"
      title="{self.i18n.t('ui.lock_tooltip_page_locked', 'Unlock all quizzes using the button on the first quiz')}"
      data-lock-state="locked-permanent">
    {icon_locked}
</span>"""
            elif granularity == 'question':
                unlock_html = f"""
<button class="mkq-unlock-btn locked"
        onclick="showUnlockModal('exercise', '{quiz_id}')"
        title="{self.i18n.t('ui.lock_tooltip_quiz_questions', 'Click to unlock all questions in this quiz')}"
        data-lock-state="locked">
    {icon_unlock}
</button>"""
            else:
                unlock_html = f"""
<button class="mkq-unlock-btn locked"
        onclick="showUnlockModal('exercise', '{quiz_id}')"
        title="{self.i18n.t('ui.lock_tooltip_locked', 'Click to unlock this quiz')}"
        data-lock-state="locked">
    {icon_unlock}
</button>"""
        else:
            unlock_html = f"""
<span class="mkq-lock-indicator unlocked"
      title="{self.i18n.t('ui.lock_tooltip_unlocked', 'Corrections always visible')}"
      data-lock-state="unlocked">
    {icon_unlocked}
</span>"""

        html = f"""
<div class="mkdocs-superquiz admonition {quiz_type}"
     data-quiz-id="{quiz_id}"
     data-quiz-type="{quiz_type}"
     data-page-uuid="{self.page_uuid}"
     data-randomize-answers="{str(randomize_answers).lower()}"
     data-randomize-questions="{str(randomize_questions).lower()}"
     data-encrypted="{encrypted_corrections}">

    <p class="admonition-title">
        {title}
        {unlock_html}
    </p>

    {preamble_html}

    {questions_html}

    <div class="quiz-actions">
        <button class="mkq-validate-btn" onclick="validateQuiz('{quiz_id}')">
            {self.i18n.t('ui.validate_button', 'Validate')}
        </button>
        <div class="mkq-score" id="score-{quiz_id}" style="display:none;"></div>
    </div>
</div>
"""
        return html

    # ─────────────────────────────────────────────────────────────────────────
    # Render one question
    # ─────────────────────────────────────────────────────────────────────────

    def render_question(self, quiz: dict, question: dict, quiz_id: str) -> str:
        quiz_type = quiz.get('type')
        qid = question.get('number', 0)
        is_meta = bool(question.get('is_meta')) or (qid == 0)
        if is_meta:
            return self._render_meta_preamble(question)
        if quiz_type in ('mcquiz', 'scquiz'):
            return self._render_choice_question(quiz, question, quiz_id)
        if quiz_type == 'fillblanks':
            return self._render_fillblanks_question(quiz, question, quiz_id)
        if quiz_type == 'scdropdown':
            return self._render_scdropdown_question(quiz, question, quiz_id)
        return ''

    # ─────────────────────────────────────────────────────────────────────────
    # Helpers
    # ─────────────────────────────────────────────────────────────────────────

    def _wrap_rich_content(self, rich_content: str) -> str:
        if not rich_content or not rich_content.strip():
            return ''
        return f'<div class="mkq-rich-content">{rich_content}</div>'

    def _render_meta_preamble(self, question: dict) -> str:
        rich_html = self._wrap_rich_content(question.get('rich_content', ''))
        if not rich_html:
            return ''
        return f'<div class="mkq-preamble mkq-meta">{rich_html}</div>'

    # ─────────────────────────────────────────────────────────────────────────
    # Choice question (mcquiz / scquiz)
    # ─────────────────────────────────────────────────────────────────────────


    def _render_choice_question(self, quiz: dict, question: dict, quiz_id: str) -> str:
        question_id = question.get('number', 0)
        input_type = 'checkbox' if quiz['type'] == 'mcquiz' else 'radio'
        raw_or_html = question.get('text_html') or question.get('text', '')
        question_text = self._render_math(raw_or_html)
        rich_html = self._wrap_rich_content(question.get('rich_content', ''))

        granularity = self.config.get('correction_granularity', 'all')
        question_unlock_html = ''
        if granularity == 'question':
            page = self.db.get_page_by_uuid(self.page_uuid) if self.db else None
            corrections_locked = page.get('corrections_locked', False) if page else False
            if corrections_locked:
                question_unlock_html = f"""
<button class="mkq-unlock-btn locked"
        onclick="showUnlockModal('question', '{quiz_id}', '{question_id}')"
        title="{self.i18n.t('ui.lock_tooltip_locked', 'Click to unlock corrections')}"
        data-lock-state="locked">
    🔒
</button>"""

        qcfg_for_rand = question.get('config') or {}
        q_randomize_answers = qcfg_for_rand.get('randomize_answers', None)

        # ✅ Normaliser : 'yes'/True → True, 'no'/False → False, None → absent
        q_rand_attr = ''
        if q_randomize_answers in (True, 'yes', 'true', 1):
            q_rand_attr = ' data-randomize-answers="true"'
        elif q_randomize_answers in (False, 'no', 'false', 0):
            q_rand_attr = ' data-randomize-answers="false"'

        answers_html = ''
        for idx, answer in enumerate(question.get('answers', [])):
            answer_id = f"{quiz_id}-q{question_id}-a{idx}"
            answer_text = self._render_math(answer['text'])
            answers_html += f"""
<label class="mkq-answer" data-answer-text="{answer['text'].replace('"', '&quot;')}">
    <input type="{input_type}" id="{answer_id}" name="{quiz_id}-q{question_id}" value="{idx}" data-answer-index="{idx}">
    <span class="mkq-answer-text">{answer_text}</span>
    <span class="mkq-answer-feedback" style="display:none;"></span>
</label>"""

        if not rich_html or not rich_html.strip():
            return f"""
<div class="mkq-question" data-question-id="{question_id}"{q_rand_attr}>
    <div class="mkq-question-text">
        <span class="mkq-number">{question_id}.</span> {question_text} {question_unlock_html}
    </div>
    <div class="mkq-answers">{answers_html}</div>
</div>"""

        return f"""
<div class="mkq-question" data-question-id="{question_id}"{q_rand_attr}>
    <div class="mkq-question-text mkq-question-number-only">
        <span class="mkq-number">{question_id}.</span> {question_unlock_html}
    </div>
    {rich_html}
    <div class="mkq-question-text mkq-question-body">{question_text}</div>
    <div class="mkq-answers">{answers_html}</div>
</div>"""



    # ─────────────────────────────────────────────────────────────────────────
    # Fillblanks question
    # ─────────────────────────────────────────────────────────────────────────

    def _render_fillblanks_question(self, quiz: dict, question: dict, quiz_id: str) -> str:
        question_id = question.get('number', 0)
        question_text = question.get('text', '')
        rich_html = self._wrap_rich_content(question.get('rich_content', ''))

        blank_pattern = r'\[([^\]]+)\](\{[^}]*\})?'

        def make_replacer(qid, qnum):
            counters = [0]
            def replacer(match):
                input_id = f"{qid}-q{qnum}-blank{counters[0]}"
                h = f'<input type="text" class="mkq-blank-input" id="{input_id}" data-blank-index="{counters[0]}">'
                counters[0] += 1
                return h
            return replacer

        blank_html = re.sub(blank_pattern, make_replacer(quiz_id, question_id), question_text)
        blank_html = self._render_math(blank_html)

        if not rich_html or not rich_html.strip():
            return f"""
<div class="mkq-question" data-question-id="{question_id}">
    <div class="mkq-question-text">
        <span class="mkq-number">{question_id}.</span> {blank_html}
    </div>
</div>"""

        return f"""
<div class="mkq-question" data-question-id="{question_id}">
    <div class="mkq-question-text mkq-question-number-only">
        <span class="mkq-number">{question_id}.</span>
    </div>
    {rich_html}
    <div class="mkq-question-text mkq-question-body">{blank_html}</div>
</div>"""

    # ─────────────────────────────────────────────────────────────────────────
    # Scdropdown question
    # ─────────────────────────────────────────────────────────────────────────

#     def _render_scdropdown_question(self, quiz: dict, question: dict, quiz_id: str) -> str:
#         question_id = question.get('number', 0)
#         question_text = question.get('text', '')
#         rich_html = self._wrap_rich_content(question.get('rich_content', ''))

#         scdropdowns = question.get('scdropdowns')
#         if not scdropdowns:
#             dd = question.get('scdropdown')
#             scdropdowns = [dd] if dd else []

#         rendered_text = question_text
#         for dd_index, scdropdown_data in enumerate(scdropdowns):
#             if not scdropdown_data:
#                 continue
#             scdropdown_id = f"{quiz_id}-q{question_id}-scdropdown-{dd_index}"
#             scdropdown_html = (
#                 f'<div class="mkq-custom-scdropdown" id="{scdropdown_id}"'
#                 f' data-quiz-id="{quiz_id}" data-question-id="{question_id}">'
#                 '<button type="button" class="mkq-custom-scdropdown-btn"'
#                 ' onclick="window.toggleScdropdownQuiz(this); return false;">'
#                 '<span class="mkq-custom-scdropdown-value">--</span>'
#                 '<span class="mkq-custom-scdropdown-arrow">▾</span>'
#                 '</button>'
#                 '<div class="mkq-custom-scdropdown-options" style="display: none;">'
#             )
#             for idx, option in enumerate(scdropdown_data.get('options', [])):
#                 selected = 'selected' if idx == 0 else ''
#                 escaped_option = option.replace('"', '&quot;')
#                 scdropdown_html += (
#                     f'<div class="mkq-custom-scdropdown-option {selected}"'
#                     f' data-value="{idx}"'
#                     f' data-option-text="{escaped_option}"'
#                     f' onclick="window.selectScdropdownQuizOption(this); return false;">'
#                     f'{option}</div>'
#                 )
#             scdropdown_html += '</div></div>'
#             placeholder = f'___DROPDOWN_{dd_index}___'
#             if placeholder in rendered_text:
#                 rendered_text = rendered_text.replace(placeholder, scdropdown_html, 1)
#             else:
#                 rendered_text = rendered_text.replace('___DROPDOWN___', scdropdown_html, 1)

#         rendered_text = self._render_math(rendered_text)

#         qcfg_for_rand = question.get('config') or {}
#         q_randomize_answers = qcfg_for_rand.get('randomize_answers', None)
#         q_rand_attr = ''
#         if q_randomize_answers is True:
#             q_rand_attr = ' data-randomize-answers="true"'
#         elif q_randomize_answers is False:
#             q_rand_attr = ' data-randomize-answers="false"'

#         if not rich_html or not rich_html.strip():
#             return f"""
# <div class="mkq-question" data-question-id="{question_id}"{q_rand_attr}>
#     <div class="mkq-question-text">
#         <span class="mkq-number">{question_id}.</span> {rendered_text}
#     </div>
# </div>"""

#         return f"""
# <div class="mkq-question" data-question-id="{question_id}"{q_rand_attr}>
#     <div class="mkq-question-text mkq-question-number-only">
#         <span class="mkq-number">{question_id}.</span>
#     </div>
#     {rich_html}
#     <div class="mkq-question-text mkq-question-body">{rendered_text}</div>
# </div>"""

    def _render_scdropdown_question(self, quiz: dict, question: dict, quiz_id: str) -> str:
        question_id = question.get('number', 0)
        question_text = question.get('text', '')
        rich_html = self._wrap_rich_content(question.get('rich_content', ''))

        scdropdowns = question.get('scdropdowns')
        if not scdropdowns:
            dd = question.get('scdropdown')
            scdropdowns = [dd] if dd else []

        rendered_text = question_text
        for dd_index, scdropdown_data in enumerate(scdropdowns):
            if not scdropdown_data:
                continue
            scdropdown_id = f"{quiz_id}-q{question_id}-scdropdown-{dd_index}"
            scdropdown_html = (
                f'<div class="mkq-custom-scdropdown" id="{scdropdown_id}"'
                f' data-quiz-id="{quiz_id}" data-question-id="{question_id}">'
                '<button type="button" class="mkq-custom-scdropdown-btn"'
                ' onclick="window.toggleScdropdownQuiz(this); return false;">'
                '<span class="mkq-custom-scdropdown-value">--</span>'
                '<span class="mkq-custom-scdropdown-arrow">▾</span>'
                '</button>'
                '<div class="mkq-custom-scdropdown-options" style="display: none;">'
            )
            for idx, option in enumerate(scdropdown_data.get('options', [])):
                selected = 'selected' if idx == 0 else ''
                escaped_option = option.replace('"', '&quot;')
                scdropdown_html += (
                    f'<div class="mkq-custom-scdropdown-option {selected}"'
                    f' data-value="{idx}"'
                    f' data-option-text="{escaped_option}"'
                    f' onclick="window.selectScdropdownQuizOption(this); return false;">'
                    f'{option}</div>'
                )
            scdropdown_html += '</div></div>'
            placeholder = f'___DROPDOWN_{dd_index}___'
            if placeholder in rendered_text:
                rendered_text = rendered_text.replace(placeholder, scdropdown_html, 1)
            else:
                rendered_text = rendered_text.replace('___DROPDOWN___', scdropdown_html, 1)

        rendered_text = self._render_math(rendered_text)

        qcfg_for_rand = question.get('config') or {}
        q_randomize_answers = qcfg_for_rand.get('randomize_answers', None)
        q_rand_attr = ''
        if q_randomize_answers in (True, 'yes', 'true', 1):
            q_rand_attr = ' data-randomize-answers="true"'
        elif q_randomize_answers in (False, 'no', 'false', 0):
            q_rand_attr = ' data-randomize-answers="false"'

        if not rich_html or not rich_html.strip():
            return f"""
<div class="mkq-question" data-question-id="{question_id}"{q_rand_attr}>
    <div class="mkq-question-text">
        <span class="mkq-number">{question_id}.</span> {rendered_text}
    </div>
</div>"""

        return f"""
<div class="mkq-question" data-question-id="{question_id}"{q_rand_attr}>
    <div class="mkq-question-text mkq-question-number-only">
        <span class="mkq-number">{question_id}.</span>
    </div>
    {rich_html}
    <div class="mkq-question-text mkq-question-body">{rendered_text}</div>
</div>"""


    # ─────────────────────────────────────────────────────────────────────────
    # Math rendering
    # ─────────────────────────────────────────────────────────────────────────

    def _render_math(self, text: str) -> str:
        def protect_html(match):
            return match.group(0).replace('$', '&#36;')
        text = re.sub(r'<[^>]+>', protect_html, text)
        text = re.sub(r'\$\$(.*?)\$\$', lambda m: f'\\[{m.group(1)}\\]', text, flags=re.DOTALL)
        text = re.sub(r'(?<!\$)\$(?!\$)(.*?)(?<!\$)\$(?!\$)', lambda m: f'\\({m.group(1)}\\)', text)
        text = text.replace('&#36;', '$')
        return text

    # ─────────────────────────────────────────────────────────────────────────
    # Correction data helpers
    # ─────────────────────────────────────────────────────────────────────────

    def _resolve_question_points(self, quiz_type: str, question: dict) -> float:
        qcfg = question.get('config') or {}
        if isinstance(qcfg, dict) and 'points' in qcfg and qcfg['points'] is not None:
            try:
                return float(qcfg['points'])
            except Exception:
                pass
        if question.get('points') is not None:
            try:
                return float(question['points'])
            except Exception:
                pass
        qt_cfg = self.config.get(quiz_type, {}) if isinstance(self.config, dict) else {}
        try:
            return float(qt_cfg.get('default_points', 1))
        except Exception:
            return 1.0


    def _get_compare_answers_for_quiz(self, quiz: dict) -> dict:
        import copy
        gcfg = self.config.get('fillblanks', {}) if isinstance(self.config, dict) else {}
        global_raw = gcfg.get('compare_answers')
        base = normalize_compare_answers(global_raw if isinstance(global_raw, dict) else {})
        quiz_cfg = quiz.get('config') or {}
        # ✅ Lire 'compare_answers' OU 'match' (alias front-matter)
        quiz_raw = quiz_cfg.get('compare_answers') or quiz_cfg.get('match') \
            if isinstance(quiz_cfg, dict) else None
        if not quiz_raw or not isinstance(quiz_raw, dict):
            return base
        # ✅ Passer par normalize_compare_answers pour normaliser l'équivalence
        quiz_norm = normalize_compare_answers(quiz_raw)
        merged = copy.deepcopy(base)
        merged['ignore'].update(quiz_norm['ignore'])
        merged['equivalence'].update(quiz_norm['equivalence'])
        return merged


    def _get_compare_answers_for_question(self, question: dict, quiz_compare: dict) -> Optional[dict]:
        import copy
        qcfg = question.get('config') or {}
        if not isinstance(qcfg, dict):
            return None
        # raw = qcfg.get('compare_answers')
        raw = qcfg.get('compare_answers') or qcfg.get('match')
        
        if not raw or not isinstance(raw, dict):
            return None
        merged = copy.deepcopy(quiz_compare)
        raw_norm = normalize_compare_answers(raw)
        merged['ignore'].update(raw_norm['ignore'])
        merged['equivalence'].update(raw_norm['equivalence'])
        return merged

    def _normalize_errors_mode_choice(self, question: dict, quiz_cfg: dict, qcfg: dict = None) -> dict:
        RESERVED_KEYS = ('default_false', 'default', 'min', 'max')

        if qcfg is None:
            raw_qcfg = question.get('config') or {}
            qcfg = _flatten_score_block(raw_qcfg) if isinstance(raw_qcfg, dict) else {}

        def _extract_rules(em_dict: dict) -> list:
            rules = []
            if not isinstance(em_dict, dict):
                return rules
            for k, v in em_dict.items():
                if k in RESERVED_KEYS:
                    continue
                score_val = _coerce_float(v)
                if score_val is not None:
                    rules.append({'key': str(k), 'score': score_val})
            return rules

        result = {'rules': [], 'default_false': None}

        # Priorité 3 : quiz_cfg.errors_mode
        quiz_em = quiz_cfg.get('errors_mode') if isinstance(quiz_cfg, dict) else None
        if isinstance(quiz_em, dict):
            result['rules'] = _extract_rules(quiz_em)
            df = quiz_em.get('default_false') if 'default_false' in quiz_em else quiz_em.get('default')
            result['default_false'] = _coerce_float(df)

        # Priorité 2 : qcfg.errors_mode (answers: --- block, aplati)
        qcfg_em = qcfg.get('errors_mode') if isinstance(qcfg, dict) else None
        if isinstance(qcfg_em, dict):
            result['rules'] = _extract_rules(qcfg_em)
            df = qcfg_em.get('default_false') if 'default_false' in qcfg_em else qcfg_em.get('default')
            result['default_false'] = _coerce_float(df)

        # Priorité 1 : question.errors_mode (inline {})
        q_em = question.get('errors_mode')
        if isinstance(q_em, dict):
            result['rules'] = _extract_rules(q_em)
            df = q_em.get('default_false') if 'default_false' in q_em else q_em.get('default')
            result['default_false'] = _coerce_float(df)

        return result

    def _normalize_errors_mode_fillblanks(self, question: dict) -> dict:
        out = {'blanks': []}
        qcfg = question.get('config') or {}
        cfg_em = qcfg.get('errors_mode') if isinstance(qcfg, dict) else None
        cfg_em = cfg_em if isinstance(cfg_em, dict) else {}

        for i, blank in enumerate(question.get('blanks', [])):
            em = blank.get('errors_mode')
            if not isinstance(em, dict):
                # ✅ Chercher blank0/blank1 OU 0/1 (clés numériques)
                em = cfg_em.get(f'blank{i}')
            if not isinstance(em, dict):
                em = cfg_em.get(i)          # clé int (parsée comme int par _coerce_scalar)
            if not isinstance(em, dict):
                em = cfg_em.get(str(i))     # clé string '0', '1'

            obj = {'default_false': None, 'rules': []}
            if isinstance(em, dict):
                if 'default_false' in em:
                    obj['default_false'] = _coerce_float(em.get('default_false'))
                elif 'default' in em:
                    obj['default_false'] = _coerce_float(em.get('default'))
                for k, v in em.items():
                    if k in ('default_false', 'default'):
                        continue
                    sc = _coerce_float(v)
                    if sc is None:
                        continue
                    obj['rules'].append({'answer': str(k), 'score': sc})
            out['blanks'].append(obj)
        return out


    def _normalize_errors_mode_scdropdown(self, question: dict) -> dict:
        out = {'scdropdowns': []}
        scdropdowns = question.get('scdropdowns') or []
        if not scdropdowns:
            dd = question.get('scdropdown')
            scdropdowns = [dd] if dd else []

        qcfg = question.get('config') or {}
        cfg_em = qcfg.get('errors_mode') if isinstance(qcfg, dict) else None
        cfg_em = cfg_em if isinstance(cfg_em, dict) else {}

        for i, dd in enumerate(scdropdowns):
            em = dd.get('errors_mode') if isinstance(dd, dict) else None
            if not isinstance(em, dict):
                em = cfg_em.get(f'scdropdown{i}')
            if not isinstance(em, dict):
                em = cfg_em.get(i)        # ✅ clé int
            if not isinstance(em, dict):
                em = cfg_em.get(str(i))   # ✅ clé string '0', '1'

            obj = {'default_false': None, 'rules': []}
            if isinstance(em, dict):
                if 'default_false' in em:
                    obj['default_false'] = _coerce_float(em.get('default_false'))
                elif 'default' in em:
                    obj['default_false'] = _coerce_float(em.get('default'))
                for k, v in em.items():
                    if k in ('default_false', 'default'):
                        continue
                    sc = _coerce_float(v)
                    if sc is None:
                        continue
                    obj['rules'].append({'option': str(k), 'score': sc})
            out['scdropdowns'].append(obj)
        return out


    def _prepare_correction_data(self, quiz: dict, quiz_id: str) -> dict:
        quiz_type = quiz.get('type')
        qt_cfg = self.config.get(quiz_type, {}) if isinstance(self.config, dict) else {}

        quiz_cfg = quiz.get('config') or {}
        if not isinstance(quiz_cfg, dict):
            quiz_cfg = {}

        # ✅ Aplatir le bloc score: du quiz
        quiz_cfg = _flatten_score_block(quiz_cfg)

        scoring_mode = quiz_cfg.get('scoring_mode') or qt_cfg.get('scoring_mode') or 'all_or_nothing'
        default_false = qt_cfg.get('default_false', qt_cfg.get('default', 0))
        try:
            default_false = float(default_false)
        except Exception:
            default_false = 0.0

        _SENTINEL = object()

        # ✅ Priorité 1 : errors_mode.min au niveau quiz_cfg (score: errors: min: none)
        def _resolve_min(raw):
            if raw is None:
                return None
            if isinstance(raw, str) and raw.strip().lower() == 'none':
                return None
            return _coerce_float(raw)

        quiz_em = quiz_cfg.get('errors_mode') if isinstance(quiz_cfg, dict) else None
        if isinstance(quiz_em, dict) and 'min' in quiz_em:
            min_score = _resolve_min(quiz_em['min'])
        else:
            # Priorité 2 : qt_cfg.min_score ou config globale
            _ms = qt_cfg.get('min_score', _SENTINEL)
            if _ms is _SENTINEL:
                _ms = self.config.get('min_score', _SENTINEL)
            min_score = None if _ms is _SENTINEL else _ms

        clamp_max = bool(self.config.get('clamp_max_score', True))

        # ✅ save_answers : lire depuis quiz_cfg, défaut True
        _save = quiz_cfg.get('save_answers', True)
        if isinstance(_save, str):
            _save = _save.lower() not in ('no', 'false', '0')
        save_answers = bool(_save)

        quiz_compare = self._get_compare_answers_for_quiz(quiz) if quiz_type == 'fillblanks' else None

        correction_data: Dict[str, Any] = {
            'quiz_id':        quiz['number'],
            'quiz_dom_id':    quiz_id,
            'quiz_type':      quiz_type,
            'scoring_mode':   scoring_mode,
            'default_false':  default_false,
            'min_score':      min_score,
            'clamp_max_score': clamp_max,
            'save_answers':   save_answers,
            'compare_answers': quiz_compare,
            'questions':      []
        }

        def _resolve_min(raw):
            if raw is None:
                return None
            if isinstance(raw, str) and raw.strip().lower() == 'none':
                return None
            return _coerce_float(raw)

        for question in quiz.get('questions', []):
            if question.get('is_meta') or question.get('number') == 0:
                continue

            points = self._resolve_question_points(quiz_type, question)

            raw_qcfg = question.get('config') or {}
            if not isinstance(raw_qcfg, dict):
                raw_qcfg = {}
            qcfg = _flatten_score_block(raw_qcfg)

            # DEBUG — à supprimer après validation
            import pprint
            print(f"DEBUG raw_qcfg q {question['number']} = {pprint.pformat(qcfg)}")

            q_default_false = None
            if 'default_false' in qcfg:
                q_default_false = _coerce_float(qcfg.get('default_false'))
            elif 'default' in qcfg:
                q_default_false = _coerce_float(qcfg.get('default'))

            q_min_score = min_score

            quiz_em = quiz_cfg.get('errors_mode') if isinstance(quiz_cfg, dict) else None
            if isinstance(quiz_em, dict) and 'min' in quiz_em:
                q_min_score = _resolve_min(quiz_em['min'])

            qcfg_em = qcfg.get('errors_mode') if isinstance(qcfg, dict) else None
            if isinstance(qcfg_em, dict) and 'min' in qcfg_em:
                q_min_score = _resolve_min(qcfg_em['min'])

            if 'min' in qcfg:
                q_min_score = _resolve_min(qcfg.get('min'))
            elif 'min_score' in qcfg:
                q_min_score = _resolve_min(qcfg.get('min_score'))

            question_data: Dict[str, Any] = {
                'number':        question['number'],
                'text':          question.get('text', ''),
                'points':        points,
                'scoring_mode':  qcfg.get('scoring_mode') or scoring_mode,
                'default_false': default_false,
                'question_default_false': q_default_false,
                'min_score':     q_min_score,
                'clamp_max_score': clamp_max,
            }

            if quiz_type in ['mcquiz', 'scquiz']:
                question_data['answers_text'] = [a.get('text', '') for a in question.get('answers', [])]
                question_data['correct_answers'] = [
                    idx for idx, answer in enumerate(question.get('answers', []))
                    if answer.get('is_correct')
                ]
                question_data['correct_answers_text'] = [
                    answer.get('text', '') for answer in question.get('answers', [])
                    if answer.get('is_correct')
                ]
                question_data['errors_mode'] = self._normalize_errors_mode_choice(question, quiz_cfg, qcfg)

            elif quiz_type == 'fillblanks':
                question_data['blanks'] = question.get('blanks', [])
                question_data['errors_mode'] = self._normalize_errors_mode_fillblanks(question)
                q_compare = self._get_compare_answers_for_question(question, quiz_compare)
                if q_compare is not None:
                    question_data['compare_answers'] = q_compare

            elif quiz_type == 'scdropdown':
                scdropdowns = question.get('scdropdowns')
                question_data['scdropdowns'] = scdropdowns if scdropdowns else [question.get('scdropdown', {})]
                question_data['errors_mode'] = self._normalize_errors_mode_scdropdown(question)

            correction_data['questions'].append(question_data)

        return correction_data


    def _encrypt_corrections(self, correction_data: dict) -> str:
        key = self.config.get('encryption_key', 'default-key-change-me')
        json_str = json.dumps(correction_data)
        key_bytes = key.encode('utf-8')
        data_bytes = json_str.encode('utf-8')
        encrypted = bytearray(byte ^ key_bytes[i % len(key_bytes)] for i, byte in enumerate(data_bytes))
        return base64.b64encode(bytes(encrypted)).decode('utf-8')
