/**
 * Project Caffeine v0.1.1
 * Copyright (c) 2025-2026 Gitconomy Research
 *
 * SPDX-License-Identifier: MIT
 *
 * Contributors:
 * - 郭晧 <guohao@gitconomy.org> (Initial Author)
 */

import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import { z } from 'zod';
import { handlePromptsGet } from './controllers/promptsController';
import { handleToolCall } from './controllers/toolsController';
import { listObsidianNotes, readObsidianNote } from './services/resourceService';

// ==========================================
// 初始化 MCP Server
// ==========================================
const server = new McpServer({
  name: 'Project-Caffeine-Arabica-Edition',
  version: '0.0.2'
});

// ==========================================
// 辅助函数
// ==========================================

/**
 * 处理可选参数的默认值转换与补全。
 *
 * 该函数执行两个主要任务：
 * 1. 遍历传入的参数对象，将空值（undefined/null/""）转换为字符串 "无"。
 * 2. 根据提供的 expectedKeys 列表，补全缺失的可选参数键，值为 "无"，确保模板替换不会失败。
 *
 * @param {Record<string, any>} args - 客户端传入的原始参数对象
 * @param {string[]} expectedKeys - 该框架预期的所有参数键名列表（用于补全缺失的键）
 * @returns {Record<string, string>} 补全且清洗后的参数对象，所有值均为非空字符串
 */
function sanitizeArgs(args: Record<string, any>, expectedKeys: string[] = []): Record<string, string> {
  const safeArgs: Record<string, string> = {};

  // 处理已有的参数，确保值不为空
  for (const [key, value] of Object.entries(args)) {
    safeArgs[key] = value !== undefined && value !== null && value !== "" ? String(value) : "无";
  }

  // 补全缺失的可选参数键，确保模板替换不会失败
  expectedKeys.forEach(key => {
    if (!(key in safeArgs)) {
      safeArgs[key] = "无";
    }
  });

  return safeArgs;
}

// ==========================================
// 注册 Prompts 原语（多维思维框架模板）
// ==========================================

/**
 * 注册并处理 SCQA 框架的 prompts/get 请求。
 *
 * @param {object} args - 客户端传入的框架参数
 * @param {string} args.situation - 当前的客观背景或情境描述（必填）
 * @param {string} [args.context] - 补充的约束条件、行业背景或已有资源（可选）
 * @param {string} [args.objective] - 期望达成的最终业务目标（可选）
 * @returns {Promise<object>} 符合 MCP 协议规范的 prompt 响应对象，包含过滤和补全元数据后的 messages 序列
 */
server.prompt(
  'scqa',
  {
    situation: z.string().describe('当前的客观背景或情境描述'),
    context: z.string().optional().describe('补充的约束条件、行业背景或已有资源（可选）'),
    objective: z.string().optional().describe('期望达成的最终业务目标（可选）')
  },
  async (args) => {
    const result = await handlePromptsGet('scqa', sanitizeArgs(args, ['situation', 'context', 'objective']));
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 注册并处理 5 Whys 框架的 prompts/get 请求。
 *
 * @param {object} args - 客户端传入的框架参数
 * @param {string} args.problem - 需要分析的核心问题、故障或不良现象（必填）
 * @param {string} [args.context] - 问题发生的背景信息、前置条件或受影响的范围（可选）
 * @param {string} [args.goal] - 期望通过解决此问题达成的最终目标（可选）
 * @returns {Promise<object>} 符合 MCP 协议规范的 prompt 响应对象
 */
server.prompt(
  '5whys',
  {
    problem: z.string().describe('需要分析的核心问题、故障或不良现象'),
    context: z.string().optional().describe('问题发生的背景信息、前置条件或受影响的范围（可选）'),
    goal: z.string().optional().describe('期望通过解决此问题达成的最终目标（可选）')
  },
  async (args) => {
    const result = await handlePromptsGet('5whys', sanitizeArgs(args, ['problem', 'context', 'goal']));
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 注册并处理 5W3H 框架的 prompts/get 请求。
 *
 * @param {object} args - 客户端传入的框架参数
 * @param {string} args.topic - 需要分析的核心主题、事件或问题（必填）
 * @param {string} [args.context] - 该主题发生的特定背景、前置条件或行业环境（可选）
 * @param {string} [args.objective] - 期望通过此次分析达成的核心目标（可选）
 * @returns {Promise<object>} 符合 MCP 协议规范的 prompt 响应对象
 */
server.prompt(
  '5w3h',
  {
    topic: z.string().describe('需要分析的核心主题、事件或问题'),
    context: z.string().optional().describe('该主题发生的特定背景、前置条件或行业环境（可选）'),
    objective: z.string().optional().describe('期望通过此次分析达成的核心目标（可选）')
  },
  async (args) => {
    const result = await handlePromptsGet('5w3h', sanitizeArgs(args, ['topic', 'context', 'objective']));
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 注册并处理 SWOT 框架的 prompts/get 请求。
 *
 * @param {object} args - 客户端传入的框架参数
 * @param {string} args.entity - 分析对象（如企业、产品、项目、个人等）（必填）
 * @param {string} [args.context] - 补充的行业背景、当前阶段或面临的核心挑战（可选）
 * @param {string} [args.competitors] - 主要竞争对手或对标对象（可选）
 * @returns {Promise<object>} 符合 MCP 协议规范的 prompt 响应对象
 */
server.prompt(
  'swot',
  {
    entity: z.string().describe('分析对象（如企业、产品、项目、个人等）'),
    context: z.string().optional().describe('补充的行业背景、当前阶段或面临的核心挑战（可选）'),
    competitors: z.string().optional().describe('主要竞争对手或对标对象（可选）')
  },
  async (args) => {
    const result = await handlePromptsGet('swot', sanitizeArgs(args, ['entity', 'context', 'competitors']));
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

/**
 * 注册并处理 PESTLE 框架的 prompts/get 请求。
 *
 * @param {object} args - 客户端传入的框架参数
 * @param {string} args.domain - 需要分析的具体行业、市场或业务领域（必填）
 * @param {string} [args.region] - 目标地域范围（可选）
 * @param {string} [args.timeframe] - 分析的时间跨度（可选）
 * @returns {Promise<object>} 符合 MCP 协议规范的 prompt 响应对象
 */
server.prompt(
  'pestle',
  {
    domain: z.string().describe('需要分析的具体行业、市场或业务领域'),
    region: z.string().optional().describe('目标地域范围（可选）'),
    timeframe: z.string().optional().describe('分析的时间跨度（可选）')
  },
  async (args) => {
    const result = await handlePromptsGet('pestle', sanitizeArgs(args, ['domain', 'region', 'timeframe']));
    return {
      ...result,
      messages: Array.isArray(result.messages)
        ? result.messages
            .filter((msg: any) => msg.role === "user" || msg.role === "assistant")
            .map((msg: any) => ({
              ...msg,
              content: {
                ...msg.content,
                annotations: msg.content.annotations ?? undefined,
                _meta: msg.content._meta ?? undefined
              }
            }))
        : []
    };
  }
);

// ==========================================
// 注册工具（Tools）
// ==========================================

/**
 * 注册意图拆解工具 generate_search_queries。
 * 接收用户的原始自然语言查询，调用意图拆解服务生成 3-5 个专业检索词。
 *
 * @param {object} args - 工具参数对象
 * @param {string} args.query - 用户的原始查询语句
 * @returns {Promise<object>} 符合 MCP 协议的工具响应对象，包含 type 为 "text" 的内容格式化结果
 */
server.tool(
  'generate_search_queries',
  { query: z.string().describe('用户的原始查询语句') },
  async (args) => {
    const result = await handleToolCall('generate_search_queries', args);
    return {
      ...result,
      content: result.content.map((item: any) => ({
        ...item,
        type: "text"
      }))
    };
  }
);

/**
 * 注册获取本地笔记列表工具 list_local_notes。
 * 允许大模型获取当前知识库目录下所有存在的 Markdown 笔记文件名。
 *
 * @param {object} args - 无需额外参数（保留以符合 MCP 签名）
 * @returns {Promise<object>} 包含笔记文件名列表的文本响应对象
 */
server.tool(
  'list_local_notes',
  {},
  async (args) => {
    const result = await handleToolCall('list_local_notes', args);
    return {
      ...result,
      content: result.content.map((item: any) => ({
        type: 'text',
        text: item.text
      }))
    };
  }
);

/**
 * 注册读取单一本地笔记工具 read_local_note。
 * 允许大模型根据指定文件名获取该 Markdown 文件的具体文本内容。
 *
 * @param {object} args - 工具参数对象
 * @param {string} args.filename - 需要读取的笔记文件名，必须包含 .md 后缀
 * @returns {Promise<object>} 包含目标文件全量文本内容的响应对象
 */
server.tool(
  'read_local_note',
  { filename: z.string().describe('需要读取的笔记文件名，必须包含 .md 后缀') },
  async (args) => {
    const result = await handleToolCall('read_local_note', args);
    return {
      ...result,
      content: result.content.map((item: any) => ({
        type: 'text',
        text: item.text
      }))
    };
  }
);

/**
 * 注册保存本地笔记工具 save_note。
 * 允许大模型将分析结果或摘要撰写并保存为本地知识库中的 Markdown 文件。
 *
 * @param {object} args - 工具参数对象
 * @param {string} args.filename - 笔记文件名，必须以 .md 结尾
 * @param {string} args.content - 需要持久化写入的笔记内容（Markdown 格式）
 * @returns {Promise<object>} 保存成功的状态回执或报错信息的响应对象
 */
server.tool(
  'save_note',
  {
    filename: z.string().describe('笔记文件名，必须以 .md 结尾'),
    content: z.string().describe('笔记内容（Markdown 格式）')
  },
  async (args) => {
    const result = await handleToolCall('save_note', args);
    return {
      ...result,
      content: result.content.map((item: any) => ({
        type: 'text',
        text: item.text
      }))
    };
  }
);

// ==========================================
// 注册资源（Resources）
// ==========================================

/**
 * 注册 local-notes 资源模板原语。
 * 提供给支持 Resources 的 MCP 客户端，使用户可以直接在界面上勾选并注入本地知识库笔记。
 *
 * - list 钩子：返回所有有效的 note://local/{filename} 资源列表。
 * - 读取钩子：根据解析出的 filename 读取本地文件内容，组装为符合 MCP 规范的响应。
 *
 * @type {import('@modelcontextprotocol/sdk/server/mcp.js').ResourceTemplate}
 */
server.resource(
  "local-notes",
  new ResourceTemplate("note://local/{filename}", {
    /**
     * 列出所有可用的笔记资源。
     *
     * @returns {Promise<{ resources: Array<{ name: string; uri: string; mimeType: string; description: string }> }>}
     *          资源列表，每个资源包含名称、URI、MIME 类型和描述
     */
    list: async () => {
      try {
        const notes = await listObsidianNotes();
        return {
          resources: notes.map(filename => ({
            name: filename,
            uri: `note://local/${encodeURIComponent(filename)}`,
            mimeType: "text/markdown",
            description: `本地笔记: ${filename}`
          }))
        };
      } catch (error: any) {
        console.error('[Resources] 列出资源失败:', error);
        return { resources: [] };
      }
    }
  }),
  /**
   * 处理资源读取请求：根据 URI 中的 filename 参数读取对应笔记内容。
   *
   * @param {URL} uri - 请求的完整 URI 对象
   * @param {{ filename: string | string[] }} params - 从 URI 中提取的参数，包含 filename
   * @returns {Promise<{ contents: Array<{ uri: string; mimeType: string; text: string }> }>}
   *          符合 MCP 规范的内容响应对象
   * @throws {Error} 当读取失败时抛出错误，错误信息将被 MCP 客户端捕获
   */
  async (uri, { filename }) => {
    try {
      const filenameStr = Array.isArray(filename) ? filename[0] : filename;
      const decodedFilename = decodeURIComponent(filenameStr);
      const content = await readObsidianNote(decodedFilename);

      return {
        contents: [{
          uri: uri.href,
          mimeType: "text/markdown",
          text: content
        }]
      };
    } catch (error: any) {
      throw new Error(`读取笔记失败: ${error.message}`);
    }
  }
);

// ==========================================
// 启动服务器
// ==========================================

/**
 * 启动 MCP 服务器的主入口函数。
 * 建立与客户端（如 Cherry Studio）的标准输入/输出 (STDIO) 传输通信通道，
 * 将服务器配置挂载至进程，并就绪等待各类协议请求。
 *
 * @returns {Promise<void>} 异步的启动过程，无返回值
 * @throws {Error} 如果连接失败，错误会被捕获并记录，进程退出
 */
async function start() {
  console.error('[S2] 正在启动 MCP Server (Prompts + Tools + Resources)...');
  const transport = new StdioServerTransport();
  await server.connect(transport);
  console.error('[S2] MCP Server 已就绪，等待 Cherry Studio 连接');
}

start().catch((err) => {
  console.error('[S2] 服务器启动失败:', err);
  process.exit(1);
});