#!/usr/bin/env node
/**
 * openai-embeddings.mjs — real-network verifier for the direct-to-OpenAI
 * (BYOK) provider (i.e. `provider: "openai"`).
 *
 * WHY THIS LIVES OUTSIDE JEST
 *
 *   `openaiProvider.test.js` uses an injected `fetchImpl` and verifies
 *   request shape + parse logic. That doesn't tell you anything about:
 *     - Whether the OpenAI key actually has embeddings access.
 *     - Whether the live endpoint returns the dimensions our client expects.
 *     - Whether rate-limit / auth failures surface correctly.
 *
 *   This script exercises the same code path the production runtime does,
 *   against api.openai.com (or a configured OpenAI-compat proxy URL).
 *
 * REQUIRED ENV
 *
 *   LOXIA_OPENAI_API_KEY   the OpenAI key (or proxy key for compat layers).
 *
 * OPTIONAL ENV
 *
 *   LOXIA_OPENAI_URL       override endpoint (for DeepInfra, Together,
 *                          or other OpenAI-compat layers). Default:
 *                          https://api.openai.com/v1/embeddings
 *   LOXIA_OPENAI_MODEL_ID  default text-embedding-3-small
 *   LOXIA_OPENAI_DIMS      declared dimensions (default 1536; use 3072 for
 *                          text-embedding-3-large)
 *
 * RUN
 *
 *   npm run verify:openai-embeddings
 *
 *   Exit codes:
 *     0  all checks passed
 *     1  one or more checks failed
 *     2  required env not set (intentional non-zero so CI can't silently
 *        skip the verification by forgetting the secret).
 */

import { OpenAIEmbeddingProvider } from '../../src/services/embeddings/providers/openaiProvider.js';

const apiKey  = process.env.LOXIA_OPENAI_API_KEY;
const url     = process.env.LOXIA_OPENAI_URL || 'https://api.openai.com/v1/embeddings';
const modelId = process.env.LOXIA_OPENAI_MODEL_ID || 'text-embedding-3-small';
const dims    = Number(process.env.LOXIA_OPENAI_DIMS || 1536);

console.log('OpenAI direct (BYOK) embeddings real-network verifier');
if (!apiKey) {
  console.error('');
  console.error('SKIP: required env vars not set.');
  console.error('  LOXIA_OPENAI_API_KEY: MISSING');
  console.error('');
  console.error('CI must either configure this secret and re-run, or');
  console.error('explicitly skip this step (and accept the coverage gap).');
  process.exit(2);
}

console.log(`Endpoint:   ${url}`);
console.log(`Model:      ${modelId}`);
console.log(`Dimensions: ${dims}`);
console.log('');

const results = [];
let failures = 0;

function check(name, fn) {
  return (async () => {
    const t0 = Date.now();
    try {
      await fn();
      const ms = Date.now() - t0;
      results.push({ name, ok: true, ms });
      console.log(`  ✔ ${name} (${ms}ms)`);
    } catch (err) {
      failures++;
      const ms = Date.now() - t0;
      results.push({ name, ok: false, ms, err: err?.message });
      console.error(`  ✗ ${name} (${ms}ms)`);
      console.error(`      ${err?.stack || err}`);
    }
  })();
}

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

const provider = new OpenAIEmbeddingProvider({
  getApiKey: () => apiKey,
  url,
  modelId,
  dimensions: dims,
});

await check('single embedQuery returns Float32Array of declared dim', async () => {
  const vec = await provider.embedQuery('hello world');
  if (!(vec instanceof Float32Array)) throw new Error('not a Float32Array');
  if (vec.length !== dims) {
    throw new Error(`endpoint returned ${vec.length}d but client declared ${dims}d`);
  }
  let sum = 0;
  for (let i = 0; i < vec.length; i++) sum += vec[i] * vec[i];
  const mag = Math.sqrt(sum);
  if (Math.abs(mag - 1) > 1e-3) throw new Error(`magnitude=${mag.toFixed(6)} (expected ≈1)`);
});

await check('batch embedDocuments returns one vector per input', async () => {
  const inputs = ['retry logic', 'rate limiting', 'circuit breakers'];
  const vecs = await provider.embedDocuments(inputs);
  if (!Array.isArray(vecs)) throw new Error('not an array');
  if (vecs.length !== inputs.length) throw new Error(`got ${vecs.length} vectors for ${inputs.length} inputs`);
  for (let i = 0; i < vecs.length; i++) {
    if (!(vecs[i] instanceof Float32Array)) throw new Error(`vec[${i}] not Float32Array`);
    if (vecs[i].length !== dims) throw new Error(`vec[${i}].length=${vecs[i].length}, expected ${dims}`);
  }
});

await check('cosine similarity is semantically meaningful', async () => {
  const seed       = await provider.embedQuery('how do I throttle rate-limited API requests?');
  const paraphrase = await provider.embedQuery('handling rate limits in HTTP API clients');
  const unrelated  = await provider.embedQuery('recipe for chocolate chip cookies');
  const sp = dot(seed, paraphrase);
  const su = dot(seed, unrelated);
  if (sp <= su) throw new Error(`paraphrase(${sp.toFixed(3)}) ≤ unrelated(${su.toFixed(3)})`);
  if ((sp - su) <= 0.03) throw new Error(`gap=${(sp - su).toFixed(3)} too small (expected >0.03)`);
});

await check('provider.health reports ok', async () => {
  const h = await provider.health();
  if (!h.ok) throw new Error(`health.ok=${h.ok} (${h.reason || 'no reason'})`);
});

console.log('');
const passed = results.filter(r => r.ok).length;
const total = results.length;
console.log(`${failures === 0 ? 'PASS' : 'FAIL'}: ${passed}/${total} checks passed`);
process.exit(failures === 0 ? 0 : 1);
