/**
 * 对话式 CLI(Hermes 风格):node cli.js chat
 * - 角色人设:知识管家
 * - 上下文记忆:多轮对话历史
 * - 工具自动挑选:LLM 判断是"普通聊天"还是"操作知识库",需要时输出工具调用 JSON
 * - 工具结果回灌:工具执行后由 LLM 用自然语言总结给用户
 */
import * as fs from "fs";
import * as path from "path";
import * as readline from "readline";
import type { LlmProvider } from "./core/provider";
import { renderSource } from "./core/engine";
import { FsWriter } from "./core/fsWriter";
import { planIncremental } from "./core/incremental";
import { applyNotes } from "./core/writer";
import { compileRaw } from "./core/compiler";
import { queryWiki } from "./core/query";
import { lintWiki } from "./core/lint";
import { extractJson } from "./core/compiler";
import type { Profile } from "./core/types";

export interface ChatOptions {
  provider: LlmProvider;
  vault: string;
  schemaWiki: string;
  schemaQuery: string;
  cwd: string;
  persona?: string;
}

const DEFAULT_PERSONA = `你是「知识管家」,一个温暖、专业、耐心的 AI 知识库助手。
你管理着一个 Obsidian 知识库(vault),职责是帮用户把资料变成会生长的知识库,并基于它回答问题。

你可以调用以下工具(用户要求操作知识库时):
- compile  把原始资料(目录/文件)编译进知识库 wiki(实体/概念/主题/索引)
- query    基于知识库回答用户问题(读索引→定位→读页→综合回答,标注来源)
- run      把结构化数据(JSON)按 profile 模板转成笔记
- lint     检查知识库健康(断链/孤立页/索引)

规则:
1. 用户要求"编译/收录/整理资料"→ 输出工具调用 JSON(单独输出,不要夹杂文字):
   {"tool":"compile","params":{"input":"<资料目录或文件路径>","instruction":"<可选的自然语言要求>"}}
2. 用户问知识库里的内容 → 输出:{"tool":"query","params":{"question":"<用户的问题>"}}
3. 用户给数据要转成笔记 → 输出:{"tool":"run","params":{"data":"<JSON 内容或文件路径>","profile":"person"}}
4. 用户要求检查健康 → 输出:{"tool":"lint","params":{}}
5. 其余情况(打招呼、闲聊、问工具用法等)→ 直接用自然语言聊天回复
6. 回答知识库问题时:先给直接结论,标注来源;知识库没有的,如实说明"知识缺口",绝不编造
7. 语气自然亲切,像一位懂行的老师`;

const MAX_HISTORY = 20;

export async function runChat(opts: ChatOptions): Promise<number> {
  const persona = opts.persona ?? DEFAULT_PERSONA;
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  let closed = false;
  rl.on("close", () => {
    closed = true;
  });

  console.log("");
  console.log("══════ 知识管家(workvault) ══════");
  console.log(`vault: ${opts.vault}`);
  console.log("用自然语言和我聊,我可以:整理资料进知识库 / 回答知识库问题 / 数据转笔记 / 检查健康");
  console.log("退出:exit / quit");
  console.log("════════════════════════════════════");

  const ask = () =>
    new Promise<string | null>((resolve) => {
      if (closed) {
        resolve(null);
        return;
      }
      try {
        rl.question("\n你 > ", (a) => resolve(a.trim()));
      } catch {
        resolve(null);
      }
    });

  const history: string[] = [];

  try {
    while (true) {
      const input = await ask();
      if (input === null) break;
      if (!input) continue;

      const lower = input.toLowerCase();
      if (["exit", "quit", "bye", "退出", "结束"].includes(lower)) {
        console.log("再见 👋 知识库随时等你回来");
        break;
      }

      // 组装带历史的问题
      const userText = buildUserText(history, input);
      let reply: string;
      try {
        reply = await opts.provider.ask(persona, userText);
      } catch (e) {
        console.log(`(调用失败:${(e as Error).message})`);
        continue;
      }

      // 判断是否为工具调用 JSON
      const intent = parseToolCall(reply);
      if (intent) {
        // 执行工具 → 结果回灌 → LLM 总结成自然语言
        const resultText = await executeTool(opts, intent.tool, intent.params);
        const summaryUser =
          `${userText}\n\n【工具执行结果】\n${resultText}\n\n请用自然、简洁的语言向用户总结刚才的操作结果(做了什么、结果如何、可补充建议),不要复述 JSON。`;
        let summary: string;
        try {
          summary = await opts.provider.ask(persona, summaryUser);
        } catch {
          summary = resultText;
        }
        console.log("\n" + summary.trim());
        history.push(`用户: ${input}`, `助手: ${summary.trim()}`);
      } else {
        console.log("\n" + reply.trim());
        history.push(`用户: ${input}`, `助手: ${reply.trim()}`);
      }

      // 裁剪历史
      if (history.length > MAX_HISTORY) {
        history.splice(0, history.length - MAX_HISTORY);
      }
    }
  } finally {
    rl.close();
  }
  return 0;
}

function buildUserText(history: string[], input: string): string {
  const parts: string[] = [];
  if (history.length > 0) {
    parts.push("【之前的对话】");
    parts.push(history.join("\n"));
    parts.push("");
  }
  parts.push("【当前用户输入】");
  parts.push(input);
  return parts.join("\n");
}

interface ToolCall {
  tool: string;
  params: Record<string, unknown>;
}

/** 解析 LLM 回复中的工具调用 JSON;不是工具调用则返回 null */
function parseToolCall(reply: string): ToolCall | null {
  const trimmed = reply.trim();
  // 严格:整个回复就是 JSON 且含 tool 字段
  if (!trimmed.startsWith("{")) return null;
  const parsed = extractJson(trimmed);
  if (!parsed || !parsed.tool) return null;
  return {
    tool: String(parsed.tool).toLowerCase(),
    params: (parsed.params ?? {}) as Record<string, unknown>,
  };
}

async function executeTool(
  opts: ChatOptions,
  tool: string,
  params: Record<string, unknown>,
): Promise<string> {
  try {
    switch (tool) {
      case "compile":
        return await execCompile(opts, params);
      case "query":
        return await execQuery(opts, params);
      case "run":
        return await execRun(opts, params);
      case "lint":
        return execLint(opts);
      default:
        return `未知工具:${tool}`;
    }
  } catch (e) {
    return `执行失败:${(e as Error).message}`;
  }
}

async function execRun(opts: ChatOptions, params: Record<string, unknown>): Promise<string> {
  let dataRaw = String(params.data ?? "");
  if (!dataRaw) return "run 缺少 data(数据内容或文件路径)";
  const candidate = path.isAbsolute(dataRaw) ? dataRaw : path.join(opts.cwd, dataRaw);
  if (fs.existsSync(candidate)) {
    dataRaw = fs.readFileSync(candidate, "utf-8");
  } else if (!dataRaw.trim().startsWith("[")) {
    return `数据文件不存在:${candidate}`;
  }
  const profileRef = String(params.profile ?? "person");
  const profile = loadProfile(profileRef, opts.cwd);
  const source = { kind: "json" as const, content: dataRaw };
  const { notes, issues, warnings } = renderSource(source, profile);
  if (issues.length > 0) {
    return `校验问题:${issues.map((i) => `${i.path}: ${i.issue}`).join("; ")}`;
  }
  const writer = new FsWriter(opts.vault);
  const existing = await writer.listExistingPaths();
  let toWrite = notes;
  let planNote = "";
  if (profile.incremental) {
    const plan = await planIncremental(notes, writer);
    toWrite = plan.notes;
    planNote = `增量:变化 ${plan.changed.length} 篇,跳过 ${plan.unchanged.length} 篇;`;
  }
  const result = await applyNotes(toWrite, writer, {
    dryRun: false,
    conflictStrategy: profile.conflictStrategy ?? "overwrite",
    existingPaths: existing,
  });
  const warns = warnings.length ? `警告:${warnings.join("; ")}` : "";
  return `${planNote}已写入 ${result.written.length} 篇,跳过 ${result.skipped.length} 篇${result.errors.length ? `,错误 ${result.errors.length}` : ""}${warns}`;
}

async function execCompile(opts: ChatOptions, params: Record<string, unknown>): Promise<string> {
  const inputRef = String(params.input ?? "");
  if (!inputRef) return "compile 需要 input(资料目录或文件路径)";
  let inputPath = "";
  if (path.isAbsolute(inputRef)) {
    inputPath = inputRef;
  } else {
    const candidates = [path.join(opts.cwd, inputRef), path.join(opts.vault, inputRef)];
    inputPath = candidates.find((p) => fs.existsSync(p)) ?? candidates[0];
  }
  if (!fs.existsSync(inputPath)) return `资料路径不存在:${inputPath}`;
  const inputs = readInput(inputPath);
  if (inputs.length === 0) return "没有可编译的文本文件";
  const result = await compileRaw({
    provider: opts.provider,
    schema: opts.schemaWiki,
    inputs,
    instruction: params.instruction ? String(params.instruction) : undefined,
    vault: opts.vault,
    wikiDir: "wiki",
  });
  const log = result.log ? `编译:${result.log}` : "";
  const stat = `已写入 ${result.written.length} 篇,跳过 ${result.skipped.length} 篇,未变 ${result.unchanged.length} 篇${result.errors.length ? `,错误 ${result.errors.length}` : ""}`;
  return [log, stat, ...result.errors.map((e) => `错误:${e}`)].filter(Boolean).join("\n");
}

async function execQuery(opts: ChatOptions, params: Record<string, unknown>): Promise<string> {
  const question = String(params.question ?? "");
  if (!question) return "query 需要 question";
  const result = await queryWiki({
    provider: opts.provider,
    vault: opts.vault,
    question,
    schema: opts.schemaQuery,
    wikiDir: "wiki",
  });
  const parts = [
    result.answer,
    result.sources.length ? `来源:${result.sources.join(", ")}` : "",
    result.gaps.length ? `知识缺口:${result.gaps.join("; ")}` : "",
    ...result.errors.map((e) => `错误:${e}`),
  ].filter(Boolean);
  return parts.join("\n") || "(没有回答)";
}

function execLint(opts: ChatOptions): string {
  const r = lintWiki(opts.vault, "wiki");
  const parts = [`wiki 页面 ${r.totalPages} 篇`];
  if (r.indexMissing) parts.push("! index.md 缺失");
  for (const b of r.brokenLinks) parts.push(`! 断链 ${b.from} → [[${b.link}]]`);
  for (const l of r.indexBrokenLinks) parts.push(`! index 断链 → [[${l}]]`);
  for (const p of r.orphanPages) parts.push(`! 孤立页 ${p}`);
  parts.push(r.ok ? "健康 ✓" : `共 ${r.brokenLinks.length + r.indexBrokenLinks.length} 处断链 / ${r.orphanPages.length} 个孤立页`);
  return parts.join("\n");
}

function loadProfile(ref: string, cwd: string): Profile {
  const candidates = [
    path.isAbsolute(ref) ? ref : path.join(cwd, ref),
    path.join(path.dirname(__filename), "templates", "profiles", `${ref}.json`),
  ];
  for (const p of candidates) {
    if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8")) as Profile;
  }
  const examplesDir = path.join(path.dirname(__filename), "examples");
  if (fs.existsSync(examplesDir)) {
    for (const pkg of fs.readdirSync(examplesDir)) {
      const p = path.join(examplesDir, pkg, "profiles", `${ref}.json`);
      if (fs.existsSync(p)) return JSON.parse(fs.readFileSync(p, "utf-8")) as Profile;
    }
  }
  throw new Error(`profile 不存在: ${ref}`);
}

const TEXT_EXTS = [".md", ".mdx", ".txt", ".json", ".yaml", ".yml", ".csv", ".html", ".py", ".js", ".ts"];

function readInput(ref: string): Array<{ name: string; content: string }> {
  const stat = fs.statSync(ref);
  if (stat.isDirectory()) {
    const out: Array<{ name: string; content: string }> = [];
    const walk = (dir: string, prefix: string) => {
      for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
        if (entry.name.startsWith(".")) continue;
        const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
        const full = path.join(dir, entry.name);
        if (entry.isDirectory()) walk(full, rel);
        else if (entry.isFile() && TEXT_EXTS.includes(path.extname(entry.name).toLowerCase())) {
          out.push({ name: rel, content: fs.readFileSync(full, "utf-8") });
        }
      }
    };
    walk(ref, "");
    return out;
  }
  return [{ name: path.basename(ref), content: fs.readFileSync(ref, "utf-8") }];
}