import crypto from "crypto";

export const LOCAL_DIMS = 384;
export const LOCAL_MODEL = "local-hash-v1";

/** Deterministic bag-of-tokens hash embedding — matches learning/model/embedder.mjs */
export function localEmbed(text: string, dims = LOCAL_DIMS): number[] {
  const vec = new Float64Array(dims);
  const normalized = String(text || "")
    .toLowerCase()
    .replace(/[^a-z0-9+#\s.-]/g, " ")
    .split(/\s+/)
    .filter(Boolean);

  for (const token of normalized) {
    const h = crypto.createHash("sha256").update(token).digest();
    for (let i = 0; i < 8; i++) {
      const idx = h[i] % dims;
      const sign = h[i + 8] % 2 === 0 ? 1 : -1;
      vec[idx] += sign * (1 + (h[i + 16] % 5) * 0.1);
    }
  }

  for (let i = 0; i < normalized.length - 1; i++) {
    const bigram = `${normalized[i]}_${normalized[i + 1]}`;
    const h = crypto.createHash("sha256").update(bigram).digest();
    const idx = h[0] % dims;
    const sign = h[1] % 2 === 0 ? 1 : -1;
    vec[idx] += sign * 1.4;
  }

  return l2Normalize(Array.from(vec));
}

export function l2Normalize(v: number[]): number[] {
  let sum = 0;
  for (const x of v) sum += x * x;
  const norm = Math.sqrt(sum) || 1;
  return v.map((x) => x / norm);
}

export function cosineSimilarity(a: number[], b: number[]): number {
  const n = Math.min(a.length, b.length);
  let dot = 0;
  for (let i = 0; i < n; i++) dot += a[i] * b[i];
  return dot;
}

async function openaiEmbed(
  text: string,
  opts: { apiKey: string; model?: string },
): Promise<number[]> {
  const model = opts.model || "text-embedding-3-small";
  const res = await fetch("https://api.openai.com/v1/embeddings", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${opts.apiKey}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ model, input: text.slice(0, 8000) }),
  });
  if (!res.ok) {
    const body = await res.text();
    throw new Error(`OpenAI embeddings failed: ${res.status} ${body}`);
  }
  const json = (await res.json()) as { data: { embedding: number[] }[] };
  return l2Normalize(json.data[0].embedding);
}

export async function embedText(text: string): Promise<{
  embedding: number[];
  embeddingModel: string;
  dims: number;
}> {
  const mode = (process.env.RAG_EMBEDDING_MODE || "local").toLowerCase();
  if (mode === "openai") {
    const apiKey = process.env.OPENAI_API_KEY?.trim();
    if (!apiKey) {
      throw new Error("RAG_EMBEDDING_MODE=openai requires OPENAI_API_KEY");
    }
    const model = process.env.RAG_EMBEDDING_MODEL || "text-embedding-3-small";
    const embedding = await openaiEmbed(text, { apiKey, model });
    return { embedding, embeddingModel: model, dims: embedding.length };
  }

  const embedding = localEmbed(text, LOCAL_DIMS);
  return { embedding, embeddingModel: LOCAL_MODEL, dims: LOCAL_DIMS };
}
