/**
 * buildMemoryContext — assemble the memory chunk for the agent-loop
 * system prompt.
 *
 * Wraps store.listBlocks (for pinned + recall) and store.search (for
 * query-driven retrieval) and post-filters out internal bookkeeping
 * blocks like the summarizer cursor/health by key prefix. The actual
 * string rendering reuses renderMemoryContext(blocks, hits) — this
 * helper is just the assembly + filtering layer so callers don't have
 * to think about it.
 *
 * Why post-filter instead of extending MemoryStore.search/listBlocks
 * with scope filters: the existing API is flat key/value across both
 * backends (LocalMemoryStore + Temper) and we want zero changes there.
 * Forge owns the key naming convention (see lib/memory/keys.ts), so we
 * own the prefix-exclusion decision client-side.
 */

import type { MemoryBlock, MemoryStore, SearchHit } from './memory-store';
import { renderMemoryContext } from './temper';
import { INTERNAL_KEY_PREFIXES } from '../memory/keys';

export interface BuildMemoryContextOpts {
  store: MemoryStore;
  /** Used as `store.search(query)` — typically the latest user message. */
  currentUserMessage?: string;
  /** Cap on hits returned from store.search. Default 6. */
  topK?: number;
  /** Cap on inlined pinned blocks. Default 50 (renderMemoryContext default). */
  maxBlocks?: number;
  /** Prefixes that mark internal-only blocks (cursor / health / etc).
   *  Defaults to lib/memory/keys.INTERNAL_KEY_PREFIXES. */
  excludeKeyPrefixes?: readonly string[];
  /** Current chat session id. When set, blocks keyed `chat:<otherId>:*`
   *  are dropped — other sessions' summaries are noise in this chat and
   *  were the primary source of "old data bleeding into new chat". */
  currentSessionId?: string;
}

export interface BuildMemoryContextResult {
  text: string;
  blocks: MemoryBlock[];
  hits: SearchHit[];
}

export async function buildMemoryContext(opts: BuildMemoryContextOpts): Promise<BuildMemoryContextResult> {
  const {
    store,
    currentUserMessage,
    topK = 6,
    maxBlocks = 50,
    excludeKeyPrefixes = INTERNAL_KEY_PREFIXES,
    currentSessionId,
  } = opts;

  const blocks = dropForeignChat(
    filterInternal(
      await safe(() => store.listBlocks({ pinned: true }), [] as MemoryBlock[]),
      excludeKeyPrefixes,
    ),
    currentSessionId,
  ).slice(0, maxBlocks);

  const q = (currentUserMessage || '').trim();
  let hits: SearchHit[] = [];
  if (q) {
    const rawHits = await safe(() => store.search(q, topK), [] as SearchHit[]);
    hits = dropForeignChatHits(filterInternalHits(rawHits, excludeKeyPrefixes), currentSessionId);
  }

  return { text: renderMemoryContext(blocks, hits), blocks, hits };
}

function filterInternal(blocks: MemoryBlock[], prefixes: readonly string[]): MemoryBlock[] {
  if (prefixes.length === 0) return blocks;
  return blocks.filter((b) => !prefixes.some((p) => b.key.startsWith(p)));
}

function filterInternalHits(hits: SearchHit[], prefixes: readonly string[]): SearchHit[] {
  if (prefixes.length === 0) return hits;
  // SearchHit.id encodes its source: LocalMemoryStore returns 'block:<key>'
  // for block-derived hits. Temper returns Graphiti UUIDs — those won't
  // match prefixes, so they pass through (correct: Temper hits aren't
  // direct block references).
  return hits.filter((h) => {
    if (!h.id?.startsWith('block:')) return true;
    const key = h.id.slice('block:'.length);
    return !prefixes.some((p) => key.startsWith(p));
  });
}

/** Strip `chat:<otherSessionId>:*` blocks. Summary blocks contain raw
 *  past-conversation excerpts; surfacing them in a different chat is
 *  what made "new empty chat" leak old session content. Facts
 *  (`fact:*`) and any non-chat-prefixed pinned blocks stay — they're
 *  the intentional cross-session signal. No-op if no sessionId given. */
function dropForeignChat(blocks: MemoryBlock[], sessionId?: string): MemoryBlock[] {
  if (!sessionId) return blocks;
  return blocks.filter((b) => isOwnChatOrNotChat(b.key, sessionId));
}

function dropForeignChatHits(hits: SearchHit[], sessionId?: string): SearchHit[] {
  if (!sessionId) return hits;
  return hits.filter((h) => {
    if (!h.id?.startsWith('block:')) return true; // Graphiti hit, no key to inspect — keep
    return isOwnChatOrNotChat(h.id.slice('block:'.length), sessionId);
  });
}

function isOwnChatOrNotChat(key: string, sessionId: string): boolean {
  if (!key.startsWith('chat:')) return true;
  // key shape: chat:<sessionId>:summary:<ts> → split[1] === sessionId
  return key.split(':', 2)[1] === sessionId;
}

async function safe<T>(fn: () => Promise<T>, fallback: T): Promise<T> {
  try {
    return await fn();
  } catch (err) {
    console.warn('[buildMemoryContext]', err instanceof Error ? err.message : err);
    return fallback;
  }
}
