// SPDX-License-Identifier: AGPL-3.0-only
// Copyright (C) 2026 Giancarlo Erra - Altaire Limited
import { createHash } from "node:crypto";
import { QdrantClient } from "@qdrant/js-client-rest";
import { QDRANT_API_KEY, QDRANT_HOST, QDRANT_PORT, QDRANT_URL, resolveQdrantPort } from "../constants.js";
import type { ArtifactIndexState, CodeGraph, FileChunk, SearchResult } from "../types.js";
import { getEmbeddingConfig } from "./embedding-config.js";
import { generateEmbeddings, generateQueryEmbedding, prepareDocumentText } from "./embeddings.js";
import { logger } from "./logger.js";

const MAX_RETRIES = 3;
const BASE_DELAY_MS = 500;

/** Retry an async operation with exponential backoff */
async function withRetry<T>(
  operation: () => Promise<T>,
  label: string,
  maxRetries = MAX_RETRIES,
): Promise<T> {
  let lastError: unknown;
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await operation();
    } catch (err) {
      lastError = err;
      if (attempt < maxRetries) {
        const delay = BASE_DELAY_MS * 2 ** (attempt - 1);
        logger.warn(`${label} failed (attempt ${attempt}/${maxRetries}), retrying in ${delay}ms`, {
          error: err instanceof Error ? err.message : String(err),
        });
        await new Promise((r) => setTimeout(r, delay));
      }
    }
  }
  throw lastError;
}

let client: QdrantClient | null = null;

function getClient(): QdrantClient {
  if (!client) {
    client = new QdrantClient(
      QDRANT_URL
        ? {
            url: QDRANT_URL,
            port: resolveQdrantPort(QDRANT_URL),
            ...(QDRANT_API_KEY ? { apiKey: QDRANT_API_KEY } : {}),
            checkCompatibility: false,
          }
        : {
            host: QDRANT_HOST,
            port: QDRANT_PORT,
            ...(QDRANT_API_KEY ? { apiKey: QDRANT_API_KEY } : {}),
            checkCompatibility: false,
          },
    );
  }
  return client;
}

/** Create a collection if it doesn't exist */
export async function ensureCollection(name: string): Promise<void> {
  const qdrant = getClient();
  const collections = await qdrant.getCollections();
  const exists = collections.collections.some((c) => c.name === name);

  if (!exists) {
    const { embeddingDimensions } = getEmbeddingConfig();
    await qdrant.createCollection(name, {
      vectors: {
        dense: {
          size: embeddingDimensions,
          distance: "Cosine",
        },
      },
      sparse_vectors: {
        bm25: {
          modifier: "idf",
        },
      },
      optimizers_config: {
        default_segment_number: 2,
      },
      on_disk_payload: true,
    });

    // Create payload indexes for faster filtering
    await qdrant.createPayloadIndex(name, {
      field_name: "filePath",
      field_schema: "keyword",
    });
    await qdrant.createPayloadIndex(name, {
      field_name: "relativePath",
      field_schema: "keyword",
    });
    await qdrant.createPayloadIndex(name, {
      field_name: "language",
      field_schema: "keyword",
    });
    await qdrant.createPayloadIndex(name, {
      field_name: "contentHash",
      field_schema: "keyword",
    });
  }
}

/** Create a payload index on a collection (idempotent — ignores "already exists" errors) */
export async function ensurePayloadIndex(collName: string, fieldName: string): Promise<void> {
  const qdrant = getClient();
  try {
    await qdrant.createPayloadIndex(collName, {
      field_name: fieldName,
      field_schema: "keyword",
    });
  } catch {
    // Index already exists — ignore
  }
}

/** Delete a collection */
export async function deleteCollection(name: string): Promise<void> {
  const qdrant = getClient();
  try {
    logger.warn("Deleting Qdrant collection", { collection: name });
    await qdrant.deleteCollection(name);
    logger.info("Deleted Qdrant collection", { collection: name });
  } catch (err) {
    // collection may not exist
    logger.info("deleteCollection: collection may not exist (ignored)", {
      collection: name,
      error: err instanceof Error ? err.message : String(err),
    });
  }
}

/** List all codebase, codegraph, and context artifact entries.
 * Codebase and context entries are actual collections; codegraph entries come from metadata. */
export async function listCodebaseCollections(): Promise<string[]> {
  const qdrant = getClient();
  const collections = await qdrant.getCollections();
  const result = collections.collections
    .map((c) => c.name)
    .filter((n) => n.startsWith("codebase_") || n.startsWith("codegraph_") || n.startsWith("context_"));

  // Also check metadata for graph and context entries (stored as metadata points, not real collections)
  try {
    await ensureMetadataCollection();
    const metaPoints = await qdrant.scroll(METADATA_COLLECTION, {
      limit: 100,
      with_payload: true,
    });
    for (const point of metaPoints.points) {
      const collName = point.payload?.collectionName as string | undefined;
      if (
        (collName?.startsWith("codegraph_") || collName?.startsWith("context_")) &&
        !result.includes(collName)
      ) {
        result.push(collName);
      }
    }
  } catch (err) {
    // Metadata collection may not exist yet (expected before first index)
    logger.info("listCodebaseCollections: metadata scroll failed (expected if no projects indexed yet)", {
      error: err instanceof Error ? err.message : String(err),
    });
  }

  return result;
}

/** Upsert chunks into a collection */
export async function upsertChunks(
  collectionName: string,
  chunks: FileChunk[],
  contentHash: string,
): Promise<void> {
  if (chunks.length === 0) return;

  const qdrant = getClient();
  const texts = chunks.map((c) => prepareDocumentText(c.content, c.relativePath));
  const embeddings = await generateEmbeddings(texts);

  const points = chunks.map((chunk, i) => ({
    id: chunk.id,
    vector: {
      dense: embeddings[i],
      bm25: {
        text: texts[i],
        model: "qdrant/bm25",
      },
    },
    payload: {
      filePath: chunk.filePath,
      relativePath: chunk.relativePath,
      content: chunk.content,
      startLine: chunk.startLine,
      endLine: chunk.endLine,
      language: chunk.language,
      type: chunk.type,
      contentHash,
    },
  }));

  // Upsert in batches of 100
  for (let i = 0; i < points.length; i += 100) {
    const batch = points.slice(i, i + 100);
    await withRetry(
      () => qdrant.upsert(collectionName, { points: batch }),
      `Qdrant upsert batch ${Math.floor(i / 100) + 1}`,
    );
  }
}

/** Maximum text length (in characters) sent to Qdrant's server-side BM25 tokenizer.
 * Oversized texts are truncated — the dense vector still captures full semantics,
 * and the stored content payload remains full-length for display. */
const MAX_BM25_TEXT_CHARS = 32_000; // ~32KB

/** Upsert pre-embedded points into a collection (no embedding generation).
 * bm25Text is forwarded to Qdrant's server-side BM25 inference (truncated if too long).
 * Returns the number of points that were skipped due to upsert errors. */
export async function upsertPreEmbeddedChunks(
  collectionName: string,
  points: Array<{
    id: string;
    vector: number[];
    bm25Text: string;
    payload: Record<string, unknown>;
  }>,
): Promise<{ pointsSkipped: number }> {
  if (points.length === 0) return { pointsSkipped: 0 };

  const qdrant = getClient();
  const namedPoints = points.map((p) => ({
    id: p.id,
    vector: {
      dense: p.vector,
      bm25: {
        text: p.bm25Text.length > MAX_BM25_TEXT_CHARS
          ? p.bm25Text.slice(0, MAX_BM25_TEXT_CHARS)
          : p.bm25Text,
        model: "qdrant/bm25",
      },
    },
    payload: p.payload,
  }));

  let totalSkipped = 0;

  // Upsert in batches of 100, with per-point fallback on failure
  for (let i = 0; i < namedPoints.length; i += 100) {
    const batch = namedPoints.slice(i, i + 100);
    const batchLabel = `Qdrant upsert batch ${Math.floor(i / 100) + 1}`;
    try {
      await withRetry(
        () => qdrant.upsert(collectionName, { points: batch }),
        batchLabel,
      );
    } catch (batchErr) {
      // Batch failed after retries — fall back to one-by-one to isolate the bad point(s)
      logger.warn(`${batchLabel} failed, falling back to per-point upsert to isolate failures`, {
        error: batchErr instanceof Error ? batchErr.message : String(batchErr),
        pointCount: batch.length,
      });
      let skipped = 0;
      for (const point of batch) {
        try {
          await qdrant.upsert(collectionName, { points: [point] });
        } catch (pointErr) {
          skipped++;
          const filePath = point.payload?.relativePath ?? point.payload?.filePath ?? point.id;
          logger.warn(`Skipping point that failed upsert`, {
            pointId: point.id,
            filePath: String(filePath),
            error: pointErr instanceof Error ? pointErr.message : String(pointErr),
          });
        }
      }
      if (skipped > 0) {
        logger.warn(`${batchLabel}: ${skipped}/${batch.length} points skipped due to errors`);
      }
      totalSkipped += skipped;
    }
  }

  return { pointsSkipped: totalSkipped };
}

/** Delete all chunks for a specific file (matched by relativePath) */
export async function deleteFileChunks(collectionName: string, relativePath: string): Promise<void> {
  const qdrant = getClient();
  logger.info("Deleting file chunks", { collection: collectionName, relativePath });
  await withRetry(
    () => qdrant.delete(collectionName, {
      filter: {
        must: [{ key: "relativePath", match: { value: relativePath } }],
      },
    }),
    "Qdrant delete chunks",
  );
}

/** Hybrid search: combines dense semantic search with BM25 lexical search via RRF fusion.
 * Dense vector is generated client-side; BM25 inference runs server-side in Qdrant (requires v1.15.2+). */
export async function searchChunks(
  collectionName: string,
  query: string,
  limit: number = 10,
  fileFilter?: string,
  languageFilter?: string,
): Promise<SearchResult[]> {
  const qdrant = getClient();
  const queryVector = await generateQueryEmbedding(query);

  const filter: { must: Array<{ key: string; match: { value: string } }> } = { must: [] };
  if (fileFilter) {
    filter.must.push({ key: "relativePath", match: { value: fileFilter } });
  }
  if (languageFilter) {
    filter.must.push({ key: "language", match: { value: languageFilter } });
  }

  // Fetch more candidates per sub-query so RRF has enough to re-rank
  const prefetchLimit = Math.max(limit * 3, 30);
  const activeFilter = filter.must.length > 0 ? filter : undefined;

  const results = await withRetry(
    () => qdrant.query(collectionName, {
      prefetch: [
        { query: queryVector, using: "dense", limit: prefetchLimit, filter: activeFilter },
        {
          query: { text: query, model: "qdrant/bm25" },
          using: "bm25",
          limit: prefetchLimit,
          filter: activeFilter,
        },
      ],
      query: { fusion: "rrf" },
      limit,
      with_payload: true,
      filter: activeFilter,
    }),
    "Qdrant hybrid search",
  );

  return results.points.map((r) => ({
    filePath: r.payload?.filePath as string,
    relativePath: r.payload?.relativePath as string,
    content: r.payload?.content as string,
    startLine: r.payload?.startLine as number,
    endLine: r.payload?.endLine as number,
    language: r.payload?.language as string,
    score: r.score,
  }));
}

/** Hybrid search with arbitrary payload filters.
 * Used by context artifacts to filter by artifactName. */
export async function searchChunksWithFilter(
  collectionName: string,
  query: string,
  limit: number,
  filters: Array<{ key: string; value: string }>,
): Promise<SearchResult[]> {
  const qdrant = getClient();
  const queryVector = await generateQueryEmbedding(query);

  const filter = filters.length > 0
    ? { must: filters.map((f) => ({ key: f.key, match: { value: f.value } })) }
    : undefined;

  const prefetchLimit = Math.max(limit * 3, 30);

  const results = await withRetry(
    () => qdrant.query(collectionName, {
      prefetch: [
        { query: queryVector, using: "dense", limit: prefetchLimit, filter },
        {
          query: { text: query, model: "qdrant/bm25" },
          using: "bm25",
          limit: prefetchLimit,
          filter,
        },
      ],
      query: { fusion: "rrf" },
      limit,
      with_payload: true,
      filter,
    }),
    "Qdrant hybrid search (filtered)",
  );

  return results.points.map((r) => ({
    filePath: r.payload?.filePath as string,
    relativePath: r.payload?.relativePath as string,
    content: r.payload?.content as string,
    startLine: r.payload?.startLine as number,
    endLine: r.payload?.endLine as number,
    language: r.payload?.language as string,
    score: r.score,
  }));
}

/** Get collection info.
 * Returns the collection info if it exists, null if the collection does not exist,
 * or throws an error if the request fails for any other reason (network, timeout, etc.).
 * This distinction is critical: callers must NOT treat transient errors as "collection missing". */
export async function getCollectionInfo(name: string): Promise<{
  pointsCount: number;
  status: string;
} | null> {
  const qdrant = getClient();
  try {
    const info = await qdrant.getCollection(name);
    return {
      pointsCount: info.points_count ?? 0,
      status: info.status,
    };
  } catch (err: unknown) {
    // Only return null for "not found" — propagate all other errors
    const message = err instanceof Error ? err.message : String(err);
    const status = (err as { status?: number })?.status;
    if (status === 404 || message.includes("Not found") || message.includes("doesn't exist") || message.includes("not found")) {
      return null;
    }
    logger.warn("getCollectionInfo failed with unexpected error (propagating)", { collection: name, error: message });
    throw err;
  }
}

// ── Project metadata collection ──────────────────────────────────────────

const METADATA_COLLECTION = "socraticode_metadata";

/** Cached flag: once the metadata collection is confirmed to exist, skip re-checking */
let metadataCollectionReady = false;

/** Reset the metadata collection readiness cache (for testing only) */
export function resetMetadataCollectionCache(): void {
  metadataCollectionReady = false;
}

/** Ensure the metadata collection exists (idempotent, cached after first success) */
async function ensureMetadataCollection(): Promise<void> {
  if (metadataCollectionReady) return;

  const qdrant = getClient();
  const collections = await qdrant.getCollections();
  const exists = collections.collections.some((c) => c.name === METADATA_COLLECTION);
  if (!exists) {
    // Metadata collection uses a dummy 1-dim vector since Qdrant requires vectors
    await qdrant.createCollection(METADATA_COLLECTION, {
      vectors: { size: 1, distance: "Cosine" },
      on_disk_payload: true,
    });
    await qdrant.createPayloadIndex(METADATA_COLLECTION, {
      field_name: "collectionName",
      field_schema: "keyword",
    });
    logger.info("Created metadata collection");
  }

  metadataCollectionReady = true;
}

/** Generate a stable UUID from a collection name (for Qdrant point ID).
 *  Uses SHA-256 to avoid collision risk inherent in simpler hashes (e.g. djb2). */
function metadataPointId(collName: string): string {
  const hash = createHash("sha256").update(collName).digest("hex").slice(0, 32);
  // Format as UUID: 8-4-4-4-12
  return `${hash.slice(0, 8)}-${hash.slice(8, 12)}-${hash.slice(12, 16)}-${hash.slice(16, 20)}-${hash.slice(20, 32)}`;
}

/** Indexing status persisted in Qdrant metadata */
export type IndexingStatus = "in-progress" | "completed";

/** Save project metadata and file hashes to Qdrant */
export async function saveProjectMetadata(
  collName: string,
  projectPath: string,
  filesTotal: number,
  filesIndexed: number,
  fileHashes: Map<string, string>,
  indexingStatus: IndexingStatus,
): Promise<void> {
  await ensureMetadataCollection();
  const qdrant = getClient();
  const id = metadataPointId(collName);

  const hashObj: Record<string, string> = {};
  for (const [k, v] of fileHashes) {
    hashObj[k] = v;
  }

  await qdrant.upsert(METADATA_COLLECTION, {
    points: [
      {
        id,
        vector: [0],
        payload: {
          collectionName: collName,
          projectPath,
          lastIndexedAt: new Date().toISOString(),
          filesTotal,
          filesIndexed,
          fileHashes: JSON.stringify(hashObj),
          indexingStatus,
        },
      },
    ],
  });

  logger.info("Saved project metadata", { collName, projectPath, filesTotal, filesIndexed, indexingStatus });
}

/** Load file hashes for a project from Qdrant.
 * Returns the hash map if found, null if the metadata point doesn't exist,
 * or throws on transient/unexpected errors so callers can distinguish
 * "no metadata" from "Qdrant unreachable". */
export async function loadProjectHashes(collName: string): Promise<Map<string, string> | null> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(collName);

    const points = await qdrant.retrieve(METADATA_COLLECTION, {
      ids: [id],
      with_payload: true,
    });

    if (points.length === 0) return null;

    const payload = points[0].payload;
    if (!payload?.fileHashes) return null;

    const hashObj = JSON.parse(payload.fileHashes as string) as Record<string, string>;
    return new Map(Object.entries(hashObj));
  } catch (err) {
    logger.warn("loadProjectHashes failed (propagating)", {
      collName,
      error: err instanceof Error ? err.message : String(err),
    });
    throw err;
  }
}

/** Get project metadata (for list display).
 * Returns null if metadata doesn't exist or on any error (logged as warning). */
export async function getProjectMetadata(collName: string): Promise<{
  projectPath: string;
  lastIndexedAt: string;
  filesTotal: number;
  filesIndexed: number;
  indexingStatus: IndexingStatus;
} | null> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(collName);

    const points = await qdrant.retrieve(METADATA_COLLECTION, {
      ids: [id],
      with_payload: true,
    });

    if (points.length === 0) return null;

    const payload = points[0].payload;
    return {
      projectPath: payload?.projectPath as string,
      lastIndexedAt: payload?.lastIndexedAt as string,
      filesTotal: (payload?.filesTotal as number) ?? (payload?.filesIndexed as number) ?? 0,
      filesIndexed: (payload?.filesIndexed as number) ?? 0,
      indexingStatus: (payload?.indexingStatus as IndexingStatus) ?? "completed",
    };
  } catch (err) {
    logger.warn("getProjectMetadata failed (returning null)", {
      collName,
      error: err instanceof Error ? err.message : String(err),
    });
    return null;
  }
}

/** Delete project metadata.
 * Errors are logged but not propagated (best-effort deletion). */
export async function deleteProjectMetadata(collName: string): Promise<void> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(collName);
    logger.warn("Deleting project metadata", { collName });
    await qdrant.delete(METADATA_COLLECTION, { points: [id] });
  } catch (err) {
    logger.warn("deleteProjectMetadata failed (ignored)", {
      collName,
      error: err instanceof Error ? err.message : String(err),
    });
  }
}

// ── Code graph persistence ──────────────────────────────────────────────

/** Save a code graph to Qdrant as a single metadata point */
export async function saveGraphData(
  graphCollName: string,
  projectPath: string,
  graph: CodeGraph,
): Promise<void> {
  await ensureMetadataCollection();
  const qdrant = getClient();
  const id = metadataPointId(graphCollName);

  await qdrant.upsert(METADATA_COLLECTION, {
    points: [
      {
        id,
        vector: [0],
        payload: {
          collectionName: graphCollName,
          projectPath,
          lastBuiltAt: new Date().toISOString(),
          nodeCount: graph.nodes.length,
          edgeCount: graph.edges.length,
          graphData: JSON.stringify(graph),
        },
      },
    ],
  });

  logger.info("Saved code graph", { graphCollName, projectPath, nodes: graph.nodes.length, edges: graph.edges.length });
}

/** Load a code graph from Qdrant.
 * Returns null if no graph exists or on any error (logged as warning). */
export async function loadGraphData(graphCollName: string): Promise<CodeGraph | null> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(graphCollName);

    const points = await qdrant.retrieve(METADATA_COLLECTION, {
      ids: [id],
      with_payload: true,
    });

    if (points.length === 0) return null;

    const payload = points[0].payload;
    if (!payload?.graphData) return null;

    return JSON.parse(payload.graphData as string) as CodeGraph;
  } catch (err) {
    logger.warn("loadGraphData failed (returning null)", {
      graphCollName,
      error: err instanceof Error ? err.message : String(err),
    });
    return null;
  }
}

/** Get graph metadata (for list/status display).
 * Returns null if no graph exists or on any error (logged as warning). */
export async function getGraphMetadata(graphCollName: string): Promise<{
  projectPath: string;
  lastBuiltAt: string;
  nodeCount: number;
  edgeCount: number;
} | null> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(graphCollName);

    const points = await qdrant.retrieve(METADATA_COLLECTION, {
      ids: [id],
      with_payload: true,
    });

    if (points.length === 0) return null;

    const payload = points[0].payload;
    return {
      projectPath: payload?.projectPath as string,
      lastBuiltAt: payload?.lastBuiltAt as string,
      nodeCount: payload?.nodeCount as number,
      edgeCount: payload?.edgeCount as number,
    };
  } catch (err) {
    logger.warn("getGraphMetadata failed (returning null)", {
      graphCollName,
      error: err instanceof Error ? err.message : String(err),
    });
    return null;
  }
}

/** Delete graph data from metadata.
 * Errors are logged but not propagated (best-effort deletion). */
export async function deleteGraphData(graphCollName: string): Promise<void> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(graphCollName);
    logger.warn("Deleting graph data", { graphCollName });
    await qdrant.delete(METADATA_COLLECTION, { points: [id] });
  } catch (err) {
    logger.warn("deleteGraphData failed (ignored)", {
      graphCollName,
      error: err instanceof Error ? err.message : String(err),
    });
  }
}

// ── Context artifact metadata ────────────────────────────────────────────

/** Save context artifact metadata to Qdrant */
export async function saveContextMetadata(
  contextCollName: string,
  projectPath: string,
  artifacts: ArtifactIndexState[],
): Promise<void> {
  await ensureMetadataCollection();
  const qdrant = getClient();
  const id = metadataPointId(contextCollName);

  await qdrant.upsert(METADATA_COLLECTION, {
    points: [
      {
        id,
        vector: [0],
        payload: {
          collectionName: contextCollName,
          projectPath,
          lastIndexedAt: new Date().toISOString(),
          artifactCount: artifacts.length,
          artifacts: JSON.stringify(artifacts),
        },
      },
    ],
  });

  logger.info("Saved context artifact metadata", { contextCollName, projectPath, artifactCount: artifacts.length });
}

/** Load context artifact metadata from Qdrant.
 * Returns null if no metadata exists or on any error (logged as warning). */
export async function loadContextMetadata(contextCollName: string): Promise<ArtifactIndexState[] | null> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(contextCollName);

    const points = await qdrant.retrieve(METADATA_COLLECTION, {
      ids: [id],
      with_payload: true,
    });

    if (points.length === 0) return null;

    const payload = points[0].payload;
    if (!payload?.artifacts) return null;

    return JSON.parse(payload.artifacts as string) as ArtifactIndexState[];
  } catch (err) {
    logger.warn("loadContextMetadata failed (returning null)", {
      contextCollName,
      error: err instanceof Error ? err.message : String(err),
    });
    return null;
  }
}

/** Get context collection metadata (for list/status display).
 * Returns null if no metadata exists or on any error (logged as warning). */
export async function getContextMetadata(contextCollName: string): Promise<{
  projectPath: string;
  lastIndexedAt: string;
  artifactCount: number;
} | null> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(contextCollName);

    const points = await qdrant.retrieve(METADATA_COLLECTION, {
      ids: [id],
      with_payload: true,
    });

    if (points.length === 0) return null;

    const payload = points[0].payload;
    return {
      projectPath: payload?.projectPath as string,
      lastIndexedAt: payload?.lastIndexedAt as string,
      artifactCount: (payload?.artifactCount as number) ?? 0,
    };
  } catch (err) {
    logger.warn("getContextMetadata failed (returning null)", {
      contextCollName,
      error: err instanceof Error ? err.message : String(err),
    });
    return null;
  }
}

/** Delete context artifact metadata.
 * Errors are logged but not propagated (best-effort deletion). */
export async function deleteContextMetadata(contextCollName: string): Promise<void> {
  try {
    await ensureMetadataCollection();
    const qdrant = getClient();
    const id = metadataPointId(contextCollName);
    logger.warn("Deleting context metadata", { contextCollName });
    await qdrant.delete(METADATA_COLLECTION, { points: [id] });
  } catch (err) {
    logger.warn("deleteContextMetadata failed (ignored)", {
      contextCollName,
      error: err instanceof Error ? err.message : String(err),
    });
  }
}

/** Delete all chunks for a specific artifact within a collection */
export async function deleteArtifactChunks(collectionName: string, artifactName: string): Promise<void> {
  const qdrant = getClient();
  logger.info("Deleting artifact chunks", { collection: collectionName, artifactName });
  await withRetry(
    () => qdrant.delete(collectionName, {
      filter: {
        must: [{ key: "artifactName", match: { value: artifactName } }],
      },
    }),
    "Qdrant delete artifact chunks",
  );
}
