import _defineProperty from "@babel/runtime/helpers/defineProperty";
/**
 * Fast Lane Predictor: Local autocomplete using weighted trie + frequency + semantic scoring.
 *
 * Two prediction modes:
 *   1. Word boundary → bigram-based next-word suggestion (grammar-filtered)
 *   2. Mid-word (≥3 chars) → trie prefix search → scoring pipeline → top result
 *
 * Scoring is delegated to scoring-pipeline.ts which handles:
 *   Stage 1 (semantic + frequency), grammar filter, Stage 2 (optional LM re-ranking).
 *
 * Context vector: average of word vectors from text before cursor (last N words).
 * Falls back to cold mode (freq-only) when vectors not yet loaded.
 *
 * Session personalization (L1): words the user types are incrementally boosted
 * via incrementSessionFreq(), called on word boundaries from the plugin.
 */

import { EXPERIENCE_NAME, failExp, startExp, succeedExp } from '../analytics/ufo';

// The vocabulary, L3 and word-index JSON payloads are dynamically imported in
// loadDefaultVocabulary / loadVectorsAsync so their (large) contents stay out of
// the editor's main chunk and only load when autocomplete is initialised.
import { isAutocompleteDebugEnabled } from './debug-mode';
import { rankCandidates, STAGE1_WEIGHT, STAGE2_WEIGHT, MIN_STAGE1_SCORE } from './scoring-pipeline';
import { getStoredContextVector, getStoredLmLogits } from './slow-lane-client';

// ─── Constants ───────────────────────────────────────────────────────────────

// eslint-disable-next-line require-unicode-regexp
const PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
// eslint-disable-next-line require-unicode-regexp
const WHITESPACE_SPLIT_REGEX = /\s+/;
// eslint-disable-next-line require-unicode-regexp
const SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
// eslint-disable-next-line require-unicode-regexp
const TRAILING_WHITESPACE_REGEX = /\s$/;
const MIN_PREFIX_LENGTH = 3;
const MAX_CANDIDATES = 200;
const CONTEXT_WORDS = 10;
const MIN_SCORE_THRESHOLD = 0.35;
const L3_BASELINE_FREQ = 0.001;

// ─── Types ───────────────────────────────────────────────────────────────────

class TrieNode {
  constructor() {
    _defineProperty(this, "children", new Map());
    _defineProperty(this, "word", null);
    _defineProperty(this, "tenantFreq", 0);
    _defineProperty(this, "docFreq", 0);
    _defineProperty(this, "authorFreq", 0);
    _defineProperty(this, "sessionFreq", 0);
  }
}
class WeightedWordTrie {
  constructor() {
    _defineProperty(this, "root", new TrieNode());
    /** Highest tenantFreq seen — used to normalize freq scores at query time */
    _defineProperty(this, "maxTenantFreq", 1);
  }
  insert(word, tenantFreq, docFreq, authorFreq) {
    let node = this.root;
    for (const char of word.toLowerCase()) {
      let next = node.children.get(char);
      if (!next) {
        next = new TrieNode();
        node.children.set(char, next);
      }
      node = next;
    }
    node.word = word;
    node.tenantFreq = tenantFreq;
    node.docFreq = docFreq;
    node.authorFreq = authorFreq;
    if (tenantFreq > this.maxTenantFreq) {
      this.maxTenantFreq = tenantFreq;
    }
  }

  /**
   * Return all words matching this prefix, up to maxResults.
   * O(prefix_length + results) — traverses to the prefix node then collects subtree.
   */
  getCandidates(prefix, maxResults = MAX_CANDIDATES) {
    let node = this.root;
    for (const char of prefix.toLowerCase()) {
      const next = node.children.get(char);
      if (!next) {
        return [];
      }
      node = next;
    }
    const candidates = [];
    const stack = [node];
    while (stack.length > 0 && candidates.length < maxResults) {
      const current = stack.pop();
      if (!current) {
        continue;
      }

      // Only add candidates that are longer than the prefix
      if (current.word && current.word.length > prefix.length) {
        candidates.push({
          word: current.word,
          node: current
        });
      }
      for (const child of current.children.values()) {
        stack.push(child);
      }
    }
    return candidates;
  }
  findNode(word) {
    let node = this.root;
    for (const char of word.toLowerCase()) {
      const next = node.children.get(char);
      if (!next) {
        return null;
      }
      node = next;
    }
    return node.word !== null ? node : null;
  }

  /**
   * Set the session frequency for a word.
   * Returns true if the word exists in the trie.
   */
  updateSessionFreq(word, count) {
    const node = this.findNode(word);
    if (!node) {
      return false;
    }
    node.sessionFreq = count;
    return true;
  }

  /**
   * Increment the session frequency for a word by 1.
   * Returns true if the word exists in the trie.
   */
  incrementSessionFreq(word) {
    const node = this.findNode(word);
    if (!node) {
      return false;
    }
    node.sessionFreq += 1;
    return true;
  }
}

// L1/L2 Trie (Session + Atlassian Domain)
const wordTrie = new WeightedWordTrie();

// L3 Trie (General English Fallback)
const l3Trie = new WeightedWordTrie();

/**
 * Loads the General English vocabulary.
 * expects a simple array of strings: ["about", "above", "actually", ...]
 */
export const initL3Vocabulary = l3Words => {
  for (const word of l3Words) {
    // Insert with a tiny baseline frequency so it mathematically
    // loses to any domain word in Stage 1, but still scores above 0.
    l3Trie.insert(word, L3_BASELINE_FREQ, 0, 0);
  }
  if (isAutocompleteDebugEnabled()) {
    // eslint-disable-next-line no-console
    console.log(`[text-predictor] L3 General English loaded: ${l3Words.length} words`);
  }
};

// const bigramMap: Map<string, Record<string, number>> = new Map(
// 	Object.entries(bigramsData as Record<string, Record<string, number>>),
// );

let isInitialized = false;
let vectorStore = null;
let vectorsLoadStarted = false;
let lastPredictionDebug = null;

/** Get vector for a word from the store. */
const getWordVector = word => {
  if (!vectorStore) {
    return null;
  }
  const idx = vectorStore.wordIndex[word.toLowerCase()];
  if (idx === undefined) {
    return null;
  }
  const start = idx * vectorStore.dim;
  return vectorStore.float32.subarray(start, start + vectorStore.dim);
};

/**
 * Compute context vector by averaging vectors of last N words in text.
 * Falls back to null (cold mode) if no words have vectors.
 */
const computeContextVectorLocal = textBefore => {
  if (!vectorStore) {
    return null;
  }
  const tokens = tokenize(textBefore);
  const words = tokens.slice(-CONTEXT_WORDS);
  const vectors = [];
  for (const word of words) {
    const v = getWordVector(word);
    if (v) {
      vectors.push(v);
    }
  }
  if (vectors.length === 0) {
    return null;
  }
  const dim = vectorStore.dim;
  const avg = new Float32Array(dim);
  for (const v of vectors) {
    for (let i = 0; i < dim; i++) {
      avg[i] += v[i];
    }
  }
  for (let i = 0; i < dim; i++) {
    avg[i] /= vectors.length;
  }
  return avg;
};

/**
 * Get context vector for scoring. Prefers Slow Lane (BE) context when available
 * and dimension matches; otherwise falls back to local averaging.
 */
const getContextVectorForScoring = textBefore => {
  const slowLaneVector = getStoredContextVector();
  if (slowLaneVector && vectorStore && slowLaneVector.length === vectorStore.dim) {
    return slowLaneVector;
  }
  return computeContextVectorLocal(textBefore);
};
const tokenize = text => {
  const tokens = [];
  // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
  for (const raw of text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)) {
    // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
    const clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
    if (clean.length >= 2) {
      tokens.push(clean);
    }
  }
  return tokens;
};
const extractPreviousWord = text => {
  // Only consider the current sentence/line the user is typing in.
  const sentences = text.split(SENTENCE_BOUNDARY_REGEX);
  const currentSentence = sentences[sentences.length - 1];
  const words = currentSentence.trimEnd().split(WHITESPACE_SPLIT_REGEX);
  return words.length >= 2 ? words[words.length - 2] : '';
};

// ─── Debug Helpers ───────────────────────────────────────────────────────────

/**
 * Get predictor status for debugging.
 * vectorsLoaded: true when semantic scoring is active
 * wordCount: number of words in vector store (0 if not loaded)
 */
export const getPredictorStatus = () => {
  return {
    vectorsLoaded: vectorStore !== null,
    wordCount: vectorStore ? Object.keys(vectorStore.wordIndex).length : 0,
    vectorsLoadStarted,
    isInitialized
  };
};

/**
 * Get details of the last prediction (for debugging).
 * Returns null if no prediction has run yet or debug was off.
 */
export const getLastPredictionDebug = () => {
  return lastPredictionDebug;
};
export const initVocabulary = vocabulary => {
  for (const term of vocabulary.terms) {
    wordTrie.insert(term.word, term.freq, term.docFreq, term.authorFreq);
  }
  isInitialized = true;
};

/**
 * Increment L1 session frequency for a single word.
 * Called from the plugin on word boundaries for efficient incremental boosting.
 */
export const incrementSessionFreq = word => {
  wordTrie.incrementSessionFreq(word);
};

/**
 * Prime session frequencies from a document page string.
 *
 * Iterates through every token in `pageContent` and increments its session
 * frequency so that words already present on the page receive an L1 boost
 * before the user starts typing.
 *
 * Pass `undefined` (or omit the argument) to skip priming — useful when the
 * calling context does not yet have a page value available.
 */
export const ingestDocumentPage = pageContent => {
  if (!pageContent) {
    return;
  }
  const words = tokenize(pageContent);
  const validBoostedWords = new Set();
  for (const word of words) {
    const didBoost = wordTrie.incrementSessionFreq(word);
    if (didBoost) {
      validBoostedWords.add(word);
    }
  }
  if (isAutocompleteDebugEnabled() && validBoostedWords.size > 0) {
    // eslint-disable-next-line no-console
    console.groupCollapsed(`%c[L1 Session] %cPrimed ${validBoostedWords.size} valid dictionary words from page`, 'color: #00b8d9; font-weight: bold;', 'color: inherit; font-style: italic;');
    // eslint-disable-next-line no-console
    console.dir(Array.from(validBoostedWords).sort());
    // eslint-disable-next-line no-console
    console.groupEnd();
  }
};
export const predict = textBefore => {
  if (!isInitialized) {
    // Vocabulary JSON is code-split and loads asynchronously. Kick off the load
    // and skip this keystroke; the plugin also primes it on focus, so the tries
    // are usually ready before the user types.
    void loadDefaultVocabulary().catch(() => {});
    return null;
  }
  const t0 = performance.now();

  // ── Step 1: Bigram-based next-word suggestion at word boundary ───────────
  // if (textBefore.length > 0 && /\s$/u.test(textBefore)) {
  // 	const words = textBefore.toLowerCase().trimEnd().split(/\s+/u);
  // 	const prevWord = words[words.length - 1];
  // 	const nextWords = bigramMap.get(prevWord);
  // 	if (nextWords) {
  // 		const sorted = Object.entries(nextWords).sort((a, b) => b[1] - a[1]);
  // 		let bestWord = '';
  // 		for (const [word] of sorted) {
  // 			if (!isGrammarAllowed(prevWord, word)) {
  // 				continue;
  // 			}
  // 			bestWord = word;
  // 			break;
  // 		}
  // 		if (bestWord) {
  // 			if (debugMode) {
  // 				const latencyMs = performance.now() - t0;
  // 				console.log(
  // 					'%c[autocomplete] BIGRAM',
  // 					'color:cyan',
  // 					'| "' + prevWord + '" -> "' + bestWord + '" | ' + latencyMs.toFixed(1) + 'ms',
  // 				);
  // 			}
  // 			return bestWord;
  // 		}
  // 	}
  // 	if (debugMode) {
  // 		console.log(
  // 			'%c[autocomplete] BIGRAM-MISS',
  // 			'color:gray',
  // 			'| no bigram for "' + words[words.length - 1] + '", skipping prefix completion',
  // 		);
  // 	}
  // 	return null;
  // }

  // ── Step 2: Prefix completion (≥3 chars typed) ──────────────────────────
  if (textBefore.length > 0 && TRAILING_WHITESPACE_REGEX.test(textBefore)) {
    return null;
  }
  const trimmed = textBefore.trimEnd();
  const lastSpaceIdx = trimmed.lastIndexOf(' ');
  const currentWord = lastSpaceIdx === -1 ? trimmed : trimmed.slice(lastSpaceIdx + 1);
  if (currentWord.length < MIN_PREFIX_LENGTH) {
    return null;
  }
  const candidates = wordTrie.getCandidates(currentWord, MAX_CANDIDATES);

  // Gap-fill from the L3 general-English trie, requesting a full buffer so
  // enough survive de-duplication against the L2 results.
  if (candidates.length < MAX_CANDIDATES) {
    const l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
    const existingWords = new Set(candidates.map(c => c.word));
    for (const l3c of l3Candidates) {
      if (candidates.length >= MAX_CANDIDATES) break;
      if (!existingWords.has(l3c.word)) {
        candidates.push(l3c);
      }
    }
  }

  // If both Tries are completely empty for this prefix
  if (candidates.length === 0) {
    return null;
  }
  const previousWord = extractPreviousWord(trimmed);
  const contextVector = getContextVectorForScoring(trimmed);
  const lmLogits = getStoredLmLogits();

  // Raw LM Output Logger
  if (isAutocompleteDebugEnabled() && lmLogits && Object.keys(lmLogits).length > 0) {
    const rawLmTop = Object.entries(lmLogits).sort((a, b) => b[1] - a[1]).slice(0, 5).map(([word, score]) => ({
      Word: word,
      Prob: Number(score.toFixed(5))
    }));
    // eslint-disable-next-line no-console
    console.log('%c[Raw LM Prediction]🧠', 'color: #e83e8c; font-weight: bold;', rawLmTop);
  }
  const mode = contextVector ? 'warm' : 'cold';

  // Build ScoringCandidate array from TrieNodes
  const scoringCandidates = candidates.map(({
    word,
    node
  }) => ({
    word,
    tenantFreq: node.tenantFreq,
    docFreq: node.docFreq,
    authorFreq: node.authorFreq,
    sessionFreq: node.sessionFreq
  }));

  // Filter the LM payload to only words matching the current prefix so that
  // lmMax in rankCandidates reflects prefix-relevant signal, not the global distribution.
  const prefix = currentWord.toLowerCase();
  const prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(([word]) => word.startsWith(prefix))) : null;
  const {
    candidates: ranked,
    grammarMeta,
    pipelineDebug
  } = rankCandidates(scoringCandidates, contextVector, w => getWordVector(w), prefixLmLogits, wordTrie.maxTenantFreq, previousWord);
  const best = ranked[0];
  const suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
  if (isAutocompleteDebugEnabled()) {
    const latencyMs = performance.now() - t0;
    const tokens = tokenize(trimmed);
    const contextWords = tokens.slice(-CONTEXT_WORDS);
    const belowThreshold = best && best.finalScore < MIN_SCORE_THRESHOLD;
    const suggestionLabel = belowThreshold ? '🚫 (below threshold)' : suggestion && suggestion.length > 0 ? `✨ "${suggestion}"` : '🚫 (no match)';
    lastPredictionDebug = {
      textBefore: trimmed,
      currentWord,
      mode,
      contextWords,
      topCandidates: ranked.slice(0, 5).map(r => ({
        word: r.word,
        finalScore: r.finalScore,
        semanticScore: r.semanticScore,
        freqScore: r.freqScore,
        lmScore: r.lmScore
      })),
      suggestion: suggestion && suggestion.length > 0 ? suggestion : null
    };

    // ── Mode label: COLD / WARM(local) / WARM(BE) ───────────────────────
    const slowLaneVec = getStoredContextVector();
    const isUsingSlowLaneVector = slowLaneVec !== null && vectorStore !== null && slowLaneVec.length === vectorStore.dim;
    const modeLabel = !contextVector ? 'COLD' : isUsingSlowLaneVector ? 'WARM(BE)' : 'WARM(local)';
    const modeColor = modeLabel === 'WARM(BE)' ? 'color: #ff9800; font-weight: bold;' : modeLabel === 'WARM(local)' ? 'color: #4caf50; font-weight: bold;' : 'color: #9e9e9e; font-weight: bold;';

    // 1. Collapsible group header
    // eslint-disable-next-line no-console
    console.groupCollapsed(`%c[Autocomplete] %c${modeLabel} %c| "${currentWord}" ➔ ${suggestionLabel} | ⏱ ${latencyMs.toFixed(1)}ms`, 'color: #00b8d9; font-weight: bold;', modeColor, 'color: inherit; font-weight: normal;');

    // 2. Context window (what local vector averaging sees)
    // eslint-disable-next-line no-console
    console.log('%cContext Window:', 'color: #888; font-style: italic;', contextWords.length ? contextWords.join(' ') : '(none)');

    // 3. Slow-lane status
    const vectorStatus = isUsingSlowLaneVector ? `✅ BE semantic vector (dim=${slowLaneVec === null || slowLaneVec === void 0 ? void 0 : slowLaneVec.length})` : vectorStore ? '⚠️ Local vector average (slow-lane not yet returned)' : '❌ No vectors (cold)';
    const logitsStatus = lmLogits && Object.keys(lmLogits).length > 0 ? `✅ LM logits active (${Object.keys(lmLogits).length} tokens)` : '⏳ No LM logits (slow-lane pending or failed)';
    // eslint-disable-next-line no-console
    console.log('%cSlow Lane:', 'color: #888; font-style: italic;', vectorStatus, '|', logitsStatus);

    // 4. Scoring formula active this prediction
    const formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? `Stage1(×${STAGE1_WEIGHT}) + LM(×${STAGE2_WEIGHT})` : 'Stage1 only (no LM logits)';
    // eslint-disable-next-line no-console
    console.log('%cFormula:', 'color: #888; font-style: italic;', formulaLabel);

    // 5. Grammar filter result (collected inside rankCandidates, logged here)
    if (grammarMeta) {
      // eslint-disable-next-line no-console
      console.log(`%c[Grammar] "${grammarMeta.prevWord}" [${grammarMeta.prevTags.join('|')}] → ${grammarMeta.before} candidates → ${grammarMeta.after} after filter`, 'color: #4caf50; font-weight: bold;');
      if (grammarMeta.dropped.length > 0) {
        // eslint-disable-next-line no-console
        console.log(`%c🚫 Dropped: ${grammarMeta.dropped.join(', ')}`, 'color: #f44336; font-style: italic;');
      }
    }

    // 6. Pipeline funnel
    // eslint-disable-next-line no-console
    console.log(`%c[Pipeline Funnel] %c📥 In: ${pipelineDebug.initial} | ❌ Stage 1 (< ${MIN_STAGE1_SCORE}): -${pipelineDebug.stage1Rejected.length} | ❌ Grammar: -${pipelineDebug.grammarRejected.length} | ✅ Final: ${pipelineDebug.final}`, 'color: #9c27b0; font-weight: bold;', 'color: inherit;');

    // 7. Candidate table
    if (ranked.length > 0) {
      const lmCoverage = ranked.slice(0, 10).filter(r => r.lmScore > 0).length;
      // eslint-disable-next-line no-console
      console.log(`%cLM coverage: ${lmCoverage}/${Math.min(ranked.length, 10)} candidates had real logit scores`, 'color: #888; font-style: italic;');
      const tableData = ranked.slice(0, 10).map(r => {
        let rawLogit = 'Not in Payload';
        if (prefixLmLogits) {
          const val = prefixLmLogits[r.word.toLowerCase()];
          if (val !== undefined) {
            rawLogit = Number(val.toFixed(5));
          }
        }
        const original = scoringCandidates.find(sc => sc.word === r.word);
        let source = 'Unknown';
        if (original) {
          if (original.docFreq === 0 && original.tenantFreq === L3_BASELINE_FREQ) {
            source = '🌍 L3 (Generic)';
          } else if (original.sessionFreq > 0 && original.tenantFreq === 0) {
            source = '👤 L1 (Session Only)';
          } else {
            source = '🏢 L2 (Domain)';
          }
        }
        return {
          Candidate: r.word,
          Source: source,
          'Final Score': Number(r.finalScore.toFixed(4)),
          Semantics: Number(r.semanticScore.toFixed(4)),
          Freq: Number(r.freqScore.toFixed(4)),
          'LM Score': Number(r.lmScore.toFixed(4)),
          'Raw Logit': rawLogit,
          'Session Freq': (original === null || original === void 0 ? void 0 : original.sessionFreq) || 0
        };
      });

      // eslint-disable-next-line no-console
      console.table(tableData);
    } else {
      // eslint-disable-next-line no-console
      console.log('No candidates found.');
    }

    // eslint-disable-next-line no-console
    console.groupEnd();
  }
  return suggestion && suggestion.length > 0 ? suggestion : null;
};

// ─── Data Loading ────────────────────────────────────────────────────────────

/**
 * Unwrap a dynamically imported JSON module to its parsed value, handling both
 * interop modes AFM's bundler chain emits: a `.default`-wrapped namespace
 * (classic webpack) and a named-exports namespace (webpack 5 / atlaspack JSON
 * modules, where `default` can be a misleading scalar). Named exports are
 * preferred when present. The caller declares the JSON `shape` because a dense
 * array and a sparse numeric-keyed object are emitted identically as named
 * exports. Kept in lock-step with the matching helper in local-slow-lane-client.ts.
 */
function unwrapJsonModule(mod, shape) {
  if (mod == null || typeof mod !== 'object') {
    return null;
  }
  const namespace = mod;
  const ownKeys = Object.keys(namespace).filter(k => k !== 'default' && k !== '__esModule');
  if (ownKeys.length > 0) {
    if (shape === 'array') {
      const len = ownKeys.length;
      const arr = new Array(len);
      for (let i = 0; i < len; i++) {
        arr[i] = namespace[String(i)];
      }
      return arr;
    }
    const obj = {};
    for (const k of ownKeys) {
      obj[k] = namespace[k];
    }
    return obj;
  }
  if ('default' in namespace && namespace.default != null) {
    return namespace.default;
  }
  return null;
}
export const loadVectorsAsync = async options => {
  var _options$isLocalLLM;
  if (vectorStore || vectorsLoadStarted) {
    return;
  }
  if (!(options !== null && options !== void 0 && options.getBinaryUrl)) {
    // eslint-disable-next-line no-console
    console.warn('[text-predictor] loadVectorsAsync called without a getBinaryUrl — vectors will not load. Pass getVectorsBinaryUrl via plugin options.');
    return;
  }
  const isLocalLLM = (_options$isLocalLLM = options === null || options === void 0 ? void 0 : options.isLocalLLM) !== null && _options$isLocalLLM !== void 0 ? _options$isLocalLLM : false;
  vectorsLoadStarted = true;
  startExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
    isLocalLLM
  });
  let url;
  try {
    url = await options.getBinaryUrl();
  } catch (e) {
    vectorsLoadStarted = false;
    failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
      isLocalLLM,
      errorType: 'resolve_url'
    });
    // eslint-disable-next-line no-console
    console.warn('[text-predictor] Failed to resolve vectors URL:', e);
    return;
  }
  try {
    var _wordIndexOuter$index;
    const res = await fetch(url);
    if (!res.ok) {
      vectorsLoadStarted = false;
      failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
        isLocalLLM,
        status: res.status,
        errorType: 'http_error'
      });
      // eslint-disable-next-line no-console
      console.warn(`[text-predictor] Failed to load vectors: ${res.status}`);
      return;
    }
    const buffer = await res.arrayBuffer();
    const float32 = new Float32Array(buffer);

    // word_index_10k.json is wrapped as `{ "index": {…} }` so no real entry
    // (e.g. the word "default") can shadow the synthetic ESM `default` export
    // the bundler creates for dynamically-imported JSON.
    const wordIndexModule = await import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */'./data/word_index_10k.json');
    const wordIndexOuter = unwrapJsonModule(wordIndexModule, 'object');
    const wordIndex = (_wordIndexOuter$index = wordIndexOuter === null || wordIndexOuter === void 0 ? void 0 : wordIndexOuter.index) !== null && _wordIndexOuter$index !== void 0 ? _wordIndexOuter$index : {};
    const nWords = Object.keys(wordIndex).length;
    if (nWords === 0) {
      // eslint-disable-next-line no-console
      console.warn('[text-predictor] word_index_10k.json missing its `index` wrapper — wordIndex is empty, semantic scoring will be a no-op.');
    }
    const dim = float32.length / nWords;
    vectorStore = {
      float32,
      wordIndex,
      dim
    };
    succeedExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
      isLocalLLM,
      wordCount: nWords,
      dim,
      sizeBytes: float32.byteLength
    });
    if (isAutocompleteDebugEnabled()) {
      // eslint-disable-next-line no-console
      console.log('[text-predictor] Vectors loaded:', {
        wordCount: nWords,
        dim,
        sizeBytes: float32.byteLength
      });
    }
  } catch (e) {
    vectorsLoadStarted = false;
    failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
      isLocalLLM,
      errorType: 'network'
    });
    // eslint-disable-next-line no-console
    console.warn('[text-predictor] Failed to load vectors:', e);
  }
};
export const initVectors = store => {
  vectorStore = store;
};
let vocabularyLoadPromise;
export const loadDefaultVocabulary = options => {
  var _options$isLocalLLM2;
  if (isInitialized) {
    return Promise.resolve();
  }
  if (vocabularyLoadPromise) {
    return vocabularyLoadPromise;
  }
  const isLocalLLM = (_options$isLocalLLM2 = options === null || options === void 0 ? void 0 : options.isLocalLLM) !== null && _options$isLocalLLM2 !== void 0 ? _options$isLocalLLM2 : false;
  vocabularyLoadPromise = (async () => {
    startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
      isLocalLLM
    });
    try {
      // The L2 vocabulary and L3 word list are code-split into their own async
      // chunks so they stay out of the editor's main bundle.
      const [vocabularyModule, l3VocabularyModule] = await Promise.all([import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-vocabulary-10k" */'./data/vocabulary_10k.json'), import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-l3-vocabulary" */'./data/l3_vocabulary.json')]);
      const vocabularyData = unwrapJsonModule(vocabularyModule, 'object');
      const l3VocabularyData = unwrapJsonModule(l3VocabularyModule, 'array');
      if ((vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null || !Array.isArray(l3VocabularyData)) {
        throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
      }
      const terms = Object.entries(vocabularyData.words).map(([word, stats]) => ({
        word,
        freq: stats.freq,
        docFreq: stats.doc_freq,
        authorFreq: stats.author_freq
      }));

      // Load L3 before L2: initVocabulary flips `isInitialized = true`, so it
      // must run last — otherwise a throw in initL3Vocabulary would strand
      // `isInitialized` true and the retry path could never reload L3.
      initL3Vocabulary(l3VocabularyData);
      initVocabulary({
        terms
      });
      succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
        isLocalLLM,
        l2WordCount: terms.length,
        l3WordCount: l3VocabularyData.length
      });
    } catch (e) {
      failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
        isLocalLLM,
        errorType: 'parse_error'
      });
      // Allow a later call to retry the load rather than caching the failure.
      vocabularyLoadPromise = undefined;
      throw e;
    }
  })();
  return vocabularyLoadPromise;
};