import json
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple


class QuizI18n:
    """Handles internationalization for the quiz plugin"""

    def __init__(self, language: str = 'en', custom_synonyms: Optional[Dict] = None):
        self.language = language
        self.fallback_language = 'en'
        self.custom_synonyms = custom_synonyms or {}
        self.i18n_dir = Path(__file__).parent / 'i18n'
        self.translations = self._load_translations()
        self.fallback = self._load_fallback()
        self.quiz_type_map = self._build_quiz_type_map()

        # Build resolution maps for config keys and values
        self._config_key_map: Dict[str, str] = {}   # synonym → canonical key
        self._config_val_map: Dict[str, Dict[str, str]] = {}  # canonical_key → {synonym → canonical_value}
        self._build_config_maps()

    # ─────────────────────────────────────────────────────────────────────────
    # Translation loading
    # ─────────────────────────────────────────────────────────────────────────

    def _load_translations(self) -> Dict:
        translation_file = self.i18n_dir / f'{self.language}.json'
        if not translation_file.exists():
            print(f"⚠️  Translation file not found: {translation_file}")
            print(f"   Falling back to {self.fallback_language}")
            return {}
        with open(translation_file, 'r', encoding='utf-8') as f:
            return json.load(f)

    def _load_fallback(self) -> Dict:
        fallback_file = self.i18n_dir / f'{self.fallback_language}.json'
        with open(fallback_file, 'r', encoding='utf-8') as f:
            return json.load(f)

    # ─────────────────────────────────────────────────────────────────────────
    # Quiz type synonym map
    # ─────────────────────────────────────────────────────────────────────────

    def _build_quiz_type_map(self) -> Dict[str, str]:
        mapping = {}
        quiz_types = self.translations.get('quiz_types', self.fallback.get('quiz_types', {}))
        for quiz_type, data in quiz_types.items():
            for synonym in data.get('synonyms', []):
                if synonym in mapping:
                    print(f"⚠️  Synonym conflict: '{synonym}' already maps to '{mapping[synonym]}'")
                else:
                    mapping[synonym.lower()] = quiz_type

            custom = self.custom_synonyms.get(quiz_type, {}).get('synonyms', '')
            if custom:
                custom_list = [s.strip() for s in custom.split(',') if s.strip()]
                for synonym in custom_list:
                    synonym_lower = synonym.lower()
                    if synonym_lower in mapping:
                        print(f"⚠️  Custom synonym '{synonym}' conflicts with existing mapping")
                        print(f"   Skipping to avoid ambiguity")
                    else:
                        mapping[synonym_lower] = quiz_type
                        print(f"✅ Added custom synonym: '{synonym}' -> '{quiz_type}'")
        return mapping

    # ─────────────────────────────────────────────────────────────────────────
    # Config key/value resolution maps
    # ─────────────────────────────────────────────────────────────────────────

    # Canonical config keys that take a restricted set of values
    # (other keys like min_score, default_false accept arbitrary numbers)
    _VALUE_KEYS = {
        'scoring_mode':        ('all_or_nothing', 'some_or_nothing', 'proportional'),
        'show_correction':     ('yes', 'no', 'on_demand'),
        'min_score':           ('none',),
        'save_answers':        ('yes', 'no'),
        'clamp_max_score':     ('yes', 'no'),
        'randomize_answers':   ('yes', 'no'),
        'randomize_questions': ('yes', 'no'),
        # spaces values (used inside match.ignore.spaces)
        'spaces':              ('spaces_all', 'spaces_inside', 'spaces_outside', 'spaces_none'),
    }

    def _build_config_maps(self) -> None:
        """
        Build two lookup dicts from en.json (always) + current lang JSON (if different).

        _config_key_map : { synonym_lower → canonical_key }
        _config_val_map : { canonical_key → { synonym_lower → canonical_value } }

        Priority rule for key map:
          1. Canonical flat keys always map to themselves (highest priority)
          2. en.json synonyms
          3. current lang synonyms (may add but not override canonical identities)

        This ensures that old-syntax keys like "scoring_mode", "min_score",
        "errors_mode", "compare_answers" always resolve to themselves, even if
        a new-syntax alias entry (e.g. "mode": ["mode","scoring_mode"]) would
        otherwise re-map "scoring_mode" → "mode".
        """
        key_map: Dict[str, str] = {}
        val_map: Dict[str, Dict[str, str]] = {}

        # Step 1: seed canonical flat keys as identity (highest priority)
        # These are the keys that the rest of the code consumes downstream.
        _CANONICAL_FLAT_KEYS = {
            'scoring_mode', 'show_correction', 'min_score', 'clamp_max_score',
            'default_false', 'errors_mode', 'compare_answers',
            'save_answers', 'randomize_answers', 'randomize_questions',
            'default_points', 'points',
            # new-syntax block keys (resolve to themselves for structural dispatch)
            'score', 'match',
            # sub-keys used in decomposition
            'mode', 'min', 'errors', 'ignore', 'equivalence',
            'spaces', 'characters', 'case', 'diacritics',
        }
        for k in _CANONICAL_FLAT_KEYS:
            key_map[k.lower()] = k

        # Step 2: merge en.json then current lang (lower priority — don't override step 1)
        sources: List[Dict] = [self.fallback]
        if self.language != self.fallback_language and self.translations:
            sources.append(self.translations)

        for src in sources:
            cfg = src.get('quiz_configs', {})
            for canonical_key, synonyms in cfg.items():
                if not isinstance(synonyms, list):
                    continue

                # Register synonyms → canonical_key, but never override an
                # already-seeded canonical identity from step 1
                for syn in synonyms:
                    s = syn.lower()
                    if s not in key_map:  # don't override canonical identities
                        key_map[s] = canonical_key

                # Register as value synonyms for keys that accept this value
                for config_key, valid_values in self._VALUE_KEYS.items():
                    if canonical_key in valid_values:
                        if config_key not in val_map:
                            val_map[config_key] = {}
                        for syn in synonyms:
                            val_map[config_key][syn.lower()] = canonical_key

        self._config_key_map = key_map
        self._config_val_map = val_map

    # ─────────────────────────────────────────────────────────────────────────
    # Public resolution API
    # ─────────────────────────────────────────────────────────────────────────

    def resolve_config_key(self, raw_key: str) -> str:
        """
        Resolve a (possibly translated/new-syntax) config key to its canonical name.

        Examples:
            "mode_evaluation"  → "scoring_mode"   (fr old syntax)
            "scoring_mode"     → "scoring_mode"   (en identity)
            "score"            → "score"           (new block key — handled by normalize_config_dict)
            "match"            → "match"           (new block key — handled by normalize_config_dict)
        """
        return self._config_key_map.get(raw_key.lower(), raw_key)

    def resolve_config_value(self, canonical_key: str, raw_value: Any) -> Any:
        """
        Resolve a (possibly translated) config value to its canonical English value.

        - bool/int/float/None pass through unchanged
        - strings are looked up in the value map for the given key
        - 'yes'/'true'/'on' → True  (for boolean keys not in _VALUE_KEYS)
        - 'no'/'false'/'off' → False

        Examples (fr):
            resolve_config_value("scoring_mode", "proportionnel") → "proportional"
            resolve_config_value("show_correction", "oui")        → "yes"
            resolve_config_value("min_score", "aucun")            → "none"
            resolve_config_value("min_score", -1)                 → -1
            resolve_config_value("spaces", "tous")                → "spaces_all"
            resolve_config_value("case", True)                    → True   (bool passthrough)
            resolve_config_value("case", "oui")                   → True   (fr bool)
        """
        # Bool values: if this key uses string values (e.g. show_correction: 'yes'/'no'),
        # re-convert bool→string so the value map lookup works correctly.
        # _coerce_scalar turns `no` → False before we get here, so we need to undo that
        # for keys like show_correction that expect the string 'no', not the bool False.
        if isinstance(raw_value, bool):
            if canonical_key in self._VALUE_KEYS:
                raw_value = 'yes' if raw_value else 'no'
            else:
                return raw_value

        # Non-string, non-bool values pass through (already coerced by _coerce_scalar)
        if not isinstance(raw_value, str):
            return raw_value

        v_lower = raw_value.lower()

        # Check key-specific value map first
        val_lookup = self._config_val_map.get(canonical_key, {})
        if v_lower in val_lookup:
            canonical_val = val_lookup[v_lower]
            # spaces_all etc. → return as canonical string
            return canonical_val

        # Generic boolean coercion for boolean-typed config keys
        # (keys not in _VALUE_KEYS but that accept yes/no/true/false)
        _BOOL_KEYS = {
            'case', 'diacritics', 'save_answers', 'clamp_max_score',
            'randomize_answers', 'randomize_questions',
        }
        if canonical_key in _BOOL_KEYS:
            if v_lower in ('yes', 'true', 'on', '1'):
                return True
            if v_lower in ('no', 'false', 'off', '0'):
                return False

        # Generic boolean coercion via i18n value map (yes/no canonical values)
        # If "oui" maps to "yes" for show_correction, also apply for boolean keys
        yes_syns = self._config_val_map.get('show_correction', {})
        # find what "yes" maps to in show_correction — its synonyms are the yes-words
        _yes_words = {s for s, c in yes_syns.items() if c == 'yes'}
        _no_words  = {s for s, c in yes_syns.items() if c == 'no'}

        if canonical_key in _BOOL_KEYS:
            if v_lower in _yes_words:
                return True
            if v_lower in _no_words:
                return False

        return raw_value

    # ─────────────────────────────────────────────────────────────────────────
    # Structural decomposition helpers (new syntax)
    # ─────────────────────────────────────────────────────────────────────────

    def _resolve_subkey(self, block_name: str, raw_subkey: str) -> str:
        """
        Resolve a sub-key inside a `score` or `match` block.
        Uses the same _config_key_map (sub-keys are registered there too).
        """
        return self._config_key_map.get(raw_subkey.lower(), raw_subkey)

    def _decompose_score_block(self, score_dict: Dict[str, Any]) -> Dict[str, Any]:
        """
        Decompose a `score:` block into flat canonical keys.

        score:
          mode: "proportional"     → scoring_mode: "proportional"
          min: 0                   → min_score: 0
          errors:                  → errors_mode: {...rules...}
            [Paris]: 1.5             + default_false: -1  (extracted from errors)
            default_false: -1

        Sub-keys are resolved via i18n then mapped to their flat canonical names.
        """
        out: Dict[str, Any] = {}

        # Map from the canonical key name (as registered in _config_key_map)
        # to the flat output key we want to produce.
        # e.g. "mode" is registered in en.json under "mode": ["mode","scoring_mode"]
        # so resolve_subkey("score","mode") → "mode"
        # but we want the OUTPUT key to be "scoring_mode".
        # Strategy: resolve the sub-key, then use this explicit map.
        _SUBKEY_TO_FLAT: Dict[str, str] = {
            'mode':          'scoring_mode',
            'scoring_mode':  'scoring_mode',
            'min':           'min_score',
            'min_score':     'min_score',
            'errors':        '_errors_block',
            'errors_mode':   '_errors_block',
            'default_false': 'default_false',
            'default':       'default_false',
        }

        for raw_sub, raw_val in score_dict.items():
            canon_sub = self._resolve_subkey('score', raw_sub)
            flat_key = _SUBKEY_TO_FLAT.get(canon_sub, canon_sub)

            if flat_key == 'scoring_mode':
                out['scoring_mode'] = self.resolve_config_value('scoring_mode', raw_val)

            elif flat_key == 'min_score':
                out['min_score'] = self.resolve_config_value('min_score', raw_val)

            elif flat_key == 'default_false':
                out['default_false'] = raw_val

            elif flat_key == '_errors_block':
                # errors block: dict with rule-keys + optional default_false
                if isinstance(raw_val, dict):
                    rules: Dict[str, Any] = {}
                    for k, v in raw_val.items():
                        # Resolve sub-keys of errors: default_false synonyms
                        canon_k = self._resolve_subkey('errors', k)
                        flat_k = _SUBKEY_TO_FLAT.get(canon_k, canon_k)
                        if flat_k == 'default_false':
                            out['default_false'] = v
                        else:
                            rules[k] = v  # preserve bracket-list keys as-is
                    if rules:
                        out['errors_mode'] = rules
                else:
                    out['errors_mode'] = raw_val

            else:
                # Unknown sub-key — pass through
                out[canon_sub] = raw_val

        return out

    def _decompose_match_block(self, match_dict: Dict[str, Any]) -> Dict[str, Any]:
        """
        Decompose a `match:` block into a canonical `compare_answers` dict.

        match:
          ignore:
            spaces: all          → spaces: "spaces_all"
            characters: "!?."
            case: true
            diacritics: true
          equivalence:
            a: ["á", "à"]

        Result: { "compare_answers": { "ignore": {...}, "equivalence": {...} } }
        """
        ignore_out: Dict[str, Any] = {}
        equiv_out: Dict[str, Any] = {}

        for raw_sub, raw_val in match_dict.items():
            canon_sub = self._resolve_subkey('match', raw_sub)

            if canon_sub == 'ignore':
                if isinstance(raw_val, dict):
                    for raw_ign_key, ign_val in raw_val.items():
                        canon_ign = self._resolve_subkey('ignore', raw_ign_key)
                        if canon_ign == 'spaces':
                            # Resolve spaces value: "all"/"tous"/"todo" → "spaces_all" → strip prefix
                            resolved = self.resolve_config_value('spaces', str(ign_val) if not isinstance(ign_val, str) else ign_val)
                            # "spaces_all" → "all", "spaces_outside" → "outside", etc.
                            if isinstance(resolved, str) and resolved.startswith('spaces_'):
                                resolved = resolved[len('spaces_'):]
                            ignore_out['spaces'] = resolved
                        elif canon_ign in ('case', 'diacritics'):
                            ignore_out[canon_ign] = self.resolve_config_value(canon_ign, ign_val)
                        elif canon_ign == 'characters':
                            ignore_out['characters'] = ign_val
                        else:
                            ignore_out[canon_ign] = ign_val

            elif canon_sub == 'equivalence':
                if isinstance(raw_val, dict):
                    equiv_out = raw_val  # pass through — values are answer lists

            else:
                # Unknown sub-key — pass through into ignore as best-effort
                pass

        compare: Dict[str, Any] = {}
        if ignore_out:
            compare['ignore'] = ignore_out
        if equiv_out:
            compare['equivalence'] = equiv_out

        return {'compare_answers': compare} if compare else {}

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

    def normalize_config_dict(self, raw: Dict[str, Any]) -> Dict[str, Any]:
        """
        Normalize an entire config dict: resolve all keys and values.
        Handles both old flat syntax and new structured syntax transparently.

        OLD syntax (still works):
            { "scoring_mode": "proportional", "show_correction": "yes",
              "errors_mode": {"[Paris]": 1.5}, "default_false": -1,
              "compare_answers": {"ignore": {"spaces": "all"}} }

        NEW syntax:
            { "score": {"mode": "proportional", "errors": {"[Paris]": 1.5, "default_false": -1}},
              "show_correction": "yes",
              "match": {"ignore": {"spaces": "all"}} }

        MIXED syntax (also works):
            { "score": {"mode": "proportional"}, "show_correction": "yes" }

        i18n works at every level for both syntaxes.
        Values without quotes work if _coerce_scalar already converted them
        (e.g. `case: true` → bool True → passes through).
        """
        out: Dict[str, Any] = {}

        for raw_key, raw_val in raw.items():
            canon_key = self.resolve_config_key(raw_key)

            # ── NEW SYNTAX: `score` block ──────────────────────────────────
            if canon_key == 'score':
                if isinstance(raw_val, dict):
                    decomposed = self._decompose_score_block(raw_val)
                    out.update(decomposed)
                # else: malformed — ignore
                continue

            # ── NEW SYNTAX: `match` block ──────────────────────────────────
            if canon_key == 'match':
                if isinstance(raw_val, dict):
                    decomposed = self._decompose_match_block(raw_val)
                    out.update(decomposed)
                # else: malformed — ignore
                continue

            # ── OLD SYNTAX: nested dict (errors_mode, compare_answers, etc.) ─
            if isinstance(raw_val, dict):
                if canon_key == 'compare_answers':
                    # Normalize ignore sub-keys and spaces values
                    out[canon_key] = self._normalize_compare_answers_dict(raw_val)
                else:
                    # errors_mode and other nested dicts: pass through as-is
                    # (bracket-list keys are answer texts, not config keys)
                    out[canon_key] = raw_val
                continue

            # ── Scalar value ───────────────────────────────────────────────
            canon_val = self.resolve_config_value(canon_key, raw_val)
            out[canon_key] = canon_val

        return out

    def _normalize_compare_answers_dict(self, raw: Dict[str, Any]) -> Dict[str, Any]:
        """
        Normalize a compare_answers dict (old syntax):
            { "ignore": { "spaces": "all", "case": true }, "equivalence": {...} }
        Sub-keys of ignore are i18n-resolved; spaces values are normalized.
        """
        out: Dict[str, Any] = {}
        for raw_sub, raw_val in raw.items():
            canon_sub = self._resolve_subkey('compare_answers', raw_sub)
            if canon_sub == 'ignore' and isinstance(raw_val, dict):
                ignore_out: Dict[str, Any] = {}
                for raw_ign, ign_val in raw_val.items():
                    canon_ign = self._resolve_subkey('ignore', raw_ign)
                    if canon_ign == 'spaces':
                        resolved = self.resolve_config_value('spaces', str(ign_val) if not isinstance(ign_val, str) else ign_val)
                        if isinstance(resolved, str) and resolved.startswith('spaces_'):
                            resolved = resolved[len('spaces_'):]
                        ignore_out['spaces'] = resolved
                    elif canon_ign in ('case', 'diacritics'):
                        ignore_out[canon_ign] = self.resolve_config_value(canon_ign, ign_val)
                    elif canon_ign == 'characters':
                        ignore_out['characters'] = ign_val
                    else:
                        ignore_out[canon_ign] = ign_val
                out['ignore'] = ignore_out
            elif canon_sub == 'equivalence':
                out['equivalence'] = raw_val  # pass through
            else:
                out[canon_sub] = raw_val
        return out

    # ─────────────────────────────────────────────────────────────────────────
    # Keywords for parser (preamble / question / answers section headers)
    # ─────────────────────────────────────────────────────────────────────────

    def get_parser_keywords(self) -> Dict[str, List[str]]:
        """
        Return merged keyword lists for parser use.
        Always includes English keywords; adds current-lang keywords if different.
        """
        en_cfg = self.fallback.get('quiz_configs', {})
        lang_cfg = self.translations.get('quiz_configs', {}) if self.language != self.fallback_language else {}

        def merge_lists(key: str) -> List[str]:
            en_list = en_cfg.get(key, [])
            lang_list = lang_cfg.get(key, [])
            combined = list(en_list)
            for item in lang_list:
                if item not in combined:
                    combined.append(item)
            return combined

        return {
            'preamble': merge_lists('preamble_keywords'),
            'question': merge_lists('question_keywords'),
            'answers':  merge_lists('answers_keywords'),
        }

    # ─────────────────────────────────────────────────────────────────────────
    # Quiz type helpers
    # ─────────────────────────────────────────────────────────────────────────

    def get_quiz_type(self, synonym: str) -> Optional[str]:
        return self.quiz_type_map.get(synonym.lower())

    def get_quiz_synonyms(self, quiz_type: str) -> List[str]:
        quiz_types = self.translations.get('quiz_types', self.fallback.get('quiz_types', {}))
        return quiz_types.get(quiz_type, {}).get('synonyms', [])

    def get_all_quiz_type_synonyms(self) -> List[str]:
        return list(self.quiz_type_map.keys())

    # ─────────────────────────────────────────────────────────────────────────
    # Translation helper
    # ─────────────────────────────────────────────────────────────────────────

    def t(self, key: str, fallback: str = '') -> str:
        keys = key.split('.')
        value = self.translations
        for k in keys:
            if isinstance(value, dict):
                value = value.get(k)
            else:
                value = None
                break
        if value is not None:
            return str(value)

        value = self.fallback
        for k in keys:
            if isinstance(value, dict):
                value = value.get(k)
            else:
                value = None
                break
        if value is not None:
            return str(value)

        return fallback or key


