/**
 * Post-processing helpers for raw provider output:
 *   - pad / truncate to declared dimensionality
 *   - L2-normalize for cosine-friendly storage
 *   - convert to Float32Array
 */

import { ERROR_CODES, MemosError } from "../../agent-contract/errors.js";
import type { EmbeddingVector } from "../types.js";

export function toFloat32(v: number[]): EmbeddingVector {
  const f = new Float32Array(v.length);
  for (let i = 0; i < v.length; i++) f[i] = v[i]!;
  return f;
}

/**
 * Enforce the configured dimensionality.
 *
 * - `expected <= 0` means "auto": preserve the provider's native length.
 * - If the provider returns *more* dimensions than configured, truncate (the
 *   old project did this so callers could safely switch to a smaller model).
 * - If fewer, throw. Silently zero-padding would poison downstream cosine.
 */
export function enforceDim(
  v: number[],
  expected: number,
  ctx: { provider: string; model: string; index?: number },
): number[] {
  if (expected <= 0) return v;
  if (v.length === expected) return v;
  if (v.length > expected) return v.slice(0, expected);
  throw new MemosError(
    ERROR_CODES.EMBEDDING_UNAVAILABLE,
    `Provider ${ctx.provider}/${ctx.model} returned ${v.length}-dim vector; expected ${expected}`,
    { provider: ctx.provider, model: ctx.model, got: v.length, expected, index: ctx.index },
  );
}

export function l2Normalize(v: Float32Array): Float32Array {
  let s = 0;
  for (let i = 0; i < v.length; i++) s += v[i]! * v[i]!;
  if (s === 0) return v;
  const inv = 1 / Math.sqrt(s);
  const out = new Float32Array(v.length);
  for (let i = 0; i < v.length; i++) out[i] = v[i]! * inv;
  return out;
}

/**
 * Process a raw provider result (arrays of numbers) into the `EmbeddingVector`
 * shape the storage layer expects. Respects `normalize` (default true).
 */
export function postProcess(
  raw: number[][],
  opts: {
    dimensions: number;
    provider: string;
    model: string;
    normalize: boolean;
  },
): EmbeddingVector[] {
  const out: EmbeddingVector[] = [];
  const inferred = opts.dimensions <= 0 ? (raw[0]?.length ?? 0) : opts.dimensions;
  for (let i = 0; i < raw.length; i++) {
    if (opts.dimensions <= 0 && raw[i]!.length !== inferred) {
      throw new MemosError(
        ERROR_CODES.EMBEDDING_UNAVAILABLE,
        `Provider ${opts.provider}/${opts.model} returned inconsistent vector dimensions in one batch`,
        {
          provider: opts.provider,
          model: opts.model,
          got: raw[i]!.length,
          expected: inferred,
          index: i,
        },
      );
    }
    const dimed = enforceDim(raw[i]!, inferred, {
      provider: opts.provider,
      model: opts.model,
      index: i,
    });
    const f32 = toFloat32(dimed);
    out.push(opts.normalize ? l2Normalize(f32) : f32);
  }
  return out;
}
