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

NEW:
- Internal API:
    render_question(quiz, question, quiz_id) -> HTML
  and render_quiz() is just a wrapper.

- Scoring metadata injected into encrypted correction payload:
    points, scoring_mode, default_false, losing_mode rules, min_score, clamp_max_score
    + per-type answer texts (for matching losing_mode by text)
    + per-question compare_answers for gapfill

- Keeps meta preamble Q0 behavior unchanged.
- Keeps dropdown multi-dropdown placeholders behavior unchanged.
"""

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

from .database import QuizDatabase


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

        # Randomize (in-place) if configured — must happen BEFORE correction payload.
        self._apply_randomization(quiz)

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

        # Backward compat preamble (older parser)
        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')

        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-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 (internal API)
    # ─────────────────────────────────────────────────────────────────────────

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

    # ─────────────────────────────────────────────────────────────────────────
    # Randomization
    # ─────────────────────────────────────────────────────────────────────────

    def _apply_randomization(self, quiz: dict) -> None:
        qt = quiz.get('type')
        cfg = self.config.get(qt, {}) if isinstance(self.config, dict) else {}

        if qt in ('mcquiz', 'scquiz'):
            if cfg.get('randomize_answers'):
                for q in quiz.get('questions', []):
                    if q.get('is_meta') or q.get('number') == 0:
                        continue
                    ans = q.get('answers', [])
                    random.shuffle(ans)

        elif qt == 'dropdown':
            if cfg.get('randomize_options'):
                for q in quiz.get('questions', []):
                    if q.get('is_meta') or q.get('number') == 0:
                        continue
                    dropdowns = q.get('dropdowns') or []
                    for dd in dropdowns:
                        opts = dd.get('options', [])
                        if not opts:
                            continue
                        ci = dd.get('correct_index', 0)
                        correct_opt = opts[ci] if 0 <= ci < len(opts) else None
                        random.shuffle(opts)
                        if correct_opt is not None and correct_opt in opts:
                            dd['correct_index'] = opts.index(correct_opt)

    # ─────────────────────────────────────────────────────────────────────────
    # Rich content helper
    # ─────────────────────────────────────────────────────────────────────────

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

    # ─────────────────────────────────────────────────────────────────────────
    # Meta preamble renderer (Q0)
    # ─────────────────────────────────────────────────────────────────────────

    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'])
            answers_html += f"""
<label class="mkq-answer">
    <input type="{input_type}"
           id="{answer_id}"
           name="{quiz_id}-q{question_id}"
           value="{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"'
                    f' 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}">'
            )
            dropdown_html += (
                '<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>'
            )
            dropdown_html += '<div class="mkq-custom-dropdown-options" style="display: none;">'

            for idx, option in enumerate(dropdown_data.get('options', [])):
                selected = 'selected' if idx == 0 else ''
                dropdown_html += (
                    f'<div class="mkq-custom-dropdown-option {selected}"'
                    f' data-value="{idx}"'
                    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 / encryption (includes scoring metadata)
    # ─────────────────────────────────────────────────────────────────────────

    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 _normalize_compare_answers(self) -> dict:
        gcfg = self.config.get('gapfill', {}) if isinstance(self.config, dict) else {}
        ca = gcfg.get('compare_answers')
        if isinstance(ca, dict):
            return ca

        return {
            'strip_spaces': gcfg.get('strip_spaces', True),
            'ignore_spaces': gcfg.get('ignore_spaces', True),
            'case_sensitive': gcfg.get('case_sensitive', False),
            'ignore_case': (not gcfg.get('case_sensitive', False)),
            'ignore_accents': False,
            'latex': {
                'mode': 'loose' if not gcfg.get('strict_latex', False) else 'strict',
            }
        }

    # ✅ losing_mode: alias default == default_false
    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 isinstance(lm, dict):
                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'))

                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': []}
        for gap in question.get('gaps', []):
            lm = gap.get('losing_mode')
            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 []

        for dd in dropdowns:
            lm = dd.get('losing_mode')
            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'

        # ✅ IMPORTANT: alias "default" == "default_false" at global/page/quiz levels
        default_false = qt_cfg.get('default_false', qt_cfg.get('default', 0))
        try:
            default_false = float(default_false)
        except Exception:
            default_false = 0.0

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

        compare_answers = self._normalize_compare_answers() 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': compare_answers,
            '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)

            # ✅ IMPORTANT: alias "default" == "default_false" at question mini-frontmatter level
            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': q_default_false if q_default_false is not None else default_false,
            }

            if quiz_type in ['mcquiz', 'scquiz']:
                answers_text = [a.get('text', '') for a in question.get('answers', [])]
                question_data['answers_text'] = answers_text
                question_data['correct_answers'] = [
                    idx for idx, answer in enumerate(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)

            elif quiz_type == 'dropdown':
                dropdowns = question.get('dropdowns')
                if dropdowns:
                    question_data['dropdowns'] = dropdowns
                else:
                    question_data['dropdowns'] = [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')
