/**
 * 前后端契约自动校验脚本（通用版）
 *
 * 功能：
 * 1. 从前端 API 文件中提取所有 apiClient.{method}(url, params) 调用
 * 2. 从后端 controllers/ 中提取所有路由定义 + DTO 类名
 * 3. 从后端 dto/ 中提取 DTO 字段名列表
 * 4. 对比前端请求参数字段 vs 后端 DTO 字段
 * 5. 对比前端 interface 的响应字段 vs 后端 Controller 中 .map() 手动映射的字段
 *
 * 运行方式:
 *   npx ts-node testing/scripts/contract-check.ts --module performance
 *   npx ts-node testing/scripts/contract-check.ts --module asset-management
 *   npx ts-node testing/scripts/contract-check.ts                          # 默认 performance
 */

/* eslint-disable @typescript-eslint/no-var-requires */
const fs = require('fs') as typeof import('fs');
const path = require('path') as typeof import('path');

const { parseModuleArg, resolveModulePaths, loadModuleConfig } = require('./module-config/resolve-module');

// ==================== 配置（从 --module 参数自动推导） ====================

const MODULE_NAME = parseModuleArg('performance');
const MODULE = resolveModulePaths(MODULE_NAME);

const ROOT = MODULE.root;
const FRONTEND_API_FILE = MODULE.frontendApiFile;
const BACKEND_CONTROLLERS_DIR = MODULE.backendControllersDir;
const BACKEND_DTO_DIR = MODULE.backendDtoDir;

// ==================== 类型定义 ====================

interface FrontendCall {
  method: string;       // get | post | put | patch | delete
  path: string;         // 如 /performance/cycles
  paramFields: string[];// 请求参数字段名
  paramSource: string;  // 参数来源（interface 名或内联对象描述）
  functionName: string; // 前端函数名
  line: number;         // 行号
}

interface BackendRoute {
  method: string;       // GET | POST | PUT | PATCH | DELETE
  path: string;         // 完整路由路径
  dtoClass: string;     // 使用的 DTO 类名（如果有）
  dtoSource: 'body' | 'query' | 'param' | 'none';
  controllerFile: string;
  mappedFields: string[]; // Controller 中手动 .map() 映射的字段
  line: number;
}

interface DtoInfo {
  className: string;
  fields: string[];
  file: string;
}

interface FrontendInterface {
  name: string;
  fields: string[];
  line: number;
}

interface ContractCheckResult {
  status: 'match' | 'warning' | 'mismatch' | 'info';
  endpoint: string;
  frontendInfo: string;
  backendInfo: string;
  detail: string;
}

// ==================== 解析函数 ====================

/**
 * 从前端文件提取所有 apiClient 调用
 */
function extractFrontendCalls(content: string): FrontendCall[] {
  const calls: FrontendCall[] = [];
  const lines = content.split('\n');

  // 提取所有 interface/type 定义及其字段
  const interfaces = extractFrontendInterfaces(content);
  const interfaceMap = new Map<string, string[]>();
  for (const iface of interfaces) {
    interfaceMap.set(iface.name, iface.fields);
  }

  // 匹配 apiClient.{method}(`${BASE_URL}/path`, ...) 或 apiClient.{method}('/performance/path', ...)
  // 同时找出所在函数及其参数类型
  let currentFunction = '';
  let currentParamType = '';
  let currentFuncLine = 0;

  for (let i = 0; i < lines.length; i++) {
    const line = lines[i];

    // 追踪当前函数定义
    const funcMatch = line.match(
      /export\s+async\s+function\s+(\w+)\s*\(([^)]*)\)/
    );
    if (funcMatch) {
      currentFunction = funcMatch[1];
      currentFuncLine = i + 1;
      // 提取参数类型
      const paramsPart = funcMatch[2];
      // 例: params: CreateCycleParams 或 id: string, params: UpdateCycleParams
      const typeMatch = paramsPart.match(/params\s*:\s*([\w<>]+(?:\s*\|\s*[\w<>]+)*)/);
      if (typeMatch) {
        currentParamType = typeMatch[1].replace(/\s/g, '');
      } else {
        currentParamType = '';
      }
    }

    // 匹配 apiClient 调用
    const apiCallMatch = line.match(
      /apiClient\.(get|post|put|patch|delete)\s*(?:<[^>]*>)?\s*\(\s*([^,)]+)/
    );
    if (!apiCallMatch) continue;

    const method = apiCallMatch[1];
    let urlExpr = apiCallMatch[2].trim();

    // 解析 URL
    let apiPath = resolveUrl(urlExpr);
    if (!apiPath) continue;

    // 提取请求参数字段
    let paramFields: string[] = [];
    let paramSource = '';

    if (method === 'get') {
      // GET: 参数在 { params } 或 { params: { ... } } 中
      // 从函数参数类型中找字段
      if (currentParamType) {
        const typeName = currentParamType.replace(/Partial<(.+)>/, '$1');
        const fields = interfaceMap.get(typeName);
        if (fields) {
          paramFields = fields;
          paramSource = typeName;
        }
      }
      // 尝试从同行/下行提取内联 params
      const paramsInline = extractInlineParams(lines, i);
      if (paramsInline.length > 0 && paramFields.length === 0) {
        paramFields = paramsInline;
        paramSource = 'inline params';
      }
    } else {
      // POST/PUT/PATCH/DELETE: 请求体是第二个参数
      // 获取后续行来组装完整调用
      const callContext = lines.slice(i, Math.min(i + 5, lines.length)).join(' ');

      // 先从函数参数类型获取字段
      if (currentParamType) {
        const typeName = currentParamType
          .replace(/Partial<(.+)>/, '$1')
          .replace(/<.+>/, '');
        const fields = interfaceMap.get(typeName);
        if (fields) {
          paramFields = fields;
          paramSource = typeName;
        }
      }

      // 检查是否内联传了对象 { key1, key2 } 或 { key1: val, key2: val }
      if (paramFields.length === 0) {
        const inlineBody = extractInlineBody(lines, i);
        if (inlineBody.length > 0) {
          paramFields = inlineBody;
          paramSource = 'inline body';
        }
      }
    }

    calls.push({
      method,
      path: apiPath,
      paramFields,
      paramSource,
      functionName: currentFunction,
      line: i + 1,
    });
  }

  return calls;
}

/**
 * 解析 URL 表达式
 */
function resolveUrl(urlExpr: string): string | null {
  // 去掉外部引号或模板字符串
  let url = urlExpr.replace(/^[`'"]+|[`'"]+$/g, '');

  // 替换 ${BASE_URL} 或 ${BASE} 等常见变量
  url = url.replace(/\$\{BASE_URL\}/g, MODULE.apiPrefix);
  url = url.replace(/\$\{BASE\}/g, MODULE.apiPrefix);

  // 替换 ${id} 等变量为 :param 格式
  url = url.replace(/\$\{(\w+)\}/g, ':$1');

  // 去掉 /api/v1 前缀（如果有）
  url = url.replace(/^\/api\/v1/, '');

  // 确保以模块前缀开头
  if (!url.startsWith(MODULE.apiPrefix)) return null;

  return url;
}

/**
 * 从 { params: { key1, key2 } } 中提取 GET 参数
 */
function extractInlineParams(lines: string[], lineIdx: number): string[] {
  const context = lines.slice(lineIdx, Math.min(lineIdx + 3, lines.length)).join(' ');
  const paramsMatch = context.match(/params\s*:\s*\{([^}]+)\}/);
  if (!paramsMatch) return [];
  return extractObjectKeys(paramsMatch[1]);
}

/**
 * 从 apiClient.post(url, body) 中提取 body 中的字段
 */
function extractInlineBody(lines: string[], lineIdx: number): string[] {
  const context = lines.slice(lineIdx, Math.min(lineIdx + 5, lines.length)).join(' ');
  // 匹配第二个参数位置的对象字面量
  const bodyMatch = context.match(/apiClient\.\w+\s*(?:<[^>]*>)?\s*\([^,]+,\s*\{([^}]*)\}/);
  if (!bodyMatch) return [];
  return extractObjectKeys(bodyMatch[1]);
}

/**
 * 从对象字面量字符串中提取 key
 */
function extractObjectKeys(objStr: string): string[] {
  const keys: string[] = [];
  // 匹配 key: value 或 简写 key
  const keyMatches = objStr.matchAll(/(\w+)\s*(?:[:,]|$)/g);
  for (const m of keyMatches) {
    const key = m[1];
    // 过滤掉常见的非字段名
    if (!['params', 'items', 'const', 'let', 'var', 'return', 'true', 'false', 'undefined', 'null'].includes(key)) {
      keys.push(key);
    }
  }
  return [...new Set(keys)];
}

/**
 * 从前端文件提取 interface 定义
 */
function extractFrontendInterfaces(content: string): FrontendInterface[] {
  const interfaces: FrontendInterface[] = [];
  const lines = content.split('\n');

  let i = 0;
  while (i < lines.length) {
    const ifaceMatch = lines[i].match(/export\s+interface\s+(\w+)/);
    if (ifaceMatch) {
      const name = ifaceMatch[1];
      const fields: string[] = [];
      const startLine = i + 1;

      let braceCount = 0;
      let started = false;
      for (let j = i; j < lines.length; j++) {
        const line = lines[j];

        if (started && braceCount === 1) {
          const fieldMatch = line.match(/^\s*(?:readonly\s+)?(\w+)\??\s*:/);
          if (fieldMatch) {
            fields.push(fieldMatch[1]);
          }
        }

        for (const ch of line) {
          if (ch === '{') {
            braceCount++;
            started = true;
          }
          if (ch === '}') {
            braceCount--;
          }
        }

        if (started && braceCount === 0) {
          i = j;
          break;
        }
      }

      interfaces.push({ name, fields, line: startLine });
    }
    i++;
  }

  return interfaces;
}

/**
 * 从后端 Controller 文件中提取路由定义
 */
function extractBackendRoutes(controllerDir: string): BackendRoute[] {
  const routes: BackendRoute[] = [];
  const files = fs.readdirSync(controllerDir).filter(f => f.endsWith('.ts') && f !== 'index.ts');

  for (const file of files) {
    const filePath = path.join(controllerDir, file);
    const content = fs.readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');

    // 提取 Controller 前缀
    const prefixMatch = content.match(/@Controller\(['"]([^'"]+)['"]\)/);
    const prefix = prefixMatch ? `/${prefixMatch[1]}` : '';

    // 追踪导入的 DTO 类
    const importedDtos = extractImportedDtos(content);

    let i = 0;
    while (i < lines.length) {
      // 匹配 @Get/@Post/@Put/@Patch/@Delete 装饰器
      const routeMatch = lines[i].match(/@(Get|Post|Put|Patch|Delete)\s*\(\s*['"]?([^'")\s]*)['"]?\s*\)/);
      if (routeMatch) {
        const httpMethod = routeMatch[1].toUpperCase();
        const routePath = routeMatch[2] || '';
        const fullPath = `${prefix}${routePath ? '/' + routePath : ''}`;

        // 在方法签名中查找 @Body() dto: XxxDto 或 @Query() query: XxxDto
        let dtoClass = '';
        let dtoSource: 'body' | 'query' | 'param' | 'none' = 'none';
        const mappedFields: string[] = [];

        // 扫描接下来的几行查找方法签名和 DTO
        for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) {
          const bodyMatch = lines[j].match(/@Body\(\)\s+\w+\??\s*:\s*(\w+)/);
          if (bodyMatch) {
            dtoClass = bodyMatch[1];
            dtoSource = 'body';
          }
          const queryMatch = lines[j].match(/@Query\(\)\s+\w+\??\s*:\s*(\w+)/);
          if (queryMatch) {
            dtoClass = queryMatch[1];
            dtoSource = 'query';
          }

          // 检测方法体的结尾（粗略：遇到下一个装饰器或方法结尾）
          if (j > i + 2 && (lines[j].match(/^\s+@(Get|Post|Put|Patch|Delete)/) || lines[j].match(/^\s*\}$/))) {
            break;
          }

          // 提取 .map() 中的字段映射
          const mapMatch = lines[j].match(/\.\s*map\s*\(\s*\(\s*(\w+)\s*(:\s*\w+)?\s*\)\s*=>\s*\({/);
          if (mapMatch) {
            // 读取 map 返回对象的字段
            for (let k = j; k < Math.min(j + 30, lines.length); k++) {
              const fieldMatch = lines[k].match(/^\s+(\w+)\s*:/);
              if (fieldMatch && !['id', 'return', 'items', 'total', 'const', 'let'].includes(fieldMatch[1])) {
                mappedFields.push(fieldMatch[1]);
              }
              if (lines[k].includes('}))')) break;
            }
          }

          // 提取 return { field1, field2 } 中手动构造的返回字段
          const returnObjMatch = lines[j].match(/return\s*\{/);
          if (returnObjMatch) {
            for (let k = j; k < Math.min(j + 15, lines.length); k++) {
              const rfMatch = lines[k].match(/^\s+(\w+)\s*[,:]/);
              if (rfMatch) {
                mappedFields.push(rfMatch[1]);
              }
              if (lines[k].includes('};')) break;
            }
          }
        }

        routes.push({
          method: httpMethod,
          path: fullPath,
          dtoClass,
          dtoSource,
          controllerFile: file,
          mappedFields: [...new Set(mappedFields)],
          line: i + 1,
        });
      }
      i++;
    }
  }

  return routes;
}

/**
 * 提取 Controller 文件中导入的 DTO
 */
function extractImportedDtos(content: string): string[] {
  const dtos: string[] = [];
  const importMatches = content.matchAll(/import\s*\{([^}]+)\}\s*from\s*['"]\.\.\/dto/g);
  for (const m of importMatches) {
    const names = m[1].split(',').map(n => n.trim()).filter(Boolean);
    dtos.push(...names);
  }
  return dtos;
}

/**
 * 从 DTO 文件中提取所有 DTO 类的字段
 */
function extractDtoFields(dtoDir: string): Map<string, DtoInfo> {
  const dtoMap = new Map<string, DtoInfo>();
  const files = fs.readdirSync(dtoDir).filter(f => f.endsWith('.ts') && f !== 'index.ts');

  for (const file of files) {
    const filePath = path.join(dtoDir, file);
    const content = fs.readFileSync(filePath, 'utf-8');
    const lines = content.split('\n');

    let i = 0;
    while (i < lines.length) {
      const classMatch = lines[i].match(/export\s+class\s+(\w+)/);
      if (classMatch) {
        const className = classMatch[1];
        const fields: string[] = [];

        // 读取 class 内容
        let braceCount = 0;
        let started = false;
        for (let j = i; j < lines.length; j++) {
          for (const ch of lines[j]) {
            if (ch === '{') { braceCount++; started = true; }
            if (ch === '}') braceCount--;
          }

          if (started && braceCount <= 0) {
            // 提取字段名
            const body = lines.slice(i, j + 1).join('\n');
            // 匹配 fieldName: type 或 fieldName?: type（跳过装饰器行）
            const fieldMatches = body.matchAll(/^\s+(?:@\w+\([^)]*\)\s*\n\s*)*(\w+)\??\s*[:=]/gm);
            for (const fm of fieldMatches) {
              const fieldName = fm[1];
              // 过滤掉装饰器名和关键字
              if (fieldName.match(/^(Is|Min|Max|Type|ValidateNested|Each|Transform)/) ||
                  ['constructor', 'readonly'].includes(fieldName)) {
                continue;
              }
              fields.push(fieldName);
            }
            i = j;
            break;
          }
        }

        // 更简单的字段提取：逐行扫描
        const simpleFields: string[] = [];
        let inClass = false;
        let depth = 0;
        for (let j = i - (lines.slice(0, i + 1).length - 1); j <= i; j++) {
          // 重新定位类开始位置
        }
        // 使用另一种方法：正则扫描类体中的字段声明
        const classBody = extractClassBody(content, className);
        if (classBody) {
          const lineArr = classBody.split('\n');
          for (const l of lineArr) {
            // 匹配 "  fieldName: type" 或 "  fieldName?: type" 或 "  fieldName = value"
            const m = l.match(/^\s{2,}(\w+)\??\s*[:=]\s*/);
            if (m) {
              const name = m[1];
              if (!name.match(/^(Is|Min|Max|Type|ValidateNested|Each|Transform|constructor)/) &&
                  !name.match(/^[A-Z]/) || name === name) {
                // 进一步过滤：字段名通常以小写开头
                if (name[0] === name[0].toLowerCase() && name !== 'constructor') {
                  simpleFields.push(name);
                }
              }
            }
          }
        }

        const finalFields = simpleFields.length > 0 ? [...new Set(simpleFields)] : [...new Set(fields)];
        dtoMap.set(className, { className, fields: finalFields, file });
      }
      i++;
    }
  }

  return dtoMap;
}

/**
 * 提取类的主体文本
 */
function extractClassBody(content: string, className: string): string | null {
  const classPattern = new RegExp(`export\\s+class\\s+${className}[^{]*\\{`);
  const match = content.match(classPattern);
  if (!match || match.index === undefined) return null;

  let braceCount = 0;
  let start = match.index + match[0].length - 1;
  for (let i = start; i < content.length; i++) {
    if (content[i] === '{') braceCount++;
    if (content[i] === '}') braceCount--;
    if (braceCount === 0) {
      return content.substring(start, i + 1);
    }
  }
  return null;
}

// ==================== 路径匹配 ====================

/**
 * 规范化路径以便比较
 * 将 :param 统一化
 */
function normalizePath(p: string): string {
  // 将 :xxxId 或 :id 统一为 :id 用于匹配
  return p.replace(/:\w+/g, ':param')
          .replace(/\/+/g, '/')
          .replace(/\/$/, '');
}

/**
 * 查找匹配的后端路由
 */
function findMatchingRoute(
  frontendCall: FrontendCall,
  backendRoutes: BackendRoute[]
): BackendRoute | null {
  const methodMap: Record<string, string> = {
    get: 'GET', post: 'POST', put: 'PUT', patch: 'PATCH', delete: 'DELETE',
  };
  const targetMethod = methodMap[frontendCall.method] || frontendCall.method.toUpperCase();
  const normalizedFePath = normalizePath(frontendCall.path);

  for (const route of backendRoutes) {
    const normalizedBePath = normalizePath(route.path);
    if (route.method === targetMethod && normalizedFePath === normalizedBePath) {
      return route;
    }
  }

  // 宽松匹配：去掉模块前缀试试
  const prefixRegex = new RegExp(`^${MODULE.apiPrefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`);
  const fePathWithoutPrefix = normalizedFePath.replace(prefixRegex, '');
  for (const route of backendRoutes) {
    const bePathWithoutPrefix = normalizePath(route.path).replace(prefixRegex, '');
    if (route.method === targetMethod && fePathWithoutPrefix === bePathWithoutPrefix) {
      return route;
    }
  }

  return null;
}

// ==================== 字段比较 ====================

interface FieldCompareResult {
  status: 'match' | 'warning' | 'mismatch';
  feOnly: string[];    // 前端有、后端没有
  beOnly: string[];    // 后端有、前端没有
  common: string[];    // 共有
  note: string;
}

function compareFields(feFields: string[], beFields: string[]): FieldCompareResult {
  const feSet = new Set(feFields);
  const beSet = new Set(beFields);

  const common = feFields.filter(f => beSet.has(f));
  const feOnly = feFields.filter(f => !beSet.has(f));
  const beOnly = beFields.filter(f => !feSet.has(f));

  if (feOnly.length === 0 && beOnly.length === 0) {
    return { status: 'match', feOnly, beOnly, common, note: '字段完全一致' };
  }

  // 如果后端 DTO 有额外的 @IsOptional 字段，这是正常的（向后兼容）
  if (feOnly.length === 0 && beOnly.length > 0) {
    return {
      status: 'match',
      feOnly, beOnly, common,
      note: `后端 DTO 有额外可选字段 [${beOnly.join(', ')}]（前端未使用）`,
    };
  }

  if (feOnly.length > 0 && beOnly.length === 0) {
    return {
      status: 'warning',
      feOnly, beOnly, common,
      note: `前端发送了后端 DTO 中不存在的字段 [${feOnly.join(', ')}]（将被忽略或报错）`,
    };
  }

  return {
    status: 'mismatch',
    feOnly, beOnly, common,
    note: `前端独有 [${feOnly.join(', ')}]，后端独有 [${beOnly.join(', ')}]`,
  };
}

// ==================== 主函数 ====================

function main() {
  console.log('=== 前后端契约校验报告 ===');
  console.log(`检查时间: ${new Date().toISOString().slice(0, 10)}`);
  console.log(`前端文件: ${path.relative(ROOT, FRONTEND_API_FILE)}`);
  console.log(`后端 Controllers: ${path.relative(ROOT, BACKEND_CONTROLLERS_DIR)}/`);
  console.log(`后端 DTOs: ${path.relative(ROOT, BACKEND_DTO_DIR)}/`);
  console.log('');

  // 1. 读取前端文件
  const feContent = fs.readFileSync(FRONTEND_API_FILE, 'utf-8');

  // 2. 提取前端 API 调用
  const frontendCalls = extractFrontendCalls(feContent);
  console.log(`[信息] 提取到 ${frontendCalls.length} 个前端 API 调用`);

  // 3. 提取后端路由
  const backendRoutes = extractBackendRoutes(BACKEND_CONTROLLERS_DIR);
  console.log(`[信息] 提取到 ${backendRoutes.length} 个后端路由定义`);

  // 4. 提取 DTO 字段
  const dtoMap = extractDtoFields(BACKEND_DTO_DIR);
  console.log(`[信息] 提取到 ${dtoMap.size} 个 DTO 类定义`);

  // 5. 提取前端 interfaces
  const feInterfaces = extractFrontendInterfaces(feContent);
  const feInterfaceMap = new Map<string, string[]>();
  for (const iface of feInterfaces) {
    feInterfaceMap.set(iface.name, iface.fields);
  }
  console.log(`[信息] 提取到 ${feInterfaces.length} 个前端 interface 定义`);
  console.log('');

  // ==================== 对比结果 ====================

  const results: ContractCheckResult[] = [];
  let matchCount = 0;
  let warningCount = 0;
  let mismatchCount = 0;
  let noRouteCount = 0;

  // 去重：同一个 path+method 只检查一次（多个前端函数可能调用同一端点）
  const checkedEndpoints = new Set<string>();

  for (const call of frontendCalls) {
    const endpointKey = `${call.method.toUpperCase()} ${call.path}`;

    if (checkedEndpoints.has(endpointKey)) continue;
    checkedEndpoints.add(endpointKey);

    // 查找匹配的后端路由
    const route = findMatchingRoute(call, backendRoutes);

    if (!route) {
      // GET 请求或无参数的端点不报未找到
      if (call.method !== 'get' || call.paramFields.length > 0) {
        noRouteCount++;
        results.push({
          status: 'info',
          endpoint: endpointKey,
          frontendInfo: `函数: ${call.functionName}()  行: ${call.line}`,
          backendInfo: '未找到匹配的后端路由',
          detail: '可能是报表/分析类 API 或路由前缀不匹配',
        });
      }
      continue;
    }

    // 只对有 body/query DTO 的路由做字段比较
    if (route.dtoClass === '' || route.dtoSource === 'none') {
      // 无 DTO 但前端有参数 -> 检查是否有问题
      if (call.paramFields.length > 0 && call.method !== 'get') {
        results.push({
          status: 'info',
          endpoint: endpointKey,
          frontendInfo: `函数: ${call.functionName}()  发送字段: [${call.paramFields.join(', ')}]`,
          backendInfo: `Controller: ${route.controllerFile}:${route.line}  无 DTO（参数通过 @Param/@Query 单独注入）`,
          detail: '后端未使用 DTO class，参数通过装饰器注入',
        });
      }
      continue;
    }

    // 获取 DTO 字段
    const dto = dtoMap.get(route.dtoClass);
    if (!dto) {
      results.push({
        status: 'info',
        endpoint: endpointKey,
        frontendInfo: `函数: ${call.functionName}()`,
        backendInfo: `DTO: ${route.dtoClass} (未能解析)`,
        detail: `未能从 DTO 文件中解析 ${route.dtoClass} 的字段`,
      });
      continue;
    }

    // 前端无参数的调用跳过
    if (call.paramFields.length === 0) {
      continue;
    }

    // 比较字段
    const comparison = compareFields(call.paramFields, dto.fields);

    if (comparison.status === 'match') {
      matchCount++;
      results.push({
        status: 'match',
        endpoint: endpointKey,
        frontendInfo: `${call.paramSource || call.functionName}`,
        backendInfo: `${route.dtoClass}`,
        detail: `共 ${comparison.common.length} 个字段一致`,
      });
    } else if (comparison.status === 'warning') {
      warningCount++;
      results.push({
        status: 'warning',
        endpoint: endpointKey,
        frontendInfo: `${call.paramSource || call.functionName}  字段: [${call.paramFields.join(', ')}]`,
        backendInfo: `${route.dtoClass}  字段: [${dto.fields.join(', ')}]`,
        detail: comparison.note,
      });
    } else {
      mismatchCount++;
      results.push({
        status: 'mismatch',
        endpoint: endpointKey,
        frontendInfo: `${call.paramSource || call.functionName}  字段: [${call.paramFields.join(', ')}]`,
        backendInfo: `${route.dtoClass}  字段: [${dto.fields.join(', ')}]`,
        detail: comparison.note,
      });
    }
  }

  // ==================== 额外检查：前端 interface vs 后端 Controller 手动映射 ====================

  console.log('--- 第二部分：响应字段映射检查 ---\n');

  // 已知的前端 interface <-> 后端返回映射关系
  const responseMapping: Array<{
    feInterface: string;
    controllerPattern: string;
    description: string;
  }> = [
    { feInterface: 'CycleStatistics', controllerPattern: 'cycle.controller.ts', description: 'GET /cycles/:id/statistics' },
    { feInterface: 'GoalSettingProgress', controllerPattern: 'cycle.controller.ts', description: 'GET /cycles/:id/goal-setting-progress' },
    { feInterface: 'EvaluationProgress', controllerPattern: 'cycle.controller.ts', description: 'GET /cycles/:id/evaluation-progress' },
    { feInterface: 'CalibrationProgress', controllerPattern: 'cycle.controller.ts', description: 'GET /cycles/:id/calibration-progress' },
    { feInterface: 'CycleResultsSummary', controllerPattern: 'cycle.controller.ts', description: 'GET /cycles/:id/cycle-results-summary' },
  ];

  // 检查有 .map() 映射的 Controller 端点
  for (const route of backendRoutes) {
    if (route.mappedFields.length > 0) {
      results.push({
        status: 'info',
        endpoint: `${route.method} ${route.path}`,
        frontendInfo: '请手动检查前端 interface',
        backendInfo: `${route.controllerFile}:${route.line}  映射字段: [${route.mappedFields.join(', ')}]`,
        detail: 'Controller 有手动 .map() 映射，返回字段可能与 DB 模型不同',
      });
    }
  }

  // ==================== 额外检查：已知易混淆字段对（从模块配置加载） ====================

  console.log('--- 第三部分：已知字段名差异检查 ---\n');

  const contractConfig = loadModuleConfig(MODULE_NAME, 'contract');
  const knownFieldDiffs: Array<{
    endpoint: string;
    feFields: string[];
    beFields: string[];
    status: 'match' | 'warning' | 'mismatch' | 'info';
    note: string;
  }> = contractConfig?.knownFieldDiffs || [];

  for (const diff of knownFieldDiffs) {
    results.push({
      status: diff.status,
      endpoint: diff.endpoint,
      frontendInfo: `前端字段: [${diff.feFields.join(', ')}]`,
      backendInfo: `后端字段: [${diff.beFields.join(', ')}]`,
      detail: diff.note,
    });
  }

  // ==================== 输出报告 ====================

  console.log('============================================================');
  console.log('                    校验结果详情');
  console.log('============================================================\n');

  const icons: Record<string, string> = {
    match: '\u2705 匹配',
    warning: '\u26A0\uFE0F  字段差异',
    mismatch: '\u274C 不匹配',
    info: '\u2139\uFE0F  信息',
  };

  // 按状态排序：mismatch > warning > info > match
  const order: Record<string, number> = { mismatch: 0, warning: 1, info: 2, match: 3 };
  results.sort((a, b) => (order[a.status] ?? 9) - (order[b.status] ?? 9));

  for (const r of results) {
    console.log(`[${icons[r.status]}] ${r.endpoint}`);
    console.log(`  前端: ${r.frontendInfo}`);
    console.log(`  后端: ${r.backendInfo}`);
    if (r.detail) {
      console.log(`  说明: ${r.detail}`);
    }
    console.log('');
  }

  // ==================== 汇总 ====================

  console.log('============================================================');
  console.log('                       汇总');
  console.log('============================================================');
  console.log(`  总检查端点数: ${results.length}`);
  console.log(`  \u2705 完全匹配:   ${results.filter(r => r.status === 'match').length}`);
  console.log(`  \u26A0\uFE0F  字段差异:   ${results.filter(r => r.status === 'warning').length}`);
  console.log(`  \u274C 不匹配:     ${results.filter(r => r.status === 'mismatch').length}`);
  console.log(`  \u2139\uFE0F  信息:       ${results.filter(r => r.status === 'info').length}`);
  console.log('');

  // 列出前端 interface 与后端 DTO 的已知对应关系摘要（从模块配置加载）
  console.log('--- 前后端类型映射表 ---');
  console.log('');
  const typePairs: Array<[string, string]> = contractConfig?.typePairs || [];

  for (const [feType, beType] of typePairs) {
    const feFields = feInterfaceMap.get(feType) || [];
    const dto = dtoMap.get(beType);
    const beFields = dto?.fields || [];

    if (feFields.length === 0 && beFields.length === 0) continue;

    const comparison = compareFields(feFields, beFields);
    const icon = comparison.status === 'match' ? '\u2705'
      : comparison.status === 'warning' ? '\u26A0\uFE0F'
      : '\u274C';

    console.log(`  ${icon} ${feType} <-> ${beType}`);
    if (comparison.feOnly.length > 0) {
      console.log(`     前端独有: [${comparison.feOnly.join(', ')}]`);
    }
    if (comparison.beOnly.length > 0) {
      console.log(`     后端独有: [${comparison.beOnly.join(', ')}]`);
    }
    if (comparison.status === 'match') {
      console.log(`     字段完全一致 (${comparison.common.length} 个)`);
    }
  }

  console.log('');
  console.log('============================================================');
  console.log('  注：本脚本基于正则解析，覆盖约 80% 的场景');
  console.log('  ⚠️  "字段差异" 也视为未通过（需人工确认后加入白名单）');
  console.log('  ❌ "不匹配" 必须修复');
  console.log('============================================================');

  // 返回退出码：mismatch 或未确认的 warning 都算失败
  const finalMismatchCount = results.filter(r => r.status === 'mismatch').length;
  const finalWarningCount = results.filter(r => r.status === 'warning').length;
  if (finalMismatchCount > 0) {
    console.log(`\n❌ 退出码 1: ${finalMismatchCount} 个不匹配`);
    process.exit(1);
  } else if (finalWarningCount > 0) {
    console.log(`\n⚠️  退出码 2: ${finalWarningCount} 个字段差异待确认（确认后可加入白名单）`);
    process.exit(2);
  } else {
    console.log('\n✅ 退出码 0: 全部通过');
    process.exit(0);
  }
}

main();
