// Todoist idea classifier (kb#170, component 2) — encoder-only, NOT a
// classifier LLM call. Per DESIGN-a2a-agents.md v2.1 §3a/§3.1/theorem 24:
// "routing classification is embedding-based on the local bge-m3 ... no
// classifier LLM, no API spend". This module applies that same idea to
// Todoist-capture classification: embed the idea text with bge-m3 (already
// GPU-resident, never-evict per model-registry.yaml) and classify by
// nearest-centroid against a small hand-labelled exemplar set — no Kimi/
// gemma call, ~0 marginal cost, no metered API.
//
// Three independent classification axes (each idea gets one label per axis,
// not a single combined class):
//   area        — which part of life the idea belongs to (kb#170 spec)
//   urgency     — how soon it stops being actionable
//   decompose   — is this a single atomic action, or a multi-step project
//                 that should eventually become a Kanboard task graph
//
// This is deliberately a NEAREST-CENTROID classifier, not a trained model:
// no labelled training set exists (kb#170 orchestrator note — inventing one
// would be guessing), so the "training data" IS the exemplar list below,
// reviewed/editable in code (git-controlled, per DESIGN-a2a-agents.md
// "Personas and Cards are code"). Extending accuracy later means adding
// exemplars here, not retraining a model.

const DEFAULT_BGE_URL = process.env.BGE_M3_URL || 'http://host.docker.internal:11436/v1/embeddings';

// --- Exemplars -------------------------------------------------------------
// Kept short and idiomatic (the kind of one-line idea a person actually
// captures), Russian-first since that's the capture language (kb#170 desc).
// Centroids are the mean of these exemplars' embeddings — adding more
// exemplars per class only requires appending strings here.

const AREA_EXEMPLARS = {
  adolf: [
    'починить квоту Kimi у Adolf',
    'настроить cron задачу в Kanboard',
    'добавить новую MCP команду',
    'проверить логи agap-mcp контейнера',
    'написать воркер для очереди задач',
    'обновить конфиг openclaw.json',
  ],
  welfare: [
    'продумать еженедельный ревью задач',
    'настроить трекер настроения и энергии',
    'сделать ежедневный брифинг по утрам',
    'завести журнал решений',
    'придумать систему напоминаний о важных вещах',
    'разобраться с личной продуктивностью',
    'спроектировать proactive-секретаря для себя',
    'построить систему, которая сама напоминает и планирует',
    'придумать, как автоматизировать личный распорядок дня',
  ],
  'дом': [
    'купить новый пылесос',
    'почистить фильтр кондиционера',
    'вызвать сантехника починить кран',
    'заказать доставку воды',
    'разобрать кладовку',
    'поменять лампочку в коридоре',
    'оплатить счёт за квартиру',
    'оплатить интернет и коммуналку',
  ],
  'семья': [
    'позвонить маме',
    'поздравить сестру с днём рождения',
    'купить подарок жене',
    'спланировать поездку с семьёй',
    'написать бабушке',
    'забрать детей из школы',
  ],
  'здоровье': [
    'записаться к врачу',
    'сдать анализы крови',
    'начать бегать по утрам',
    'купить витамины',
    'сходить к стоматологу',
    'записаться на массаж',
  ],
};

const URGENCY_EXEMPLARS = {
  high: [
    'сделать это сегодня, срочно',
    'дедлайн завтра утром',
    'оплатить штраф до пятницы, иначе пени',
    'нужно решить прямо сейчас',
  ],
  medium: [
    'сделать на этой неделе',
    'стоит сделать в ближайшие дни',
    'через пару дней надо разобраться',
    'неплохо бы успеть до конца месяца',
  ],
  low: [
    'когда-нибудь было бы неплохо',
    'не к спеху, просто идея на будущее',
    'если будет время',
    'мысль про потом, без срока',
  ],
};

const DECOMPOSE_EXEMPLARS = {
  'needs-decomposition': [
    'организовать переезд на новую квартиру',
    'спроектировать и запустить новый сервис на сервере',
    'спланировать отпуск в другую страну',
    'построить систему проактивного секретаря',
    'провести ремонт в квартире',
    'подготовить и провести презентацию проекта',
  ],
  'simple-task': [
    'позвонить маме',
    'купить хлеб',
    'оплатить счёт за интернет',
    'отправить один email',
    'поставить будильник',
    'записать одну мысль в заметки',
  ],
};

// --- Embeddings + cosine similarity -----------------------------------------

async function embed(text, bgeUrl = DEFAULT_BGE_URL) {
  const res = await fetch(bgeUrl, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: 'bge-m3', input: text }),
  });
  if (!res.ok) {
    throw new Error(`bge-m3 embeddings ${res.status}: ${(await res.text()).slice(0, 300)}`);
  }
  const body = await res.json();
  const vec = body?.data?.[0]?.embedding;
  if (!Array.isArray(vec)) throw new Error('bge-m3 embeddings: no vector in response');
  return vec;
}

function dot(a, b) {
  let s = 0;
  for (let i = 0; i < a.length; i++) s += a[i] * b[i];
  return s;
}

function norm(a) {
  return Math.sqrt(dot(a, a));
}

function normalize(a) {
  const n = norm(a) || 1;
  return a.map((x) => x / n);
}

function mean(vectors) {
  const dim = vectors[0].length;
  const out = new Array(dim).fill(0);
  for (const v of vectors) for (let i = 0; i < dim; i++) out[i] += v[i];
  return out.map((x) => x / vectors.length);
}

function cosine(a, b) {
  return dot(a, b) / ((norm(a) || 1) * (norm(b) || 1));
}

// --- Centroid cache ----------------------------------------------------------
// Computed once per process (exemplars are static, embedding a few dozen
// short strings at startup is cheap and happens lazily on first classify()
// call, not at import time — keeps agap-mcp's init() path unaffected).

let _centroidsPromise = null;

async function buildCentroidSet(exemplarMap, bgeUrl) {
  const labels = Object.keys(exemplarMap);
  const centroids = {};
  for (const label of labels) {
    const vectors = await Promise.all(exemplarMap[label].map((t) => embed(t, bgeUrl).then(normalize)));
    centroids[label] = normalize(mean(vectors));
  }
  return centroids;
}

async function getCentroids(bgeUrl = DEFAULT_BGE_URL) {
  if (!_centroidsPromise) {
    _centroidsPromise = Promise.all([
      buildCentroidSet(AREA_EXEMPLARS, bgeUrl),
      buildCentroidSet(URGENCY_EXEMPLARS, bgeUrl),
      buildCentroidSet(DECOMPOSE_EXEMPLARS, bgeUrl),
    ]).then(([area, urgency, decompose]) => ({ area, urgency, decompose }));
  }
  return _centroidsPromise;
}

// Test-only: let tests reset the cache (e.g. to inject a different BGE_URL).
export function _resetCentroidCacheForTests() {
  _centroidsPromise = null;
}

// nearestLabel: pick argmax cosine similarity; also report the runner-up
// and the margin between them. A small margin means the idea sits between
// two classes — surfaced as `ambiguous: true` rather than silently forced,
// so the periodic-review pass (kb#170 component 4) can have Adolf confirm
// instead of trusting a low-confidence auto-tag.
function nearestLabel(vec, centroidMap) {
  const scored = Object.entries(centroidMap)
    .map(([label, centroid]) => ({ label, score: cosine(vec, centroid) }))
    .sort((a, b) => b.score - a.score);
  const [top, second] = scored;
  const margin = second ? top.score - second.score : 1;
  return {
    label: top.label,
    score: Number(top.score.toFixed(4)),
    margin: Number(margin.toFixed(4)),
    ambiguous: margin < 0.03, // empirical starting threshold — revisit once real captures accumulate (same posture as DESIGN-proactive-prioritization.md's tunable constants)
  };
}

// classify: the one entry point. Embeds the idea text ONCE, reuses it
// across all three axes (one bge-m3 call, not three) — consistent with
// the "no metered/needless calls" cost discipline in DESIGN-a2a-agents.md.
export async function classifyIdea(text, { bgeUrl = DEFAULT_BGE_URL } = {}) {
  if (!text || !text.trim()) throw new Error('text is required');
  const [vec, centroids] = await Promise.all([embed(text, bgeUrl).then(normalize), getCentroids(bgeUrl)]);
  return {
    area: nearestLabel(vec, centroids.area),
    urgency: nearestLabel(vec, centroids.urgency),
    decompose: nearestLabel(vec, centroids.decompose),
  };
}

export const _internal = { AREA_EXEMPLARS, URGENCY_EXEMPLARS, DECOMPOSE_EXEMPLARS, cosine, embed, getCentroids };
