/**
 * ★ 记忆层:workvault 的 Agent 会话记忆核心(纯 Node,无 Obsidian 依赖)。
 *
 * 理念:记忆 = 可见、可手改、git 可追踪的 Markdown(透明记忆)。
 * 分类目录:facts / preferences / decisions / findings / learnings / references
 * 生命周期:session_start(注入记忆上下文)→ 会话中 write/search/capture
 *          → session_end(写 journal + 重建概览 + 可选 auto_capture 提炼到 .pending/)
 * 纪律:本工具生成的文件带指纹;无指纹的「手写」记忆绝不覆盖(与 vault 笔记同一规则)。
 */
import * as fs from "fs";
import * as path from "path";
import { execSync } from "child_process";
import type { LlmProvider } from "./provider";
import { extractFrontmatter } from "./engine";
import { embedFingerprint, extractFingerprint, stripFingerprint } from "./incremental";
import { pathSafe, assertSafeRelativePath } from "./path";
import { simpleHash } from "./template";
import { extractJson } from "./compiler";

export const MEMORY_TYPES = ["fact", "preference", "decision", "finding", "learning", "reference"] as const;
export type MemoryType = (typeof MEMORY_TYPES)[number];

/** type → 目录名(复数) */
export const TYPE_DIRS: Record<MemoryType, string> = {
  fact: "facts",
  preference: "preferences",
  decision: "decisions",
  finding: "findings",
  learning: "learnings",
  reference: "references",
};

export interface MemoryEntry {
  type: MemoryType;
  title: string;
  description: string;
  content: string;
  project?: string;
  agent?: string;
  tags?: string[];
  source?: string;
}

export interface MemoryReadEntry extends MemoryEntry {
  path: string; // 相对 memory 根,如 facts/xxx.md
  created?: string;
  updated?: string;
}

export function resolveMemoryDir(vault: string, memoryDir?: string): string {
  return path.join(vault, memoryDir ?? "memory");
}

function today(): string {
  return new Date().toISOString().slice(0, 10);
}

function nowStamp(): string {
  const d = new Date();
  const p = (n: number) => String(n).padStart(2, "0");
  return `${today()} ${p(d.getHours())}:${p(d.getMinutes())}`;
}

/** frontmatter 单行值:含 YAML 特殊字符时加引号 */
function fmValue(v: string | undefined): string {
  if (v === undefined) return "";
  if (/[:#\[\]{}&*!|>'"%@`]|^\s|\s$|^[-?]/.test(v)) return JSON.stringify(v);
  return v;
}

/** 生成记忆 frontmatter 头部(不含指纹行,由 embedFingerprint 添加) */
function buildFrontmatter(e: MemoryEntry, created: string, updated: string): string {
  const lines = [
    `type: ${e.type}`,
    `description: ${fmValue(e.description)}`,
    `created: ${created}`,
    `updated: ${updated}`,
  ];
  if (e.project) lines.push(`project: ${fmValue(e.project)}`);
  if (e.agent) lines.push(`agent: ${fmValue(e.agent)}`);
  if (e.tags && e.tags.length) lines.push(`tags: [${e.tags.join(", ")}]`);
  if (e.source) lines.push(`source: ${fmValue(e.source)}`);
  return `---\n${lines.join("\n")}\n---\n\n`;
}

/** 解析记忆文件 → 元数据(返回 null 表示不是合法的记忆文件) */
export function parseMemoryFile(absPath: string, relPath: string): MemoryReadEntry | null {
  try {
    const content = fs.readFileSync(absPath, "utf-8").replace(/^\uFEFF/, "");
    const fm = extractFrontmatter(content);
    if (!fm) return null;
    const kv: Record<string, string> = {};
    for (const line of fm.split("\n")) {
      const i = line.indexOf(":");
      if (i <= 0) continue;
      kv[line.slice(0, i).trim()] = line
        .slice(i + 1)
        .trim()
        .replace(/^"(.*)"$/, "$1")
        .replace(/^'(.*)'$/, "$1");
    }
    const type = kv["type"] as MemoryType;
    if (!type || !MEMORY_TYPES.includes(type)) return null;
    const body = content.slice(content.indexOf("\n---", 3) + 4).trim() || "";
    return {
      type,
      title: path.basename(relPath).replace(/\.md$/, ""),
      description: kv["description"] ?? "",
      content: body,
      project: kv["project"] || undefined,
      agent: kv["agent"] || undefined,
      source: kv["source"] || undefined,
      path: relPath,
      created: kv["created"] || undefined,
      updated: kv["updated"] || undefined,
    };
  } catch {
    return null;
  }
}

/** 扫描记忆目录 → 全部条目(跳过 .pending/ 与 index.md) */
export function listMemory(
  vault: string,
  opts: { type?: string; project?: string; limit?: number } = {},
  memoryDir?: string,
): MemoryReadEntry[] {
  const root = resolveMemoryDir(vault, memoryDir);
  if (!fs.existsSync(root)) return [];
  const out: MemoryReadEntry[] = [];
  for (const dir of Object.values(TYPE_DIRS)) {
    const full = path.join(root, dir);
    if (!fs.existsSync(full)) continue;
    for (const f of fs.readdirSync(full)) {
      if (!f.endsWith(".md")) continue;
      const entry = parseMemoryFile(path.join(full, f), `${dir}/${f}`);
      if (!entry) continue;
      if (opts.type && entry.type !== opts.type) continue;
      if (opts.project && entry.project !== opts.project) continue;
      out.push(entry);
    }
  }
  const sorted = out.sort((a, b) =>
    String(b.updated ?? b.created ?? "").localeCompare(String(a.updated ?? a.created ?? "")),
  );
  return opts.limit ? sorted.slice(0, opts.limit) : sorted;
}

/** 检索:query 分词全部命中(title + description + 正文);无 query 返回全部 */
export function searchMemory(
  vault: string,
  opts: { query?: string; type?: string; project?: string; limit?: number } = {},
  memoryDir?: string,
): MemoryReadEntry[] {
  const q = (opts.query ?? "").trim().toLowerCase();
  const all = listMemory(vault, { type: opts.type, project: opts.project }, memoryDir);
  if (!q) return all;
  const tokens = q.split(/\s+/).filter(Boolean);
  return all.filter((e) => {
    const hay = `${e.title} ${e.description} ${e.content}`.toLowerCase();
    return tokens.every((t) => hay.includes(t));
  });
}

/**
 * 写入一条记忆(自动重建概览)。
 * 纪律:已存在且带指纹(我们生成的)→ 允许覆盖(保留 created,更新 updated);
 *       已存在但无指纹(手写)→ 拒绝覆盖,返回 skipped。
 */
export function writeMemory(
  vault: string,
  e: MemoryEntry,
  opts: { memoryDir?: string; dryRun?: boolean } = {},
): { ok: boolean; path?: string; skipped?: boolean; reason?: string } {
  const dir = TYPE_DIRS[e.type];
  if (!dir) return { ok: false, reason: `未知记忆类型: ${e.type}` };
  const title = pathSafe(e.title) || "untitled";
  const rel = `${dir}/${title}.md`;
  try {
    assertSafeRelativePath(rel);
  } catch (err) {
    return { ok: false, reason: (err as Error).message };
  }
  const root = resolveMemoryDir(vault, opts.memoryDir);
  const full = path.join(root, ...rel.split("/"));
  let created = today();
  if (fs.existsSync(full)) {
    const existing = fs.readFileSync(full, "utf-8");
    if (extractFingerprint(existing) === null) {
      return { ok: false, skipped: true, reason: `已存在无指纹(手写)文件,拒绝覆盖: ${rel}` };
    }
    const prev = parseMemoryFile(full, rel);
    if (prev?.created) created = prev.created;
  }
  const withFm = buildFrontmatter(e, created, today()) + (e.content ?? "").trim() + "\n";
  const final = embedFingerprint(withFm, simpleHash(stripFingerprint(withFm)));
  if (!opts.dryRun) {
    fs.mkdirSync(path.dirname(full), { recursive: true });
    fs.writeFileSync(full, final, "utf-8");
    rebuildIndex(vault, opts.memoryDir);
  }
  return { ok: true, path: rel };
}

/** 重建 memory/index.md 概览(工具生成,带指纹) */
export function rebuildIndex(vault: string, memoryDir?: string): void {
  const root = resolveMemoryDir(vault, memoryDir);
  const entries = listMemory(vault, {}, memoryDir);
  const lines = [
    "# 记忆概览",
    "",
    "> 由 workvault 自动维护,请勿手改(记忆正文可自由编辑,本页随每次写入自动重建)。",
    "",
  ];
  for (const type of MEMORY_TYPES) {
    const group = entries.filter((e) => e.type === type);
    lines.push(`## ${type}s(${group.length})`);
    if (group.length === 0) {
      lines.push("");
      continue;
    }
    for (const e of group) {
      const date = e.updated ?? e.created ?? "";
      const desc = e.description ? `— ${e.description}` : "";
      lines.push(`- ${date} [${e.type}] ${desc} → [[${e.path.replace(/\.md$/, "")}]]`);
    }
    lines.push("");
  }
  const pending = listPendingMemory(vault, memoryDir);
  lines.push(`## 待确认(${pending.length})`);
  if (pending.length > 0) {
    for (const p of pending) {
      lines.push(`- ${p.type} — ${p.description} → [[${p.path.replace(/\.md$/, "")}]]`);
    }
  } else {
    lines.push("- (无)");
  }
  lines.push("");
  lines.push("## 会话日志");
  lines.push(`- 今日:[[journals/${today()}]]`);
  lines.push("");
  const content = lines.join("\n");
  const final = embedFingerprint(content, simpleHash(stripFingerprint(content)));
  fs.mkdirSync(root, { recursive: true });
  fs.writeFileSync(path.join(root, "index.md"), final, "utf-8");
}

/** 列出挂起记忆 */
export function listPendingMemory(vault: string, memoryDir?: string): MemoryReadEntry[] {
  const root = resolveMemoryDir(vault, memoryDir);
  const dir = path.join(root, ".pending");
  if (!fs.existsSync(dir)) return [];
  const out: MemoryReadEntry[] = [];
  for (const f of fs.readdirSync(dir)) {
    if (!f.endsWith(".md")) continue;
    const entry = parseMemoryFile(path.join(dir, f), `.pending/${f}`);
    if (entry) out.push(entry);
  }
  return out.sort((a, b) => a.path.localeCompare(b.path));
}

/**
 * LLM 提炼候选记忆(不直接入库)。
 * writePending=false → 只返回草稿(预览);true → 写入 .pending/<stamp>-<i>.md。
 */
export async function captureMemory(
  vault: string,
  opts: {
    provider: LlmProvider;
    text: string;
    schema?: string;
    type?: string;
    project?: string;
    agent?: string;
    memoryDir?: string;
    writePending?: boolean;
  },
): Promise<{ drafts: MemoryEntry[]; written: string[]; error?: string }> {
  const schema =
    opts.schema ??
    [
      "# 记忆提炼规范",
      "从会话文本中提炼值得长期记住的记忆,只输出一个 JSON 对象:",
      '{"memories":[{"type":"fact|preference|decision|finding|learning|reference","title":"短标题","description":"一句话钩子","content":"正文(Markdown)","project":"可选"}]}',
      "规则:",
      "- 事实/偏好/决定/新发现/学到的方法/参考资料,各归其类",
      "- 含糊、临时性内容不提炼;一条记忆只讲一件事",
      "- description 必须自包含(脱离上下文也能看懂)",
      "- 不要编造会话中不存在的信息",
    ].join("\n");
  const sys = `你是记忆提炼器。请严格按以下规范输出:\n---\n${schema}\n---\n只输出 JSON,不要任何解释。`;
  const user = `把以下会话记录提炼为记忆:\n\n${opts.text.slice(0, 12000)}`;
  let parsed: Record<string, unknown> | null = null;
  let raw = "";
  try {
    raw = await opts.provider.ask(sys, user);
    parsed = extractJson(raw);
    if (!parsed || !Array.isArray(parsed.memories)) {
      const retry = await opts.provider.ask(
        sys,
        `${user}\n\n注意:你上一次的输出不是合法 JSON。请严格只输出 {"memories":[...]},不要输出其他文字。`,
      );
      parsed = extractJson(retry);
    }
  } catch (e) {
    return { drafts: [], written: [], error: (e as Error).message };
  }
  if (!parsed || !Array.isArray(parsed.memories)) {
    return { drafts: [], written: [], error: `LLM 输出不是合法 JSON(前 200 字):${raw.slice(0, 200)}` };
  }
  const drafts: MemoryEntry[] = [];
  for (const m of parsed.memories as Array<Record<string, unknown>>) {
    const type = String(m.type ?? "") as MemoryType;
    if (!MEMORY_TYPES.includes(type)) continue;
    const title = String(m.title ?? "").trim();
    const description = String(m.description ?? "").trim();
    const content = String(m.content ?? "").trim();
    if (!title || !description) continue;
    drafts.push({
      type,
      title,
      description,
      content,
      project: opts.project ?? (m.project ? String(m.project) : undefined),
      agent: opts.agent,
      source: opts.agent ? `auto_capture:${opts.agent}` : undefined,
    });
  }
  const written: string[] = [];
  if (opts.writePending !== false) {
    const root = resolveMemoryDir(vault, opts.memoryDir);
    const stamp = nowStamp().replace(/[\s:]/g, "-");
    fs.mkdirSync(path.join(root, ".pending"), { recursive: true });
    drafts.forEach((d, i) => {
      const rel = `.pending/${stamp}-${i + 1}.md`;
      const withFm = buildFrontmatter(d, today(), today()) + d.content + "\n";
      fs.writeFileSync(
        path.join(root, ...rel.split("/")),
        embedFingerprint(withFm, simpleHash(stripFingerprint(withFm))),
        "utf-8",
      );
      written.push(rel);
    });
  }
  return { drafts, written };
}

/** 确认一条挂起记忆 → 入库 + 重建概览 + 删除挂起文件 */
export function confirmPending(
  vault: string,
  name: string,
  opts: { memoryDir?: string } = {},
): { ok: boolean; path?: string; reason?: string } {
  const rel = name.startsWith(".pending/") ? name : `.pending/${pathSafe(name)}.md`;
  const root = resolveMemoryDir(vault, opts.memoryDir);
  const full = path.join(root, ...rel.split("/"));
  if (!fs.existsSync(full)) return { ok: false, reason: `挂起记忆不存在: ${rel}` };
  const entry = parseMemoryFile(full, rel);
  if (!entry) return { ok: false, reason: `无法解析挂起记忆: ${rel}` };
  const res = writeMemory(
    vault,
    {
      type: entry.type,
      title: entry.title,
      description: entry.description,
      content: entry.content,
      project: entry.project,
      agent: entry.agent,
      source: entry.source,
    },
    opts,
  );
  if (!res.ok) return res;
  fs.unlinkSync(full);
  return { ok: true, path: res.path };
}

// ---------------------------------------------------------------------------
// 会话生命周期
// ---------------------------------------------------------------------------

/**
 * session_start:为 Agent 注入「记忆上下文」——概览 + 相关记忆 + 今日日志 + 待确认。
 * 纯读取,不写任何文件。
 */
export function sessionStart(
  vault: string,
  opts: { project?: string; focus?: string; memoryDir?: string } = {},
): string {
  const root = resolveMemoryDir(vault, opts.memoryDir);
  const parts: string[] = [];
  const entries = listMemory(vault, { project: opts.project }, opts.memoryDir);
  if (entries.length === 0) {
    parts.push("【记忆概览】暂无记忆。");
  } else {
    const byType = MEMORY_TYPES.map((t) => ({ t, n: entries.filter((e) => e.type === t).length }))
      .filter((x) => x.n > 0)
      .map((x) => `${x.t}:${x.n}`)
      .join(" · ");
    parts.push(`【记忆概览】共 ${entries.length} 条(${byType})。`);
  }
  if (opts.focus?.trim()) {
    const hit = searchMemory(vault, { query: opts.focus, project: opts.project, limit: 8 }, opts.memoryDir);
    if (hit.length > 0) {
      parts.push(
        `【相关记忆(focus: ${opts.focus})】\n` +
          hit
            .map((e) => `- [${e.type}] ${e.title}(${e.created ?? ""})${e.description ? ":" + e.description : ""}`)
            .join("\n"),
      );
    }
  }
  const journal = path.join(root, "journals", `${today()}.md`);
  if (fs.existsSync(journal)) {
    const txt = fs.readFileSync(journal, "utf-8").trim();
    parts.push(`【今日会话日志】\n${txt.slice(-800)}`);
  }
  const pending = listPendingMemory(vault, opts.memoryDir);
  if (pending.length > 0) {
    parts.push(
      `【待确认记忆 ${pending.length} 条】用 memory confirm <名称> 逐条确认:\n` +
        pending.map((p) => `- ${p.path}:${p.description}`).join("\n"),
    );
  } else {
    parts.push("【待确认记忆】无。");
  }
  return parts.join("\n\n");
}

/**
 * session_end:追加今日会话日志 + 重建概览;可选 auto_capture 把摘要提炼成
 * 候选记忆写入 .pending/(待确认入库);可选 git 自动提交。
 */
export async function sessionEnd(
  vault: string,
  opts: {
    summary: string;
    project?: string;
    agent?: string;
    memoryDir?: string;
    provider?: LlmProvider;
    autoCapture?: boolean;
    schema?: string;
    autoCommit?: boolean;
  },
): Promise<{ ok: boolean; journal?: string; pending?: string[]; captureError?: string; commit?: string; error?: string }> {
  try {
    const root = resolveMemoryDir(vault, opts.memoryDir);
    const jDir = path.join(root, "journals");
    fs.mkdirSync(jDir, { recursive: true });
    const jPath = path.join(jDir, `${today()}.md`);
    const stamp = nowStamp();
    const section = [`## ${stamp}${opts.project ? ` · ${opts.project}` : ""}`, ""];
    for (const line of opts.summary.split("\n")) {
      const l = line.trim();
      section.push(l ? `- ${l}` : "-");
    }
    section.push("");
    const existed = fs.existsSync(jPath);
    const append = (existed ? "\n" : "# 会话日志\n\n") + section.join("\n");
    fs.appendFileSync(jPath, append, "utf-8");
    rebuildIndex(vault, opts.memoryDir);

    let pending: string[] = [];
    let captureError: string | undefined;
    if (opts.provider && opts.autoCapture !== false && opts.summary.trim()) {
      const cap = await captureMemory(vault, {
        provider: opts.provider,
        text: opts.summary,
        schema: opts.schema,
        project: opts.project,
        agent: opts.agent,
        memoryDir: opts.memoryDir,
        writePending: true,
      });
      pending = cap.written;
      captureError = cap.error;
      if (cap.written.length > 0) rebuildIndex(vault, opts.memoryDir);
    }

    let commit: string | undefined;
    if (opts.autoCommit) commit = gitCommitMemory(vault, opts.memoryDir) ?? undefined;
    return { ok: true, journal: `journals/${today()}.md`, pending, captureError, commit };
  } catch (e) {
    return { ok: false, error: (e as Error).message };
  }
}

/** vault 是 git 仓库时提交记忆目录;失败静默返回 null */
function gitCommitMemory(vault: string, memoryDir?: string): string | null {
  try {
    if (!fs.existsSync(path.join(vault, ".git"))) return null;
    const rel = memoryDir ?? "memory";
    execSync(`git add "${rel}" && git commit -m "workvault: memory update"`, {
      cwd: vault,
      stdio: "pipe",
      encoding: "utf-8",
    });
    return "已提交到 git";
  } catch {
    return null;
  }
}