/**
 * Model Boss embedding client for the mac-sync server.
 *
 * Routes through the Model Boss coordinator at MODEL_BOSS_URL (default:
 * http://10.0.0.116:8210), which manages model loading and GPU scheduling.
 * The coordinator exposes /v1/embeddings backed by nomic-embed-text-v1.5.
 */

const COORDINATOR_URL = process.env['MODEL_BOSS_URL'] ?? 'http://10.0.0.116:8210';
const EMBED_MODEL = 'nomic-embed';

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

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[]> {
  let res: Response;
  try {
    res = await fetch(`${COORDINATOR_URL}/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) {
    throw new Error(
      `embedQuery: model-boss coordinator unreachable at ${COORDINATOR_URL}: ${err instanceof Error ? err.message : String(err)}`,
      { cause: err },
    );
  }

  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;
}

export type ChatRole = 'system' | 'user' | 'assistant';

export interface ChatMessage {
  role: ChatRole;
  content: string;
}

interface OpenAIChatCompletion {
  choices: Array<{
    message: { content: string };
  }>;
}

/**
 * Send a chat completion request to model-boss and return a parsed structured value.
 *
 * Uses the OpenAI-compatible `/v1/chat/completions` endpoint.
 * Pass a JSON Schema via `responseSchema` to request structured JSON output from the model.
 */
export async function chatJson<T>(
  messages: ChatMessage[],
  opts: {
    model?: string;
    responseSchema?: Record<string, unknown>;
    schemaName?: string;
    parse: (raw: unknown) => T;
    timeoutMs?: number;
  },
): Promise<T> {
  const timeoutMs = opts.timeoutMs ?? 60_000;
  const body: Record<string, unknown> = {
    model: opts.model ?? 'auto',
    messages,
  };
  if (opts.responseSchema) {
    body.response_format = {
      type: 'json_schema',
      json_schema: {
        name: opts.schemaName ?? 'response',
        schema: opts.responseSchema,
        strict: true,
      },
    };
  }

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

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

  const completion = (await res.json()) as OpenAIChatCompletion;
  const content = completion.choices?.[0]?.message?.content;
  if (typeof content !== 'string') {
    throw new Error('chatJson: model-boss response missing choices[0].message.content');
  }

  let payload: unknown;
  try {
    payload = JSON.parse(content);
  } catch {
    throw new Error(`chatJson: model-boss content was not valid JSON: ${content.slice(0, 200)}`);
  }

  return opts.parse(payload);
}
