import {
  isTemporalQuery,
  recencyWindowBoundsFromPrompt
} from "./chunk-V3RXWQIE.js";
import {
  log
} from "./chunk-2ODBA7MQ.js";

// src/retrieval-agents.ts
import path from "path";
import { readdir, readFile, stat } from "fs/promises";
var PARALLEL_AGENT_WEIGHTS = {
  direct: 1,
  // entity-first: high precision
  temporal: 0.85,
  // recency-boosted: relevant but broader
  contextual: 0.7
  // semantic: broadest, lower precision
};
function shouldRunAgent(agent, query, knownEntityCount) {
  switch (agent) {
    case "direct":
      return knownEntityCount > 0 || /\b\w{2,}/.test(query);
    case "temporal":
      return isTemporalQuery(query);
    case "contextual":
      return true;
    default:
      return true;
  }
}
function tokenize(value) {
  return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim().split(/\s+/).filter((t) => t.length >= 2);
}
function overlapScore(queryTokens, candidateTokens) {
  if (queryTokens.length === 0 || candidateTokens.length === 0) return 0;
  const candidateSet = new Set(candidateTokens);
  const hits = queryTokens.filter((t) => candidateSet.has(t)).length;
  return hits / Math.max(queryTokens.length, candidateTokens.length);
}
async function runDirectAgent(query, memoryDir, maxResults = 10) {
  try {
    const entitiesDir = path.join(memoryDir, "entities");
    let entries;
    try {
      entries = await readdir(entitiesDir);
    } catch {
      return [];
    }
    const queryTokens = tokenize(query);
    if (queryTokens.length === 0) return [];
    const results = [];
    for (const entry of entries) {
      if (!entry.endsWith(".md")) continue;
      const nameWithoutExt = entry.slice(0, -3);
      const entityTokens = nameWithoutExt.split("-").filter((t) => t.length >= 2).map((t) => t.toLowerCase());
      const score = overlapScore(queryTokens, entityTokens);
      if (score <= 0) continue;
      results.push({
        docid: nameWithoutExt,
        path: path.join(entitiesDir, entry),
        snippet: "",
        // populated by augmentWithDirectAndTemporal after merge
        score,
        transport: "scoped_prefilter",
        agentSource: "direct"
      });
    }
    return results.sort((a, b) => b.score - a.score).slice(0, maxResults);
  } catch (err) {
    log.debug(`DirectFactAgent error: ${err}`);
    return [];
  }
}
async function runTemporalAgent(query, memoryDir, maxResults = 20, candidatePaths) {
  try {
    let dateIndex = {};
    try {
      const indexPath = path.join(memoryDir, "state", "index_time.json");
      const raw = await readFile(indexPath, "utf-8");
      const parsed = JSON.parse(raw);
      dateIndex = parsed.dates ?? {};
    } catch {
      return [];
    }
    const { fromDate, toDate } = recencyWindowBoundsFromPrompt(query);
    const pathToDate = /* @__PURE__ */ new Map();
    for (const [date, datePaths] of Object.entries(dateIndex)) {
      if (date >= fromDate && date < toDate) {
        for (const p of datePaths) {
          if (candidatePaths && !candidatePaths.has(p)) continue;
          if (!pathToDate.has(p)) pathToDate.set(p, date);
        }
      }
    }
    if (pathToDate.size === 0) return [];
    const todayMs = Date.now();
    const TEMPORAL_STOPWORDS = /* @__PURE__ */ new Set([
      "today",
      "yesterday",
      "this",
      "last",
      "week",
      "month",
      "year",
      "morning",
      "night",
      "now",
      "earlier",
      "since",
      "after",
      "before",
      "ago",
      "hours",
      "days",
      "weeks",
      "months",
      "january",
      "february",
      "march",
      "april",
      "may",
      "june",
      "july",
      "august",
      "september",
      "october",
      "november",
      "december",
      "monday",
      "tuesday",
      "wednesday",
      "thursday",
      "friday",
      "saturday",
      "sunday",
      "what",
      "when",
      "where",
      "who",
      "why",
      "how",
      "the",
      "a",
      "an",
      "and",
      "or",
      "in",
      "on",
      "at",
      "to",
      "of",
      "for",
      "with",
      "by",
      "from",
      "changed",
      "happened",
      "was",
      "were",
      "is",
      "are",
      "did",
      "about",
      "that",
      "have",
      "had",
      "has",
      "been",
      "any",
      "all"
    ]);
    const topicTokens = tokenize(query).filter((t) => !TEMPORAL_STOPWORDS.has(t));
    const entries = [...pathToDate.entries()];
    const tagIndexPromise = topicTokens.length > 0 ? readFile(path.join(memoryDir, "state", "index_tags.json"), "utf-8").then((raw) => {
      const parsed = JSON.parse(raw);
      const result = /* @__PURE__ */ new Map();
      for (const [tag, node] of Object.entries(parsed.tags ?? {})) {
        const tagPaths = Array.isArray(node) ? node : node.paths ?? [];
        for (const tp of tagPaths) {
          const existing = result.get(tp);
          if (existing) existing.push(tag);
          else result.set(tp, [tag]);
        }
      }
      return result;
    }).catch(() => /* @__PURE__ */ new Map()) : Promise.resolve(/* @__PURE__ */ new Map());
    const [existenceResults, pathToTags] = await Promise.all([
      Promise.all(entries.map(([p]) => stat(p).then(() => true).catch(() => false))),
      tagIndexPromise
    ]);
    const results = [];
    for (let i = 0; i < entries.length; i++) {
      if (!existenceResults[i]) continue;
      const [p, dateStr] = entries[i];
      const ageMs = todayMs - new Date(dateStr).getTime();
      const ageDays = ageMs / 864e5;
      const recencyScore = Math.exp(-ageDays / 30);
      let topicBoost = 0;
      if (topicTokens.length > 0) {
        const tags = pathToTags.get(p);
        if (tags && tags.length > 0) {
          const tagSet = new Set(tags.flatMap((t) => tokenize(t)));
          const hits = topicTokens.filter((t) => tagSet.has(t)).length;
          topicBoost = hits / topicTokens.length;
        }
      }
      const score = recencyScore * (1 + topicBoost * 0.3);
      const baseName = path.basename(p, ".md");
      results.push({
        docid: baseName,
        path: p,
        snippet: "",
        // populated by augmentWithDirectAndTemporal after merge
        score,
        transport: "scoped_prefilter",
        agentSource: "temporal"
      });
    }
    return results.sort((a, b) => b.score - a.score).slice(0, maxResults);
  } catch (err) {
    log.debug(`TemporalAgent error: ${err}`);
    return [];
  }
}
async function runContextualAgent(query, qmd, collection, maxResults, signal) {
  try {
    const results = await qmd.hybridSearch(
      query,
      collection,
      maxResults,
      signal ? { signal } : void 0
    );
    return results.map((r) => ({ ...r, agentSource: "contextual" }));
  } catch (err) {
    log.debug(`ContextualAgent error: ${err}`);
    return [];
  }
}
function mergeAgentResults(allResults, weights, maxResults) {
  const merged = /* @__PURE__ */ new Map();
  for (const result of allResults) {
    const key = result.path || result.docid;
    const weightedScore = result.score * weights[result.agentSource];
    const existing = merged.get(key);
    if (!existing || weightedScore > existing.score) {
      merged.set(key, {
        docid: result.docid,
        path: result.path,
        // Preserve any snippet from the existing entry when the new one has none
        snippet: result.snippet || existing?.snippet || "",
        score: weightedScore,
        transport: "hybrid"
      });
    }
  }
  return [...merged.values()].sort((a, b) => b.score - a.score).slice(0, maxResults);
}
var SNIPPET_PREVIEW_CHARS = 200;
var MAX_SNIPPET_READS = 8;
function stripFrontmatter(raw) {
  if (!raw.startsWith("---")) return raw;
  const end = raw.indexOf("\n---", 3);
  if (end === -1) return raw;
  return raw.slice(end + 4).replace(/^\n+/, "");
}
async function populateEmptySnippets(results) {
  const needsSnippet = results.filter((r) => !r.snippet && r.path);
  if (needsSnippet.length === 0) return results;
  const toRead = needsSnippet.slice(0, MAX_SNIPPET_READS);
  const snippetMap = /* @__PURE__ */ new Map();
  await Promise.all(
    toRead.map(async (r) => {
      try {
        const raw = await readFile(r.path, "utf-8");
        const body = stripFrontmatter(raw);
        const preview = body.slice(0, SNIPPET_PREVIEW_CHARS).replace(/\s+/g, " ").trim();
        if (preview) snippetMap.set(r.path, preview);
      } catch {
      }
    })
  );
  if (snippetMap.size === 0) return results;
  return results.map((r) => r.path && snippetMap.has(r.path) ? { ...r, snippet: snippetMap.get(r.path) } : r);
}
async function mergeWithAgentResults(contextualResults, directResults, temporalResults, weights, maxResults) {
  if (directResults.length === 0 && temporalResults.length === 0) {
    return contextualResults.slice(0, maxResults);
  }
  const contextualTagged = contextualResults.map((r) => ({
    ...r,
    agentSource: "contextual"
  }));
  const merged = mergeAgentResults(
    [...contextualTagged, ...directResults, ...temporalResults],
    weights,
    maxResults
  );
  return populateEmptySnippets(merged);
}
async function augmentWithDirectAndTemporal(query, memoryDir, contextualResults, weights, maxPerAgent, maxResults, candidatePaths) {
  if (maxPerAgent === 0) return contextualResults;
  const knownEntityCount = (query.match(/\b[A-Z][a-z]{1,}/g) ?? []).length;
  const runDirect = shouldRunAgent("direct", query, knownEntityCount);
  const runTemporal = shouldRunAgent("temporal", query, knownEntityCount);
  const startMs = Date.now();
  const [directResults, temporalResults] = await Promise.all([
    runDirect ? runDirectAgent(query, memoryDir, maxPerAgent).catch((err) => {
      log.debug(`augmentWithDirectAndTemporal: DirectAgent failed \u2014 ${err}`);
      return [];
    }) : Promise.resolve([]),
    runTemporal ? runTemporalAgent(query, memoryDir, maxPerAgent, candidatePaths).catch((err) => {
      log.debug(`augmentWithDirectAndTemporal: TemporalAgent failed \u2014 ${err}`);
      return [];
    }) : Promise.resolve([])
  ]);
  const durationMs = Date.now() - startMs;
  const scopedDirect = directResults;
  const contextualTagged = contextualResults.map((r) => ({
    ...r,
    agentSource: "contextual"
  }));
  log.debug(
    `augmentWithDirectAndTemporal: direct=${scopedDirect.length} temporal=${temporalResults.length} contextual=${contextualTagged.length} agentMs=${durationMs}ms`
  );
  if (scopedDirect.length === 0 && temporalResults.length === 0) {
    return contextualResults.slice(0, maxResults);
  }
  const merged = mergeAgentResults(
    [...contextualTagged, ...scopedDirect, ...temporalResults],
    weights,
    maxResults
  );
  return populateEmptySnippets(merged);
}
async function parallelRetrieval(query, qmd, memoryDir, options = {}) {
  const maxResults = options.maxResults ?? 20;
  const maxPerAgent = options.maxResultsPerAgent ?? maxResults;
  const startMs = Date.now();
  const knownEntityCount = (query.match(/\b[A-Z][a-z]{1,}/g) ?? []).length;
  const runDirect = shouldRunAgent("direct", query, knownEntityCount);
  const runTemporal = shouldRunAgent("temporal", query, knownEntityCount);
  const runContextual = !options.skipContextual && shouldRunAgent("contextual", query, knownEntityCount);
  const [directResults, temporalResults, contextualResults] = await Promise.all([
    runDirect ? runDirectAgent(query, memoryDir, maxPerAgent).catch((err) => {
      log.debug(`parallelRetrieval: DirectAgent failed \u2014 ${err}`);
      return [];
    }) : Promise.resolve([]),
    runTemporal ? runTemporalAgent(query, memoryDir, maxPerAgent).catch((err) => {
      log.debug(`parallelRetrieval: TemporalAgent failed \u2014 ${err}`);
      return [];
    }) : Promise.resolve([]),
    runContextual ? runContextualAgent(query, qmd, options.collection, maxPerAgent, options.signal).catch((err) => {
      log.debug(`parallelRetrieval: ContextualAgent failed \u2014 ${err}`);
      return [];
    }) : Promise.resolve([])
  ]);
  const durationMs = Date.now() - startMs;
  log.debug(
    `parallelRetrieval: direct=${directResults.length} temporal=${temporalResults.length} contextual=${contextualResults.length} durationMs=${durationMs}ms`
  );
  const merged = mergeAgentResults(
    [...contextualResults, ...directResults, ...temporalResults],
    options.agentWeights ?? PARALLEL_AGENT_WEIGHTS,
    maxResults
  );
  return populateEmptySnippets(merged);
}

export {
  PARALLEL_AGENT_WEIGHTS,
  shouldRunAgent,
  runDirectAgent,
  runTemporalAgent,
  runContextualAgent,
  mergeWithAgentResults,
  augmentWithDirectAndTemporal,
  parallelRetrieval
};
//# sourceMappingURL=chunk-K4FLSOR5.js.map