// {{ display_name }} MCP server — generated by mcpify. Do not hand-edit.
//
// Single data-access layer against mcp_store.db — both the relational
// `endpoints` table and the `semantic_endpoints` vec0 virtual table
// (REQ-2.4.1's single-store consolidation, replacing the reference
// servers' split embeddings.db + operations.json + schemas.json).

import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

import Database from 'better-sqlite3';
import * as sqliteVec from 'sqlite-vec';

export interface EndpointRecord {
  operationId: string;
  path: string;
  method: string;
  summary: string | null;
  description: string | null;
  inputSchema: unknown;
  outputSchema: unknown;
  authSchemeRef: string | null;
}

export interface SearchResult {
  operationId: string;
  summary: string | null;
  similarity: number;
}

export interface Store {
  db: Database.Database;
  healthCheck(): Promise<void>;
}

// mcpify:versions:begin
export const VERSION_STORE_FILES: Record<string, string> = {
{%- for v in version_entries %}
  '{{ v.label }}': '{{ v.db_file }}',
{%- endfor %}
};
// mcpify:versions:end

/**
 * Resolves `apiVersion` (from the config cascade's `api_version` field) to
 * its store file — the default version's data always lives in the
 * project-root `mcp_store.db` this target has always used; every other
 * version lives in its own `mcp_store_v<label>.db` sibling, added by a
 * later `mcpify add-version` call.
 */
export function openStore(apiVersion: string): Store {
  const fileName = VERSION_STORE_FILES[apiVersion];
  if (!fileName) {
    throw new Error(`unknown api_version '${apiVersion}' — run the 'versions' command to see what's available`);
  }
  const storePath = join(dirname(fileURLToPath(import.meta.url)), '..', '..', fileName);
  const db = new Database(storePath, { readonly: true });
  sqliteVec.load(db);
  return {
    db,
    async healthCheck() {
      db.prepare('SELECT 1 FROM endpoints LIMIT 1').get();
    },
  };
}

export function closeStore(store: Store): void {
  store.db.close();
}

const cachedStores = new Map<string, Store>();

/**
 * Returns a process-wide, in-memory-backed store for `apiVersion`: on
 * first access, opens the on-disk file read-only via `openStore`, copies
 * its entire contents into memory (via `better-sqlite3`'s `serialize()`,
 * which better-sqlite3's `Database` constructor can load directly from a
 * `Buffer`), then closes the on-disk connection immediately — so its
 * file handle/lock is held only for that brief copy, not for the
 * process's lifetime. This means an external process (a fresh
 * `populate-embeddings` run, a deployment replacing the file) can update
 * `mcp_store.db` without hitting "database is locked" against a live
 * server, at the cost of the running process only picking up such an
 * update on its next restart.
 *
 * Unlike `openStore`, the returned store must not be passed to
 * `closeStore` by the caller — it's cached and reused for the rest of
 * the process's lifetime.
 */
export function getCachedStore(apiVersion: string): Store {
  const cached = cachedStores.get(apiVersion);
  if (cached) {
    return cached;
  }

  const diskStore = openStore(apiVersion);
  const buffer = diskStore.db.serialize();
  closeStore(diskStore);

  const db = new Database(buffer);
  sqliteVec.load(db);
  const store: Store = {
    db,
    async healthCheck() {
      db.prepare('SELECT 1 FROM endpoints LIMIT 1').get();
    },
  };
  cachedStores.set(apiVersion, store);
  return store;
}

interface EndpointRow {
  operation_id: string;
  path: string;
  method: string;
  summary: string | null;
  description: string | null;
  input_schema: string;
  output_schema: string;
  auth_scheme_ref: string | null;
}

function toEndpointRecord(row: EndpointRow): EndpointRecord {
  return {
    operationId: row.operation_id,
    path: row.path,
    method: row.method,
    summary: row.summary,
    description: row.description,
    inputSchema: JSON.parse(row.input_schema),
    outputSchema: JSON.parse(row.output_schema),
    authSchemeRef: row.auth_scheme_ref,
  };
}

export function getEndpoint(store: Store, operationId: string): EndpointRecord | undefined {
  const row = store.db
    .prepare(
      'SELECT operation_id, path, method, summary, description, input_schema, output_schema, auth_scheme_ref FROM endpoints WHERE operation_id = ?',
    )
    .get(operationId) as EndpointRow | undefined;
  return row ? toEndpointRecord(row) : undefined;
}

export function listEndpoints(store: Store): EndpointRecord[] {
  const rows = store.db
    .prepare('SELECT operation_id, path, method, summary, description, input_schema, output_schema, auth_scheme_ref FROM endpoints')
    .all() as EndpointRow[];
  return rows.map(toEndpointRecord);
}

interface SearchRow {
  operationId: string;
  summary: string | null;
  distance: number;
}

/**
 * k-nearest-neighbor search over `semantic_endpoints` (sqlite-vec's
 * documented `MATCH ... AND k = ...` query form). `queryEmbedding` must
 * come from the same model as the vectors it's compared against — see
 * services/embedding-service.ts.
 *
 * `similarity` is a simple `1 - distance` transform of sqlite-vec's L2
 * distance, not a true cosine-similarity computation; since embeddings are
 * normalized (unit vectors), L2-distance ordering already matches
 * cosine-similarity ordering, so result *ranking* is correct even though
 * the displayed score is an approximation.
 */
export function searchEndpoints(store: Store, queryEmbedding: number[], limit: number): SearchResult[] {
  const rows = store.db
    .prepare(
      `SELECT e.operation_id AS operationId, e.summary AS summary, s.distance AS distance
       FROM semantic_endpoints s
       JOIN endpoints e ON e.operation_id = s.operation_id
       WHERE s.embedding MATCH ? AND k = ?
       ORDER BY s.distance`,
    )
    .all(JSON.stringify(queryEmbedding), limit) as SearchRow[];

  return rows.map((row) => ({
    operationId: row.operationId,
    summary: row.summary,
    similarity: 1 - row.distance,
  }));
}
