import {
  All,
  Controller,
  HttpStatus,
  Logger,
  Req,
  Res,
} from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
import type { Request, Response } from 'express';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { z } from 'zod';
import { Public } from '@common/decorators/public.decorator';
import { SkipTransform } from '@common/decorators/skip-transform.decorator';
import { Auditable } from '@core/observability/audit/decorators/auditable.decorator';
import { InternalAppTokenService } from './services/token.service';
import { InternalAppMcpToolsService } from './services/mcp-tools.service';
import type { McpToolName } from './dto/mcp.dto';

const ONBOARD_URL = 'https://ffworkspace.faradayfuture.com/internal-apps';
const SERVER_NAME = 'ffoa-apps';
const SERVER_VERSION = '1.0.0';

/**
 * 远程 MCP server endpoint（标准 Streamable HTTP transport，JSON-RPC 2.0）
 *
 * 实现：用 @modelcontextprotocol/sdk 的 McpServer + StreamableHTTPServerTransport，
 * SDK 自动处理 initialize 握手 / protocolVersion 协商 / JSON-RPC 包装 / 错误码标准化。
 *
 * 工具：list_apps / deploy_prepare / logs / env / destroy（5 个，详见 mcp-tools.service.ts）。
 *
 * 历史：旧版自创了 {ok, data} shape，与 MCP spec 不兼容（Claude Code 握手失败），
 * 详见 .learnings/2026-05-18-mcp-controller-not-jsonrpc-compliant.md
 */
@ApiTags('Internal App Platform - MCP')
@SkipTransform()
@Controller('internal-apps/mcp')
export class InternalAppMcpController {
  private readonly logger = new Logger(InternalAppMcpController.name);

  constructor(
    private readonly tokenSvc: InternalAppTokenService,
    private readonly toolsSvc: InternalAppMcpToolsService,
  ) {}

  /**
   * MCP 入口：Streamable HTTP transport
   *
   * 用 `@All` 让 GET（SSE 长连接）/ POST（JSON-RPC 请求）/ DELETE（关闭会话）
   * 都路由到同一个 SDK transport，由 SDK 内部按 method 分发。
   */
  @Public()
  @All()
  @Auditable()
  @ApiOperation({ summary: 'MCP Streamable HTTP endpoint (JSON-RPC 2.0)' })
  async handle(@Req() req: Request, @Res() res: Response): Promise<void> {
    const authHeader = req.headers.authorization;
    if (!authHeader?.startsWith('Bearer ')) {
      this.sendJsonRpcAuthError(
        res,
        -32001,
        'invalid_token: 缺少或格式错误的 Authorization 头',
      );
      return;
    }

    let employeeSlug: string;
    let tokenWarning: string | undefined;
    try {
      const auth = await this.tokenSvc.verify(authHeader.slice(7));
      employeeSlug = auth.employeeSlug;
      tokenWarning = auth.warning;
    } catch (e) {
      const code = e instanceof Error ? e.message : 'invalid_token';
      this.sendJsonRpcAuthError(res, -32001, `${code}: ${this.tokenErrorMessage(code)}`);
      return;
    }

    // SDK 从 req.auth 读 authInfo 透传给 tool handler，把 employeeSlug 挂上去。
    (req as Request & { auth?: unknown }).auth = {
      token: authHeader.slice(7),
      clientId: employeeSlug,
      scopes: [],
      // SDK 不识别但会原样透传，handler 里强转读取
      extra: { employeeSlug, tokenWarning },
    };

    // Stateless：每次请求开新 server + transport。Open issue 后续如要做 SSE 长连接 +
    // session 状态，改为 stateful（带 sessionIdGenerator）即可。
    const server = this.buildMcpServer();
    const transport = new StreamableHTTPServerTransport({
      sessionIdGenerator: undefined,
    });

    res.on('close', () => {
      transport.close().catch(() => undefined);
      server.close().catch(() => undefined);
    });

    try {
      await server.connect(transport);
      await transport.handleRequest(req, res, req.body);
    } catch (err) {
      this.logger.error(
        `MCP transport error: ${err instanceof Error ? err.message : String(err)}`,
      );
      if (!res.headersSent) {
        this.sendJsonRpcAuthError(
          res,
          -32603,
          'internal_error: MCP transport 处理失败',
        );
      }
    }
  }

  /**
   * 每次请求构造新 McpServer（stateless）。
   * 5 个工具注册在这里——闭包持有 toolsSvc，handler 从 extra.authInfo 取 employeeSlug。
   */
  private buildMcpServer(): McpServer {
    const server = new McpServer(
      { name: SERVER_NAME, version: SERVER_VERSION },
      { capabilities: { tools: {} } },
    );

    server.registerTool(
      'list_apps',
      {
        description:
          '列出当前员工已部署的 app（默认不含已销毁；传 include_destroyed=true 拉 30 天恢复期内的销毁记录）',
        inputSchema: {
          include_destroyed: z
            .boolean()
            .optional()
            .describe('是否包含已销毁的 app（默认 false）'),
        },
      },
      async (args, extra) => {
        const slug = this.extractEmployeeSlug(extra);
        const result = await this.toolsSvc.callTool(slug, 'list_apps', {
          includeDestroyed: args.include_destroyed,
        });
        return this.toMcpResult(result, this.extractTokenWarning(extra));
      },
    );

    server.registerTool(
      'deploy_prepare',
      {
        description:
          '部署准备：建仓 + 颁发 5 分钟 TTL 推送凭据。Claude Code 拿到后用 git 命令把代码推到 Gitea，平台 webhook 接住后自动部署。',
        inputSchema: {
          app_slug: z
            .string()
            .min(3)
            .max(22)
            .describe('app slug，[a-z0-9-] 3-22 字符，首尾非短横，不能撞保留字'),
          display_name: z.string().optional().describe('app 展示名（可选）'),
          detected: z
            .object({
              has_package_json: z.boolean(),
              has_start_script: z.boolean(),
              has_index_html: z.boolean(),
              has_dockerfile: z.boolean().optional(),
              has_requirements_txt: z.boolean().optional(),
              has_go_mod: z.boolean().optional(),
              has_pom_xml: z.boolean().optional(),
            })
            .optional()
            .describe('Claude Code 探测到的项目特征，用于 runtime 判别'),
        },
      },
      async (args, extra) => {
        const slug = this.extractEmployeeSlug(extra);
        const result = await this.toolsSvc.callTool(slug, 'deploy_prepare', {
          appSlug: args.app_slug,
          displayName: args.display_name,
          detected: args.detected
            ? {
                hasPackageJson: args.detected.has_package_json,
                hasStartScript: args.detected.has_start_script,
                hasIndexHtml: args.detected.has_index_html,
                hasDockerfile: args.detected.has_dockerfile,
                hasRequirementsTxt: args.detected.has_requirements_txt,
                hasGoMod: args.detected.has_go_mod,
                hasPomXml: args.detected.has_pom_xml,
              }
            : undefined,
        });
        return this.toMcpResult(result, this.extractTokenWarning(extra));
      },
    );

    server.registerTool(
      'logs',
      {
        description: '取 app 容器近 N 行日志（默认 100，最大 1000）',
        inputSchema: {
          app_slug: z.string().describe('app slug'),
          lines: z.number().int().min(1).max(1000).optional()
            .describe('要拉的最近行数，默认 100'),
        },
      },
      async (args, extra) => {
        const slug = this.extractEmployeeSlug(extra);
        const result = await this.toolsSvc.callTool(slug, 'logs', {
          appSlug: args.app_slug,
          lines: args.lines,
        });
        return this.toMcpResult(result, this.extractTokenWarning(extra));
      },
    );

    server.registerTool(
      'env',
      {
        description: 'app 环境变量管理 (list/get/set/unset)。set/unset 触发滚动重启。',
        inputSchema: {
          app_slug: z.string().describe('app slug'),
          action: z.enum(['list', 'get', 'set', 'unset']),
          key: z.string().optional().describe('env key，set/get/unset 必填'),
          value: z.string().optional().describe('env value，set 必填'),
        },
      },
      async (args, extra) => {
        const slug = this.extractEmployeeSlug(extra);
        const result = await this.toolsSvc.callTool(slug, 'env', {
          appSlug: args.app_slug,
          action: args.action,
          key: args.key,
          value: args.value,
        });
        return this.toMcpResult(result, this.extractTokenWarning(extra));
      },
    );

    server.registerTool(
      'destroy',
      {
        description:
          '销毁 app：停容器 + 归档 Gitea 仓库 + 数据保留 30 天。**必须**传 confirm=true 显式确认。',
        inputSchema: {
          app_slug: z.string().describe('app slug'),
          confirm: z.literal(true).describe('必须显式传 true 确认销毁'),
        },
      },
      async (args, extra) => {
        const slug = this.extractEmployeeSlug(extra);
        const result = await this.toolsSvc.callTool(slug, 'destroy', {
          appSlug: args.app_slug,
          confirm: args.confirm,
        });
        return this.toMcpResult(result, this.extractTokenWarning(extra));
      },
    );

    return server;
  }

  /**
   * Tools service 仍返 {ok, data/error}（保留作为 OA 内部契约，便于直接 HTTP 测试），
   * 这里转 MCP content 格式：成功 → text content；失败 → isError:true + 文字描述。
   *
   * structuredContent 字段让支持 MCP 2025-06-18+ 的客户端能直接拿到结构化对象，
   * 同时 text content 兜底老客户端。
   *
   * tokenWarning：token 即将过期时由 tokenSvc.verify 返回，挂在 req.auth.extra.tokenWarning，
   * 每个 tool handler 从 extra 取出后透传给 toMcpResult。同时打到 content（让 Claude 主动告知
   * 用户）+ structuredContent（让支持 2025-06-18+ 的客户端能编程读取）+ 服务端 logger.warn
   * （运维侧也能看到 token 即将过期的员工）。
   */
  private toMcpResult(
    result: {
      ok: boolean;
      data?: unknown;
      error?: { code: string; message: string; details?: unknown; onboardUrl?: string };
    },
    extra?: { tokenWarning?: string },
  ) {
    const tokenWarning = extra?.tokenWarning;
    if (tokenWarning) {
      this.logger.warn(`tokenWarning surfaced to client: ${tokenWarning}`);
    }
    const warningTextBlock = tokenWarning
      ? [{ type: 'text' as const, text: `⚠️ tokenWarning: ${tokenWarning}` }]
      : [];

    if (result.ok && result.data !== undefined) {
      const dataObj = result.data as Record<string, unknown>;
      return {
        content: [
          ...warningTextBlock,
          { type: 'text' as const, text: JSON.stringify(dataObj) },
        ],
        structuredContent: tokenWarning ? { ...dataObj, tokenWarning } : dataObj,
      };
    }
    const err = result.error ?? { code: 'unknown_error', message: '未知错误' };
    return {
      content: [
        ...warningTextBlock,
        {
          type: 'text' as const,
          text: `error[${err.code}]: ${err.message}${err.onboardUrl ? `\n（去 ${err.onboardUrl} 处理）` : ''}`,
        },
      ],
      isError: true,
      structuredContent: tokenWarning ? { error: err, tokenWarning } : { error: err },
    };
  }

  /**
   * 从 SDK 传入的 extra 里取 tokenWarning（可选）。路径与 extractEmployeeSlug 对称：
   * controller 入口把它挂在 req.auth.extra.tokenWarning，SDK 原样透传 → handler 收到的
   * extra 形如 { authInfo: { extra: { tokenWarning?: string } } }。
   */
  private extractTokenWarning(extra: unknown): { tokenWarning?: string } {
    const authInfo = (extra as { authInfo?: { extra?: { tokenWarning?: string } } })
      .authInfo;
    const w = authInfo?.extra?.tokenWarning;
    return w ? { tokenWarning: w } : {};
  }

  /**
   * 从 SDK 传入的 extra 里取 employeeSlug。
   * 我们在 controller 入口把它挂在 `req.auth.extra.employeeSlug`，SDK 原样透传到 handler。
   *
   * 入口已经在 verify token 后必定 set extra.employeeSlug，所以理论上永远能取到；
   * 拿不到 → SDK 调用路径出 bug（直接转 JSON-RPC -32603 internal_error，比静默落空串
   * 让 handler 用 employeeSlug='' 查无 app 友好——后者会让 Claude 收到 app_not_found
   * 误以为"app 没建过"，实际是 controller 鉴权链路坏了，根因离症状远）。
   */
  private extractEmployeeSlug(extra: unknown): string {
    const authInfo = (extra as { authInfo?: { clientId?: string; extra?: { employeeSlug?: string } } }).authInfo;
    const slug = authInfo?.extra?.employeeSlug ?? authInfo?.clientId;
    if (!slug) {
      throw new Error('internal_error: employeeSlug missing from SDK extra (auth pipeline bug)');
    }
    return slug;
  }

  /**
   * 鉴权失败时直接吐 JSON-RPC 2.0 error（绕过 SDK，因为还没进 SDK）。
   * 用 HTTP 200 + 含 jsonrpc error 是 spec-compliant（JSON-RPC 不强制非 200）。
   */
  private sendJsonRpcAuthError(res: Response, code: number, message: string): void {
    res.status(HttpStatus.UNAUTHORIZED).json({
      jsonrpc: '2.0',
      id: null,
      error: {
        code,
        message,
        data: { onboardUrl: ONBOARD_URL },
      },
    });
  }

  private tokenErrorMessage(code: string): string {
    switch (code) {
      case 'invalid_token':
        return 'token 无效，请去 FF AI Workspace 重新生成';
      case 'expired_token':
        return 'token 已过期，请去 FF AI Workspace 续期';
      case 'revoked_token':
        return 'token 已被撤销';
      case 'disabled_token':
        return 'token 已禁用（账户状态变更）';
      default:
        return 'token 校验失败';
    }
  }

  /** 让其他模块需要时也能直接调（list_apps 的实现还是去 toolsSvc 里走）。 */
  toolsAvailable(): readonly McpToolName[] {
    return ['list_apps', 'deploy_prepare', 'logs', 'env', 'destroy'] as const;
  }
}
