# """
# 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


# 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']

#         # ✅ Randomization is now handled entirely client-side (JS).
#         # _apply_randomization() has been removed.
#         # The renderer only passes the flags to the HTML via data attributes.

#         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, {})
#         show_correction = quiz_type_config.get('show_correction', 'yes')
#         granularity = self.config.get('correction_granularity', 'exercise')

#         # ✅ Randomization flags — read by JS at page load
#         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 == 'gapfill':
#             return self._render_gapfill_question(quiz, question, quiz_id)
#         if quiz_type == 'dropdown':
#             return self._render_dropdown_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>"""

#         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'])
#             # ✅ data-answer-text stocke le texte de la réponse pour la comparaison
#             #    post-shuffle (le scoring JS compare par texte, pas par index DOM)
#             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}">
#     <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}">
#     <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>"""

#     # ─────────────────────────────────────────────────────────────────────────
#     # Gapfill question
#     # ─────────────────────────────────────────────────────────────────────────

#     def _render_gapfill_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', ''))

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

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

#         gap_html = re.sub(gap_pattern, make_replacer(quiz_id, question_id), question_text)
#         gap_html = self._render_math(gap_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> {gap_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">{gap_html}</div>
# </div>"""

#     # ─────────────────────────────────────────────────────────────────────────
#     # Dropdown question
#     # ─────────────────────────────────────────────────────────────────────────

#     def _render_dropdown_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', ''))

#         dropdowns = question.get('dropdowns')
#         if not dropdowns:
#             dd = question.get('dropdown')
#             dropdowns = [dd] if dd else []

#         rendered_text = question_text
#         for dd_index, dropdown_data in enumerate(dropdowns):
#             if not dropdown_data:
#                 continue
#             dropdown_id = f"{quiz_id}-q{question_id}-dropdown-{dd_index}"
#             dropdown_html = (
#                 f'<div class="mkq-custom-dropdown" id="{dropdown_id}"'
#                 f' data-quiz-id="{quiz_id}" data-question-id="{question_id}">'
#                 '<button type="button" class="mkq-custom-dropdown-btn"'
#                 ' onclick="window.toggleDropdownQuiz(this); return false;">'
#                 '<span class="mkq-custom-dropdown-value">--</span>'
#                 '<span class="mkq-custom-dropdown-arrow">▾</span>'
#                 '</button>'
#                 '<div class="mkq-custom-dropdown-options" style="display: none;">'
#             )
#             for idx, option in enumerate(dropdown_data.get('options', [])):
#                 selected = 'selected' if idx == 0 else ''
#                 # ✅ data-option-text stocke le texte brut pour scoring post-shuffle
#                 escaped_option = option.replace('"', '&quot;')
#                 dropdown_html += (
#                     f'<div class="mkq-custom-dropdown-option {selected}"'
#                     f' data-value="{idx}"'
#                     f' data-option-text="{escaped_option}"'
#                     f' onclick="window.selectDropdownQuizOption(this); return false;">'
#                     f'{option}</div>'
#                 )
#             dropdown_html += '</div></div>'
#             placeholder = f'___DROPDOWN_{dd_index}___'
#             if placeholder in rendered_text:
#                 rendered_text = rendered_text.replace(placeholder, dropdown_html, 1)
#             else:
#                 rendered_text = rendered_text.replace('___DROPDOWN___', dropdown_html, 1)

#         rendered_text = self._render_math(rendered_text)

#         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> {rendered_text}
#     </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">{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

#         # 1. Global config (mkdocs.yml) as base
#         gcfg = self.config.get('gapfill', {}) if isinstance(self.config, dict) else {}
#         global_raw = gcfg.get('compare_answers')
#         base = normalize_compare_answers(global_raw if isinstance(global_raw, dict) else {})

#         # 2. Quiz front-matter override — only explicitly set keys win
#         quiz_cfg = quiz.get('config') or {}
#         quiz_raw = quiz_cfg.get('compare_answers') if isinstance(quiz_cfg, dict) else None
#         if not quiz_raw or not isinstance(quiz_raw, dict):
#             return base

#         merged = copy.deepcopy(base)
#         quiz_ignore = quiz_raw.get('ignore', {})
#         if isinstance(quiz_ignore, dict):
#             merged['ignore'].update(quiz_ignore)
#         quiz_equiv = quiz_raw.get('equivalence', {})
#         if isinstance(quiz_equiv, dict):
#             merged['equivalence'].update(quiz_equiv)
#         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')
#         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_losing_mode_choice(self, question: dict) -> dict:
#         out = {'rules': [], 'default_false': None}
#         qcfg = question.get('config') or {}
#         if isinstance(qcfg, dict) and isinstance(qcfg.get('losing_mode'), dict):
#             lm = qcfg.get('losing_mode') or {}
#             if 'default_false' in lm:
#                 out['default_false'] = _coerce_float(lm.get('default_false'))
#             elif 'default' in lm:
#                 out['default_false'] = _coerce_float(lm.get('default'))
#             elif 'default_false' in qcfg:
#                 out['default_false'] = _coerce_float(qcfg.get('default_false'))
#             elif 'default' in qcfg:
#                 out['default_false'] = _coerce_float(qcfg.get('default'))
#             for k, v in lm.items():
#                 if k in ('default_false', 'default'):
#                     continue
#                 score = _coerce_float(v)
#                 if score is None:
#                     continue
#                 out['rules'].append({'key': str(k), 'score': score})
#             return out
#         lm2 = question.get('losing_mode')
#         if isinstance(lm2, dict):
#             if 'default_false' in lm2:
#                 out['default_false'] = _coerce_float(lm2.get('default_false'))
#             elif 'default' in lm2:
#                 out['default_false'] = _coerce_float(lm2.get('default'))
#             for k, v in lm2.items():
#                 if k in ('default_false', 'default'):
#                     continue
#                 score = _coerce_float(v)
#                 if score is None:
#                     continue
#                 out['rules'].append({'key': str(k), 'score': score})
#         return out

#     def _normalize_losing_mode_gapfill(self, question: dict) -> dict:
#         out = {'gaps': []}
#         qcfg = question.get('config') or {}
#         cfg_lm = qcfg.get('losing_mode') if isinstance(qcfg, dict) else None
#         cfg_lm = cfg_lm if isinstance(cfg_lm, dict) else {}

#         for i, gap in enumerate(question.get('gaps', [])):
#             lm = gap.get('losing_mode')
#             if not isinstance(lm, dict):
#                 lm = cfg_lm.get(f'gap{i}')
#             obj = {'default_false': None, 'rules': []}
#             if isinstance(lm, dict):
#                 if 'default_false' in lm:
#                     obj['default_false'] = _coerce_float(lm.get('default_false'))
#                 elif 'default' in lm:
#                     obj['default_false'] = _coerce_float(lm.get('default'))
#                 for k, v in lm.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['gaps'].append(obj)
#         return out

#     def _normalize_losing_mode_dropdown(self, question: dict) -> dict:
#         out = {'dropdowns': []}
#         dropdowns = question.get('dropdowns') or []
#         if not dropdowns:
#             dd = question.get('dropdown')
#             dropdowns = [dd] if dd else []

#         # ✅ Front-matter losing_mode: récupérer depuis question config
#         qcfg = question.get('config') or {}
#         cfg_lm = qcfg.get('losing_mode') if isinstance(qcfg, dict) else None
#         cfg_lm = cfg_lm if isinstance(cfg_lm, dict) else {}

#         for i, dd in enumerate(dropdowns):
#             # Priorité : inline losing_mode sur le dropdown > front-matter cfg_lm
#             lm = dd.get('losing_mode') if isinstance(dd, dict) else None
#             if not isinstance(lm, dict):
#                 lm = cfg_lm.get(f'dropdown{i}')

#             obj = {'default_false': None, 'rules': []}
#             if isinstance(lm, dict):
#                 if 'default_false' in lm:
#                     obj['default_false'] = _coerce_float(lm.get('default_false'))
#                 elif 'default' in lm:
#                     obj['default_false'] = _coerce_float(lm.get('default'))
#                 for k, v in lm.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['dropdowns'].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 {}

#         scoring_mode = 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()
#         _ms = qt_cfg.get('min_score', _SENTINEL)
#         min_score = _ms if _ms is not _SENTINEL else self.config.get('min_score', 0)
#         clamp_max = bool(self.config.get('clamp_max_score', True))

#         quiz_compare = self._get_compare_answers_for_quiz(quiz) if quiz_type == 'gapfill' 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,
#             'compare_answers': quiz_compare,
#             'questions':      []
#         }

#         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)
#             qcfg = question.get('config') or {}
#             q_default_false = None
#             if isinstance(qcfg, dict):
#                 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'))

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

#             if quiz_type in ['mcquiz', 'scquiz']:
#                 # ✅ On stocke le TEXTE des réponses correctes (pas seulement les indices)
#                 #    pour que le scoring JS fonctionne correctement après shuffle
#                 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['losing_mode'] = self._normalize_losing_mode_choice(question)

#             elif quiz_type == 'gapfill':
#                 question_data['gaps'] = question.get('gaps', [])
#                 question_data['losing_mode'] = self._normalize_losing_mode_gapfill(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 == 'dropdown':
#                 dropdowns = question.get('dropdowns')
#                 question_data['dropdowns'] = dropdowns if dropdowns else [question.get('dropdown', {})]
#                 question_data['losing_mode'] = self._normalize_losing_mode_dropdown(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')

# ===================================================================================================================
# ===================================================================================================================
# ===================================================================================================================


"""
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


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']

        # ✅ Randomization is now handled entirely client-side (JS).
        # _apply_randomization() has been removed.
        # The renderer only passes the flags to the HTML via data attributes.

        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, {})
        show_correction = quiz_type_config.get('show_correction', 'yes')
        granularity = self.config.get('correction_granularity', 'exercise')

        # ✅ Randomization flags — read by JS at page load
        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 == 'gapfill':
            return self._render_gapfill_question(quiz, question, quiz_id)
        if quiz_type == 'dropdown':
            return self._render_dropdown_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>"""

        # ✅ randomize_answers par question : priorité config question > config quiz
        qcfg_for_rand = question.get('config') or {}
        q_randomize_answers = qcfg_for_rand.get('randomize_answers', None)
        # None = pas défini au niveau question → JS utilisera le flag du quiz
        # True/False = défini au niveau question → écrase le flag du quiz
        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"'
        # si None : pas d'attribut → JS hérite du flag quiz

        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'])
            # ✅ data-answer-text stocke le texte de la réponse pour la comparaison
            #    post-shuffle (le scoring JS compare par texte, pas par index DOM)
            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>"""

    # ─────────────────────────────────────────────────────────────────────────
    # Gapfill question
    # ─────────────────────────────────────────────────────────────────────────

    def _render_gapfill_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', ''))

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

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

        gap_html = re.sub(gap_pattern, make_replacer(quiz_id, question_id), question_text)
        gap_html = self._render_math(gap_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> {gap_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">{gap_html}</div>
</div>"""

    # ─────────────────────────────────────────────────────────────────────────
    # Dropdown question
    # ─────────────────────────────────────────────────────────────────────────

    def _render_dropdown_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', ''))

        dropdowns = question.get('dropdowns')
        if not dropdowns:
            dd = question.get('dropdown')
            dropdowns = [dd] if dd else []

        rendered_text = question_text
        for dd_index, dropdown_data in enumerate(dropdowns):
            if not dropdown_data:
                continue
            dropdown_id = f"{quiz_id}-q{question_id}-dropdown-{dd_index}"
            dropdown_html = (
                f'<div class="mkq-custom-dropdown" id="{dropdown_id}"'
                f' data-quiz-id="{quiz_id}" data-question-id="{question_id}">'
                '<button type="button" class="mkq-custom-dropdown-btn"'
                ' onclick="window.toggleDropdownQuiz(this); return false;">'
                '<span class="mkq-custom-dropdown-value">--</span>'
                '<span class="mkq-custom-dropdown-arrow">▾</span>'
                '</button>'
                '<div class="mkq-custom-dropdown-options" style="display: none;">'
            )
            for idx, option in enumerate(dropdown_data.get('options', [])):
                selected = 'selected' if idx == 0 else ''
                # ✅ data-option-text stocke le texte brut pour scoring post-shuffle
                escaped_option = option.replace('"', '&quot;')
                dropdown_html += (
                    f'<div class="mkq-custom-dropdown-option {selected}"'
                    f' data-value="{idx}"'
                    f' data-option-text="{escaped_option}"'
                    f' onclick="window.selectDropdownQuizOption(this); return false;">'
                    f'{option}</div>'
                )
            dropdown_html += '</div></div>'
            placeholder = f'___DROPDOWN_{dd_index}___'
            if placeholder in rendered_text:
                rendered_text = rendered_text.replace(placeholder, dropdown_html, 1)
            else:
                rendered_text = rendered_text.replace('___DROPDOWN___', dropdown_html, 1)

        rendered_text = self._render_math(rendered_text)

        # ✅ randomize_answers par question dropdown
        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>"""

    # ─────────────────────────────────────────────────────────────────────────
    # 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

        # 1. Global config (mkdocs.yml) as base
        gcfg = self.config.get('gapfill', {}) if isinstance(self.config, dict) else {}
        global_raw = gcfg.get('compare_answers')
        base = normalize_compare_answers(global_raw if isinstance(global_raw, dict) else {})

        # 2. Quiz front-matter override — only explicitly set keys win
        quiz_cfg = quiz.get('config') or {}
        quiz_raw = quiz_cfg.get('compare_answers') if isinstance(quiz_cfg, dict) else None
        if not quiz_raw or not isinstance(quiz_raw, dict):
            return base

        merged = copy.deepcopy(base)
        quiz_ignore = quiz_raw.get('ignore', {})
        if isinstance(quiz_ignore, dict):
            merged['ignore'].update(quiz_ignore)
        quiz_equiv = quiz_raw.get('equivalence', {})
        if isinstance(quiz_equiv, dict):
            merged['equivalence'].update(quiz_equiv)
        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')
        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_losing_mode_choice(self, question: dict) -> dict:
        out = {'rules': [], 'default_false': None}
        qcfg = question.get('config') or {}
        if isinstance(qcfg, dict) and isinstance(qcfg.get('losing_mode'), dict):
            lm = qcfg.get('losing_mode') or {}
            if 'default_false' in lm:
                out['default_false'] = _coerce_float(lm.get('default_false'))
            elif 'default' in lm:
                out['default_false'] = _coerce_float(lm.get('default'))
            elif 'default_false' in qcfg:
                out['default_false'] = _coerce_float(qcfg.get('default_false'))
            elif 'default' in qcfg:
                out['default_false'] = _coerce_float(qcfg.get('default'))
            for k, v in lm.items():
                if k in ('default_false', 'default'):
                    continue
                score = _coerce_float(v)
                if score is None:
                    continue
                out['rules'].append({'key': str(k), 'score': score})
            return out
        lm2 = question.get('losing_mode')
        if isinstance(lm2, dict):
            if 'default_false' in lm2:
                out['default_false'] = _coerce_float(lm2.get('default_false'))
            elif 'default' in lm2:
                out['default_false'] = _coerce_float(lm2.get('default'))
            for k, v in lm2.items():
                if k in ('default_false', 'default'):
                    continue
                score = _coerce_float(v)
                if score is None:
                    continue
                out['rules'].append({'key': str(k), 'score': score})
        return out

    def _normalize_losing_mode_gapfill(self, question: dict) -> dict:
        out = {'gaps': []}
        qcfg = question.get('config') or {}
        cfg_lm = qcfg.get('losing_mode') if isinstance(qcfg, dict) else None
        cfg_lm = cfg_lm if isinstance(cfg_lm, dict) else {}

        for i, gap in enumerate(question.get('gaps', [])):
            lm = gap.get('losing_mode')
            if not isinstance(lm, dict):
                lm = cfg_lm.get(f'gap{i}')
            obj = {'default_false': None, 'rules': []}
            if isinstance(lm, dict):
                if 'default_false' in lm:
                    obj['default_false'] = _coerce_float(lm.get('default_false'))
                elif 'default' in lm:
                    obj['default_false'] = _coerce_float(lm.get('default'))
                for k, v in lm.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['gaps'].append(obj)
        return out

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

        # ✅ Front-matter losing_mode: récupérer depuis question config
        qcfg = question.get('config') or {}
        cfg_lm = qcfg.get('losing_mode') if isinstance(qcfg, dict) else None
        cfg_lm = cfg_lm if isinstance(cfg_lm, dict) else {}

        for i, dd in enumerate(dropdowns):
            # Priorité : inline losing_mode sur le dropdown > front-matter cfg_lm
            lm = dd.get('losing_mode') if isinstance(dd, dict) else None
            if not isinstance(lm, dict):
                lm = cfg_lm.get(f'dropdown{i}')

            obj = {'default_false': None, 'rules': []}
            if isinstance(lm, dict):
                if 'default_false' in lm:
                    obj['default_false'] = _coerce_float(lm.get('default_false'))
                elif 'default' in lm:
                    obj['default_false'] = _coerce_float(lm.get('default'))
                for k, v in lm.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['dropdowns'].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 {}

        scoring_mode = 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()
        _ms = qt_cfg.get('min_score', _SENTINEL)
        min_score = _ms if _ms is not _SENTINEL else self.config.get('min_score', 0)
        clamp_max = bool(self.config.get('clamp_max_score', True))

        quiz_compare = self._get_compare_answers_for_quiz(quiz) if quiz_type == 'gapfill' 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,
            'compare_answers': quiz_compare,
            'questions':      []
        }

        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)
            qcfg = question.get('config') or {}
            q_default_false = None
            if isinstance(qcfg, dict):
                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'))

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

            if quiz_type in ['mcquiz', 'scquiz']:
                # ✅ On stocke le TEXTE des réponses correctes (pas seulement les indices)
                #    pour que le scoring JS fonctionne correctement après shuffle
                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['losing_mode'] = self._normalize_losing_mode_choice(question)

            elif quiz_type == 'gapfill':
                question_data['gaps'] = question.get('gaps', [])
                question_data['losing_mode'] = self._normalize_losing_mode_gapfill(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 == 'dropdown':
                dropdowns = question.get('dropdowns')
                question_data['dropdowns'] = dropdowns if dropdowns else [question.get('dropdown', {})]
                question_data['losing_mode'] = self._normalize_losing_mode_dropdown(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')
