import { debug } from "../core/logger.js";
import { safeMatchGroups } from "../core/helpers.js";
/**
 * API 契约文件路径关键词。
 * 文件路径（小写）包含这些关键词的，被视为 API 契约相关文件，需要进行 diff 解析。
 */
const API_PATH_KEYWORDS = [
    "controller",
    "route",
    "api",
    "dto",
    "request",
    "response",
    "model",
    "entity",
    "schema",
];
/**
 * 检测变更文件中的 API 契约变更。
 *
 * 处理流程：
 * 1. 筛选出与 API 契约相关的文件
 * 2. 获取每个文件的 Git diff
 * 3. 解析 diff 中的端点变更和字段变更
 * 4. 后处理：将同一路径的删除+新增配对为方法变更
 * 5. 评估跨服务影响并生成摘要
 *
 * @param input - 契约检查输入，包含变更文件列表、项目路径、配置和 Git 操作接口
 * @returns 契约检查结果，包含变更列表、破坏性/兼容性变更计数和跨服务影响
 */
export async function checkContractChanges(input) {
    const { changedFiles, projectPath, config, gitOps } = input;
    debug("契约守卫", "开始检测契约变更");
    const changes = [];
    // 筛选出 API 契约相关文件（controller、dto、route 等）
    const contractFiles = changedFiles.filter((f) => isApiContractFile(f));
    if (contractFiles.length === 0) {
        debug("契约守卫", "无 API 契约文件变更，跳过检测");
        return emptyResult("No API contract files changed.");
    }
    // 收集每个契约文件的 diff 内容（仅暂存区的变更）
    const diffs = [];
    for (const file of contractFiles) {
        try {
            const diff = await gitOps.getDiffPatch(projectPath, file, {
                cached: true,
            });
            if (diff.trim()) {
                diffs.push({ file, diff });
            }
        }
        catch (e) {
            debug("契约守卫", "获取文件diff失败 —", e);
            // 跳过无法获取 diff 的文件
        }
    }
    if (diffs.length === 0) {
        debug("契约守卫", "API 契约文件无 diff 内容，跳过检测");
        return emptyResult("No diff content found in API contract files.");
    }
    // 解析每个 diff 中的契约变更（端点变更、字段变更等）
    for (const { file, diff } of diffs) {
        const parsed = parseDiffForContractChanges(file, diff);
        changes.push(...parsed);
    }
    // 统计破坏性变更和兼容性变更
    const breaking = changes.filter((c) => c.severity === "breaking");
    const compatible = changes.filter((c) => c.severity === "compatible");
    // 跨服务影响分析（仅在多仓库场景下有意义）
    const crossServiceImpact = analyzeCrossServiceImpact(breaking, config);
    const summary = buildSummary(changes.length, breaking.length, compatible.length, crossServiceImpact);
    if (breaking.length > 0) {
        debug("契约守卫", `检测到 ${breaking.length} 个破坏性变更，需要人工审查`);
    }
    else {
        debug("契约守卫", `检测完成，共 ${changes.length} 个变更（${breaking.length} 破坏性，${compatible.length} 兼容性）`);
    }
    return {
        changes,
        breaking_count: breaking.length,
        compatible_count: compatible.length,
        cross_service_impact: crossServiceImpact,
        requires_review: breaking.length > 0,
        summary,
    };
}
/**
 * 判断文件是否为 API 契约相关文件。
 * 通过路径关键词匹配，覆盖 controller、route、dto、schema 等类型。
 * @param filePath - 文件路径
 * @returns true 表示该文件属于 API 契约文件
 */
function isApiContractFile(filePath) {
    const lower = filePath.toLowerCase();
    return API_PATH_KEYWORDS.some((kw) => lower.includes(kw));
}
/**
 * 解析 Git diff 内容，识别其中的 API 契约变更。
 *
 * 解析策略（按优先级）：
 * 1. Spring MVC 注解变更（@GetMapping、@PostMapping 等）
 * 2. Express 路由变更（router.get()、router.post() 等）
 * 3. DTO/Entity 字段变更（Java 和 TypeScript 字段定义）
 *
 * 后处理步骤：将同一路径的 path_removed + path_added 配对为 method_changed。
 *
 * @param file - 文件路径
 * @param diff - Git diff 文本
 * @returns 契约变更列表
 */
function parseDiffForContractChanges(file, diff) {
    const changes = [];
    const lines = diff.split("\n");
    // 已消费的行索引集合，防止同一行被多个规则重复匹配
    const consumedIndices = new Set();
    for (let i = 0; i < lines.length; i++) {
        if (consumedIndices.has(i))
            continue;
        const line = lines[i];
        // 仅处理新增行（+）和删除行（-），跳过 diff 元数据行（+++、---）
        const isAdded = line.startsWith("+") && !line.startsWith("+++");
        const isRemoved = line.startsWith("-") && !line.startsWith("---");
        if (!isAdded && !isRemoved)
            continue;
        const content = line.slice(1); // 去掉行首的 +/- 和空格，获取实际内容
        // 检测 1: Spring MVC 注解变更
        const springChange = detectSpringAnnotationChange(content, isAdded, isRemoved, file);
        if (springChange) {
            changes.push(springChange);
            continue;
        }
        // 检测 2: Express 路由变更
        const expressChange = detectExpressRouteChange(content, isAdded, isRemoved, file);
        if (expressChange) {
            changes.push(expressChange);
            continue;
        }
        // 检测 3: DTO/Entity 字段变更
        const fieldChange = detectFieldChange(content, isAdded, isRemoved, file, lines, i, consumedIndices);
        if (fieldChange) {
            changes.push(fieldChange);
            if (fieldChange.change_type === "field_type_changed" && isRemoved) {
                // 字段类型变更已检测到，继续正常处理
            }
        }
    }
    // 后处理：检测同一路径上的 HTTP 方法变更
    // 将 path_removed + path_added 中 URL 路径相同的配对为 method_changed（破坏性变更）
    const methodChanged = detectMethodChanges(changes);
    return methodChanged;
}
/**
 * 检测 HTTP 方法变更。
 * 将同一路径的 path_removed 和 path_added 配对，
 * 如果 HTTP 方法不同则合并为一个 method_changed 变更。
 * @param changes - 原始契约变更列表
 * @returns 合并后的契约变更列表（method_changed + 未消费的原始变更）
 */
function detectMethodChanges(changes) {
    const removed = new Map();
    const added = new Map();
    const consumed = new Set();
    // 按 URL 路径建立索引，用于快速配对删除和新增的端点
    for (let i = 0; i < changes.length; i++) {
        const change = changes[i];
        if (change.change_type === "path_removed") {
            const pathStr = extractPath(change.before ?? change.description);
            if (pathStr)
                removed.set(pathStr, change);
        }
        else if (change.change_type === "path_added") {
            const pathStr = extractPath(change.after ?? change.description);
            if (pathStr)
                added.set(pathStr, change);
        }
    }
    // 查找匹配对：同一 URL 路径但不同 HTTP 方法的删除+新增
    const result = [];
    for (const [urlPath, removedChange] of removed) {
        const addedChange = added.get(urlPath);
        if (addedChange) {
            const removedMethod = extractHttpMethod(removedChange.before ?? removedChange.description);
            const addedMethod = extractHttpMethod(addedChange.after ?? addedChange.description);
            if (removedMethod && addedMethod && removedMethod !== addedMethod) {
                consumed.add(changes.indexOf(removedChange));
                consumed.add(changes.indexOf(addedChange));
                result.push({
                    file: removedChange.file,
                    change_type: "method_changed",
                    severity: "breaking",
                    description: `HTTP method changed: ${removedMethod} ${urlPath} -> ${addedMethod} ${urlPath}`,
                    before: `${removedMethod} ${urlPath}`,
                    after: `${addedMethod} ${urlPath}`,
                });
            }
        }
    }
    // 返回 method_changed + 未消费的原始变更
    for (let i = 0; i < changes.length; i++) {
        if (!consumed.has(i)) {
            result.push(changes[i]);
        }
    }
    return result;
}
/**
 * 从端点描述字符串中提取 URL 路径。
 * 支持格式如 "GetMapping /api/v1/users" 或 "GET /api/v1/users"。
 * @param endpointStr - 端点描述字符串
 * @returns 提取到的 URL 路径，匹配失败返回 null
 */
function extractPath(endpointStr) {
    // 匹配以 / 开头的路径，支持字母、数字、连字符、花括号（路径参数）、点和斜杠
    const match = endpointStr.match(/\/[\w\-{}./]+/);
    return match ? match[0] : null;
}
/**
 * 从端点描述字符串中提取 HTTP 方法。
 * 支持 Spring 注解风格（GetMapping）和标准 HTTP 方法风格（GET）。
 * @param endpointStr - 端点描述字符串
 * @returns HTTP 方法字符串，匹配失败返回 null
 */
function extractHttpMethod(endpointStr) {
    // 匹配 Spring 注解风格的方法名（如 GetMapping、PostMapping）
    const match = endpointStr.match(/^(Get|Post|Put|Delete|Patch|Mapping)\w*\s/i);
    if (match) {
        const raw = match[1].toUpperCase();
        // 将 Spring 注解名映射为标准 HTTP 方法
        const methodMap = {
            GET: "GET", POST: "POST", PUT: "PUT", DELETE: "DELETE", PATCH: "PATCH",
            REQUESTMAPPING: "REQUEST", // 通用映射，不区分方法
        };
        return methodMap[raw] ?? raw;
    }
    // 回退：匹配标准大写 HTTP 方法（如 GET、POST）
    const httpMatch = endpointStr.match(/^(GET|POST|PUT|DELETE|PATCH)\s/);
    return httpMatch ? httpMatch[1] : null;
}
/**
 * 检测 Spring MVC 注解变更。
 * 匹配 @GetMapping、@PostMapping、@PutMapping、@DeleteMapping、@RequestMapping。
 * @param content - diff 行内容（已去除 +/- 前缀）
 * @param isAdded - 是否为新增行
 * @param isRemoved - 是否为删除行
 * @param file - 文件路径
 * @returns 契约变更对象，未匹配返回 null
 */
function detectSpringAnnotationChange(content, isAdded, isRemoved, file) {
    // Spring MVC 注解正则：匹配 @XxxMapping(value="/path") 或 @XxxMapping("/path") 形式
    const springAnnotationRe = /^\s*@(?<method>RequestMapping|GetMapping|PostMapping|PutMapping|DeleteMapping)\s*\(?\s*(?:value\s*=\s*)?["'`](?<path>\/[^"'`]*)["'`]/;
    const match = content.match(springAnnotationRe);
    if (!match)
        return null;
    const path = safeMatchGroups(match).path;
    const httpMethod = safeMatchGroups(match).method;
    if (isAdded) {
        return {
            file,
            change_type: "path_added",
            severity: "compatible",
            description: `New endpoint added: ${httpMethod} ${path}`,
            after: `${httpMethod} ${path}`,
        };
    }
    if (isRemoved) {
        return {
            file,
            change_type: "path_removed",
            severity: "breaking",
            description: `Endpoint removed: ${httpMethod} ${path}`,
            before: `${httpMethod} ${path}`,
        };
    }
    return null;
}
/**
 * 检测 Express 路由变更。
 * 匹配 router.get()、router.post()、router.put()、router.delete()、router.patch() 形式。
 * @param content - diff 行内容（已去除 +/- 前缀）
 * @param isAdded - 是否为新增行
 * @param isRemoved - 是否为删除行
 * @param file - 文件路径
 * @returns 契约变更对象，未匹配返回 null
 */
function detectExpressRouteChange(content, isAdded, isRemoved, file) {
    // Express 路由正则：匹配 router.method('/path') 形式
    const expressRouteRe = /router\.(?<method>get|post|put|delete|patch)\s*\(\s*['"`](?<path>\/[^'"`]*)['"`]/;
    const match = content.match(expressRouteRe);
    if (!match)
        return null;
    const path = safeMatchGroups(match).path;
    const httpMethod = safeMatchGroups(match).method.toUpperCase();
    if (isAdded) {
        return {
            file,
            change_type: "path_added",
            severity: "compatible",
            description: `New Express route added: ${httpMethod} ${path}`,
            after: `${httpMethod} ${path}`,
        };
    }
    if (isRemoved) {
        return {
            file,
            change_type: "path_removed",
            severity: "breaking",
            description: `Express route removed: ${httpMethod} ${path}`,
            before: `${httpMethod} ${path}`,
        };
    }
    return null;
}
/**
 * 检测 DTO/Entity 字段变更。
 * 支持 Java 字段声明（private Type name）和 TypeScript 字段声明（name: Type）。
 * 当同一字段名出现删除和新增时，会尝试识别为类型变更。
 * @param content - diff 行内容（已去除 +/- 前缀）
 * @param isAdded - 是否为新增行
 * @param isRemoved - 是否为删除行
 * @param file - 文件路径
 * @param lines - diff 全部行（用于向前/向后查找关联行）
 * @param currentIndex - 当前行索引
 * @param consumedIndices - 已消费行索引集合（用于避免重复匹配）
 * @returns 契约变更对象，未匹配返回 null
 */
function detectFieldChange(content, isAdded, isRemoved, file, lines, currentIndex, consumedIndices) {
    // Java 字段正则：匹配 private/protected/public Type fieldName 形式
    const javaFieldRe = /^\s*(private|protected|public)\s+(?<type>[\w<>[\]]+)\s+(?<name>\w+)/;
    // TypeScript 字段正则：匹配 fieldName: Type 或 fieldName?: Type 形式
    const tsFieldRe = /^\s*(?<name>\w+)\s*(?<optional>\??):\s*(?<type>[\w<>[\]|.]+)\s*[;=]?/;
    const javaMatch = content.match(javaFieldRe);
    const tsMatch = !javaMatch ? content.match(tsFieldRe) : null;
    if (javaMatch) {
        const { type, name } = safeMatchGroups(javaMatch);
        const fieldName = `${name}: ${type}`;
        if (isAdded) {
            // 尝试在同一字段名下查找关联的删除行，检测类型变更
            const typeChange = detectTypeChangeForField(name, lines, currentIndex, consumedIndices);
            if (typeChange) {
                return {
                    file,
                    change_type: "field_type_changed",
                    severity: "breaking",
                    description: `Field type changed: ${name}`,
                    before: typeChange.before,
                    after: typeChange.after,
                };
            }
            return {
                file,
                change_type: "field_added",
                severity: "compatible",
                description: `Field added: ${fieldName}`,
                after: fieldName,
            };
        }
        if (isRemoved) {
            // 尝试在同一字段名下查找关联的新增行，检测类型变更
            const typeChange = detectTypeChangeForField(name, lines, currentIndex, consumedIndices);
            if (typeChange) {
                return {
                    file,
                    change_type: "field_type_changed",
                    severity: "breaking",
                    description: `Field type changed: ${name}`,
                    before: typeChange.before,
                    after: typeChange.after,
                };
            }
            return {
                file,
                change_type: "field_removed",
                severity: "breaking",
                description: `Field removed: ${fieldName}`,
                before: fieldName,
            };
        }
    }
    if (tsMatch) {
        const { type, name } = safeMatchGroups(tsMatch);
        const optional = safeMatchGroups(tsMatch).optional === "?";
        const fieldName = `${name}${optional ? "?" : ""}: ${type}`;
        if (isAdded) {
            // 尝试在同一字段名下查找关联的删除行，检测类型变更
            const typeChange = detectTypeChangeForField(name, lines, currentIndex, consumedIndices);
            if (typeChange) {
                return {
                    file,
                    change_type: "field_type_changed",
                    severity: "breaking",
                    description: `Field type changed: ${name}`,
                    before: typeChange.before,
                    after: typeChange.after,
                };
            }
            return {
                file,
                change_type: "field_added",
                severity: "compatible",
                description: `Field added: ${fieldName}`,
                after: fieldName,
            };
        }
        if (isRemoved) {
            const typeChange = detectTypeChangeForField(name, lines, currentIndex, consumedIndices);
            if (typeChange) {
                return {
                    file,
                    change_type: "field_type_changed",
                    severity: "breaking",
                    description: `Field type changed: ${name}`,
                    before: typeChange.before,
                    after: typeChange.after,
                };
            }
            return {
                file,
                change_type: "field_removed",
                severity: "breaking",
                description: `Field removed: ${fieldName}`,
                before: fieldName,
            };
        }
    }
    return null;
}
/**
 * 检测字段的类型变更。
 * 在 diff 的相邻行中查找同名字段但类型不同的删除/新增行对。
 * 搜索范围限制为前后各 5 行，避免误匹配距离太远的无关行。
 * @param fieldName - 字段名称
 * @param lines - diff 全部行
 * @param currentIndex - 当前行索引
 * @param consumedIndices - 已消费行索引集合
 * @returns 类型变更信息（变更前后的类型字符串），未检测到返回 null
 */
function detectTypeChangeForField(fieldName, lines, currentIndex, consumedIndices) {
    const javaRe = /^\s*(private|protected|public)\s+(?<type>[\w<>[\]]+)\s+(?<name>\w+)/;
    const tsRe = /^\s*(?<name>\w+)\s*(?<optional>\??):\s*(?<type>[\w<>[\]|.]+)\s*[;=]?/;
    const currentLine = lines[currentIndex];
    const isRemoved = currentLine.startsWith("-") && !currentLine.startsWith("---");
    const isAdded = currentLine.startsWith("+") && !currentLine.startsWith("+++");
    // 辅助函数：从 diff 行内容中提取类型信息（去除 +/- 前缀后）
    function extractTypeInfo(lineIndex) {
        const line = lines[lineIndex];
        const isPlus = line.startsWith("+") && !line.startsWith("+++");
        const isMinus = line.startsWith("-") && !line.startsWith("---");
        if (!isPlus && !isMinus)
            return null;
        const content = line.slice(1);
        const jMatch = content.match(javaRe);
        const tMatch = !jMatch ? content.match(tsRe) : null;
        const m = jMatch ?? tMatch;
        if (!m)
            return null;
        return { type: safeMatchGroups(m).type, name: safeMatchGroups(m).name };
    }
    function getCurrentTypeInfo() {
        return extractTypeInfo(currentIndex);
    }
    const maxLook = 5; // 最大向前/向后查找行数
    if (isRemoved) {
        // 删除行：向前查找同名字段的新增行（- 行后面紧跟 + 行）
        for (let j = currentIndex + 1; j < Math.min(currentIndex + maxLook, lines.length); j++) {
            if (consumedIndices.has(j))
                continue;
            const info = extractTypeInfo(j);
            if (info && info.name === fieldName) {
                const currentInfo = getCurrentTypeInfo();
                if (info.type !== currentInfo.type) {
                    consumedIndices.add(j);
                    return { before: `${fieldName}: ${currentInfo.type}`, after: `${fieldName}: ${info.type}` };
                }
            }
        }
        // 也在后方查找（diff 中 - 行出现在 + 行之后的情况）
        for (let j = currentIndex - 1; j >= Math.max(0, currentIndex - maxLook); j--) {
            if (consumedIndices.has(j))
                continue;
            const info = extractTypeInfo(j);
            if (info && info.name === fieldName) {
                const currentInfo = getCurrentTypeInfo();
                if (info.type !== currentInfo.type) {
                    consumedIndices.add(j);
                    return { before: `${fieldName}: ${currentInfo.type}`, after: `${fieldName}: ${info.type}` };
                }
            }
        }
    }
    if (isAdded) {
        // 新增行：向后查找同名字段的删除行（+ 行前面是 - 行）
        for (let j = currentIndex - 1; j >= Math.max(0, currentIndex - maxLook); j--) {
            if (consumedIndices.has(j))
                continue;
            const info = extractTypeInfo(j);
            if (info && info.name === fieldName) {
                const currentInfo = getCurrentTypeInfo();
                if (info.type !== currentInfo.type) {
                    consumedIndices.add(j);
                    return { before: `${fieldName}: ${info.type}`, after: `${fieldName}: ${currentInfo.type}` };
                }
            }
        }
        // 也在前方查找（diff 中 + 行出现在 - 行之前的情况）
        for (let j = currentIndex + 1; j < Math.min(currentIndex + maxLook, lines.length); j++) {
            if (consumedIndices.has(j))
                continue;
            const info = extractTypeInfo(j);
            if (info && info.name === fieldName) {
                const currentInfo = getCurrentTypeInfo();
                if (info.type !== currentInfo.type) {
                    consumedIndices.add(j);
                    return { before: `${fieldName}: ${info.type}`, after: `${fieldName}: ${currentInfo.type}` };
                }
            }
        }
    }
    return null;
}
/**
 * 分析破坏性变更的跨服务影响。
 * 仅在多仓库（多服务）场景下有效。当前版本将所有破坏性端点
 * 归类为对前端的潜在影响。
 * @param breakingChanges - 破坏性变更列表
 * @param config - 项目配置
 * @returns 受影响的服务列表及其关联端点
 */
function analyzeCrossServiceImpact(breakingChanges, config) {
    const impact = [];
    // 仅在多仓库（微服务）场景下进行跨服务影响分析
    if (config.repos.length < 2)
        return impact;
    // 从破坏性变更中提取受影响的端点路径
    const affectedEndpoints = [];
    for (const change of breakingChanges) {
        if (change.change_type === "path_removed" ||
            change.change_type === "method_changed" ||
            change.change_type === "field_removed" ||
            change.change_type === "field_type_changed") {
            const endpoint = change.before ?? change.description;
            affectedEndpoints.push(endpoint);
        }
    }
    if (affectedEndpoints.length > 0) {
        impact.push({
            service: "frontend",
            endpoints: affectedEndpoints,
        });
    }
    return impact;
}
/**
 * 构建契约检查的人类可读摘要。
 * @param total - 变更总数
 * @param breaking - 破坏性变更数
 * @param compatible - 兼容性变更数
 * @param crossService - 跨服务影响列表
 * @returns 摘要文本
 */
function buildSummary(total, breaking, compatible, crossService) {
    const parts = [];
    if (total === 0) {
        return "No contract changes detected.";
    }
    parts.push(`Detected ${total} contract change(s): ${breaking} breaking, ${compatible} compatible.`);
    if (crossService.length > 0) {
        const services = crossService.map((c) => `${c.service} (${c.endpoints.length} endpoint(s))`);
        parts.push(`Cross-service impact: ${services.join(", ")}.`);
    }
    if (breaking > 0) {
        parts.push("Review required due to breaking changes.");
    }
    return parts.join(" ");
}
/**
 * 创建空的契约检查结果。
 * @param summary - 摘要说明（通常描述为何无变更）
 * @returns 所有计数为零的空契约检查结果
 */
function emptyResult(summary) {
    return {
        changes: [],
        breaking_count: 0,
        compatible_count: 0,
        cross_service_impact: [],
        requires_review: false,
        summary,
    };
}
//# sourceMappingURL=contract_guard.js.map