/**
 * Crow Persistent Memory — Server Factory
 *
 * Creates a configured McpServer with all memory tools registered.
 * Transport-agnostic: used by both stdio (index.js) and HTTP (gateway).
 */

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";
import { createDbClient, sanitizeFtsQuery, escapeLikePattern, isSqliteVecAvailable } from "../db.js";
import { generateCrowContext, PROTECTED_SECTIONS, invalidateContextCache } from "./crow-context.js";
import { generateToken, validateToken, shouldSkipGates } from "../shared/confirm.js";
import { createNotification, cleanupNotifications } from "../shared/notifications.js";
import {
  embedText as phase4EmbedText,
  embedProviderInfo,
  upsertMemoryEmbedding,
  loadMemoryEmbeddings,
  rankByCosine,
  cosineSim,
} from "./embeddings.js";
import { rerank as phase4Rerank } from "./rerank.js";

export function createMemoryServer(dbPath, options = {}) {
  const db = createDbClient(dbPath);

  // Optional embedding function: (text) => Float32Array | null
  // Passed via options by the gateway when an AI provider with embeddings is configured.
  const getEmbedding = options.getEmbedding || null;

  // Optional sync manager for instance-to-instance replication.
  // When set, mutations emit change entries to connected instances.
  const syncManager = options.syncManager || null;

  // Cache sqlite-vec availability (checked once on first use)
  let _vecAvailable = null;
  async function hasVec() {
    if (_vecAvailable === null) {
      _vecAvailable = await isSqliteVecAvailable(db);
    }
    return _vecAvailable;
  }

  // --- Phase 4: BLOB-based semantic memory via grackle-embed ---
  // Cache provider-health check so we don't probe on every write.
  let _phase4ProviderInfo = null;
  async function phase4ProviderHealthy() {
    if (_phase4ProviderInfo === null) {
      _phase4ProviderInfo = await embedProviderInfo().catch(() => ({ ok: false }));
    }
    return _phase4ProviderInfo;
  }

  // Store embedding for a memory (fire-and-forget, errors don't block).
  // Prefers:
  //   1. Legacy options.getEmbedding + sqlite-vec path (kept for backward compat)
  //   2. Phase 4 BLOB + grackle-embed path (default when provider is healthy)
  async function storeEmbedding(memoryId, content) {
    // Legacy injected path (preserved)
    if (getEmbedding && await hasVec()) {
      try {
        const vec = await getEmbedding(content);
        if (!vec || vec.length === 0) return;
        const blob = Buffer.from(vec.buffer);
        await db.execute({
          sql: "INSERT OR REPLACE INTO memory_embeddings (memory_id, embedding) VALUES (?, ?)",
          args: [memoryId, blob],
        });
        return;
      } catch {}
    }
    // Phase 4 default path — non-blocking, degrades to no-op on provider failure
    const info = await phase4ProviderHealthy();
    if (!info.ok) return;
    try {
      const vec = await phase4EmbedText(content);
      if (!vec || vec.length === 0) return;
      await upsertMemoryEmbedding(db, memoryId, vec, {
        model: info.model,
        dim: vec.length,
      });
    } catch {
      // Silently ignore — search still works FTS-only
    }
  }

  const server = new McpServer(
    { name: "crow-memory", version: "0.1.0" },
    options.instructions ? { instructions: options.instructions } : undefined
  );

  // --- Tools ---

  server.tool(
    "crow_store_memory",
    "Store a new piece of information in persistent memory. Use this whenever you learn something important about the user, their projects, preferences, or any context that should persist across sessions.",
    {
      content: z.string().max(50000).describe("The information to remember"),
      category: z.string().max(500).default("general").describe("Category: general, project, preference, person, process, decision, learning, goal"),
      context: z.string().max(50000).optional().describe("Additional context about when/why this was stored"),
      tags: z.string().max(500).optional().describe("Comma-separated tags for filtering"),
      source: z.string().max(500).optional().describe("Where this information came from"),
      importance: z.number().min(1).max(10).default(5).describe("1-10 importance rating"),
      instance_id: z.string().max(100).optional().describe("Instance ID (origin). Omit to use local instance."),
      project_id: z.number().optional().describe("Project ID for project-scoped memories"),
    },
    async ({ content, category, context, tags, source, importance, instance_id, project_id }) => {
      const result = await db.execute({
        sql: "INSERT INTO memories (content, category, context, tags, source, importance, instance_id, project_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
        args: [content, category, context ?? null, tags ?? null, source ?? null, importance, instance_id ?? null, project_id ?? null],
      });
      const memoryId = Number(result.lastInsertRowid);

      // Store embedding asynchronously (non-blocking)
      storeEmbedding(memoryId, content);

      // Emit sync entry (non-blocking)
      if (syncManager) {
        syncManager.emitChange("memories", "insert", {
          id: memoryId, content, category, context: context ?? null,
          tags: tags ?? null, source: source ?? null, importance,
          instance_id: instance_id ?? null, project_id: project_id ?? null,
        }).catch(() => {});
      }

      return {
        content: [
          {
            type: "text",
            text: `Memory stored (id: ${memoryId}, category: ${category}, importance: ${importance})`,
          },
        ],
      };
    }
  );

  server.tool(
    "crow_search_memories",
    "Search persistent memory using full-text search (and semantic search when available). Returns memories ranked by relevance. Use this to recall information from previous sessions.",
    {
      query: z.string().max(500).describe("Search query"),
      category: z.string().max(500).optional().describe("Filter by category"),
      min_importance: z.number().min(1).max(10).optional().describe("Minimum importance threshold"),
      limit: z.number().max(100).default(10).describe("Maximum results to return"),
      semantic: z.boolean().default(true).describe("Enable semantic search + reranker (auto-falls back to FTS-only if grackle-embed offline)"),
      instance_id: z.string().max(100).optional().describe("Filter by origin instance ID"),
      project_id: z.number().optional().describe("Filter by project ID"),
    },
    async ({ query, category, min_importance, limit, semantic, instance_id, project_id }) => {
      // Try semantic search first if requested and available
      let semanticRows = [];

      // Legacy sqlite-vec path (kept for backward compat with injected getEmbedding)
      if (semantic && getEmbedding && await hasVec()) {
        try {
          const queryVec = await getEmbedding(query);
          if (queryVec && queryVec.length > 0) {
            const blob = Buffer.from(queryVec.buffer);
            let vecSql = `
              SELECT m.*, e.distance
              FROM memory_embeddings e
              JOIN memories m ON m.id = e.memory_id
              WHERE e.embedding MATCH ?
            `;
            const vecParams = [blob];

            if (category) { vecSql += " AND m.category = ?"; vecParams.push(category); }
            if (min_importance) { vecSql += " AND m.importance >= ?"; vecParams.push(min_importance); }
            if (instance_id) { vecSql += " AND m.instance_id = ?"; vecParams.push(instance_id); }
            if (project_id) { vecSql += " AND m.project_id = ?"; vecParams.push(project_id); }

            vecSql += ` ORDER BY e.distance LIMIT ?`;
            vecParams.push(limit);

            const { rows: vRows } = await db.execute({ sql: vecSql, args: vecParams });
            semanticRows = vRows;
          }
        } catch {
          // Semantic search failed — fall through to FTS5
        }
      }

      // Phase 4: BLOB + in-process cosine path (default when semantic=true
      // and no legacy injection). Loads all memory embeddings, scores by
      // cosine similarity, optionally reranks top-K via grackle-rerank.
      if (semantic && semanticRows.length === 0) {
        try {
          const info = await phase4ProviderHealthy();
          if (info.ok) {
            const queryVec = await phase4EmbedText(query);
            const allEmbs = await loadMemoryEmbeddings(db, { model: info.model });
            if (queryVec && allEmbs.length > 0) {
              const topK = Math.min(Math.max(limit * 3, 30), 200);
              const topCandidates = rankByCosine(queryVec, allEmbs, topK);
              // Fetch the full memory rows for those IDs, with filters
              const ids = topCandidates.map((c) => c.memory_id);
              if (ids.length > 0) {
                const placeholders = ids.map(() => "?").join(",");
                let fetchSql = `SELECT * FROM memories WHERE id IN (${placeholders})`;
                const fetchArgs = [...ids];
                if (category) { fetchSql += " AND category = ?"; fetchArgs.push(category); }
                if (min_importance) { fetchSql += " AND importance >= ?"; fetchArgs.push(min_importance); }
                if (instance_id) { fetchSql += " AND instance_id = ?"; fetchArgs.push(instance_id); }
                if (project_id) { fetchSql += " AND project_id = ?"; fetchArgs.push(project_id); }
                const { rows: rawRows } = await db.execute({ sql: fetchSql, args: fetchArgs });
                const byId = new Map(rawRows.map((r) => [r.id, r]));
                // Preserve cosine-sorted order + attach score
                const ordered = topCandidates
                  .map((c) => byId.get(c.memory_id) && ({ ...byId.get(c.memory_id), cosine: c.score }))
                  .filter(Boolean);

                // Optional reranker pass over top candidates
                const rerankInputs = ordered.map((m) => ({ id: m.id, text: m.content, _memory: m }));
                const reranked = await phase4Rerank(query, rerankInputs, { topK: limit });
                semanticRows = reranked.map((r) => ({ ...(r._memory), rerank: r.relevance ?? null }));
              }
            }
          }
        } catch {
          // Silent fallback to FTS
        }
      }

      // FTS5 search (always runs as primary or fallback)
      const safeQuery = sanitizeFtsQuery(query);
      if (!safeQuery && semanticRows.length === 0) {
        return { content: [{ type: "text", text: "Search query is empty or contains only special characters." }] };
      }

      let rows = [];
      if (safeQuery) {
        let sql = `
          SELECT m.*, rank
          FROM memories_fts fts
          JOIN memories m ON m.id = fts.rowid
          WHERE memories_fts MATCH ?
        `;
        const params = [safeQuery];

        if (category) {
          sql += " AND m.category = ?";
          params.push(category);
        }
        if (min_importance) {
          sql += " AND m.importance >= ?";
          params.push(min_importance);
        }
        if (instance_id) {
          sql += " AND m.instance_id = ?";
          params.push(instance_id);
        }
        if (project_id) {
          sql += " AND m.project_id = ?";
          params.push(project_id);
        }

        sql += " ORDER BY rank LIMIT ?";
        params.push(limit);

        const result = await db.execute({ sql, args: params });
        rows = result.rows;
      }

      // Merge: semantic results first, then FTS results (deduplicated)
      if (semanticRows.length > 0) {
        const seenIds = new Set(semanticRows.map((r) => r.id));
        const merged = [...semanticRows, ...rows.filter((r) => !seenIds.has(r.id))];
        rows = merged.slice(0, limit);
      }

      if (rows.length > 0) {
        await db.execute({
          sql: `UPDATE memories SET accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${rows.map(() => "?").join(",")})`,
          args: rows.map((r) => r.id),
        });
      }

      if (rows.length === 0) {
        return { content: [{ type: "text", text: "No memories found matching that query." }] };
      }

      const formatted = rows
        .map(
          (r) =>
            `[#${r.id}] (${r.category}, importance: ${r.importance})\n--- stored content ---\n${r.content}\n--- end stored content ---${r.context ? `\n  Context: ${r.context}` : ""}${r.tags ? `\n  Tags: ${r.tags}` : ""}${r.source ? `\n  Source: ${r.source}` : ""}`
        )
        .join("\n\n");

      return { content: [{ type: "text", text: `Found ${rows.length} memories:\n\n${formatted}` }] };
    }
  );

  server.tool(
    "crow_recall_by_context",
    "Retrieve memories relevant to a given context. Uses full-text search across content, context, and tags. Memories tagged source='maker-lab' are excluded by default to keep kid-session memories out of generic recall; pass include_maker_lab=true to include them (use this when operating within the maker-lab skill).",
    {
      context: z.string().max(2000).describe("Describe the current context or topic to find relevant memories"),
      limit: z.number().max(100).default(5).describe("Maximum results"),
      instance_id: z.string().max(100).optional().describe("Filter by origin instance ID"),
      project_id: z.number().optional().describe("Filter by project ID"),
      include_maker_lab: z.boolean().optional().describe("When true, include memories tagged source='maker-lab'. Default false."),
    },
    async ({ context, limit, instance_id, project_id, include_maker_lab }) => {
      const contextWords = context.split(/\s+/).filter((w) => w.length > 2).slice(0, 10).join(" ");
      const safeQuery = sanitizeFtsQuery(contextWords);

      if (!safeQuery) {
        return { content: [{ type: "text", text: "Context too short to search." }] };
      }

      let sql = `
        SELECT m.*, rank FROM memories_fts fts
        JOIN memories m ON m.id = fts.rowid
        WHERE memories_fts MATCH ?
      `;
      const params = [safeQuery];

      if (instance_id) { sql += " AND m.instance_id = ?"; params.push(instance_id); }
      if (project_id) { sql += " AND m.project_id = ?"; params.push(project_id); }
      if (!include_maker_lab) {
        sql += " AND (m.source IS NULL OR m.source != 'maker-lab')";
      }

      sql += " ORDER BY rank LIMIT ?";
      params.push(limit);

      const { rows } = await db.execute({ sql, args: params });

      if (rows.length === 0) {
        return { content: [{ type: "text", text: "No relevant memories found for this context." }] };
      }

      const formatted = rows
        .map((r) => `[#${r.id}] (${r.category})\n--- stored content ---\n${r.content}\n--- end stored content ---`)
        .join("\n\n");

      return {
        content: [{ type: "text", text: `Relevant memories for "${context}":\n\n${formatted}` }],
      };
    }
  );

  server.tool(
    "crow_deep_recall",
    "Proactive recall: search ALL knowledge sources (memories, research sources, research notes, blog posts) for topic-relevant context. Use before writing, creating content, or analyzing to pull in findings from prior sessions.",
    {
      topic: z.string().max(500).describe("Topic to recall context for"),
      per_source_limit: z.number().max(20).default(5).describe("Max results per source type"),
      include_sources: z.array(z.enum(["memories", "research_sources", "research_notes", "blog_posts"]))
        .optional().describe("Filter to specific source types (default: all)"),
      project_id: z.number().optional().describe("Filter research sources/notes by project ID"),
      min_importance: z.number().min(1).max(10).optional().describe("Min importance for memories"),
    },
    async ({ topic, per_source_limit, include_sources, project_id, min_importance }) => {
      const safeQuery = sanitizeFtsQuery(topic);
      const safeLike = escapeLikePattern(topic);
      const sources = include_sources || ["memories", "research_sources", "research_notes", "blog_posts"];
      const searched = [];
      const failed = [];
      const allResults = [];

      // --- Memories (FTS5) ---
      if (sources.includes("memories")) {
        try {
          if (safeQuery) {
            let sql = `
              SELECT m.*, rank FROM memories_fts fts
              JOIN memories m ON m.id = fts.rowid
              WHERE memories_fts MATCH ?
            `;
            const params = [safeQuery];
            if (project_id) { sql += " AND m.project_id = ?"; params.push(project_id); }
            if (min_importance) { sql += " AND m.importance >= ?"; params.push(min_importance); }
            sql += " ORDER BY rank LIMIT ?";
            params.push(per_source_limit);

            const { rows } = await db.execute({ sql, args: params });

            // Update access tracking for returned memories
            if (rows.length > 0) {
              await db.execute({
                sql: `UPDATE memories SET accessed_at = datetime('now'), access_count = access_count + 1 WHERE id IN (${rows.map(() => "?").join(",")})`,
                args: rows.map((r) => r.id),
              });
            }

            for (const r of rows) {
              allResults.push({
                type: "memory",
                text: `[memory #${r.id}] (${r.category}, importance: ${r.importance})\n--- stored content ---\n${r.content}\n--- end stored content ---${r.tags ? `\nTags: ${r.tags}` : ""}`,
              });
            }
          }
          searched.push("memories");
        } catch (err) {
          failed.push(`memories: ${err.message}`);
        }
      }

      // --- Research Sources (FTS5 — scoped to title/authors/abstract/content_summary/tags) ---
      if (sources.includes("research_sources")) {
        try {
          if (safeQuery) {
            let sql = `
              SELECT s.*, rank FROM sources_fts sfts
              JOIN research_sources s ON s.id = sfts.rowid
              WHERE sources_fts MATCH '{title authors abstract content_summary tags} : ' || ?
            `;
            const params = [safeQuery];
            if (project_id) { sql += " AND s.project_id = ?"; params.push(project_id); }
            sql += " ORDER BY rank LIMIT ?";
            params.push(per_source_limit);

            const { rows } = await db.execute({ sql, args: params });

            for (const r of rows) {
              const snippet = r.abstract
                ? (r.abstract.length > 500 ? r.abstract.substring(0, 500) + "..." : r.abstract)
                : (r.content_summary ? (r.content_summary.length > 500 ? r.content_summary.substring(0, 500) + "..." : r.content_summary) : "(no abstract)");
              allResults.push({
                type: "source",
                text: `[source #${r.id}] "${r.title}" (${r.source_type || "unknown"}${r.year ? `, ${r.year}` : ""})\n${snippet}`,
              });
            }
          }
          searched.push("research_sources");
        } catch (err) {
          failed.push(`research_sources: ${err.message}`);
        }
      }

      // --- Research Notes (LIKE — no FTS table) ---
      if (sources.includes("research_notes")) {
        try {
          if (safeLike) {
            const likeParam = `%${safeLike}%`;
            let sql = `
              SELECT rn.*, rs.title as source_title FROM research_notes rn
              LEFT JOIN research_sources rs ON rn.source_id = rs.id
              WHERE (rn.content LIKE ? ESCAPE '\\' OR rn.title LIKE ? ESCAPE '\\')
            `;
            const params = [likeParam, likeParam];
            if (project_id) { sql += " AND rn.project_id = ?"; params.push(project_id); }
            sql += " ORDER BY rn.created_at DESC LIMIT ?";
            params.push(per_source_limit);

            const { rows } = await db.execute({ sql, args: params });

            for (const r of rows) {
              const noteContent = r.content.length > 500 ? r.content.substring(0, 500) + "..." : r.content;
              allResults.push({
                type: "note",
                text: `[note #${r.id}]${r.source_title ? ` re: "${r.source_title}"` : ""}${r.title ? ` (${r.title})` : ""}\n${noteContent}`,
              });
            }
          }
          searched.push("research_notes");
        } catch (err) {
          failed.push(`research_notes: ${err.message}`);
        }
      }

      // --- Blog Posts (FTS5 — include draft and published, exclude archived) ---
      if (sources.includes("blog_posts")) {
        try {
          if (safeQuery) {
            const sql = `
              SELECT p.*, rank FROM blog_posts_fts bfts
              JOIN blog_posts p ON p.id = bfts.rowid
              WHERE blog_posts_fts MATCH ?
              AND p.status != 'archived'
              ORDER BY rank LIMIT ?
            `;
            const { rows } = await db.execute({ sql, args: [safeQuery, per_source_limit] });

            for (const r of rows) {
              const excerpt = r.excerpt
                ? (r.excerpt.length > 300 ? r.excerpt.substring(0, 300) + "..." : r.excerpt)
                : (r.content ? (r.content.length > 300 ? r.content.substring(0, 300) + "..." : r.content) : "");
              allResults.push({
                type: "blog",
                text: `[blog] "${r.title}" (${r.status}${r.published_at ? `, published ${r.published_at}` : ""})\n${excerpt}`,
              });
            }
          }
          searched.push("blog_posts");
        } catch (err) {
          failed.push(`blog_posts: ${err.message}`);
        }
      }

      // --- Format response ---
      const total = allResults.length;
      let header = `Deep recall for "${topic}":\n\n`;
      header += `Sources searched: ${searched.join(", ")}\n`;
      if (failed.length > 0) {
        header += `Sources failed: ${failed.join("; ")}\n`;
      }
      header += `Found ${total} results:\n`;

      if (total === 0) {
        return { content: [{ type: "text", text: header }] };
      }

      const formattedResults = allResults.map((r) => r.text).join("\n\n");
      return { content: [{ type: "text", text: header + "\n" + formattedResults }] };
    }
  );

  server.tool(
    "crow_get_memory",
    "Fetch a single memory by ID. Returns a compact JSON blob with id, content, category, importance, tags, and timestamps. Used by the dashboard's federated memory view so primary can render a memory that lives on a paired instance.",
    {
      id: z.number().describe("Memory ID"),
    },
    async ({ id }) => {
      const { rows } = await db.execute({
        sql: "SELECT id, content, category, importance, tags, source, created_at, updated_at, accessed_at, access_count, context, instance_id, project_id FROM memories WHERE id = ?",
        args: [id],
      });
      if (rows.length === 0) {
        return { content: [{ type: "text", text: `{"error":"not_found","id":${id}}` }] };
      }
      const r = rows[0];
      // Serialize numeric fields explicitly — libsql may return BigInt for INTEGER columns.
      const payload = {
        id: Number(r.id),
        content: r.content,
        category: r.category,
        importance: Number(r.importance),
        tags: r.tags,
        source: r.source,
        context: r.context,
        instance_id: r.instance_id,
        project_id: r.project_id != null ? Number(r.project_id) : null,
        created_at: r.created_at,
        updated_at: r.updated_at,
        accessed_at: r.accessed_at,
        access_count: Number(r.access_count || 0),
      };
      return { content: [{ type: "text", text: JSON.stringify(payload) }] };
    }
  );

  server.tool(
    "crow_list_memories",
    "List memories with optional filtering by category, tags, or importance. Good for browsing what's stored.",
    {
      category: z.string().max(500).optional().describe("Filter by category"),
      tag: z.string().max(500).optional().describe("Filter by tag (partial match)"),
      min_importance: z.number().min(1).max(10).optional().describe("Minimum importance"),
      sort_by: z.enum(["recent", "importance", "accessed"]).default("recent").describe("Sort order"),
      limit: z.number().max(100).default(20).describe("Max results"),
      instance_id: z.string().max(100).optional().describe("Filter by origin instance ID"),
      project_id: z.number().optional().describe("Filter by project ID"),
    },
    async ({ category, tag, min_importance, sort_by, limit, instance_id, project_id }) => {
      let sql = "SELECT * FROM memories WHERE 1=1";
      const params = [];

      if (category) {
        sql += " AND category = ?";
        params.push(category);
      }
      if (tag) {
        sql += " AND tags LIKE ? ESCAPE '\\'";
        params.push(`%${escapeLikePattern(tag)}%`);
      }
      if (min_importance) {
        sql += " AND importance >= ?";
        params.push(min_importance);
      }
      if (instance_id) {
        sql += " AND instance_id = ?";
        params.push(instance_id);
      }
      if (project_id) {
        sql += " AND project_id = ?";
        params.push(project_id);
      }

      const sortMap = {
        recent: "created_at DESC",
        importance: "importance DESC, created_at DESC",
        accessed: "accessed_at DESC",
      };
      sql += ` ORDER BY ${sortMap[sort_by]} LIMIT ?`;
      params.push(limit);

      const { rows } = await db.execute({ sql, args: params });

      if (rows.length === 0) {
        return { content: [{ type: "text", text: "No memories found with those filters." }] };
      }

      const formatted = rows
        .map(
          (r) =>
            `[#${r.id}] ${r.category} | imp:${r.importance} | ${r.created_at}\n  ${r.content}${r.tags ? `\n  Tags: ${r.tags}` : ""}`
        )
        .join("\n\n");

      return { content: [{ type: "text", text: `${rows.length} memories:\n\n${formatted}` }] };
    }
  );

  server.tool(
    "crow_update_memory",
    "Update an existing memory's content, category, tags, or importance.",
    {
      id: z.number().describe("Memory ID to update"),
      content: z.string().max(50000).optional().describe("New content"),
      category: z.string().max(500).optional().describe("New category"),
      tags: z.string().max(500).optional().describe("New tags (replaces existing)"),
      importance: z.number().min(1).max(10).optional().describe("New importance"),
      context: z.string().max(50000).optional().describe("Updated context"),
    },
    async ({ id, content, category, tags, importance, context }) => {
      const { rows } = await db.execute({ sql: "SELECT * FROM memories WHERE id = ?", args: [id] });
      if (rows.length === 0) {
        return { content: [{ type: "text", text: `Memory #${id} not found.` }] };
      }

      const updates = [];
      const params = [];
      if (content !== undefined) { updates.push("content = ?"); params.push(content); }
      if (category !== undefined) { updates.push("category = ?"); params.push(category); }
      if (tags !== undefined) { updates.push("tags = ?"); params.push(tags); }
      if (importance !== undefined) { updates.push("importance = ?"); params.push(importance); }
      if (context !== undefined) { updates.push("context = ?"); params.push(context); }

      if (updates.length === 0) {
        return { content: [{ type: "text", text: "No updates provided." }] };
      }

      updates.push("updated_at = datetime('now')");
      params.push(id);

      await db.execute({ sql: `UPDATE memories SET ${updates.join(", ")} WHERE id = ?`, args: params });

      // Re-embed if content changed (non-blocking)
      if (content !== undefined) {
        storeEmbedding(id, content);
      }

      // Emit sync entry
      if (syncManager) {
        const updated = { id, content, category, tags, importance, context };
        // Remove undefined fields
        for (const k of Object.keys(updated)) { if (updated[k] === undefined) delete updated[k]; }
        syncManager.emitChange("memories", "update", updated).catch(() => {});
      }

      return { content: [{ type: "text", text: `Memory #${id} updated.` }] };
    }
  );

  server.tool(
    "crow_regenerate_embeddings",
    "Re-embed memories to fix stale embeddings (e.g. after content updates or after enabling semantic search). Optionally filter by category, id list, or re-embed all memories.",
    {
      category: z.string().max(500).optional().describe("Filter by category (omit = all)"),
      id_list: z.array(z.number()).optional().describe("Specific memory IDs to re-embed (omit = all)"),
    },
    async ({ category, id_list }) => {
      const info = await phase4ProviderHealthy();
      if (!info.ok) {
        return { content: [{ type: "text", text: `Embedding provider not available: ${info.error}.` }] };
      }

      let sql = "SELECT id, content FROM memories WHERE 1=1";
      const params = [];
      if (category) { sql += " AND category = ?"; params.push(category); }
      if (id_list && id_list.length > 0) { sql += " AND id IN (" + id_list.map(() => "?").join(",") + ")"; params.push(...id_list); }
      sql += " ORDER BY id";

      const { rows } = await db.execute({ sql, args: params });
      if (rows.length === 0) {
        return { content: [{ type: "text", text: "No memories to re-embed." }] };
      }

      let succeeded = 0, failed = 0;
      for (const m of rows) {
        try {
          const vec = await phase4EmbedText(m.content);
          if (vec && vec.length > 0) {
            await upsertMemoryEmbedding(db, m.id, vec, { model: info.model, dim: vec.length });
            succeeded++;
          } else {
            failed++;
          }
        } catch {
          failed++;
        }
      }

      return {
        content: [{ type: "text", text: `Re-embedded ${succeeded} memories${failed > 0 ? `, ${failed} failed` : ""}. Total processed: ${rows.length}.` }],
      };
    }
  );

  server.tool(
    "crow_delete_memory",
    "Permanently delete a memory. This cannot be undone. Returns a preview and confirmation token on first call; pass the token back to execute.",
    {
      id: z.number().describe("Memory ID to delete"),
      confirm_token: z.string().max(100).describe('Confirmation token — pass "" on first call to get a preview, then pass the returned token to execute'),
    },
    async ({ id, confirm_token }) => {
      const existing = await db.execute({ sql: "SELECT * FROM memories WHERE id = ?", args: [id] });
      if (existing.rows.length === 0) {
        return { content: [{ type: "text", text: `Memory #${id} not found.` }] };
      }
      const memory = existing.rows[0];

      if (!shouldSkipGates()) {
        if (confirm_token) {
          if (!validateToken(confirm_token, "delete_memory", id)) {
            return { content: [{ type: "text", text: "Invalid or expired confirmation token. Pass confirm_token: \"\" to get a new preview." }], isError: true };
          }
        } else {
          const preview = memory.content.length > 100 ? memory.content.substring(0, 100) + "..." : memory.content;
          const token = generateToken("delete_memory", id);
          return {
            content: [{
              type: "text",
              text: `⚠️ This will permanently delete:\n  Memory #${id} (${memory.category}, importance: ${memory.importance})\n  "${preview}"\n\nThis cannot be undone.\nTo proceed, call again with confirm_token: "${token}"`,
            }],
          };
        }
      }

      await db.execute({ sql: "DELETE FROM memories WHERE id = ?", args: [id] });

      // Emit sync entry
      if (syncManager) {
        syncManager.emitChange("memories", "delete", { id }).catch(() => {});
      }

      return { content: [{ type: "text", text: `Memory #${id} deleted.` }] };
    }
  );

  server.tool(
    "crow_memory_stats",
    "Get statistics about stored memories - counts by category, total count, importance distribution.",
    {},
    async () => {
      const total = (await db.execute("SELECT COUNT(*) as count FROM memories")).rows[0];
      const byCategory = (await db.execute("SELECT category, COUNT(*) as count FROM memories GROUP BY category ORDER BY count DESC")).rows;
      const byImportance = (await db.execute("SELECT importance, COUNT(*) as count FROM memories GROUP BY importance ORDER BY importance DESC")).rows;
      const recent = (await db.execute("SELECT id, category, substr(content, 1, 80) as preview, created_at FROM memories ORDER BY created_at DESC LIMIT 5")).rows;

      let text = `Memory Statistics:\n`;
      text += `  Total memories: ${total.count}\n\n`;
      text += `By Category:\n${byCategory.map((r) => `  ${r.category}: ${r.count}`).join("\n")}\n\n`;
      text += `By Importance:\n${byImportance.map((r) => `  Level ${r.importance}: ${r.count}`).join("\n")}\n\n`;
      text += `Recent:\n${recent.map((r) => `  [#${r.id}] ${r.category} - ${r.preview}...`).join("\n")}`;

      return { content: [{ type: "text", text }] };
    }
  );

  // --- Dream Consolidation Tool ---

  server.tool(
    "crow_dream",
    "Analyze memory health: find stale, contradictory, or redundant memories. Returns a consolidation report without making changes. Use with the /crow-dream skill for the full workflow.",
    {
      stale_days: z.number().default(30).describe("Flag memories not accessed in N days"),
      low_importance_threshold: z.number().default(3).describe("Flag old low-importance memories"),
      dry_run: z.boolean().default(true).describe("If true, only report — don't delete or update anything"),
    },
    async ({ stale_days, low_importance_threshold, dry_run }) => {
      const staleDaysModifier = `-${stale_days} days`;

      // 1. Stale memories: not accessed in N days AND importance <= threshold
      const { rows: staleRows } = await db.execute({
        sql: `SELECT id, category, importance, content, created_at, accessed_at, access_count
              FROM memories
              WHERE ((accessed_at IS NULL AND created_at < datetime('now', ?))
                 OR (accessed_at < datetime('now', ?)))
              AND importance <= ?
              ORDER BY importance ASC, accessed_at ASC`,
        args: [staleDaysModifier, staleDaysModifier, low_importance_threshold],
      });

      // 2. Category health stats
      const { rows: categoryRows } = await db.execute({
        sql: `SELECT category, COUNT(*) as total,
                     ROUND(AVG(importance), 1) as avg_importance,
                     MIN(accessed_at) as oldest_access,
                     SUM(CASE WHEN accessed_at < datetime('now', '-30 days') OR accessed_at IS NULL THEN 1 ELSE 0 END) as stale_count
              FROM memories GROUP BY category`,
      });

      // 3. Total count
      const { rows: totalRows } = await db.execute("SELECT COUNT(*) as count FROM memories");
      const totalMemories = totalRows[0].count;

      // 4. Duplicate detection via shingle similarity (within same category, capped)
      function getShingles(text, n = 3) {
        const words = text.toLowerCase().split(/\s+/).slice(0, 67);
        const shingles = new Set();
        for (let i = 0; i <= words.length - n; i++) {
          shingles.add(words.slice(i, i + n).join(" "));
        }
        return shingles;
      }

      function jaccardSimilarity(a, b) {
        const intersection = new Set([...a].filter(x => b.has(x)));
        const union = new Set([...a, ...b]);
        return union.size === 0 ? 0 : intersection.size / union.size;
      }

      // Get recent memories grouped by category for duplicate detection
      const { rows: recentMemories } = await db.execute({
        sql: `SELECT id, category, content, importance, created_at
              FROM memories ORDER BY created_at DESC LIMIT 1000`,
      });

      const byCategory = {};
      for (const m of recentMemories) {
        const cat = m.category || "general";
        if (!byCategory[cat]) byCategory[cat] = [];
        if (byCategory[cat].length < 200) {
          byCategory[cat].push(m);
        }
      }

      const duplicatePairs = [];
      for (const [cat, memories] of Object.entries(byCategory)) {
        const shingleCache = memories.map(m => ({
          id: m.id,
          importance: m.importance,
          preview: m.content.substring(0, 100),
          shingles: getShingles(m.content),
        }));

        for (let i = 0; i < shingleCache.length; i++) {
          for (let j = i + 1; j < shingleCache.length; j++) {
            if (shingleCache[i].shingles.size === 0 || shingleCache[j].shingles.size === 0) continue;
            const sim = jaccardSimilarity(shingleCache[i].shingles, shingleCache[j].shingles);
            if (sim > 0.5) {
              duplicatePairs.push({
                category: cat,
                id_a: shingleCache[i].id,
                id_b: shingleCache[j].id,
                preview_a: shingleCache[i].preview,
                preview_b: shingleCache[j].preview,
                similarity: Math.round(sim * 100),
              });
            }
          }
        }
      }

      // Build report
      let text = `# Dream Consolidation Report\n\n`;
      text += `**Total memories:** ${totalMemories}\n`;
      text += `**Stale threshold:** ${stale_days} days, importance <= ${low_importance_threshold}\n`;
      text += `**Mode:** ${dry_run ? "dry run (no changes)" : "LIVE — changes will be applied"}\n\n`;

      // Category health
      text += `## Category Health\n`;
      for (const c of categoryRows) {
        text += `- **${c.category}**: ${c.total} memories, avg importance ${c.avg_importance}, ${c.stale_count} stale\n`;
      }
      text += `\n`;

      // Stale memories
      text += `## Stale Memories (${staleRows.length})\n`;
      if (staleRows.length === 0) {
        text += `No stale memories found.\n\n`;
      } else {
        for (const s of staleRows) {
          const preview = s.content.substring(0, 120).replace(/\n/g, " ");
          text += `- [#${s.id}] ${s.category}, importance ${s.importance}, `;
          text += `accessed: ${s.accessed_at || "never"}, `;
          text += `accesses: ${s.access_count}\n  "${preview}..."\n`;
        }
        text += `\n`;
      }

      // Potential duplicates
      text += `## Potential Duplicates (${duplicatePairs.length} pairs)\n`;
      if (duplicatePairs.length === 0) {
        text += `No duplicates detected.\n\n`;
      } else {
        for (const d of duplicatePairs) {
          text += `- [#${d.id_a}] ↔ [#${d.id_b}] (${d.category}, ${d.similarity}% similar)\n`;
          text += `  A: "${d.preview_a}..."\n`;
          text += `  B: "${d.preview_b}..."\n`;
        }
        text += `\n`;
      }

      text += `## Recommended Actions\n`;
      text += `- Stale memories to review: ${staleRows.length}\n`;
      text += `- Duplicate pairs to merge: ${duplicatePairs.length}\n`;
      text += `Use crow_delete_memory or crow_update_memory to act on these findings.\n`;

      return { content: [{ type: "text", text }] };
    }
  );

  // --- Cross-Platform Context (crow.md) Tools ---

  server.tool(
    "crow_get_context",
    "Generate and return the full crow.md cross-platform behavioral context document. This document defines how Crow behaves across all AI platforms — personality, memory protocols, transparency rules, and more. Includes optional dynamic data (memory stats, active projects, preferences). Use device_id to get device-specific overrides merged with global context.",
    {
      include_dynamic: z.boolean().default(true).describe("Include dynamic sections (memory stats, active projects, preferences)"),
      platform: z.string().default("generic").describe("Target platform hint: claude, chatgpt, gemini, grok, cursor, windsurf, cline, generic"),
      device_id: z.string().max(200).optional().describe("Device ID to merge device-specific overrides with global context"),
      project_id: z.number().optional().describe("Project ID to merge project-specific overrides with global context"),
    },
    async ({ include_dynamic, platform, device_id, project_id }) => {
      const markdown = await generateCrowContext(db, { includeDynamic: include_dynamic, platform, deviceId: device_id ?? null, projectId: project_id ?? null });
      return { content: [{ type: "text", text: markdown }] };
    }
  );

  server.tool(
    "crow_update_context_section",
    "Update an existing crow.md section's content, title, enabled status, or sort order. Works on both protected and custom sections. Use device_id/project_id to target a scoped override instead of the global section.",
    {
      section_key: z.string().max(500).describe("The section key to update (e.g. 'identity', 'memory_protocol')"),
      content: z.string().max(50000).optional().describe("New content for the section"),
      section_title: z.string().max(500).optional().describe("New title for the section"),
      enabled: z.boolean().optional().describe("Enable or disable this section"),
      sort_order: z.number().optional().describe("New sort order (lower = earlier)"),
      device_id: z.string().max(200).optional().describe("Device ID to update a device-specific override. Omit for global section."),
      project_id: z.number().optional().describe("Project ID to update a project-specific override. Omit for global section."),
    },
    async ({ section_key, content, section_title, enabled, sort_order, device_id, project_id }) => {
      // Build WHERE clause based on device_id and project_id
      const whereParts = ["section_key = ?"];
      const whereArgs = [section_key];

      if (device_id) { whereParts.push("device_id = ?"); whereArgs.push(device_id); }
      else { whereParts.push("device_id IS NULL"); }

      if (project_id) { whereParts.push("project_id = ?"); whereArgs.push(project_id); }
      else { whereParts.push("project_id IS NULL"); }

      const whereClause = whereParts.join(" AND ");

      const { rows } = await db.execute({
        sql: `SELECT * FROM crow_context WHERE ${whereClause}`,
        args: whereArgs,
      });
      if (rows.length === 0) {
        const scope = [device_id ? `device: ${device_id}` : null, project_id ? `project: ${project_id}` : null].filter(Boolean).join(", ");
        const target = scope ? ` (${scope})` : "";
        return { content: [{ type: "text", text: `Section "${section_key}"${target} not found.` }] };
      }

      const updates = [];
      const params = [];
      if (content !== undefined) { updates.push("content = ?"); params.push(content); }
      if (section_title !== undefined) { updates.push("section_title = ?"); params.push(section_title); }
      if (enabled !== undefined) { updates.push("enabled = ?"); params.push(enabled ? 1 : 0); }
      if (sort_order !== undefined) { updates.push("sort_order = ?"); params.push(sort_order); }

      if (updates.length === 0) {
        return { content: [{ type: "text", text: "No updates provided." }] };
      }

      updates.push("updated_at = datetime('now')");
      params.push(...whereArgs);

      await db.execute({
        sql: `UPDATE crow_context SET ${updates.join(", ")} WHERE ${whereClause}`,
        args: params,
      });
      invalidateContextCache();

      // Emit sync entry — post-UPDATE re-select is MANDATORY so the emitted row
      // carries the actual committed values (not the partial param set, which omits
      // enabled/sort_order when not provided, and never carries the pre-update values).
      if (syncManager) {
        const { buildCrowContextWireRow } = await import("../sharing/instance-sync.js").catch(() => ({}));
        if (buildCrowContextWireRow) {
          const { rows: emitRows } = await db.execute({
            sql: `SELECT * FROM crow_context WHERE ${whereClause}`,
            args: whereArgs,
          });
          if (emitRows.length > 0) {
            syncManager.emitChange("crow_context", "update", buildCrowContextWireRow(emitRows[0])).catch(() => {});
          }
        }
      }

      const scope = [device_id ? `device: ${device_id}` : null, project_id ? `project: ${project_id}` : null].filter(Boolean).join(", ");
      const target = scope ? ` (${scope})` : "";
      return { content: [{ type: "text", text: `Section "${section_key}"${target} updated.` }] };
    }
  );

  server.tool(
    "crow_add_context_section",
    "Add a new custom section to crow.md. Custom sections extend Crow's behavioral context with project-specific, device-specific, or user-specific instructions. Scoped overrides take precedence over global sections.",
    {
      section_key: z.string().max(500).describe("Unique key for the section (e.g. 'project_guidelines', 'coding_style')"),
      section_title: z.string().max(500).describe("Display title for the section"),
      content: z.string().max(50000).describe("Markdown content for the section"),
      sort_order: z.number().default(100).describe("Sort order (lower = earlier, default 100)"),
      device_id: z.string().max(200).optional().describe("Device ID to create a device-specific override. Omit for global section."),
      project_id: z.number().optional().describe("Project ID to create a project-specific override. Omit for global section."),
    },
    async ({ section_key, section_title, content, sort_order, device_id, project_id }) => {
      // Protected sections can have scoped overrides but not new global entries
      if (PROTECTED_SECTIONS.includes(section_key) && !device_id && !project_id) {
        return { content: [{ type: "text", text: `Cannot create section with protected key "${section_key}". Use crow_update_context_section to modify it.` }] };
      }

      try {
        await db.execute({
          sql: "INSERT INTO crow_context (section_key, section_title, content, sort_order, device_id, project_id) VALUES (?, ?, ?, ?, ?, ?)",
          args: [section_key, section_title, content, sort_order, device_id ?? null, project_id ?? null],
        });
        invalidateContextCache();

        // Emit sync entry — post-INSERT re-select captures enabled (DB default)
        // so the wire row is always complete regardless of which fields the tool call provided.
        if (syncManager) {
          const { buildCrowContextWireRow } = await import("../sharing/instance-sync.js").catch(() => ({}));
          if (buildCrowContextWireRow) {
            const { rows: emitRows } = await db.execute({
              sql: "SELECT * FROM crow_context WHERE section_key = ? AND device_id IS ? AND project_id IS ?",
              args: [section_key, device_id ?? null, project_id ?? null],
            });
            if (emitRows.length > 0) {
              syncManager.emitChange("crow_context", "insert", buildCrowContextWireRow(emitRows[0])).catch(() => {});
            }
          }
        }

        const scope = [device_id ? `device: ${device_id}` : null, project_id ? `project: ${project_id}` : null].filter(Boolean).join(", ");
        const target = scope ? ` (${scope})` : "";
        return { content: [{ type: "text", text: `Section "${section_key}"${target} added to crow.md.` }] };
      } catch (err) {
        if (err.message?.includes("UNIQUE")) {
          const scope = [device_id ? `device: ${device_id}` : null, project_id ? `project: ${project_id}` : null].filter(Boolean).join(", ");
          const target = scope ? ` (${scope})` : "";
          return { content: [{ type: "text", text: `Section "${section_key}"${target} already exists. Use crow_update_context_section to modify it.` }] };
        }
        throw err;
      }
    }
  );

  server.tool(
    "crow_list_context_sections",
    "List all crow.md sections with their metadata (key, title, sort order, enabled status, device/project scope). Does not return full content — use crow_get_context for that.",
    {
      device_id: z.string().max(200).optional().describe("Filter to sections for this device (also shows global sections). Omit to show all sections."),
      project_id: z.number().optional().describe("Filter to sections for this project (also shows global sections). Omit to show all sections."),
    },
    async ({ device_id, project_id } = {}) => {
      const { rows } = await db.execute(
        "SELECT section_key, section_title, sort_order, enabled, updated_at, device_id, project_id FROM crow_context ORDER BY sort_order ASC, id ASC"
      );

      if (rows.length === 0) {
        return { content: [{ type: "text", text: "No crow.md sections found. Run `npm run init-db` to seed defaults." }] };
      }

      // Filter: show global + matching device/project sections
      const filtered = rows.filter((r) => {
        const deviceMatch = !device_id || !r.device_id || r.device_id === device_id;
        const projectMatch = !project_id || !r.project_id || r.project_id === project_id;
        return deviceMatch && projectMatch;
      });

      const formatted = filtered
        .map((r) => {
          const status = r.enabled ? "enabled" : "disabled";
          const prot = PROTECTED_SECTIONS.includes(r.section_key) ? " [protected]" : "";
          const scope = [r.device_id ? `device: ${r.device_id}` : null, r.project_id ? `project: ${r.project_id}` : null].filter(Boolean).join(", ");
          const scopeLabel = scope ? ` [${scope}]` : "";
          return `- ${r.section_key}${prot}${scopeLabel}: "${r.section_title}" (order: ${r.sort_order}, ${status}, updated: ${r.updated_at})`;
        })
        .join("\n");

      return { content: [{ type: "text", text: `crow.md sections:\n\n${formatted}` }] };
    }
  );

  server.tool(
    "crow_delete_context_section",
    "Delete a custom crow.md section. Protected sections cannot be deleted — only disabled. Scoped overrides (device/project) of protected sections CAN be deleted (restores the global version).",
    {
      section_key: z.string().max(500).describe("The section key to delete"),
      device_id: z.string().max(200).optional().describe("Device ID to delete only the device-specific override. Omit for global."),
      project_id: z.number().optional().describe("Project ID to delete only the project-specific override. Omit for global."),
    },
    async ({ section_key, device_id, project_id }) => {
      // Protected global sections cannot be deleted, but scoped overrides can
      if (PROTECTED_SECTIONS.includes(section_key) && !device_id && !project_id) {
        return {
          content: [{ type: "text", text: `Cannot delete protected section "${section_key}". Use crow_update_context_section with enabled=false to disable it instead.` }],
        };
      }

      const whereParts = ["section_key = ?"];
      const whereArgs = [section_key];

      if (device_id) { whereParts.push("device_id = ?"); whereArgs.push(device_id); }
      else { whereParts.push("device_id IS NULL"); }

      if (project_id) { whereParts.push("project_id = ?"); whereArgs.push(project_id); }
      else { whereParts.push("project_id IS NULL"); }

      const whereClause = whereParts.join(" AND ");

      const result = await db.execute({
        sql: `DELETE FROM crow_context WHERE ${whereClause}`,
        args: whereArgs,
      });
      invalidateContextCache();

      // Emit sync entry
      if (syncManager) {
        syncManager.emitChange("crow_context", "delete", { section_key, device_id: device_id ?? null, project_id: project_id ?? null }).catch(() => {});
      }

      const scope = [device_id ? `device: ${device_id}` : null, project_id ? `project: ${project_id}` : null].filter(Boolean).join(", ");
      const target = scope ? ` (${scope})` : "";
      return {
        content: [
          {
            type: "text",
            text: result.rowsAffected > 0
              ? `Section "${section_key}"${target} deleted.`
              : `Section "${section_key}"${target} not found.`,
          },
        ],
      };
    }
  );

  // --- Schedule Tools ---

  server.tool(
    "crow_create_schedule",
    "Create a scheduled or recurring task. Uses cron expressions for timing (e.g. '0 9 * * *' for daily at 9am, '0 */6 * * *' for every 6 hours).",
    {
      task: z.string().max(1000).describe("The task to schedule"),
      cron_expression: z.string().max(50).describe("Cron expression for scheduling (e.g. '0 9 * * *' for daily at 9am)"),
      description: z.string().max(500).optional().describe("Optional description of the schedule"),
    },
    async ({ task, cron_expression, description }) => {
      // Validate and compute next_run
      let nextRun = null;
      try {
        const { CronExpressionParser } = await import("cron-parser");
        const interval = CronExpressionParser.parse(cron_expression);
        nextRun = interval.next().toISOString();
      } catch {
        return { content: [{ type: "text", text: `Invalid cron expression: "${cron_expression}". Use standard 5-field format (e.g. "0 9 * * *" for daily at 9am).` }] };
      }

      const result = await db.execute({
        sql: "INSERT INTO schedules (task, cron_expression, description, next_run) VALUES (?, ?, ?, ?)",
        args: [task, cron_expression, description ?? null, nextRun],
      });
      return {
        content: [
          {
            type: "text",
            text: `Schedule created (id: ${Number(result.lastInsertRowid)}, task: ${task}, cron: ${cron_expression}, next run: ${nextRun})`,
          },
        ],
      };
    }
  );

  server.tool(
    "crow_list_schedules",
    "List all scheduled tasks, optionally filtering to only enabled schedules.",
    {
      enabled_only: z.boolean().optional().describe("If true, only return enabled schedules"),
    },
    async ({ enabled_only }) => {
      let sql = "SELECT * FROM schedules";
      const params = [];

      if (enabled_only) {
        sql += " WHERE enabled = 1";
      }

      sql += " ORDER BY created_at DESC";

      const { rows } = await db.execute({ sql, args: params });

      if (rows.length === 0) {
        return { content: [{ type: "text", text: "No schedules found." }] };
      }

      const formatted = rows
        .map(
          (r) =>
            `[#${r.id}] ${r.enabled ? "enabled" : "disabled"} | ${r.cron_expression}\n  Task: ${r.task}${r.description ? `\n  Description: ${r.description}` : ""}${r.last_run ? `\n  Last run: ${r.last_run}` : ""}${r.next_run ? `\n  Next run: ${r.next_run}` : ""}\n  Created: ${r.created_at}`
        )
        .join("\n\n");

      return { content: [{ type: "text", text: `${rows.length} schedules:\n\n${formatted}` }] };
    }
  );

  server.tool(
    "crow_update_schedule",
    "Update or delete a scheduled task by ID. Provide only the fields you want to change, or set delete=true to remove it.",
    {
      id: z.number().describe("Schedule ID to update or delete"),
      enabled: z.boolean().optional().describe("Enable or disable the schedule"),
      task: z.string().max(1000).optional().describe("New task description"),
      cron_expression: z.string().max(50).optional().describe("New cron expression"),
      delete: z.boolean().optional().describe("If true, delete the schedule entirely"),
    },
    async ({ id, enabled, task, cron_expression, delete: doDelete }) => {
      const { rows } = await db.execute({ sql: "SELECT * FROM schedules WHERE id = ?", args: [id] });
      if (rows.length === 0) {
        return { content: [{ type: "text", text: `Schedule #${id} not found.` }] };
      }

      if (doDelete) {
        await db.execute({ sql: "DELETE FROM schedules WHERE id = ?", args: [id] });
        return { content: [{ type: "text", text: `Schedule #${id} deleted.` }] };
      }

      const updates = [];
      const params = [];
      if (task !== undefined) { updates.push("task = ?"); params.push(task); }
      if (cron_expression !== undefined) {
        // Validate and recompute next_run
        try {
          const { CronExpressionParser } = await import("cron-parser");
          const interval = CronExpressionParser.parse(cron_expression);
          const nextRun = interval.next().toISOString();
          updates.push("cron_expression = ?"); params.push(cron_expression);
          updates.push("next_run = ?"); params.push(nextRun);
        } catch {
          return { content: [{ type: "text", text: `Invalid cron expression: "${cron_expression}". Use standard 5-field format.` }] };
        }
      }
      if (enabled !== undefined) {
        updates.push("enabled = ?"); params.push(enabled ? 1 : 0);
        // Recompute next_run when re-enabling
        if (enabled && cron_expression === undefined) {
          try {
            const { CronExpressionParser } = await import("cron-parser");
            const existingCron = rows[0].cron_expression;
            const interval = CronExpressionParser.parse(existingCron);
            updates.push("next_run = ?"); params.push(interval.next().toISOString());
          } catch {}
        }
      }

      if (updates.length === 0) {
        return { content: [{ type: "text", text: "No updates provided." }] };
      }

      updates.push("updated_at = datetime('now')");
      params.push(id);

      await db.execute({ sql: `UPDATE schedules SET ${updates.join(", ")} WHERE id = ?`, args: params });

      return { content: [{ type: "text", text: `Schedule #${id} updated.` }] };
    }
  );

  // --- Notification Tools ---

  server.tool(
    "crow_check_notifications",
    "Check pending notifications. Returns unread notifications filtered by type. Automatically cleans expired and snoozed items. Call at session start to surface reminders.",
    {
      unread_only: z.boolean().default(true).describe("Only return unread, non-dismissed notifications"),
      type: z.string().max(50).optional().describe("Filter by type: reminder, media, peer, system"),
      limit: z.number().max(100).default(20).describe("Maximum results"),
    },
    async ({ unread_only, type, limit }) => {
      // Clean expired on each check
      await cleanupNotifications(db);

      const now = new Date().toISOString();
      let sql = "SELECT * FROM notifications WHERE 1=1";
      const params = [];

      if (unread_only) {
        sql += " AND is_read = 0 AND is_dismissed = 0";
        sql += " AND (snoozed_until IS NULL OR snoozed_until <= ?)";
        params.push(now);
      }
      if (type) {
        sql += " AND type = ?";
        params.push(type);
      }

      sql += " ORDER BY CASE priority WHEN 'high' THEN 0 WHEN 'normal' THEN 1 WHEN 'low' THEN 2 ELSE 1 END, created_at DESC LIMIT ?";
      params.push(limit);

      const { rows } = await db.execute({ sql, args: params });

      if (rows.length === 0) {
        return { content: [{ type: "text", text: "No pending notifications." }] };
      }

      const formatted = rows.map((r) => {
        let line = `[#${r.id}] ${r.priority === "high" ? "!" : ""}${r.type}: ${r.title}`;
        if (r.body) line += `\n  ${r.body}`;
        if (r.action_url) line += `\n  Link: ${r.action_url}`;
        if (r.source) line += `\n  Source: ${r.source}`;
        line += `\n  Created: ${r.created_at}`;
        return line;
      }).join("\n\n");

      return { content: [{ type: "text", text: `${rows.length} notification(s):\n\n${formatted}` }] };
    }
  );

  server.tool(
    "crow_create_notification",
    "Create a notification for the user. Use for reminders, alerts, or any information the user should see in the Crow's Nest or at next session start.",
    {
      title: z.string().max(500).describe("Short headline"),
      body: z.string().max(5000).optional().describe("Longer description"),
      type: z.string().max(50).default("reminder").describe("Type: reminder, media, peer, system"),
      priority: z.enum(["low", "normal", "high"]).default("normal").describe("Priority level"),
      action_url: z.string().max(1000).optional().describe("Dashboard link to navigate to"),
      metadata: z.record(z.any()).optional().describe("Structured data (stored as JSON)"),
      expires_in_minutes: z.number().optional().describe("Auto-expire after N minutes"),
    },
    async ({ title, body, type, priority, action_url, metadata, expires_in_minutes }) => {
      const result = await createNotification(db, {
        title,
        body,
        type,
        source: "mcp:ai",
        priority,
        action_url,
        metadata,
        expires_in_minutes,
      });

      if (!result) {
        return { content: [{ type: "text", text: `Notification type "${type}" is disabled in user preferences.` }] };
      }

      return { content: [{ type: "text", text: `Notification created (id: ${result.id}, type: ${type}, priority: ${priority})` }] };
    }
  );

  server.tool(
    "crow_dismiss_notification",
    "Dismiss a notification or snooze it for later. Snoozing hides the notification until the specified time.",
    {
      id: z.number().describe("Notification ID to dismiss"),
      snooze_minutes: z.number().optional().describe("Snooze for N minutes instead of dismissing permanently"),
    },
    async ({ id, snooze_minutes }) => {
      const { rows } = await db.execute({ sql: "SELECT * FROM notifications WHERE id = ?", args: [id] });
      if (rows.length === 0) {
        return { content: [{ type: "text", text: `Notification #${id} not found.` }] };
      }

      if (snooze_minutes) {
        const snoozedUntil = new Date(Date.now() + snooze_minutes * 60000).toISOString();
        await db.execute({
          sql: "UPDATE notifications SET snoozed_until = ?, updated_at = datetime('now') WHERE id = ?",
          args: [snoozedUntil, id],
        });
        return { content: [{ type: "text", text: `Notification #${id} snoozed until ${snoozedUntil}.` }] };
      }

      await db.execute({
        sql: "UPDATE notifications SET is_dismissed = 1, updated_at = datetime('now') WHERE id = ?",
        args: [id],
      });
      return { content: [{ type: "text", text: `Notification #${id} dismissed.` }] };
    }
  );

  server.tool(
    "crow_dismiss_all_notifications",
    "Dismiss all notifications, optionally filtered by type or date.",
    {
      type: z.string().max(50).optional().describe("Only dismiss this type"),
      before: z.string().max(50).optional().describe("Only dismiss notifications created before this ISO date"),
    },
    async ({ type, before }) => {
      let sql = "UPDATE notifications SET is_dismissed = 1, updated_at = datetime('now') WHERE is_dismissed = 0";
      const params = [];

      if (type) {
        sql += " AND type = ?";
        params.push(type);
      }
      if (before) {
        sql += " AND created_at < ?";
        params.push(before);
      }

      const result = await db.execute({ sql, args: params });
      return { content: [{ type: "text", text: `${result.rowsAffected} notification(s) dismissed.` }] };
    }
  );

  server.tool(
    "crow_notification_settings",
    "Get or update notification preferences (which types are enabled).",
    {
      action: z.enum(["get", "set"]).describe("Get current settings or set new ones"),
      types_enabled: z.array(z.string()).optional().describe("Enabled notification types (only for action=set). E.g. ['reminder', 'media', 'peer', 'system']"),
    },
    async ({ action, types_enabled }) => {
      if (action === "get") {
        const { rows } = await db.execute({
          sql: "SELECT value FROM dashboard_settings WHERE key = 'notification_prefs'",
          args: [],
        });
        if (rows.length === 0) {
          return { content: [{ type: "text", text: 'Notification preferences: all types enabled (default)\n  Types: reminder, media, peer, system' }] };
        }
        let prefs;
        try {
          prefs = JSON.parse(rows[0].value);
        } catch {
          prefs = {};
        }
        return { content: [{ type: "text", text: `Notification preferences:\n  Enabled types: ${prefs.types_enabled?.join(", ") || "all"}` }] };
      }

      // action === "set"
      if (!types_enabled) {
        return { content: [{ type: "text", text: "Provide types_enabled array to update settings." }] };
      }

      const prefs = { types_enabled };
      await db.execute({
        sql: "INSERT OR REPLACE INTO dashboard_settings (key, value, updated_at) VALUES ('notification_prefs', ?, datetime('now'))",
        args: [JSON.stringify(prefs)],
      });
      return { content: [{ type: "text", text: `Notification preferences updated. Enabled types: ${types_enabled.join(", ")}` }] };
    }
  );

  // --- Resources ---

  server.resource("crow-context", "crow://context", async (uri) => {
    const markdown = await generateCrowContext(db, { includeDynamic: true, platform: "generic" });
    return {
      contents: [{ uri: uri.href, mimeType: "text/markdown", text: markdown }],
    };
  });

  server.resource("memory-categories", "memory://categories", async (uri) => {
    const { rows: categories } = await db.execute(
      "SELECT DISTINCT category, COUNT(*) as count FROM memories GROUP BY category ORDER BY count DESC"
    );
    return {
      contents: [
        {
          uri: uri.href,
          mimeType: "application/json",
          text: JSON.stringify(categories, null, 2),
        },
      ],
    };
  });

  // --- Prompts ---

  server.prompt(
    "session-start",
    "Session start/end protocol — how to begin and end conversations with Crow",
    async () => {
      let text;
      try {
        const result = await db.execute({
          sql: "SELECT content FROM crow_context WHERE enabled = 1 AND section_key IN ('memory_protocol', 'session_protocol')",
          args: [],
        });
        if (result.rows.length > 0) {
          text = result.rows.map((r) => r.content).join("\n\n");
        }
      } catch {
        // Fallback
      }

      if (!text) {
        text = `Session Start Protocol:
1. Call crow_recall_by_context with the user's first message to load relevant memories
2. Use recalled memories to personalize your response
3. Throughout the conversation, store important new information with crow_store_memory

Session End Protocol:
- Before ending, store any important learnings, decisions, or preferences
- Use appropriate categories: general, project, preference, person, process, decision, learning, goal
- Set importance 8-10 for critical information the user would expect you to remember`;
      }

      return { messages: [{ role: "user", content: { type: "text", text } }] };
    }
  );

  server.prompt(
    "crow-guide",
    "Full Crow behavioral context (crow.md) — identity, memory protocols, transparency rules, and more",
    { platform: z.string().default("generic").describe("Target platform: claude, chatgpt, gemini, grok, cursor, generic") },
    async ({ platform }) => {
      let text;
      try {
        text = await generateCrowContext(db, { includeDynamic: false, platform });
      } catch {
        text = "Unable to load crow.md context. Use crow_get_context tool as an alternative.";
      }
      return { messages: [{ role: "user", content: { type: "text", text } }] };
    }
  );

  return server;
}
