/**
 * Cascade Forget — Directed forgetting with association propagation.
 *
 * Agent-First reasoning: when a user says "forget X", the primary memory
 * gets deleted/archived. But if related memories (meeting notes, decisions,
 * preferences mentioning X) keep their original importance, X still
 * surfaces indirectly through retrieval. Cascade forget finds related
 * entries and demotes them so X doesn't leak back via associations.
 *
 * Brain-science label: "directed forgetting" — suppressing a memory also
 * weakens retrieval of associated memories. We keep the name for branding;
 * the real mechanism is vector search + importance reduction.
 *
 * Rules:
 * - Does NOT delete related entries — only demotes importance and tier
 * - Demotion is proportional to similarity (higher sim = bigger demotion)
 * - Writes metadata breadcrumb for auditability
 * - No LLM calls — pure vector search + arithmetic
 */

import type { MemoryStore, MemoryEntry } from "./store.js";
import { isActiveMemory } from "./memory-evolution.js";

// ---------------------------------------------------------------------------
// Config
// ---------------------------------------------------------------------------

export interface CascadeForgetConfig {
  /** Minimum vector similarity to consider "related" (default: 0.70) */
  similarityThreshold: number;
  /** Maximum entries to demote per forget operation (default: 10) */
  maxDemotePerForget: number;
  /** Maximum importance reduction (at similarity=1.0) (default: 0.3) */
  maxDemotion: number;
  /** Floor: importance never drops below this (default: 0.05) */
  importanceFloor: number;
}

export const DEFAULT_CASCADE_FORGET_CONFIG: CascadeForgetConfig = {
  similarityThreshold: 0.70,
  maxDemotePerForget: 10,
  maxDemotion: 0.3,
  importanceFloor: 0.05,
};

// ---------------------------------------------------------------------------
// Core
// ---------------------------------------------------------------------------

export interface CascadeForgetResult {
  demotedCount: number;
  demotedIds: string[];
}

/**
 * After deleting/archiving a memory, cascade-demote related entries.
 *
 * @param store - Memory store
 * @param forgottenEntry - The entry that was just deleted/archived (need its vector + scope)
 * @param config - Cascade config
 */
export async function cascadeForget(
  store: MemoryStore,
  forgottenEntry: Pick<MemoryEntry, "id" | "vector" | "scope">,
  config: CascadeForgetConfig = DEFAULT_CASCADE_FORGET_CONFIG,
): Promise<CascadeForgetResult> {
  if (!forgottenEntry.vector?.length) {
    return { demotedCount: 0, demotedIds: [] };
  }

  // Find related entries in the same scope
  const candidates = await store.vectorSearch(
    forgottenEntry.vector,
    config.maxDemotePerForget * 2, // fetch extra, filter down
    config.similarityThreshold,
    [forgottenEntry.scope],
  );

  const demotedIds: string[] = [];

  for (const candidate of candidates) {
    if (demotedIds.length >= config.maxDemotePerForget) break;

    const entry = candidate.entry;

    // Skip the forgotten entry itself (may still be in index)
    if (entry.id === forgottenEntry.id) continue;

    // Skip already-archived/superseded entries (unified via evolution.status)
    if (!isActiveMemory(entry.metadata)) continue;
    let meta: Record<string, any> = {};
    try { meta = JSON.parse(entry.metadata || "{}"); } catch { /* skip */ }

    // Proportional demotion: higher similarity → bigger cut
    // At sim=1.0 → full maxDemotion, at threshold → near-zero
    const simRange = 1.0 - config.similarityThreshold;
    const simNormalized = simRange > 0
      ? (candidate.score - config.similarityThreshold) / simRange
      : 1.0;
    const demotion = config.maxDemotion * simNormalized;

    const newImportance = Math.max(
      entry.importance - demotion,
      config.importanceFloor,
    );

    // Skip if no meaningful change
    if (Math.abs(newImportance - entry.importance) < 0.01) continue;

    // Audit trail
    if (!Array.isArray(meta.cascade_forget)) meta.cascade_forget = [];
    meta.cascade_forget.push({
      forgottenId: forgottenEntry.id.slice(0, 8),
      from: entry.importance,
      to: newImportance,
      similarity: candidate.score,
      date: new Date().toISOString().slice(0, 10),
    });

    // Demote tier if importance drops below working threshold
    if (meta.tier === "working" && newImportance < 0.5) {
      meta.tier = "peripheral";
    } else if (meta.tier === "core" && newImportance < 0.8) {
      meta.tier = "working";
    }

    await store.update(entry.id, {
      importance: newImportance,
      metadata: JSON.stringify(meta),
    }, [forgottenEntry.scope]);

    demotedIds.push(entry.id);
  }

  return { demotedCount: demotedIds.length, demotedIds };
}
