import _asyncToGenerator from "@babel/runtime/helpers/asyncToGenerator";
import _typeof from "@babel/runtime/helpers/typeof";
import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
import _createClass from "@babel/runtime/helpers/createClass";
import _classCallCheck from "@babel/runtime/helpers/classCallCheck";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _regeneratorRuntime from "@babel/runtime/regenerator";
function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
/**
 * 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
var PUNCTUATION_BOUNDARY_REGEX = /^[.,;:!?()\[\]{}"'`]+|[.,;:!?()\[\]{}"'`]+$/g;
// eslint-disable-next-line require-unicode-regexp
var WHITESPACE_SPLIT_REGEX = /\s+/;
// eslint-disable-next-line require-unicode-regexp
var SENTENCE_BOUNDARY_REGEX = /[\n.?!]+/;
// eslint-disable-next-line require-unicode-regexp
var TRAILING_WHITESPACE_REGEX = /\s$/;
var MIN_PREFIX_LENGTH = 3;
var MAX_CANDIDATES = 200;
var CONTEXT_WORDS = 10;
var MIN_SCORE_THRESHOLD = 0.35;
var L3_BASELINE_FREQ = 0.001;

// ─── Types ───────────────────────────────────────────────────────────────────
var TrieNode = /*#__PURE__*/_createClass(function TrieNode() {
  _classCallCheck(this, TrieNode);
  _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);
});
var WeightedWordTrie = /*#__PURE__*/function () {
  function WeightedWordTrie() {
    _classCallCheck(this, WeightedWordTrie);
    _defineProperty(this, "root", new TrieNode());
    /** Highest tenantFreq seen — used to normalize freq scores at query time */
    _defineProperty(this, "maxTenantFreq", 1);
  }
  return _createClass(WeightedWordTrie, [{
    key: "insert",
    value: function insert(word, tenantFreq, docFreq, authorFreq) {
      var node = this.root;
      var _iterator = _createForOfIteratorHelper(word.toLowerCase()),
        _step;
      try {
        for (_iterator.s(); !(_step = _iterator.n()).done;) {
          var char = _step.value;
          var next = node.children.get(char);
          if (!next) {
            next = new TrieNode();
            node.children.set(char, next);
          }
          node = next;
        }
      } catch (err) {
        _iterator.e(err);
      } finally {
        _iterator.f();
      }
      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.
     */
  }, {
    key: "getCandidates",
    value: function getCandidates(prefix) {
      var maxResults = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : MAX_CANDIDATES;
      var node = this.root;
      var _iterator2 = _createForOfIteratorHelper(prefix.toLowerCase()),
        _step2;
      try {
        for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
          var char = _step2.value;
          var next = node.children.get(char);
          if (!next) {
            return [];
          }
          node = next;
        }
      } catch (err) {
        _iterator2.e(err);
      } finally {
        _iterator2.f();
      }
      var candidates = [];
      var stack = [node];
      while (stack.length > 0 && candidates.length < maxResults) {
        var 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
          });
        }
        var _iterator3 = _createForOfIteratorHelper(current.children.values()),
          _step3;
        try {
          for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
            var child = _step3.value;
            stack.push(child);
          }
        } catch (err) {
          _iterator3.e(err);
        } finally {
          _iterator3.f();
        }
      }
      return candidates;
    }
  }, {
    key: "findNode",
    value: function findNode(word) {
      var node = this.root;
      var _iterator4 = _createForOfIteratorHelper(word.toLowerCase()),
        _step4;
      try {
        for (_iterator4.s(); !(_step4 = _iterator4.n()).done;) {
          var char = _step4.value;
          var next = node.children.get(char);
          if (!next) {
            return null;
          }
          node = next;
        }
      } catch (err) {
        _iterator4.e(err);
      } finally {
        _iterator4.f();
      }
      return node.word !== null ? node : null;
    }

    /**
     * Set the session frequency for a word.
     * Returns true if the word exists in the trie.
     */
  }, {
    key: "updateSessionFreq",
    value: function updateSessionFreq(word, count) {
      var 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.
     */
  }, {
    key: "incrementSessionFreq",
    value: function incrementSessionFreq(word) {
      var node = this.findNode(word);
      if (!node) {
        return false;
      }
      node.sessionFreq += 1;
      return true;
    }
  }]);
}(); // L1/L2 Trie (Session + Atlassian Domain)
var wordTrie = new WeightedWordTrie();

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

/**
 * Loads the General English vocabulary.
 * expects a simple array of strings: ["about", "above", "actually", ...]
 */
export var initL3Vocabulary = function initL3Vocabulary(l3Words) {
  var _iterator5 = _createForOfIteratorHelper(l3Words),
    _step5;
  try {
    for (_iterator5.s(); !(_step5 = _iterator5.n()).done;) {
      var word = _step5.value;
      // 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);
    }
  } catch (err) {
    _iterator5.e(err);
  } finally {
    _iterator5.f();
  }
  if (isAutocompleteDebugEnabled()) {
    // eslint-disable-next-line no-console
    console.log("[text-predictor] L3 General English loaded: ".concat(l3Words.length, " words"));
  }
};

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

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

/** Get vector for a word from the store. */
var getWordVector = function getWordVector(word) {
  if (!vectorStore) {
    return null;
  }
  var idx = vectorStore.wordIndex[word.toLowerCase()];
  if (idx === undefined) {
    return null;
  }
  var 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.
 */
var computeContextVectorLocal = function computeContextVectorLocal(textBefore) {
  if (!vectorStore) {
    return null;
  }
  var tokens = tokenize(textBefore);
  var words = tokens.slice(-CONTEXT_WORDS);
  var vectors = [];
  var _iterator6 = _createForOfIteratorHelper(words),
    _step6;
  try {
    for (_iterator6.s(); !(_step6 = _iterator6.n()).done;) {
      var word = _step6.value;
      var _v = getWordVector(word);
      if (_v) {
        vectors.push(_v);
      }
    }
  } catch (err) {
    _iterator6.e(err);
  } finally {
    _iterator6.f();
  }
  if (vectors.length === 0) {
    return null;
  }
  var dim = vectorStore.dim;
  var avg = new Float32Array(dim);
  for (var _i = 0, _vectors = vectors; _i < _vectors.length; _i++) {
    var v = _vectors[_i];
    for (var i = 0; i < dim; i++) {
      avg[i] += v[i];
    }
  }
  for (var _i2 = 0; _i2 < dim; _i2++) {
    avg[_i2] /= 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.
 */
var getContextVectorForScoring = function getContextVectorForScoring(textBefore) {
  var slowLaneVector = getStoredContextVector();
  if (slowLaneVector && vectorStore && slowLaneVector.length === vectorStore.dim) {
    return slowLaneVector;
  }
  return computeContextVectorLocal(textBefore);
};
var tokenize = function tokenize(text) {
  var tokens = [];
  // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
  var _iterator7 = _createForOfIteratorHelper(text.toLowerCase().split(WHITESPACE_SPLIT_REGEX)),
    _step7;
  try {
    for (_iterator7.s(); !(_step7 = _iterator7.n()).done;) {
      var raw = _step7.value;
      // eslint-disable-next-line @atlassian/perf-linting/no-expensive-split-replace
      var clean = raw.replace(PUNCTUATION_BOUNDARY_REGEX, '');
      if (clean.length >= 2) {
        tokens.push(clean);
      }
    }
  } catch (err) {
    _iterator7.e(err);
  } finally {
    _iterator7.f();
  }
  return tokens;
};
var extractPreviousWord = function extractPreviousWord(text) {
  // Only consider the current sentence/line the user is typing in.
  var sentences = text.split(SENTENCE_BOUNDARY_REGEX);
  var currentSentence = sentences[sentences.length - 1];
  var 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 var getPredictorStatus = function getPredictorStatus() {
  return {
    vectorsLoaded: vectorStore !== null,
    wordCount: vectorStore ? Object.keys(vectorStore.wordIndex).length : 0,
    vectorsLoadStarted: vectorsLoadStarted,
    isInitialized: isInitialized
  };
};

/**
 * Get details of the last prediction (for debugging).
 * Returns null if no prediction has run yet or debug was off.
 */
export var getLastPredictionDebug = function getLastPredictionDebug() {
  return lastPredictionDebug;
};
export var initVocabulary = function initVocabulary(vocabulary) {
  var _iterator8 = _createForOfIteratorHelper(vocabulary.terms),
    _step8;
  try {
    for (_iterator8.s(); !(_step8 = _iterator8.n()).done;) {
      var term = _step8.value;
      wordTrie.insert(term.word, term.freq, term.docFreq, term.authorFreq);
    }
  } catch (err) {
    _iterator8.e(err);
  } finally {
    _iterator8.f();
  }
  isInitialized = true;
};

/**
 * Increment L1 session frequency for a single word.
 * Called from the plugin on word boundaries for efficient incremental boosting.
 */
export var incrementSessionFreq = function 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 var ingestDocumentPage = function ingestDocumentPage(pageContent) {
  if (!pageContent) {
    return;
  }
  var words = tokenize(pageContent);
  var validBoostedWords = new Set();
  var _iterator9 = _createForOfIteratorHelper(words),
    _step9;
  try {
    for (_iterator9.s(); !(_step9 = _iterator9.n()).done;) {
      var word = _step9.value;
      var didBoost = wordTrie.incrementSessionFreq(word);
      if (didBoost) {
        validBoostedWords.add(word);
      }
    }
  } catch (err) {
    _iterator9.e(err);
  } finally {
    _iterator9.f();
  }
  if (isAutocompleteDebugEnabled() && validBoostedWords.size > 0) {
    // eslint-disable-next-line no-console
    console.groupCollapsed("%c[L1 Session] %cPrimed ".concat(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 var predict = function 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(function () {});
    return null;
  }
  var 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;
  }
  var trimmed = textBefore.trimEnd();
  var lastSpaceIdx = trimmed.lastIndexOf(' ');
  var currentWord = lastSpaceIdx === -1 ? trimmed : trimmed.slice(lastSpaceIdx + 1);
  if (currentWord.length < MIN_PREFIX_LENGTH) {
    return null;
  }
  var 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) {
    var l3Candidates = l3Trie.getCandidates(currentWord, MAX_CANDIDATES);
    var existingWords = new Set(candidates.map(function (c) {
      return c.word;
    }));
    var _iterator0 = _createForOfIteratorHelper(l3Candidates),
      _step0;
    try {
      for (_iterator0.s(); !(_step0 = _iterator0.n()).done;) {
        var l3c = _step0.value;
        if (candidates.length >= MAX_CANDIDATES) break;
        if (!existingWords.has(l3c.word)) {
          candidates.push(l3c);
        }
      }
    } catch (err) {
      _iterator0.e(err);
    } finally {
      _iterator0.f();
    }
  }

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

  // Raw LM Output Logger
  if (isAutocompleteDebugEnabled() && lmLogits && Object.keys(lmLogits).length > 0) {
    var rawLmTop = Object.entries(lmLogits).sort(function (a, b) {
      return b[1] - a[1];
    }).slice(0, 5).map(function (_ref) {
      var _ref2 = _slicedToArray(_ref, 2),
        word = _ref2[0],
        score = _ref2[1];
      return {
        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);
  }
  var mode = contextVector ? 'warm' : 'cold';

  // Build ScoringCandidate array from TrieNodes
  var scoringCandidates = candidates.map(function (_ref3) {
    var word = _ref3.word,
      node = _ref3.node;
    return {
      word: 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.
  var prefix = currentWord.toLowerCase();
  var prefixLmLogits = lmLogits ? Object.fromEntries(Object.entries(lmLogits).filter(function (_ref4) {
    var _ref5 = _slicedToArray(_ref4, 1),
      word = _ref5[0];
    return word.startsWith(prefix);
  })) : null;
  var _rankCandidates = rankCandidates(scoringCandidates, contextVector, function (w) {
      return getWordVector(w);
    }, prefixLmLogits, wordTrie.maxTenantFreq, previousWord),
    ranked = _rankCandidates.candidates,
    grammarMeta = _rankCandidates.grammarMeta,
    pipelineDebug = _rankCandidates.pipelineDebug;
  var best = ranked[0];
  var suggestion = best && best.finalScore >= MIN_SCORE_THRESHOLD ? best.word.slice(currentWord.length) : null;
  if (isAutocompleteDebugEnabled()) {
    var latencyMs = performance.now() - t0;
    var tokens = tokenize(trimmed);
    var contextWords = tokens.slice(-CONTEXT_WORDS);
    var belowThreshold = best && best.finalScore < MIN_SCORE_THRESHOLD;
    var suggestionLabel = belowThreshold ? '🚫 (below threshold)' : suggestion && suggestion.length > 0 ? "\u2728 \"".concat(suggestion, "\"") : '🚫 (no match)';
    lastPredictionDebug = {
      textBefore: trimmed,
      currentWord: currentWord,
      mode: mode,
      contextWords: contextWords,
      topCandidates: ranked.slice(0, 5).map(function (r) {
        return {
          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) ───────────────────────
    var slowLaneVec = getStoredContextVector();
    var isUsingSlowLaneVector = slowLaneVec !== null && vectorStore !== null && slowLaneVec.length === vectorStore.dim;
    var modeLabel = !contextVector ? 'COLD' : isUsingSlowLaneVector ? 'WARM(BE)' : 'WARM(local)';
    var 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".concat(modeLabel, " %c| \"").concat(currentWord, "\" \u2794 ").concat(suggestionLabel, " | \u23F1 ").concat(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
    var vectorStatus = isUsingSlowLaneVector ? "\u2705 BE semantic vector (dim=".concat(slowLaneVec === null || slowLaneVec === void 0 ? void 0 : slowLaneVec.length, ")") : vectorStore ? '⚠️ Local vector average (slow-lane not yet returned)' : '❌ No vectors (cold)';
    var logitsStatus = lmLogits && Object.keys(lmLogits).length > 0 ? "\u2705 LM logits active (".concat(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
    var formulaLabel = lmLogits && Object.keys(lmLogits).length > 0 ? "Stage1(\xD7".concat(STAGE1_WEIGHT, ") + LM(\xD7").concat(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] \"".concat(grammarMeta.prevWord, "\" [").concat(grammarMeta.prevTags.join('|'), "] \u2192 ").concat(grammarMeta.before, " candidates \u2192 ").concat(grammarMeta.after, " after filter"), 'color: #4caf50; font-weight: bold;');
      if (grammarMeta.dropped.length > 0) {
        // eslint-disable-next-line no-console
        console.log("%c\uD83D\uDEAB Dropped: ".concat(grammarMeta.dropped.join(', ')), 'color: #f44336; font-style: italic;');
      }
    }

    // 6. Pipeline funnel
    // eslint-disable-next-line no-console
    console.log("%c[Pipeline Funnel] %c\uD83D\uDCE5 In: ".concat(pipelineDebug.initial, " | \u274C Stage 1 (< ").concat(MIN_STAGE1_SCORE, "): -").concat(pipelineDebug.stage1Rejected.length, " | \u274C Grammar: -").concat(pipelineDebug.grammarRejected.length, " | \u2705 Final: ").concat(pipelineDebug.final), 'color: #9c27b0; font-weight: bold;', 'color: inherit;');

    // 7. Candidate table
    if (ranked.length > 0) {
      var lmCoverage = ranked.slice(0, 10).filter(function (r) {
        return r.lmScore > 0;
      }).length;
      // eslint-disable-next-line no-console
      console.log("%cLM coverage: ".concat(lmCoverage, "/").concat(Math.min(ranked.length, 10), " candidates had real logit scores"), 'color: #888; font-style: italic;');
      var tableData = ranked.slice(0, 10).map(function (r) {
        var rawLogit = 'Not in Payload';
        if (prefixLmLogits) {
          var val = prefixLmLogits[r.word.toLowerCase()];
          if (val !== undefined) {
            rawLogit = Number(val.toFixed(5));
          }
        }
        var original = scoringCandidates.find(function (sc) {
          return sc.word === r.word;
        });
        var 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;
  }
  var namespace = mod;
  var ownKeys = Object.keys(namespace).filter(function (k) {
    return k !== 'default' && k !== '__esModule';
  });
  if (ownKeys.length > 0) {
    if (shape === 'array') {
      var len = ownKeys.length;
      var arr = new Array(len);
      for (var i = 0; i < len; i++) {
        arr[i] = namespace[String(i)];
      }
      return arr;
    }
    var obj = {};
    var _iterator1 = _createForOfIteratorHelper(ownKeys),
      _step1;
    try {
      for (_iterator1.s(); !(_step1 = _iterator1.n()).done;) {
        var k = _step1.value;
        obj[k] = namespace[k];
      }
    } catch (err) {
      _iterator1.e(err);
    } finally {
      _iterator1.f();
    }
    return obj;
  }
  if ('default' in namespace && namespace.default != null) {
    return namespace.default;
  }
  return null;
}
export var loadVectorsAsync = /*#__PURE__*/function () {
  var _ref6 = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee(options) {
    var _options$isLocalLLM;
    var isLocalLLM, url, _wordIndexOuter$index, res, buffer, float32, wordIndexModule, wordIndexOuter, wordIndex, nWords, dim, _t, _t2;
    return _regeneratorRuntime.wrap(function (_context) {
      while (1) switch (_context.prev = _context.next) {
        case 0:
          if (!(vectorStore || vectorsLoadStarted)) {
            _context.next = 1;
            break;
          }
          return _context.abrupt("return");
        case 1:
          if (options !== null && options !== void 0 && options.getBinaryUrl) {
            _context.next = 2;
            break;
          }
          // 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 _context.abrupt("return");
        case 2:
          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: isLocalLLM
          });
          _context.prev = 3;
          _context.next = 4;
          return options.getBinaryUrl();
        case 4:
          url = _context.sent;
          _context.next = 6;
          break;
        case 5:
          _context.prev = 5;
          _t = _context["catch"](3);
          vectorsLoadStarted = false;
          failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
            isLocalLLM: isLocalLLM,
            errorType: 'resolve_url'
          });
          // eslint-disable-next-line no-console
          console.warn('[text-predictor] Failed to resolve vectors URL:', _t);
          return _context.abrupt("return");
        case 6:
          _context.prev = 6;
          _context.next = 7;
          return fetch(url);
        case 7:
          res = _context.sent;
          if (res.ok) {
            _context.next = 8;
            break;
          }
          vectorsLoadStarted = false;
          failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
            isLocalLLM: isLocalLLM,
            status: res.status,
            errorType: 'http_error'
          });
          // eslint-disable-next-line no-console
          console.warn("[text-predictor] Failed to load vectors: ".concat(res.status));
          return _context.abrupt("return");
        case 8:
          _context.next = 9;
          return res.arrayBuffer();
        case 9:
          buffer = _context.sent;
          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.
          _context.next = 10;
          return import( /* webpackChunkName: "@atlaskit-internal_editor-plugin-autocomplete-word-index-10k" */'./data/word_index_10k.json');
        case 10:
          wordIndexModule = _context.sent;
          wordIndexOuter = unwrapJsonModule(wordIndexModule, 'object');
          wordIndex = (_wordIndexOuter$index = wordIndexOuter === null || wordIndexOuter === void 0 ? void 0 : wordIndexOuter.index) !== null && _wordIndexOuter$index !== void 0 ? _wordIndexOuter$index : {};
          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.');
          }
          dim = float32.length / nWords;
          vectorStore = {
            float32: float32,
            wordIndex: wordIndex,
            dim: dim
          };
          succeedExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
            isLocalLLM: isLocalLLM,
            wordCount: nWords,
            dim: dim,
            sizeBytes: float32.byteLength
          });
          if (isAutocompleteDebugEnabled()) {
            // eslint-disable-next-line no-console
            console.log('[text-predictor] Vectors loaded:', {
              wordCount: nWords,
              dim: dim,
              sizeBytes: float32.byteLength
            });
          }
          _context.next = 12;
          break;
        case 11:
          _context.prev = 11;
          _t2 = _context["catch"](6);
          vectorsLoadStarted = false;
          failExp(EXPERIENCE_NAME.LOAD_VECTORS, 'singleton', {
            isLocalLLM: isLocalLLM,
            errorType: 'network'
          });
          // eslint-disable-next-line no-console
          console.warn('[text-predictor] Failed to load vectors:', _t2);
        case 12:
        case "end":
          return _context.stop();
      }
    }, _callee, null, [[3, 5], [6, 11]]);
  }));
  return function loadVectorsAsync(_x) {
    return _ref6.apply(this, arguments);
  };
}();
export var initVectors = function initVectors(store) {
  vectorStore = store;
};
var vocabularyLoadPromise;
export var loadDefaultVocabulary = function loadDefaultVocabulary(options) {
  var _options$isLocalLLM2;
  if (isInitialized) {
    return Promise.resolve();
  }
  if (vocabularyLoadPromise) {
    return vocabularyLoadPromise;
  }
  var isLocalLLM = (_options$isLocalLLM2 = options === null || options === void 0 ? void 0 : options.isLocalLLM) !== null && _options$isLocalLLM2 !== void 0 ? _options$isLocalLLM2 : false;
  vocabularyLoadPromise = _asyncToGenerator( /*#__PURE__*/_regeneratorRuntime.mark(function _callee2() {
    var _yield$Promise$all, _yield$Promise$all2, vocabularyModule, l3VocabularyModule, vocabularyData, l3VocabularyData, terms, _t3;
    return _regeneratorRuntime.wrap(function (_context2) {
      while (1) switch (_context2.prev = _context2.next) {
        case 0:
          startExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
            isLocalLLM: isLocalLLM
          });
          _context2.prev = 1;
          _context2.next = 2;
          return 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')]);
        case 2:
          _yield$Promise$all = _context2.sent;
          _yield$Promise$all2 = _slicedToArray(_yield$Promise$all, 2);
          vocabularyModule = _yield$Promise$all2[0];
          l3VocabularyModule = _yield$Promise$all2[1];
          vocabularyData = unwrapJsonModule(vocabularyModule, 'object');
          l3VocabularyData = unwrapJsonModule(l3VocabularyModule, 'array');
          if (!((vocabularyData === null || vocabularyData === void 0 ? void 0 : vocabularyData.words) == null || !Array.isArray(l3VocabularyData))) {
            _context2.next = 3;
            break;
          }
          throw new Error('[text-predictor] vocabulary JSON modules could not be unwrapped');
        case 3:
          terms = Object.entries(vocabularyData.words).map(function (_ref8) {
            var _ref9 = _slicedToArray(_ref8, 2),
              word = _ref9[0],
              stats = _ref9[1];
            return {
              word: 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: terms
          });
          succeedExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
            isLocalLLM: isLocalLLM,
            l2WordCount: terms.length,
            l3WordCount: l3VocabularyData.length
          });
          _context2.next = 5;
          break;
        case 4:
          _context2.prev = 4;
          _t3 = _context2["catch"](1);
          failExp(EXPERIENCE_NAME.LOAD_VOCABULARY, 'singleton', {
            isLocalLLM: isLocalLLM,
            errorType: 'parse_error'
          });
          // Allow a later call to retry the load rather than caching the failure.
          vocabularyLoadPromise = undefined;
          throw _t3;
        case 5:
        case "end":
          return _context2.stop();
      }
    }, _callee2, null, [[1, 4]]);
  }))();
  return vocabularyLoadPromise;
};