#!/usr/bin/env node
'use strict';

const http = require('http');
const https = require('https');

const SERVER_INFO = {
  name: 'ccweb',
  version: '1.0.0',
};

const TOOLS = [
  {
    name: 'ccweb_list_conversations',
    description: '列出当前 ccweb 中可投递消息的对话。只返回 ID、标题、Agent、运行状态和更新时间，不返回对话正文。',
    inputSchema: {
      type: 'object',
      properties: {
        agent: {
          type: 'string',
          enum: ['claude', 'codex', 'codexapp'],
          description: '可选。只返回指定 Agent 的对话。',
        },
        status: {
          type: 'string',
          enum: ['all', 'running', 'idle'],
          description: '可选。按运行状态过滤，默认 all。',
        },
        limit: {
          type: 'integer',
          minimum: 1,
          maximum: 100,
          description: '可选。最多返回多少条，默认 50。',
        },
      },
      additionalProperties: false,
    },
  },
  {
    name: 'ccweb_set_title',
    description: '设置当前 ccweb 对话标题。只作用于当前来源对话；用户手动重命名后会成功返回但忽略，不再覆盖用户标题。',
    inputSchema: {
      type: 'object',
      properties: {
        title: {
          type: 'string',
          maxLength: 120,
          description: '新的当前对话标题。应简短概括用户主要目标。',
        },
      },
      required: ['title'],
      additionalProperties: false,
    },
  },
  {
    name: 'ccweb_create_conversation',
    description: '创建一个新的 ccweb 持久对话。Agent 固定继承来源对话，不作为参数指定；只用于需要在会话列表中长期追踪、后续可继续对话的工作流；一次性并行研究应优先使用子代能力。',
    inputSchema: {
      type: 'object',
      properties: {
        cwd: {
          type: 'string',
          description: '可选。新对话工作目录；指定时必须是已存在的绝对路径，默认继承来源对话 cwd。',
        },
        title: {
          type: 'string',
          maxLength: 120,
          description: '可选。新对话标题。',
        },
        mode: {
          type: 'string',
          enum: ['default', 'plan', 'yolo'],
          description: '可选。权限模式，默认 yolo；只有显式传 default/plan/yolo 时才使用指定模式。',
        },
        initialMessage: {
          type: 'string',
          description: '可选。创建后立即发送到新对话的首条消息。',
        },
        requestReply: {
          type: 'boolean',
          description: '可选。若为 true，会在新对话完成本轮输出后把回复写回来源对话，并继续触发来源对话运行。默认 false。',
        },
      },
      additionalProperties: false,
    },
  },
  {
    name: 'ccweb_send_message',
    description: '向指定 ccweb 对话发送一条消息，并以“来自某对话”的气泡在目标对话中展示。',
    inputSchema: {
      type: 'object',
      properties: {
        targetConversationId: {
          type: 'string',
          description: '目标对话 ID。',
        },
        content: {
          type: 'string',
          description: '要发送到目标对话的纯文本消息。',
        },
      },
      required: ['targetConversationId', 'content'],
      additionalProperties: false,
    },
  },
  {
    name: 'ccweb_list_pending_replies',
    description: '列出当前来源对话等待中的跨对话回复，包括已完成但尚未处理的子对话返回摘要。',
    inputSchema: {
      type: 'object',
      properties: {
        status: {
          type: 'string',
          enum: ['all', 'waiting', 'ready', 'delivering', 'returned', 'failed'],
          description: '可选。按回复状态过滤，默认 all。',
        },
      },
      additionalProperties: false,
    },
  },
  {
    name: 'ccweb_get_pending_reply',
    description: '读取指定 requestId 的跨对话回复状态和正文；用于主线程判断是否继续追问指定子对话。',
    inputSchema: {
      type: 'object',
      properties: {
        requestId: {
          type: 'string',
          description: '等待回复 requestId。',
        },
      },
      required: ['requestId'],
      additionalProperties: false,
    },
  },
  {
    name: 'ccweb_request_reply',
    description: '向指定 ccweb 对话发送一条消息，并在目标对话本轮输出完成后把回复写回当前对话，然后继续触发当前对话运行。',
    inputSchema: {
      type: 'object',
      properties: {
        targetConversationId: {
          type: 'string',
          description: '目标对话 ID。',
        },
        content: {
          type: 'string',
          description: '要发送到目标对话的纯文本消息。',
        },
      },
      required: ['targetConversationId', 'content'],
      additionalProperties: false,
    },
  },
  {
    name: 'ccweb_prompt_user',
    description: '在当前来源 ccweb 对话前台渲染一个多问题表单。工具会立即返回，不等待用户；用户提交后，ccweb 会把问题、选择和答案作为一条普通用户消息发回当前对话。',
    inputSchema: {
      type: 'object',
      properties: {
        title: {
          type: 'string',
          maxLength: 160,
          description: '表单标题。',
        },
        description: {
          type: 'string',
          maxLength: 2000,
          description: '可选。表单整体说明。',
        },
        questions: {
          type: 'array',
          minItems: 1,
          maxItems: 10,
          description: '问题数组。每个问题都会渲染候选项和可编辑答案输入区。',
          items: {
            type: 'object',
            properties: {
              id: {
                type: 'string',
                maxLength: 80,
                description: '问题 ID。建议稳定唯一；缺失时 ccweb 会按顺序生成。',
              },
              title: {
                type: 'string',
                maxLength: 160,
                description: '问题标题。',
              },
              question: {
                type: 'string',
                maxLength: 4000,
                description: '问题正文。',
              },
              required: {
                type: 'boolean',
                description: '是否必答，默认 true。',
              },
              selectionMode: {
                type: 'string',
                enum: ['single', 'multi', 'none'],
                description: '候选项选择模式，默认 single；none 表示只输入答案。',
              },
              answerPlaceholder: {
                type: 'string',
                maxLength: 240,
                description: '答案输入区占位文案。',
              },
              defaultAnswer: {
                type: 'string',
                maxLength: 4000,
                description: '答案输入区默认值。',
              },
              options: {
                type: 'array',
                maxItems: 8,
                description: '候选/推荐选项。点击选项会把 answerText 写入该问题的答案输入区。',
                items: {
                  type: 'object',
                  properties: {
                    id: {
                      type: 'string',
                      maxLength: 80,
                      description: '选项 ID。建议稳定唯一；缺失时 ccweb 会按顺序生成。',
                    },
                    label: {
                      type: 'string',
                      maxLength: 240,
                      description: '选项展示文本。',
                    },
                    description: {
                      type: 'string',
                      maxLength: 1000,
                      description: '选项说明。',
                    },
                    answerText: {
                      type: 'string',
                      maxLength: 4000,
                      description: '点击该选项后预填到答案输入区的文本。',
                    },
                    recommended: {
                      type: 'boolean',
                      description: '是否推荐选项。',
                    },
                  },
                  additionalProperties: false,
                },
              },
            },
            required: ['question'],
            additionalProperties: false,
          },
        },
      },
      required: ['questions'],
      additionalProperties: false,
    },
  },
];

function writeMessage(message) {
  process.stdout.write(`${JSON.stringify(message)}\n`);
}

function jsonRpcResult(id, result) {
  writeMessage({ jsonrpc: '2.0', id, result });
}

function jsonRpcError(id, code, message, data) {
  const error = { code, message };
  if (data !== undefined) error.data = data;
  writeMessage({ jsonrpc: '2.0', id, error });
}

function parseInteger(value, fallback = 0) {
  const parsed = Number.parseInt(String(value || ''), 10);
  return Number.isFinite(parsed) ? parsed : fallback;
}

function callCcweb(tool, args) {
  return new Promise((resolve) => {
    const urlText = String(process.env.CC_WEB_MCP_URL || '').trim();
    const token = String(process.env.CC_WEB_MCP_TOKEN || '').trim();
    if (!urlText || !token) {
      resolve({
        ok: false,
        code: 'mcp_not_configured',
        message: 'ccweb MCP 环境变量未配置完整。',
      });
      return;
    }

    let url;
    try {
      url = new URL(urlText);
    } catch {
      resolve({
        ok: false,
        code: 'mcp_bad_url',
        message: 'ccweb MCP 内部地址无效。',
      });
      return;
    }

    const body = JSON.stringify({
      tool,
      args: args && typeof args === 'object' ? args : {},
      sourceSessionId: process.env.CC_WEB_SOURCE_SESSION_ID || '',
      sourceHopCount: parseInteger(process.env.CC_WEB_CROSS_HOP_COUNT, 0),
    });

    const transport = url.protocol === 'https:' ? https : http;
    const req = transport.request(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Content-Length': Buffer.byteLength(body),
        'X-CC-Web-MCP-Token': token,
      },
      timeout: 60000,
    }, (res) => {
      let data = '';
      res.setEncoding('utf8');
      res.on('data', (chunk) => { data += chunk; });
      res.on('end', () => {
        try {
          const payload = JSON.parse(data || '{}');
          if (res.statusCode >= 200 && res.statusCode < 300) {
            resolve(payload);
          } else {
            resolve({
              ok: false,
              code: payload.code || 'ccweb_http_error',
              message: payload.message || `ccweb 内部接口返回 HTTP ${res.statusCode}`,
              statusCode: res.statusCode,
            });
          }
        } catch {
          resolve({
            ok: false,
            code: 'ccweb_bad_response',
            message: 'ccweb 内部接口返回了无法解析的响应。',
            statusCode: res.statusCode,
          });
        }
      });
    });

    req.on('timeout', () => {
      req.destroy();
      resolve({
        ok: false,
        code: 'ccweb_timeout',
        message: 'ccweb 内部接口调用超时。',
      });
    });
    req.on('error', (err) => {
      resolve({
        ok: false,
        code: 'ccweb_request_failed',
        message: err.message || 'ccweb 内部接口调用失败。',
      });
    });
    req.write(body);
    req.end();
  });
}

function toolResponse(payload) {
  const text = JSON.stringify(payload, null, 2);
  return {
    content: [{ type: 'text', text }],
    structuredContent: payload,
    isError: !payload?.ok,
  };
}

async function handleRequest(message) {
  const { id, method } = message;
  const hasId = Object.prototype.hasOwnProperty.call(message, 'id');

  if (!hasId) return;

  try {
    switch (method) {
      case 'initialize': {
        jsonRpcResult(id, {
          protocolVersion: message.params?.protocolVersion || '2024-11-05',
          capabilities: { tools: {} },
          serverInfo: SERVER_INFO,
        });
        break;
      }
      case 'ping':
        jsonRpcResult(id, {});
        break;
      case 'tools/list':
        jsonRpcResult(id, { tools: TOOLS });
        break;
      case 'tools/call': {
        const name = String(message.params?.name || '');
        const args = message.params?.arguments || {};
        if (!TOOLS.some((tool) => tool.name === name)) {
          jsonRpcResult(id, toolResponse({
            ok: false,
            code: 'unknown_tool',
            message: `未知工具: ${name}`,
          }));
          break;
        }
        const payload = await callCcweb(name, args);
        jsonRpcResult(id, toolResponse(payload));
        break;
      }
      case 'resources/list':
        jsonRpcResult(id, { resources: [] });
        break;
      case 'prompts/list':
        jsonRpcResult(id, { prompts: [] });
        break;
      default:
        jsonRpcError(id, -32601, `Method not found: ${method}`);
    }
  } catch (err) {
    jsonRpcError(id, -32603, err.message || 'Internal error');
  }
}

function runStdioServer() {
  let lineBuffer = '';

  process.stdin.setEncoding('utf8');
  process.stdin.on('data', (chunk) => {
    lineBuffer += chunk;
    let index;
    while ((index = lineBuffer.indexOf('\n')) >= 0) {
      const line = lineBuffer.slice(0, index).trim();
      lineBuffer = lineBuffer.slice(index + 1);
      if (!line) continue;
      let message;
      try {
        message = JSON.parse(line);
      } catch (err) {
        jsonRpcError(null, -32700, 'Parse error', err.message);
        continue;
      }
      handleRequest(message);
    }
  });

  process.stdin.on('end', () => {
    process.exit(0);
  });
}

module.exports = { TOOLS, runStdioServer };

if (require.main === module) {
  runStdioServer();
}
