import { buildLsaCorpus, projectQuery, type LsaModel } from "@/lib/lsa";
import { normalize } from "@/lib/matrix";

/**
 * Embedding providers.
 *
 * Two implementations share one interface so the rest of the search pipeline
 * never learns which one is active:
 *
 *  - `remote`  — any OpenAI-compatible `/embeddings` endpoint, used when
 *                `EMBEDDING_API_KEY` is set.
 *  - `local-lsa` — the offline Latent Semantic Analysis provider. It needs no
 *                API key, no network and no model download, so the demo works
 *                on a fresh clone with nothing configured.
 *
 * Vectors from different providers are not comparable, so a provider's
 * identity (`id` + `model`) is recorded alongside every index and query vector.
 */

export const LOCAL_PROVIDER_ID = "local-lsa";
export const REMOTE_PROVIDER_ID = "remote";

export interface EmbeddingProvider {
  readonly id: string;
  readonly model: string;
  readonly dimensions: number;
  embed(text: string): Promise<number[]>;
  embedMany(texts: string[]): Promise<number[][]>;
}

/** Identity of a provider, used to decide whether cached vectors are reusable. */
export interface ProviderKey {
  id: string;
  model: string;
}

export function isSameProvider(left: ProviderKey | null, right: ProviderKey): boolean {
  return left !== null && left.id === right.id && left.model === right.model;
}

/**
 * Offline provider: projects text into the LSA space of the product corpus.
 *
 * Constructing one requires the corpus, so the model is built once per process
 * by `vector-index` and shared. `dimensions` is `0` until documents are
 * projected, which is why the interface exposes it as a mutable field.
 */
export class LocalLsaEmbeddingProvider implements EmbeddingProvider {
  readonly id = LOCAL_PROVIDER_ID;
  readonly model = "lsa-tfidf";
  dimensions = 0;
  private readonly model_: LsaModel;

  constructor(model: LsaModel) {
    this.model_ = model;
    this.dimensions = model.dimensions;
  }

  async embed(text: string): Promise<number[]> {
    return projectQuery(this.model_, text);
  }

  async embedMany(texts: string[]): Promise<number[][]> {
    return texts.map((text) => projectQuery(this.model_, text));
  }

  get lsaModel(): LsaModel {
    return this.model_;
  }
}

export interface RemoteEmbeddingConfig {
  apiKey: string;
  model: string;
  baseUrl: string;
  batchSize: number;
  timeoutMs: number;
}

export class RemoteEmbeddingError extends Error {
  constructor(message: string, options?: { cause?: unknown }) {
    super(message, options);
    this.name = "RemoteEmbeddingError";
  }
}

/** OpenAI-compatible embedding client. The API key never leaves the server. */
export class RemoteEmbeddingProvider implements EmbeddingProvider {
  readonly id = REMOTE_PROVIDER_ID;
  readonly model: string;
  dimensions: number;
  private readonly config: RemoteEmbeddingConfig;

  constructor(config: RemoteEmbeddingConfig, dimensions: number) {
    this.config = config;
    this.model = config.model;
    this.dimensions = dimensions;
  }

  async embed(text: string): Promise<number[]> {
    const [vector] = await this.embedMany([text]);
    return vector;
  }

  async embedMany(texts: string[]): Promise<number[][]> {
    const vectors: number[][] = [];

    for (let start = 0; start < texts.length; start += this.config.batchSize) {
      const batch = texts.slice(start, start + this.config.batchSize);
      const response = await this.requestBatch(batch);
      vectors.push(...response);
    }

    this.dimensions = vectors[0]?.length ?? this.dimensions;
    return vectors;
  }

  private async requestBatch(input: string[]): Promise<number[][]> {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.config.timeoutMs);

    try {
      const response = await fetch(`${this.config.baseUrl.replace(/\/$/, "")}/embeddings`, {
        method: "POST",
        headers: {
          "content-type": "application/json",
          authorization: `Bearer ${this.config.apiKey}`,
        },
        body: JSON.stringify({ model: this.config.model, input }),
        signal: controller.signal,
      });

      if (!response.ok) {
        const detail = await response.text().catch(() => "");
        throw new RemoteEmbeddingError(
          `Embedding provider responded ${response.status}${detail ? `: ${detail.slice(0, 200)}` : ""}`,
        );
      }

      const payload = (await response.json()) as {
        data?: { embedding?: number[]; index?: number }[];
      };
      const items = payload.data ?? [];
      if (items.length !== input.length) {
        throw new RemoteEmbeddingError(
          `Embedding provider returned ${items.length} vectors for ${input.length} inputs`,
        );
      }

      return items
        .map((item) => item.embedding ?? [])
        .map((embedding) => normalize(embedding));
    } catch (error) {
      if (error instanceof RemoteEmbeddingError) throw error;
      throw new RemoteEmbeddingError(
        error instanceof Error ? error.message : "Embedding request failed",
        { cause: error },
      );
    } finally {
      clearTimeout(timeout);
    }
  }
}

export interface ResolvedEmbeddingConfig {
  apiKey: string | null;
  model: string;
  baseUrl: string;
  batchSize: number;
  timeoutMs: number;
}

/** Read embedding settings from the environment. All of it is server-only. */
export function resolveEmbeddingConfig(): ResolvedEmbeddingConfig {
  const apiKey = process.env.EMBEDDING_API_KEY?.trim();
  return {
    apiKey: apiKey ? apiKey : null,
    model: process.env.EMBEDDING_MODEL?.trim() || "text-embedding-3-small",
    baseUrl: process.env.EMBEDDING_API_BASE?.trim() || "https://api.openai.com/v1",
    batchSize: toPositiveInt(process.env.EMBEDDING_BATCH_SIZE, 64),
    timeoutMs: toPositiveInt(process.env.EMBEDDING_TIMEOUT_MS, 10_000),
  };
}

function toPositiveInt(value: string | undefined, fallback: number): number {
  const parsed = Number.parseInt(value ?? "", 10);
  return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}

/**
 * LSA needs a corpus, so the local provider is only buildable from a
 * `buildLocalProvider` call. This helper keeps the corpus construction and the
 * provider construction in a single place.
 */
export function buildLocalProvider(
  documents: string[],
  dimensions?: number,
): LocalLsaEmbeddingProvider {
  const { model } = buildLsaCorpus(documents, dimensions);
  return new LocalLsaEmbeddingProvider(model);
}

/** Shared vocabulary check used by the search utils to expand out-of-vocabulary queries. */
export function knownTerms(model: LsaModel): Set<string> {
  return new Set(model.terms);
}
