/**
 * Model Boss embedding client for the mac-sync server.
 *
 * Routes through the Model Boss coordinator at MODEL_BOSS_URL, which manages
 * model loading and GPU scheduling. The coordinator exposes /v1/embeddings
 * backed by nomic-embed-text-v1.5. Required from env — no LAN default (a baked
 * host silently dials dead infra; fail fast on misconfig instead).
 */

const COORDINATOR_URL = process.env['MODEL_BOSS_URL'];

function coordinatorUrl(): string {
  if (!COORDINATOR_URL) throw new Error('MODEL_BOSS_URL is required (no default — set it in env)');
  return COORDINATOR_URL;
}
const EMBED_MODEL = 'nomic-embed';

export const EMBEDDING_DIM = 768;
export const EMBEDDING_MODEL_VERSION = 'nomic-embed-text-v1.5@q8_0';

/*
 * Circuit breaker for the coordinator connection.
 *
 * The coordinator's GPU host can be offline for long stretches. Without a
 * breaker, every call pays the full TCP-connect timeout before failing, and a
 * batch of embedding calls serialises into a pile of slow failures — enough to
 * stall the whole server (this first wedged ingest for hours on 2026-06-23, via
 * a contact-summary sweep of ~1700 doomed 3s-timeout calls; that sweep has since
 * been removed, but the embedding-worker calls the same coordinator). The
 * breaker trips after a few consecutive *connection* failures, then fails fast
 * for a cooldown window, letting a single probe through afterwards (half-open)
 * to auto-recover when the host returns. HTTP error responses (4xx/5xx) count as
 * "reachable" — the breaker tracks connectivity, not request success.
 */
const CIRCUIT_FAILURE_THRESHOLD = 3;
const CIRCUIT_COOLDOWN_MS = 60_000;
let circuitFailureCount = 0;
let circuitOpenUntil = 0;

function recordCoordinatorReachable(): void {
  circuitFailureCount = 0;
  circuitOpenUntil = 0;
}

function recordCoordinatorUnreachable(): void {
  circuitFailureCount += 1;
  if (circuitFailureCount >= CIRCUIT_FAILURE_THRESHOLD) {
    circuitOpenUntil = Date.now() + CIRCUIT_COOLDOWN_MS;
  }
}

function assertCircuitClosed(label: string): void {
  if (Date.now() < circuitOpenUntil) {
    throw new Error(
      `${label}: model-boss circuit open — coordinator ${COORDINATOR_URL} unreachable, failing fast until cooldown elapses`,
    );
  }
}

interface OpenAIEmbeddingData {
  readonly object: 'embedding';
  readonly index: number;
  readonly embedding: number[];
}

interface OpenAIEmbeddingResponse {
  readonly object: 'list';
  readonly data: readonly OpenAIEmbeddingData[];
  readonly model: string;
}

async function postEmbeddings(
  input: string[],
  timeoutMs: number,
): Promise<readonly OpenAIEmbeddingData[]> {
  assertCircuitClosed('embedQuery');

  let res: Response;
  try {
    res = await fetch(`${coordinatorUrl()}/v1/embeddings`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Client-Id': 'mac-sync-server' },
      body: JSON.stringify({ model: EMBED_MODEL, input }),
      signal: AbortSignal.timeout(timeoutMs),
    });
  } catch (err) {
    recordCoordinatorUnreachable();
    throw new Error(
      `embedQuery: model-boss coordinator unreachable at ${COORDINATOR_URL}: ${err instanceof Error ? err.message : String(err)}`,
      { cause: err },
    );
  }
  recordCoordinatorReachable();

  if (!res.ok) {
    const body = await res.text().catch(() => '');
    throw new Error(`embedQuery: model-boss returned ${res.status}: ${body.slice(0, 200)}`);
  }

  const data = (await res.json()) as OpenAIEmbeddingResponse;
  if (!Array.isArray(data.data) || data.data.length === 0) {
    throw new Error(`embedQuery: model-boss returned no embedding data for ${input.length} input(s)`);
  }
  return data.data;
}

/**
 * Embed a batch of document strings.
 * Uses nomic-embed's `search_document:` prefix for stored message bodies.
 */
export async function embedTexts(texts: string[], timeoutMs = 120_000): Promise<number[][]> {
  if (texts.length === 0) return [];
  const prefixed = texts.map((t) => `search_document: ${t}`);
  const data = await postEmbeddings(prefixed, timeoutMs);
  if (data.length !== texts.length) {
    throw new Error(`embedTexts: expected ${texts.length} embeddings, got ${data.length}`);
  }
  return data.map((d) => d.embedding);
}

/**
 * Embed a single query string.
 * Uses nomic-embed's `search_query:` prefix for improved retrieval quality.
 */
export async function embedQuery(queryText: string, timeoutMs = 30_000): Promise<number[]> {
  const data = await postEmbeddings([`search_query: ${queryText}`], timeoutMs);
  const vec = data[0]?.embedding;
  if (!vec || vec.length === 0) {
    throw new Error('embedQuery: model-boss returned no vector for query');
  }
  return vec;
}

/**
 * Embed a single document text (uses search_document: prefix).
 */
export async function embedText(text: string): Promise<number[]> {
  const results = await embedTexts([text]);
  const vec = results[0];
  if (!vec) {
    throw new Error('embedText: model-boss returned no vector for input');
  }
  return vec;
}

