"""
Parser pour extraire les quiz du Markdown
Supports rich content (images, iframes, markdown, html) in questions/preambles.

Current behavior:
- Preamble stored as meta question 0 (is_meta=True, number=0).
- Exercise-level config ("mini front-matter") at the beginning of the quiz body:
    ---
    key: value
    ---
  Parsed into quiz['config'] (dict) and removed from the content.

NEW in this version:
- Points per question:
    priority: points: directive (mini frontmatter inside question) >
              q{X}: in question header >
              default_points from merged config (handled later in renderer/JS)
  Parser stores:
    question['points'] if q{X} is used
    question['config'] if mini block used (can include points/default_false/losing_mode)

- losing_mode support:
  * mcquiz:
      - inline last-line { [Paris, Londres]: 1.0, default_false: 0.1 }
      - or mini-frontmatter in answers block with losing_mode mapping
  * scquiz:
      - per-answer suffix: 1754 {0.5} (absolute points)
      - or final-line {1754: 0.5, $1792$: 0.25, default_false: 0.1}
      - or mini-frontmatter in answers block with losing_mode mapping
  * gapfill:
      - per-gap suffix: [Paris, Marseille]{Genève: 1.5, default_false: 0.1}
      - or question mini-frontmatter losing_mode with per-gap blocks (advanced)
  * dropdown:
      - per-dropdown suffix: [..]{Marseille: 0.5, default_false: 0.1}
      - or question mini-frontmatter losing_mode with per-dropdown blocks (advanced)

- dropdown still supports MULTIPLE dropdowns per question (placeholders ___DROPDOWN_i___).
"""

import re
from typing import Any, Dict, List, Tuple, Optional


# ─────────────────────────────────────────────────────────────────────────────
# Admonition body-line detector  (shared rule — must match plugin.py exactly)
# ─────────────────────────────────────────────────────────────────────────────

_HTML_BLOCK_TAG_RE = re.compile(
    r'^<(?:iframe|div|figure|figcaption|table|thead|tbody|tr|th|td|'
    r'blockquote|pre|video|audio|script|style|details|summary)\b',
    re.IGNORECASE
)


def _is_admonition_body_line(line: str) -> bool:
    return (
        line == '' or
        line.startswith('    ') or
        line.startswith('\t') or
        bool(_HTML_BLOCK_TAG_RE.match(line))
    )


# ─────────────────────────────────────────────────────────────────────────────
# Mini YAML-ish parser
# ─────────────────────────────────────────────────────────────────────────────

def _coerce_scalar(value: str) -> Any:
    v = value.strip()
    if v == '':
        return ''
    low = v.lower()
    if low in ('true', 'yes', 'on'):
        return True
    if low in ('false', 'no', 'off'):
        return False
    if low in ('null', 'none', '~'):
        return None
    if re.fullmatch(r'[+-]?\d+', v):
        try:
            return int(v)
        except Exception:
            pass
    if re.fullmatch(r'[+-]?\d+\.\d+', v):
        try:
            return float(v)
        except Exception:
            pass
    if (len(v) >= 2) and ((v[0] == v[-1] == '"') or (v[0] == v[-1] == "'")):
        return v[1:-1]
    return v


def _parse_mini_yaml(lines: List[str]) -> Dict[str, Any]:
    root: Dict[str, Any] = {}
    stack: List[Tuple[int, Dict[str, Any]]] = [(0, root)]

    for raw in lines:
        if not raw.strip():
            continue
        line = raw.rstrip()
        indent = len(line) - len(line.lstrip(' '))
        s = line.strip()

        m = re.match(r'^([^:#]+)\s*:\s*(.*)$', s)
        if not m:
            continue

        key = m.group(1).strip()
        val = m.group(2)

        while stack and indent < stack[-1][0]:
            stack.pop()
        if not stack:
            stack = [(0, root)]

        parent = stack[-1][1]

        if val.strip() == '':
            new_map: Dict[str, Any] = {}
            parent[key] = new_map
            stack.append((indent + 2, new_map))
        else:
            parent[key] = _coerce_scalar(val)

    return root


def _strip_common_indent(lines: List[str]) -> List[str]:
    indents = []
    for ln in lines:
        if ln.strip() and ln.startswith(' '):
            indents.append(len(ln) - len(ln.lstrip(' ')))
    if not indents:
        return lines[:]
    cut = min(indents)
    out = []
    for ln in lines:
        if ln.startswith(' ' * cut):
            out.append(ln[cut:])
        else:
            out.append(ln)
    return out


# ─────────────────────────────────────────────────────────────────────────────
# Inline "{...}" parsing helpers (simple, robust enough)
# ─────────────────────────────────────────────────────────────────────────────

def _split_top_level_commas(s: str) -> List[str]:
    parts = []
    cur = []
    depth = 0
    in_quote = None
    i = 0
    while i < len(s):
        ch = s[i]
        if ch in ('"', "'"):
            if in_quote == ch:
                in_quote = None
            elif in_quote is None:
                in_quote = ch
            cur.append(ch)
            i += 1
            continue
        if in_quote is not None:
            cur.append(ch)
            i += 1
            continue
        if ch == '[':
            depth += 1
            cur.append(ch)
            i += 1
            continue
        if ch == ']':
            depth = max(0, depth - 1)
            cur.append(ch)
            i += 1
            continue
        if ch == ',' and depth == 0:
            parts.append(''.join(cur).strip())
            cur = []
            i += 1
            continue
        cur.append(ch)
        i += 1
    tail = ''.join(cur).strip()
    if tail:
        parts.append(tail)
    return parts


def _parse_inline_braces_dict(line: str) -> Optional[Dict[str, Any]]:
    """
    Parse a single-line "{ ... }" dict with entries:
      key: value, key2: value2
    Keys can be:
      - default_false
      - [Paris, Londres]   (kept as raw string key)
      - 1754
      - $1792$
    Values: float/int
    """
    s = line.strip()
    if not (s.startswith('{') and s.endswith('}')):
        return None
    inner = s[1:-1].strip()
    if not inner:
        return {}
    out: Dict[str, Any] = {}
    for item in _split_top_level_commas(inner):
        if not item:
            continue
        if ':' not in item:
            continue
        k, v = item.split(':', 1)
        key = k.strip()
        val_raw = v.strip()
        try:
            val = float(val_raw)
            if val.is_integer():
                val = int(val)
        except Exception:
            val = _coerce_scalar(val_raw)
        out[key] = val
    return out


def _parse_bracket_list_key(key: str) -> Optional[List[str]]:
    s = key.strip()
    if not (s.startswith('[') and s.endswith(']')):
        return None
    inner = s[1:-1].strip()
    if not inner:
        return []
    items = [x.strip() for x in inner.split(',')]
    items = [x for x in items if x]
    return items


# ─────────────────────────────────────────────────────────────────────────────
# Parser
# ─────────────────────────────────────────────────────────────────────────────

class QuizParser:
    def __init__(self, i18n, language: str = 'en'):
        self.i18n = i18n
        self.language = language

        from .config import build_keywords_dict
        self.keywords = build_keywords_dict(language)

    def parse_markdown(self, markdown: str) -> list:
        all_synonyms = self.i18n.get_all_quiz_type_synonyms()
        synonym_set = {s.lower() for s in all_synonyms}
        header_re = re.compile(r'^!!!\s+(\S+)\s+"([^"]+)"\s*$', re.IGNORECASE)

        src_lines = markdown.split('\n')
        quizzes = []
        quiz_number = 1
        i = 0

        while i < len(src_lines):
            line = src_lines[i]
            hm = header_re.match(line)

            if hm and hm.group(1).lower() in synonym_set:
                original_synonym = hm.group(1).lower()
                title = hm.group(2)

                i += 1
                body_lines = []
                while i < len(src_lines) and _is_admonition_body_line(src_lines[i]):
                    body_lines.append(src_lines[i])
                    i += 1
                content = '\n'.join(body_lines)

                quiz_type = self.i18n.get_quiz_type(original_synonym)
                if not quiz_type:
                    continue

                if quiz_type in ('mcquiz', 'scquiz'):
                    quiz = self._parse_choice_quiz(content, quiz_type, title, quiz_number)
                elif quiz_type == 'gapfill':
                    quiz = self._parse_gapfill(content, 'gapfill', title, quiz_number)
                elif quiz_type == 'dropdown':
                    quiz = self._parse_dropdown(content, 'dropdown', title, quiz_number)
                else:
                    continue

                quiz['original_synonym'] = original_synonym
                quizzes.append(quiz)
                quiz_number += 1
            else:
                i += 1

        return quizzes

    def _build_keyword_patterns(self):
        preamble_keywords = self.keywords.get('preamble', ['preamble', 'pre'])
        question_keywords = self.keywords.get('question', ['question', 'q'])
        answers_keywords = self.keywords.get('answers', ['answers', 'ans'])

        pre_patterns = sorted(set(preamble_keywords), key=len, reverse=True)
        q_patterns = sorted(set(question_keywords), key=len, reverse=True)

        expanded_answers = set(answers_keywords)
        for kw in list(answers_keywords):
            if kw.endswith('s') and len(kw) > 3:
                expanded_answers.add(kw[:-1])
        a_patterns = sorted(expanded_answers, key=len, reverse=True)

        p_pattern = '|'.join(re.escape(kw) for kw in pre_patterns)
        q_pattern = '|'.join(re.escape(kw) for kw in q_patterns)
        a_pattern = '|'.join(re.escape(kw) for kw in a_patterns)

        return p_pattern, q_pattern, a_pattern

    def _extract_exercise_config(self, content: str) -> Tuple[Dict[str, Any], str]:
        lines = content.split('\n')

        start = 0
        while start < len(lines) and not lines[start].strip():
            start += 1
        if start >= len(lines):
            return {}, content

        if lines[start].strip() != '---':
            return {}, content

        end = start + 1
        while end < len(lines) and lines[end].strip() != '---':
            end += 1
        if end >= len(lines):
            return {}, content

        raw_cfg_lines = lines[start + 1:end]
        raw_cfg_lines = _strip_common_indent(raw_cfg_lines)
        cfg = _parse_mini_yaml(raw_cfg_lines)

        remaining = lines[:start] + lines[end + 1:]
        return cfg, '\n'.join(remaining)

    @staticmethod
    def _trim_blank_edges(lines: list) -> list:
        while lines and not lines[0].strip():
            lines.pop(0)
        while lines and not lines[-1].strip():
            lines.pop()
        return lines

    def _normalize_explicit_preamble_lines(self, preamble_lines: list, p_pattern: str) -> list:
        if not preamble_lines:
            return preamble_lines

        first_idx = None
        for i, ln in enumerate(preamble_lines):
            if ln.strip():
                first_idx = i
                break
        if first_idx is None:
            return preamble_lines

        first_line = preamble_lines[first_idx]
        first_stripped = first_line.strip()

        m = re.match(rf'^({p_pattern})\s*:\s*(.*)$', first_stripped, re.IGNORECASE)
        if not m:
            return preamble_lines

        inline = (m.group(2) or '').rstrip()
        base_indent = len(first_line) - len(first_line.lstrip(' '))

        out: List[str] = []
        out.extend(preamble_lines[:first_idx])

        if inline:
            out.append((' ' * base_indent) + inline)

        for ln in preamble_lines[first_idx + 1:]:
            if not ln.strip():
                out.append('')
                continue
            if ln.startswith(' '):
                cur_indent = len(ln) - len(ln.lstrip(' '))
                if cur_indent >= base_indent + 4:
                    out.append(ln[4:])
                else:
                    out.append(ln)
            else:
                out.append(ln)

        return out

    def _split_preamble_and_body(self, content: str, p_pattern: str, q_pattern: str) -> Tuple[List[str], List[str]]:
        lines = content.split('\n')
        preamble_lines: List[str] = []
        idx = 0

        q_re = re.compile(rf'^({q_pattern})(\d*)\s*(\{{[^}}]*\}})?\s*:\s*', re.IGNORECASE)

        while idx < len(lines):
            stripped = lines[idx].strip()
            if q_re.match(stripped):
                break
            preamble_lines.append(lines[idx])
            idx += 1

        preamble_lines = self._normalize_explicit_preamble_lines(preamble_lines, p_pattern)
        preamble_lines = self._trim_blank_edges(preamble_lines)

        remaining_lines = lines[idx:]
        return preamble_lines, remaining_lines

    def _inject_meta_preamble_question(self, questions: list, preamble_lines: list) -> list:
        if not preamble_lines:
            return questions
        meta = {
            'number': 0,
            'text': '',
            'rich_content': '\n'.join(preamble_lines),
            'answers': [],
            'is_meta': True,
        }
        return [meta] + questions

    def _parse_points_brace(self, brace: Optional[str]) -> Optional[float]:
        if not brace:
            return None
        s = brace.strip()
        if not (s.startswith('{') and s.endswith('}')):
            return None
        inner = s[1:-1].strip()
        if not inner:
            return None
        try:
            return float(inner)
        except Exception:
            return None

    # ─────────────────────────────────────────────────────────────────────────
    # mcquiz / scquiz
    # ─────────────────────────────────────────────────────────────────────────

    def _parse_choice_quiz(self, content: str, quiz_type: str, title: str, number: int) -> dict:
        exercise_cfg, content2 = self._extract_exercise_config(content)

        p_pattern, q_pattern, a_pattern = self._build_keyword_patterns()
        preamble_lines, remaining_lines = self._split_preamble_and_body(content2, p_pattern, q_pattern)

        q_re = re.compile(rf'^({q_pattern})(\d*)\s*(\{{[^}}]*\}})?\s*:\s*(.*)$', re.IGNORECASE)
        a_re = re.compile(rf'^({a_pattern})\d*\s*:\s*$', re.IGNORECASE)

        questions = []
        current_question = None
        question_number = 1
        state = 'scan'  # 'question' | 'answers' | 'scan'

        pending_answers_cfg: Dict[str, Any] = {}
        pending_answers_started = False
        answers_cfg_collecting = False
        answers_cfg_lines: List[str] = []

        # ✅ FIX: attach answers mini frontmatter to the CURRENT QUESTION config
        def flush_answers_cfg():
            nonlocal pending_answers_cfg, answers_cfg_collecting, answers_cfg_lines, current_question

            if answers_cfg_lines:
                raw = _strip_common_indent(answers_cfg_lines)
                pending_answers_cfg = _parse_mini_yaml(raw)
            else:
                pending_answers_cfg = {}

            # ✅ MERGE into question['config']
            if current_question is not None and pending_answers_cfg:
                qcfg = current_question.get('config')
                if not isinstance(qcfg, dict):
                    qcfg = {}
                # shallow merge is enough here (keys: scoring_mode/default_false/losing_mode/points/compare_answers...)
                qcfg.update(pending_answers_cfg)
                current_question['config'] = qcfg

            answers_cfg_lines = []
            answers_cfg_collecting = False

        for line in remaining_lines:
            stripped = line.strip()

            qm = q_re.match(stripped)
            if qm:
                # close previous question
                if current_question is not None:
                    rc = current_question.get('rich_content', [])
                    while rc and not rc[0].strip():
                        rc.pop(0)
                    while rc and not rc[-1].strip():
                        rc.pop()
                    current_question['rich_content'] = '\n'.join(rc)
                    questions.append(current_question)

                pts = self._parse_points_brace(qm.group(3))
                current_question = {
                    'number': question_number,
                    'text': qm.group(4).strip(),
                    'rich_content': [],
                    'answers': [],
                    'is_meta': False,
                    'points': pts,
                    'config': {},                  # ✅ will now receive answers frontmatter too
                    'losing_mode': None,
                    'default_false_in_losing': None,
                }
                question_number += 1
                state = 'question'

                pending_answers_cfg = {}
                pending_answers_started = False
                answers_cfg_collecting = False
                answers_cfg_lines = []
                continue

            am = a_re.match(stripped)
            if am:
                if current_question is not None:
                    state = 'answers'
                    pending_answers_started = True
                    pending_answers_cfg = {}
                    answers_cfg_collecting = False
                    answers_cfg_lines = []
                continue

            # Within answers: allow mini frontmatter at top (--- ... ---)
            if state == 'answers' and pending_answers_started:
                if not current_question:
                    continue

                if not answers_cfg_collecting and stripped == '---' and not pending_answers_cfg:
                    answers_cfg_collecting = True
                    answers_cfg_lines = []
                    continue

                if answers_cfg_collecting:
                    if stripped == '---':
                        flush_answers_cfg()
                        continue
                    answers_cfg_lines.append(line)
                    continue

                # Answer bullet
                if stripped and stripped[0] in ('-', '*', '+'):
                    is_correct_marker = stripped.startswith('+')
                    answer_text = re.sub(r'^[-*+]\s*', '', stripped).strip()

                    # scquiz: allow suffix "{points}" on an answer line (absolute points)
                    ans_points = None
                    m_pts = re.search(r'\{([+-]?\d+(?:\.\d+)?)\}\s*$', answer_text)
                    if m_pts:
                        try:
                            ans_points = float(m_pts.group(1))
                        except Exception:
                            ans_points = None
                        answer_text = re.sub(r'\{[+-]?\d+(?:\.\d+)?\}\s*$', '', answer_text).rstrip()

                    is_correct_brackets = False
                    if answer_text.startswith('[') and answer_text.endswith(']'):
                        is_correct_brackets = True
                        answer_text = answer_text[1:-1].strip()

                    current_question['answers'].append({
                        'text': answer_text,
                        'is_correct': is_correct_marker or is_correct_brackets,
                        'ans_points': ans_points,
                    })
                    continue

                # Inline losing_mode "{...}" line
                bdict = _parse_inline_braces_dict(stripped)
                if bdict is not None:
                    current_question['losing_mode'] = bdict
                    if 'default_false' in bdict:
                        current_question['default_false_in_losing'] = bdict.get('default_false')
                    continue

            # Accumulate rich content between question and answers
            if state == 'question' and current_question is not None:
                current_question['rich_content'].append(line)

        # flush last question
        if current_question is not None:
            rc = current_question.get('rich_content', [])
            while rc and not rc[0].strip():
                rc.pop(0)
            while rc and not rc[-1].strip():
                rc.pop()
            current_question['rich_content'] = '\n'.join(rc)
            questions.append(current_question)

        # scquiz: if any answer has ans_points -> treat as losing_mode mapping (absolute points)
        for q in questions:
            if quiz_type == 'scquiz':
                lm: Dict[str, Any] = {}
                any_pts = False
                for ans in q.get('answers', []):
                    ap = ans.get('ans_points')
                    if ap is not None:
                        any_pts = True
                        lm[ans['text']] = ap
                if any_pts:
                    for ans in q.get('answers', []):
                        if ans.get('is_correct') and ans['text'] not in lm:
                            val = q.get('points')
                            try:
                                lm[ans['text']] = float(val) if val is not None else 1.0
                            except Exception:
                                lm[ans['text']] = 1.0
                    q['config'] = q.get('config') or {}
                    q['config']['losing_mode'] = lm

        questions = self._inject_meta_preamble_question(questions, preamble_lines)

        return {
            'type': quiz_type,
            'title': title,
            'number': number,
            'preamble': '',
            'questions': questions,
            'config': exercise_cfg or {},
        }

    # ─────────────────────────────────────────────────────────────────────────
    # gapfill
    # ─────────────────────────────────────────────────────────────────────────

    def _parse_gapfill(self, content: str, quiz_type: str, title: str, number: int) -> dict:
        exercise_cfg, content2 = self._extract_exercise_config(content)
        p_pattern, q_pattern, _ = self._build_keyword_patterns()
        quiz = self._parse_gapfill_or_dropdown(
            content2, quiz_type, title, number, p_pattern, q_pattern, mode='gapfill'
        )
        quiz['config'] = exercise_cfg or {}
        return quiz

    # ─────────────────────────────────────────────────────────────────────────
    # dropdown
    # ─────────────────────────────────────────────────────────────────────────

    def _parse_dropdown(self, content: str, quiz_type: str, title: str, number: int) -> dict:
        exercise_cfg, content2 = self._extract_exercise_config(content)
        p_pattern, q_pattern, _ = self._build_keyword_patterns()
        quiz = self._parse_gapfill_or_dropdown(
            content2, quiz_type, title, number, p_pattern, q_pattern, mode='dropdown'
        )
        quiz['config'] = exercise_cfg or {}
        return quiz

    # ─────────────────────────────────────────────────────────────────────────
    # Shared gapfill / dropdown parser
    # ─────────────────────────────────────────────────────────────────────────

    def _extract_question_mini_cfg(self, body_lines: List[str]) -> Tuple[Dict[str, Any], List[str]]:
        lines = body_lines[:]
        start = 0
        while start < len(lines) and not lines[start].strip():
            start += 1
        if start >= len(lines):
            return {}, body_lines

        if lines[start].strip() != '---':
            return {}, body_lines

        end = start + 1
        while end < len(lines) and lines[end].strip() != '---':
            end += 1
        if end >= len(lines):
            return {}, body_lines

        raw_cfg_lines = lines[start + 1:end]
        raw_cfg_lines = _strip_common_indent(raw_cfg_lines)
        cfg = _parse_mini_yaml(raw_cfg_lines)

        remaining = lines[:start] + lines[end + 1:]
        return cfg, remaining

    def _parse_gapfill_or_dropdown(
        self,
        content: str,
        quiz_type: str,
        title: str,
        number: int,
        p_pattern: str,
        q_pattern: str,
        mode: str
    ) -> dict:
        preamble_lines, remaining_lines = self._split_preamble_and_body(content, p_pattern, q_pattern)

        q_re = re.compile(rf'^({q_pattern})(\d*)\s*(\{{[^}}]*\}})?\s*:\s*(.*)$', re.IGNORECASE)

        blocks = []
        current_block = None

        for line in remaining_lines:
            stripped = line.strip()
            qm = q_re.match(stripped)
            if qm:
                if current_block is not None:
                    blocks.append(current_block)
                pts = self._parse_points_brace(qm.group(3))
                current_block = {
                    'inline_text': qm.group(4).strip(),
                    'body_lines': [],
                    'points': pts,
                }
            else:
                if current_block is not None:
                    current_block['body_lines'].append(line)

        if current_block is not None:
            blocks.append(current_block)

        questions = []
        question_number = 1

        for block in blocks:
            inline_text = block['inline_text']
            body_lines = block['body_lines'][:]
            q_points = block.get('points')

            q_cfg, body_lines2 = self._extract_question_mini_cfg(body_lines)
            body_lines = body_lines2

            while body_lines and not body_lines[0].strip():
                body_lines.pop(0)
            while body_lines and not body_lines[-1].strip():
                body_lines.pop()

            if inline_text:
                question_text = inline_text
                rich_content = '\n'.join(body_lines) if body_lines else ''
            else:
                non_empty = [(i, l) for i, l in enumerate(body_lines) if l.strip()]
                if not non_empty:
                    continue
                last_idx, question_text_raw = non_empty[-1]
                question_text = question_text_raw.strip()
                rich_lines = body_lines[:last_idx]
                while rich_lines and not rich_lines[-1].strip():
                    rich_lines.pop()
                rich_content = '\n'.join(rich_lines)

            if mode == 'gapfill':
                q = self._make_gapfill_question(question_number, question_text, rich_content)
            else:
                q = self._make_dropdown_question(question_number, question_text, rich_content)

            if q is not None:
                q['points'] = q_points
                q['config'] = q_cfg or {}
                questions.append(q)
                question_number += 1

        questions = self._inject_meta_preamble_question(questions, preamble_lines)

        return {
            'type': quiz_type,
            'title': title,
            'number': number,
            'preamble': '',
            'questions': questions,
        }

    # ─────────────────────────────────────────────────────────────────────────
    # Gapfill helper (supports per-gap losing_mode via [..]{...})
    # ─────────────────────────────────────────────────────────────────────────

    def _make_gapfill_question(self, number: int, question_text: str, rich_content: str):
        pattern = re.compile(r'\[([^\]]+)\](\{[^}]*\})?')
        matches = list(pattern.finditer(question_text))
        if not matches:
            return None

        gaps = []
        for m in matches:
            ans_raw = m.group(1)
            losing_raw = m.group(2)

            answers = [a.strip() for a in ans_raw.split(',') if a.strip()]
            gap_obj: Dict[str, Any] = {'answers': answers}

            if losing_raw:
                d = _parse_inline_braces_dict(losing_raw.strip())
                if d is not None:
                    gap_obj['losing_mode'] = d

            gaps.append(gap_obj)

        return {
            'number': number,
            'text': question_text,
            'rich_content': rich_content,
            'gaps': gaps,
            'is_meta': False,
            'config': {},
            'points': None,
        }

    # ─────────────────────────────────────────────────────────────────────────
    # Dropdown helper (MULTI dropdowns + optional {losing_mode} after each [])
    # ─────────────────────────────────────────────────────────────────────────

    def _make_dropdown_question(self, number: int, question_text: str, rich_content: str):
        dropdown_pattern = r'\[([^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*)\](\{[^}]*\})?'
        matches = list(re.finditer(dropdown_pattern, question_text))
        if not matches:
            return None

        def parse_dropdown_content(dropdown_content: str):
            options = []
            correct_index = None
            current_opt = ''
            inside_correct = False

            for part in dropdown_content.split(','):
                part = part.strip()

                if part.startswith('[') and not part.endswith(']'):
                    inside_correct = True
                    current_opt = part[1:]
                    continue

                if inside_correct and part.endswith(']'):
                    current_opt = (current_opt + ',' + part[:-1]) if current_opt else part[:-1]
                    options.append(current_opt.strip())
                    correct_index = len(options) - 1
                    current_opt = ''
                    inside_correct = False
                    continue

                if part.startswith('[') and part.endswith(']'):
                    options.append(part[1:-1].strip())
                    correct_index = len(options) - 1
                    continue

                if inside_correct:
                    current_opt = (current_opt + ',' + part) if current_opt else part
                    continue

                options.append(part)

            if not options:
                return None, None
            if correct_index is None:
                correct_index = 0
            return options, correct_index

        dropdowns = []
        out_parts = []
        last_end = 0

        for m in matches:
            options, correct_index = parse_dropdown_content(m.group(1))
            if options is None:
                continue

            losing_raw = m.group(2)
            losing_dict = None
            if losing_raw:
                losing_dict = _parse_inline_braces_dict(losing_raw.strip())

            dd_index = len(dropdowns)
            dd_obj: Dict[str, Any] = {'options': options, 'correct_index': correct_index}
            if losing_dict is not None:
                dd_obj['losing_mode'] = losing_dict

            dropdowns.append(dd_obj)

            out_parts.append(question_text[last_end:m.start()])
            out_parts.append(f'___DROPDOWN_{dd_index}___')
            last_end = m.end()

        out_parts.append(question_text[last_end:])
        question_text_display = ''.join(out_parts)

        if not dropdowns:
            return None

        return {
            'number': number,
            'text': question_text_display,
            'rich_content': rich_content,
            'dropdowns': dropdowns,
            'dropdown': dropdowns[0],
            'is_meta': False,
            'config': {},
            'points': None,
        }


