import _slicedToArray from "@babel/runtime/helpers/slicedToArray";
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; }
/**
 * Scoring Pipeline: Stage 1 (Semantic + Frequency), Grammar Filter, Stage 2 (LM Re-ranking).
 *
 * Operates synchronously on pre-loaded data. Each stage gracefully degrades
 * when its required data isn't available (cold → warm → full warm).
 */

import posTagsData from './data/combined_l2_l3_pos_tags.json';
import ghostPosTagsData from './data/ghost_pos_tags.json';
import grammarTransitionsData from './data/grammar_transitions_10k.json';

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

/** Metadata returned by the grammar filter for debug logging in the caller. */

// ─── Scoring Constants ──────────────────────────────────────

var ALPHA = 0.5;
var BETA = 0.5;
var NEUTRAL_SCORE = 0.5;
export var STAGE1_WEIGHT = 0.35;
export var STAGE2_WEIGHT = 0.65;
export var MIN_STAGE1_SCORE = 0.35;
var L1_SESSION_CAP = 1.2;
// Minimum prefix-payload max LM probability before Stage 2 activates.
// Below this threshold the LM signal is too weak to suppress Stage 1 — finalScore
// falls back to stage1Score directly. Prevents weak prefixes (e.g. "ins" → "instances"
// at 0.00024) from triggering re-ranking.
var LM_GATE_THRESHOLD = 0.0005;

// ─── Grammar Data (loaded once on import) ───────────────────

var posTags = new Map(Object.entries(posTagsData));
var grammarTransitions = grammarTransitionsData;

/**
 * Precomputed map from each POS tag to the set of allowed next POS tags.
 * Built once at module load from grammarTransitions so applyGrammarFilter
 * never re-iterates the transition rules per call.
 */
var precomputedAllowedByPos = new Map(Object.entries(grammarTransitions.transitions).map(function (_ref) {
  var _ref2 = _slicedToArray(_ref, 2),
    pos = _ref2[0],
    rule = _ref2[1];
  return [pos, new Set(rule.allowed)];
}));

// ─── Math ───────────────────────────────────────────────────

function cosineSimilarity(a, b) {
  var dot = 0;
  var normA = 0;
  var normB = 0;
  for (var i = 0; i < a.length; i++) {
    dot += a[i] * b[i];
    normA += a[i] * a[i];
    normB += b[i] * b[i];
  }
  var dNormA = Math.sqrt(normA);
  var dNormB = Math.sqrt(normB);
  if (dNormA === 0 || dNormB === 0) {
    return NEUTRAL_SCORE;
  }
  return (1 + dot / (dNormA * dNormB)) / 2;
}

// ─── Stage 1: Semantic + Frequency ──────────────────────────

function scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq) {
  // 1. Calculate Base Global Score (Normalized Log)
  var maxPossibleLog = Math.log10(maxTenantFreq + 1);

  // Diversity Adjustment
  var diversityRaw = (Math.log10(candidate.tenantFreq + 1) * 0.5 + Math.log10(candidate.docFreq + 1) * 0.25 + Math.log10(candidate.authorFreq + 1) * 0.25) / maxPossibleLog;
  var sessionMultiplier = candidate.sessionFreq > 0 ? 1 + Math.log10(candidate.sessionFreq + 1) * 2.5 : 1;

  // Apply multiplier; capped at L1_SESSION_CAP (default 1.2) to prevent excessive over-indexing
  var freqScore = Math.min(diversityRaw * sessionMultiplier, L1_SESSION_CAP);

  // 3. Semantic Scoring
  var semanticScore = NEUTRAL_SCORE;
  if (contextVector) {
    var wordVec = getWordVector(candidate.word);
    semanticScore = wordVec ? cosineSimilarity(contextVector, wordVec) : NEUTRAL_SCORE;
  }
  return {
    semanticScore: semanticScore,
    freqScore: freqScore,
    stage1Score: ALPHA * semanticScore + BETA * freqScore
  };
}

// ─── Grammar Filter ─────────────────────────────────────────

/**
 * GHOST POS DICTIONARY
 * A hardcoded mapping of common structural English words that were stripped
 * from the main domain vocabulary. This allows the grammar filter to understand
 * context without suggesting these words to the user.
 */
var ghostPosTags = ghostPosTagsData;
function applyGrammarFilter(candidates, previousWord) {
  if (!previousWord) return {
    filtered: candidates,
    grammarMeta: null
  };
  var lowerPrev = previousWord.toLowerCase();
  var prevTags = ghostPosTags[lowerPrev] || posTags.get(lowerPrev);
  if (!prevTags || prevTags.length === 0) {
    return {
      filtered: candidates,
      grammarMeta: null
    };
  }
  var allowedNextTags;
  if (prevTags.length === 1) {
    var _precomputedAllowedBy;
    // Common case: single POS tag — reuse the precomputed Set directly (no allocation)
    allowedNextTags = (_precomputedAllowedBy = precomputedAllowedByPos.get(prevTags[0])) !== null && _precomputedAllowedBy !== void 0 ? _precomputedAllowedBy : new Set();
  } else {
    allowedNextTags = new Set();
    var _iterator = _createForOfIteratorHelper(prevTags),
      _step;
    try {
      for (_iterator.s(); !(_step = _iterator.n()).done;) {
        var pt = _step.value;
        var allowed = precomputedAllowedByPos.get(pt);
        if (allowed) allowed.forEach(function (tag) {
          return allowedNextTags.add(tag);
        });
      }
    } catch (err) {
      _iterator.e(err);
    } finally {
      _iterator.f();
    }
  }
  var filtered = [];
  var dropped = [];
  var _iterator2 = _createForOfIteratorHelper(candidates),
    _step2;
  try {
    for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
      var entry = _step2.value;
      var candidateTags = posTags.get(entry.candidate.word.toLowerCase());

      // If candidate has no tags (unknown word), let it pass to be safe
      if (!candidateTags || candidateTags.length === 0) {
        filtered.push(entry);
        continue;
      }
      if (candidateTags.some(function (ct) {
        return allowedNextTags.has(ct);
      })) {
        filtered.push(entry);
      } else {
        dropped.push(entry.candidate.word);
      }
    }

    // Grammar is authoritative.
  } catch (err) {
    _iterator2.e(err);
  } finally {
    _iterator2.f();
  }
  return {
    filtered: filtered,
    grammarMeta: {
      prevWord: lowerPrev,
      prevTags: prevTags,
      before: candidates.length,
      after: filtered.length,
      dropped: dropped
    }
  };
}

// ─── Stage 2: LM Re-ranking ────────────────────────────────
function getLmScore(word, lmLogits) {
  if (!lmLogits) return 0;

  // Look up the word directly! No more tokens.
  var val = lmLogits[word.toLowerCase()];
  if (typeof val === 'number') {
    return val;
  }
  return 0;
}

// ─── Public API ─────────────────────────────────────────────

export function rankCandidates(candidates, contextVector, getWordVector, lmLogits, maxTenantFreq, previousWord) {
  var _grammarMeta$dropped;
  // Stage 1
  var stage1Results = candidates.map(function (candidate) {
    var _scoreStage = scoreStage1(candidate, contextVector, getWordVector, maxTenantFreq),
      semanticScore = _scoreStage.semanticScore,
      freqScore = _scoreStage.freqScore,
      stage1Score = _scoreStage.stage1Score;
    return {
      candidate: candidate,
      semanticScore: semanticScore,
      freqScore: freqScore,
      stage1Score: stage1Score
    };
  });
  var stage1Survivors = [];
  var stage1Rejected = [];
  var _iterator3 = _createForOfIteratorHelper(stage1Results),
    _step3;
  try {
    for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
      var entry = _step3.value;
      if (entry.stage1Score >= MIN_STAGE1_SCORE) {
        stage1Survivors.push(entry);
      } else {
        stage1Rejected.push(entry.candidate.word);
      }
    }

    // Grammar Filter
  } catch (err) {
    _iterator3.e(err);
  } finally {
    _iterator3.f();
  }
  var _applyGrammarFilter = applyGrammarFilter(stage1Survivors, previousWord),
    filtered = _applyGrammarFilter.filtered,
    grammarMeta = _applyGrammarFilter.grammarMeta;

  // Stage 2 + final assembly
  var lmMax = 0;
  if (lmLogits) {
    var values = Object.values(lmLogits);
    if (values.length > 0) lmMax = Math.max.apply(Math, values);
  }
  var scored = filtered.map(function (entry) {
    var lmScore = 0;
    var finalScore = entry.stage1Score;
    if (lmLogits && lmMax >= LM_GATE_THRESHOLD) {
      var rawLm = getLmScore(entry.candidate.word, lmLogits);
      if (rawLm !== 0) {
        var logitDiff = Math.log(rawLm) - Math.log(lmMax);
        lmScore = Math.exp(logitDiff);
      }
      // Words absent from the prefix-filtered payload get lmScore = 0,
      // not 0.05, so they don't outrank genuine LM predictions.

      finalScore = STAGE1_WEIGHT * entry.stage1Score + STAGE2_WEIGHT * lmScore;
    }
    return {
      word: entry.candidate.word,
      freqScore: entry.freqScore,
      semanticScore: entry.semanticScore,
      lmScore: lmScore,
      finalScore: finalScore
    };
  });
  scored.sort(function (a, b) {
    if (b.finalScore !== a.finalScore) return b.finalScore - a.finalScore;
    return a.word.length - b.word.length;
  });
  return {
    candidates: scored,
    grammarMeta: grammarMeta,
    pipelineDebug: {
      initial: candidates.length,
      stage1Rejected: stage1Rejected,
      grammarRejected: (_grammarMeta$dropped = grammarMeta === null || grammarMeta === void 0 ? void 0 : grammarMeta.dropped) !== null && _grammarMeta$dropped !== void 0 ? _grammarMeta$dropped : [],
      final: scored.length
    }
  };
}