/**
 * Memory store - CRUD operations with prepared statements and caching
 */

import { getDbInstance } from "../db/core";
import { upsertSemanticMemoryPoint, deleteSemanticMemoryPoint } from "./qdrant";
import { Memory, MemoryType } from "./types";
import { logger } from "../../../open-sse/utils/logger.ts";
import { sanitizeErrorMessage } from "../../../open-sse/utils/error.ts";
import { resolveEmbeddingSource, embed } from "./embedding";
import { getVectorStore } from "./vectorStore";
import { getMemorySettings } from "./settings";
import { markMemoryNeedsReindex } from "@/lib/localDb";

const log = logger("MEMORY_STORE");

interface CacheEntry<T> {
  value: T;
  timestamp: number;
}

interface MemoryRow {
  id: string;
  api_key_id: string;
  session_id: string | null;
  type: MemoryType;
  key: string | null;
  content: string;
  metadata: string | null;
  created_at: string;
  updated_at: string;
  expires_at: string | null;
}

// Memory cache configuration
const MEMORY_CACHE_TTL = 60_000; // 1 minute
const MEMORY_MAX_CACHE_SIZE = 500;

// Cache for recently accessed memories
const _memoryCache = new Map<string, CacheEntry<Memory | null>>();

// Helper function to safely parse JSON strings
function parseJSON(value: unknown): Record<string, unknown> {
  if (!value || typeof value !== "string" || value.trim() === "") {
    return {};
  }
  try {
    const parsed = JSON.parse(value);
    return typeof parsed === "object" && parsed !== null ? parsed : {};
  } catch {
    return {};
  }
}

function invalidateMemoryCache(key: string) {
  _memoryCache.delete(key);
}

function evictIfNeeded<TKey, TValue>(cache: Map<TKey, TValue>) {
  if (cache.size > MEMORY_MAX_CACHE_SIZE) {
    // Remove oldest entries first
    const keysArray = Array.from(cache.keys());
    const entriesToRemove = Math.floor(cache.size * 0.2);
    for (let i = 0; i < entriesToRemove; i++) {
      cache.delete(keysArray[i]);
    }
  }
}

function rowToMemory(row: MemoryRow): Memory {
  return {
    id: String(row.id),
    apiKeyId: String(row.api_key_id),
    sessionId: typeof row.session_id === "string" ? row.session_id : "",
    type: row.type as MemoryType,
    key: typeof row.key === "string" ? row.key : "",
    content: String(row.content),
    metadata: parseJSON(row.metadata),
    createdAt: new Date(String(row.created_at)),
    updatedAt: new Date(String(row.updated_at)),
    expiresAt: row.expires_at ? new Date(String(row.expires_at)) : null,
  };
}

/**
 * Find existing memory by apiKeyId and key (for UPSERT logic)
 */
function findExistingMemory(
  db: ReturnType<typeof getDbInstance>,
  apiKeyId: string,
  key: string
): MemoryRow | undefined {
  if (!key) return undefined;
  const stmt = db.prepare(
    "SELECT * FROM memories WHERE api_key_id = ? AND key = ? ORDER BY created_at DESC LIMIT 1"
  );
  return stmt.get(apiKeyId, key) as MemoryRow | undefined;
}

/**
 * Fire-and-forget: generate embedding for a memory and upsert into sqlite-vec.
 * Errors are logged but never thrown — this must never block the SQLite write.
 */
/**
 * Best-effort: try to mark a memory needs_reindex. Swallows errors so that DB-closed
 * states (e.g. test teardown after the parent promise resolved) never escape as
 * unhandledRejection. Producing this side-effect is opportunistic by design.
 */
function safeMarkNeedsReindex(id: string, needs: boolean): void {
  try {
    markMemoryNeedsReindex(id, needs);
  } catch {
    // intentional swallow — DB may be closed (test teardown) or schema not yet ready
  }
}

function scheduleVectorUpsert(id: string, content: string): void {
  setImmediate(async () => {
    try {
      const settings = await getMemorySettings();
      const resolution = resolveEmbeddingSource(settings);
      if (!resolution.source) return;

      const embeddingResult = await embed(content, settings);
      if (!("vector" in embeddingResult)) {
        log.warn("memory.vec.embed.fail", {
          id,
          reason: embeddingResult.reason,
          message: sanitizeErrorMessage(embeddingResult.message),
        });
        safeMarkNeedsReindex(id, true);
        return;
      }

      const vec = getVectorStore();
      if (!vec) {
        safeMarkNeedsReindex(id, true);
        return;
      }

      await vec.ensureReady(resolution);
      await vec.upsertVector(id, embeddingResult.vector);
      safeMarkNeedsReindex(id, false);
    } catch (err: unknown) {
      log.warn("memory.vec.upsert.fail", {
        id,
        error: sanitizeErrorMessage(err instanceof Error ? err.message : String(err)),
      });
      safeMarkNeedsReindex(id, true);
    }
  });
}

/**
 * Create a new memory entry (UPSERT: updates existing if same apiKeyId + key)
 */
export async function createMemory(
  memory: Omit<Memory, "id" | "createdAt" | "updatedAt">
): Promise<Memory> {
  const db = getDbInstance();
  const now = new Date().toISOString();

  // Check for existing memory with same apiKeyId + key (UPSERT logic)
  const existing = memory.key ? findExistingMemory(db, memory.apiKeyId, memory.key) : undefined;

  if (existing) {
    // UPDATE existing record
    const updatedMetadata = { ...parseJSON(existing.metadata), ...memory.metadata };
    const stmt = db.prepare(
      "UPDATE memories SET content = ?, metadata = ?, updated_at = ?, session_id = ?, type = ?, expires_at = ? WHERE id = ?"
    );
    stmt.run(
      memory.content,
      JSON.stringify(updatedMetadata),
      now,
      memory.sessionId,
      memory.type,
      memory.expiresAt ?? null,
      existing.id
    );

    const updatedMemory: Memory = {
      id: String(existing.id),
      apiKeyId: memory.apiKeyId,
      sessionId: memory.sessionId,
      type: memory.type,
      key: memory.key,
      content: memory.content,
      metadata: updatedMetadata,
      createdAt: new Date(String(existing.created_at)),
      updatedAt: new Date(now),
      expiresAt: memory.expiresAt ?? null,
    };

    // Invalidate and update cache
    invalidateMemoryCache(existing.id);
    evictIfNeeded(_memoryCache);
    _memoryCache.set(existing.id, { value: updatedMemory, timestamp: Date.now() });

    log.info("memory.updated", {
      apiKeyId: memory.apiKeyId,
      type: memory.type,
      id: existing.id,
      key: memory.key,
    });

    // Best-effort vector upsert (fire-and-forget — content changed so regenerate)
    scheduleVectorUpsert(String(existing.id), memory.content);

    // Best-effort re-sync to Qdrant after update
    upsertSemanticMemoryPoint({
      id: String(existing.id),
      apiKeyId: memory.apiKeyId || "",
      sessionId: memory.sessionId || "",
      key: memory.key || "",
      content: memory.content,
      metadata: updatedMetadata || {},
      createdAt: String(existing.created_at),
      expiresAt: memory.expiresAt ? memory.expiresAt.toISOString() : null,
    })
      .then((r) => {
        if (r.ok) log.debug?.("qdrant.upsert.ok", { id: existing.id, latencyMs: r.latencyMs });
        else if (r.error && r.error !== "not_configured")
          log.warn?.("qdrant.upsert.fail", { id: existing.id, error: r.error });
      })
      .catch((e) => log.warn?.("qdrant.upsert.error", { id: existing.id, error: String(e) }));

    return updatedMemory;
  }

  // INSERT new record if not exists
  const id = crypto.randomUUID();
  const stmt = db.prepare(
    "INSERT INTO memories (id, api_key_id, session_id, type, key, content, metadata, created_at, updated_at, expires_at) " +
      "VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
  );

  stmt.run(
    id,
    memory.apiKeyId,
    memory.sessionId,
    memory.type,
    memory.key,
    memory.content,
    JSON.stringify(memory.metadata ?? {}),
    now,
    now,
    memory.expiresAt?.toISOString() ?? null
  );

  const createdMemory: Memory = {
    id,
    apiKeyId: memory.apiKeyId,
    sessionId: memory.sessionId,
    type: memory.type,
    key: memory.key,
    content: memory.content,
    metadata: memory.metadata,
    createdAt: new Date(now),
    updatedAt: new Date(now),
    expiresAt: memory.expiresAt ?? null,
  };

  // Cache the newly created memory
  invalidateMemoryCache(id);
  evictIfNeeded(_memoryCache);
  _memoryCache.set(id, { value: createdMemory, timestamp: Date.now() });

  log.info("memory.stored", { apiKeyId: memory.apiKeyId, type: memory.type, id });

  // Best-effort vector upsert (fire-and-forget)
  scheduleVectorUpsert(id, memory.content);

  // Best-effort sync to semantic memory store (Qdrant). Failures do not block the SQLite write.
  upsertSemanticMemoryPoint({
    id,
    apiKeyId: memory.apiKeyId || "",
    sessionId: memory.sessionId || "",
    key: memory.key || "",
    content: memory.content,
    metadata: memory.metadata || {},
    createdAt: now,
    expiresAt: memory.expiresAt ? memory.expiresAt.toISOString() : null,
  })
    .then((r) => {
      if (r.ok) log.debug?.("qdrant.upsert.ok", { id, latencyMs: r.latencyMs });
      else if (r.error && r.error !== "not_configured")
        log.warn?.("qdrant.upsert.fail", { id, error: r.error });
    })
    .catch((e) => log.warn?.("qdrant.upsert.error", { id, error: String(e) }));

  return createdMemory;
}

/**
 * Get a memory by ID
 */
export async function getMemory(id: string): Promise<Memory | null> {
  if (!id || typeof id !== "string") return null;

  // Check cache first
  const cached = _memoryCache.get(id);
  if (cached && Date.now() - cached.timestamp < MEMORY_CACHE_TTL) {
    return cached.value;
  }

  const db = getDbInstance();
  const stmt = db.prepare("SELECT * FROM memories WHERE id = ?");
  const row = stmt.get(id) as MemoryRow | undefined;

  if (!row) {
    // Cache negative result briefly to prevent repeated DB hits
    evictIfNeeded(_memoryCache);
    _memoryCache.set(id, { value: null, timestamp: Date.now() });
    return null;
  }

  const memory = rowToMemory(row);

  // Cache the result
  evictIfNeeded(_memoryCache);
  _memoryCache.set(id, { value: memory, timestamp: Date.now() });

  return memory;
}

/**
 * Update a memory entry
 */
export async function updateMemory(
  id: string,
  updates: Partial<Omit<Memory, "id" | "createdAt">>
): Promise<boolean> {
  if (!id || typeof id !== "string") return false;

  const db = getDbInstance();
  const now = new Date().toISOString();

  // Fetch current state to detect content/key change (needed for vector re-gen)
  const currentRow = db.prepare("SELECT content, key FROM memories WHERE id = ?").get(id) as
    | { content: string; key: string | null }
    | undefined;

  // Build dynamic update query
  const fields: string[] = [];
  const values: unknown[] = [];

  if (updates.type !== undefined) {
    fields.push("type = ?");
    values.push(updates.type);
  }
  if (updates.key !== undefined) {
    fields.push("key = ?");
    values.push(updates.key);
  }
  if (updates.content !== undefined) {
    fields.push("content = ?");
    values.push(updates.content);
  }
  if (updates.metadata !== undefined) {
    fields.push("metadata = ?");
    values.push(JSON.stringify(updates.metadata));
  }
  if (updates.expiresAt !== undefined) {
    fields.push("expires_at = ?");
    values.push(updates.expiresAt?.toISOString() ?? null);
  }

  // Always update the updatedAt timestamp
  fields.push("updated_at = ?");
  values.push(now);

  values.push(id); // For WHERE clause

  const stmt = db.prepare(`UPDATE memories SET ${fields.join(", ")} WHERE id = ?`);

  const result = stmt.run(...values);

  if (result.changes === 0) {
    return false;
  }

  // Invalidate cache for this memory
  invalidateMemoryCache(id);

  // Regenerate vector if content or key changed (fire-and-forget)
  const contentChanged =
    updates.content !== undefined && updates.content !== currentRow?.content;
  const keyChanged = updates.key !== undefined && updates.key !== currentRow?.key;

  if (contentChanged || keyChanged) {
    const newContent = updates.content ?? currentRow?.content ?? "";
    scheduleVectorUpsert(id, newContent);
  }

  return true;
}

/**
 * Delete a memory by ID.
 * D15 (bug #3): MUST call both vec.deleteVector AND deleteSemanticMemoryPoint
 * before the SQLite DELETE to keep all stores in sync.
 */
export async function deleteMemory(id: string): Promise<boolean> {
  if (!id || typeof id !== "string") return false;

  // 1. Delete from sqlite-vec (best-effort — does not fail if vec not loaded)
  const vec = getVectorStore();
  if (vec) {
    await vec.deleteVector(id).catch((e: unknown) =>
      log.warn("memory.vec.delete.fail", {
        id,
        error: sanitizeErrorMessage(e instanceof Error ? e.message : String(e)),
      })
    );
  }

  // 2. Delete from Qdrant (best-effort — already existed before plan 21)
  await deleteSemanticMemoryPoint(id).catch((e: unknown) =>
    log.warn("memory.qdrant.delete.fail", {
      id,
      error: sanitizeErrorMessage(e instanceof Error ? e.message : String(e)),
    })
  );

  // 3. Delete from SQLite
  const db = getDbInstance();
  const stmt = db.prepare("DELETE FROM memories WHERE id = ?");
  const result = stmt.run(id);

  if (result.changes === 0) {
    return false;
  }

  // Invalidate cache for this memory
  invalidateMemoryCache(id);

  log.info("memory.deleted", { id });

  return true;
}

/**
 * List memories with optional filtering and pagination
 */
export async function listMemories(filters: {
  apiKeyId?: string;
  type?: MemoryType;
  sessionId?: string;
  query?: string;
  limit?: number;
  offset?: number;
  page?: number;
}): Promise<{ data: Memory[]; total: number; byType: Record<string, number> }> {
  const db = getDbInstance();

  // Build dynamic query conditions
  const whereClauses: string[] = [];
  const whereParams: unknown[] = [];

  if (filters.apiKeyId) {
    whereClauses.push("api_key_id = ?");
    whereParams.push(filters.apiKeyId);
  }

  if (filters.type) {
    whereClauses.push("type = ?");
    whereParams.push(filters.type);
  }

  if (filters.sessionId) {
    whereClauses.push("session_id = ?");
    whereParams.push(filters.sessionId);
  }

  if (typeof filters.query === "string" && filters.query.trim().length > 0) {
    const likeQuery = `%${filters.query.trim().toLowerCase()}%`;
    whereClauses.push("(LOWER(content) LIKE ? OR LOWER(key) LIKE ?)");
    whereParams.push(likeQuery, likeQuery);
  }

  // Run COUNT query + byType aggregation in a single query
  let countQuery = "SELECT COUNT(*) as total FROM memories";
  if (whereClauses.length > 0) {
    countQuery += " WHERE " + whereClauses.join(" AND ");
  }
  const countStmt = db.prepare(countQuery);
  const countRow = countStmt.get(...whereParams) as { total: number };
  const total = countRow.total;

  // Build byType aggregation (counts ALL matching rows, not just the page)
  let byTypeQuery = "SELECT type, COUNT(*) as count FROM memories";
  const byTypeParams: unknown[] = [...whereParams];
  if (whereClauses.length > 0) {
    byTypeQuery += " WHERE " + whereClauses.join(" AND ");
  }
  byTypeQuery += " GROUP BY type";
  const byTypeStmt = db.prepare(byTypeQuery);
  const byTypeRows = byTypeStmt.all(...byTypeParams) as { type: string; count: number }[];
  const byType = Object.fromEntries(byTypeRows.map((r) => [r.type, r.count])) as Record<
    string,
    number
  >;

  // Calculate effective limit and offset
  const effectiveLimit = filters.limit ?? 50;
  const effectivePage = filters.page ?? 1;
  const effectiveOffset = filters.offset ?? (effectivePage - 1) * effectiveLimit;

  // Build SELECT query with pagination
  let query = "SELECT * FROM memories";
  if (whereClauses.length > 0) {
    query += " WHERE " + whereClauses.join(" AND ");
  }

  // Add ordering and pagination
  query += " ORDER BY created_at DESC LIMIT ? OFFSET ?";

  // Build params for SELECT query (WHERE params + pagination params)
  const params = [...whereParams, effectiveLimit, effectiveOffset];

  const stmt = db.prepare(query);
  const rows = stmt.all(...params);

  return {
    data: (rows as MemoryRow[]).map(rowToMemory),
    total,
    byType,
  };
}

/**
 * Total estimated tokens across stored memories (4 chars ≈ 1 token), computed in
 * SQL so we never load every memory's content into process memory. Scoped to a
 * single API key when `apiKeyId` is provided, otherwise counts all memories.
 */
export function getMemoryTokensUsed(apiKeyId?: string): number {
  const db = getDbInstance();
  const stmt = db.prepare(
    "SELECT COALESCE(SUM((LENGTH(content) + 3) / 4), 0) as tokensUsed FROM memories" +
      (apiKeyId ? " WHERE api_key_id = ?" : "")
  );
  const row = stmt.get(...(apiKeyId ? [apiKeyId] : [])) as { tokensUsed: number } | undefined;
  return row?.tokensUsed ?? 0;
}
