export class StructuredApplyPatchError extends Error {
    reason;
    constructor(reason, message) {
        super(message);
        this.reason = reason;
    }
}
const SUPPORTED_KINDS = [
    'insert_after',
    'insert_before',
    'replace',
    'delete',
    'create_file',
    'delete_file'
];
const FILE_PATH_INVALID_RE = /[\r\n]/;
const decodeEscapedNewlinesIfObvious = (value) => {
    if (!value)
        return value;
    if (value.includes('\n'))
        return value;
    const lower = value.toLowerCase();
    const looksEscaped = value.includes('\\r\\n') ||
        (value.includes('\\n') && /\\n[ \t]/.test(value)) ||
        lower.includes('\\u000a') ||
        lower.includes('\\u000d');
    if (!looksEscaped) {
        return value;
    }
    let out = value;
    out = out.replace(/\\r\\n/g, '\n');
    out = out.replace(/\\n/g, '\n');
    out = out.replace(/\\r/g, '\n');
    out = out.replace(/\\u000a/gi, '\n');
    out = out.replace(/\\u000d/gi, '\n');
    return out;
};
const toSafeString = (value, label) => {
    const str = typeof value === 'string' ? value : '';
    if (!str.trim()) {
        throw new StructuredApplyPatchError('missing_field', `${label} is required`);
    }
    return str;
};
const normalizeFilePath = (raw, label) => {
    let trimmed = raw.trim();
    if (!trimmed) {
        throw new StructuredApplyPatchError('invalid_file', `${label} must not be empty`);
    }
    if (FILE_PATH_INVALID_RE.test(trimmed)) {
        const firstLine = trimmed.split(/[\r\n]/)[0]?.trim() ?? '';
        if (!firstLine) {
            throw new StructuredApplyPatchError('invalid_file', `${label} must be a single-line path`);
        }
        trimmed = firstLine;
    }
    return trimmed.replace(/\\/g, '/');
};
const readStringish = (value) => {
    if (typeof value === 'string')
        return value;
    if (Array.isArray(value) && value.length === 1 && typeof value[0] === 'string')
        return String(value[0]);
    if (value && typeof value === 'object') {
        const rec = value;
        const nested = readStringish(rec.path ?? rec.file ?? rec.filename ?? rec.filepath);
        if (nested)
            return nested;
    }
    return undefined;
};
const splitTextIntoLines = (input) => {
    const decoded = decodeEscapedNewlinesIfObvious(input);
    const normalized = decoded.replace(/\r/g, '');
    const parts = normalized.split('\n');
    if (parts.length && parts[parts.length - 1] === '') {
        parts.pop();
    }
    return parts.length ? parts : [''];
};
const normalizeLines = (value, label) => {
    if (Array.isArray(value)) {
        if (!value.length) {
            return [];
        }
        const out = [];
        for (const [idx, entry] of value.entries()) {
            if (typeof entry !== 'string') {
                if (entry === null || entry === undefined) {
                    out.push('');
                    continue;
                }
                out.push(String(entry));
                continue;
            }
            // Preserve intentional whitespace
            const normalized = entry.replace(/\r/g, '');
            const decoded = decodeEscapedNewlinesIfObvious(normalized);
            if (decoded.includes('\n')) {
                out.push(...splitTextIntoLines(decoded));
            }
            else {
                out.push(decoded);
            }
        }
        return out;
    }
    if (typeof value === 'string') {
        return splitTextIntoLines(value);
    }
    if (value === null || value === undefined) {
        throw new StructuredApplyPatchError('invalid_lines', `${label} must be an array of strings or a multi-line string`);
    }
    return [String(value)];
};
const buildContextLines = (raw) => splitTextIntoLines(raw).map((line) => ` ${line}`);
const buildPrefixedLines = (lines, prefix) => lines.map((line) => `${prefix}${line}`);
const findSubsequence = (haystack, needle) => {
    if (!needle.length)
        return -1;
    outer: for (let i = 0; i + needle.length <= haystack.length; i += 1) {
        for (let j = 0; j < needle.length; j += 1) {
            if ((haystack[i + j] ?? '') !== (needle[j] ?? ''))
                continue outer;
        }
        return i;
    }
    return -1;
};
const tryApplyToFileLines = (section, kindRaw, change, changeRec, index) => {
    const lines = section.lines;
    if (kindRaw === 'insert_after' || kindRaw === 'insert_before') {
        const anchorSource = change.anchor ??
            change.target ??
            change.context ??
            change.from ??
            change.old ??
            change.oldText ??
            change.old_text ??
            change.beforeText ??
            change.before_text;
        const anchor = toSafeString(anchorSource, `changes[${index}].anchor`);
        const anchorLines = splitTextIntoLines(anchor);
        const anchorIndex = findSubsequence(lines, anchorLines);
        if (anchorIndex < 0)
            return false;
        const linesSource = changeRec.lines ??
            changeRec.text ??
            changeRec.content ??
            changeRec.body ??
            changeRec.replacement ??
            changeRec.newText ??
            changeRec.new_text ??
            changeRec.afterText ??
            changeRec.after_text;
        const additions = normalizeLines(linesSource, `changes[${index}].lines`);
        if (!additions.length) {
            throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
        }
        const prepared = kindRaw === 'insert_after'
            ? applyAnchorIndent(additions, anchorLines, 'last', change.use_anchor_indent)
            : applyAnchorIndent(additions, anchorLines, 'first', change.use_anchor_indent);
        const insertAt = kindRaw === 'insert_after' ? anchorIndex + anchorLines.length : anchorIndex;
        lines.splice(insertAt, 0, ...prepared);
        return true;
    }
    if (kindRaw === 'replace' || kindRaw === 'delete') {
        const targetSource = change.target ??
            change.anchor ??
            change.context ??
            change.from ??
            change.old ??
            change.oldText ??
            change.old_text ??
            change.beforeText ??
            change.before_text;
        const target = toSafeString(targetSource, `changes[${index}].target`);
        const targetLines = splitTextIntoLines(target);
        const targetIndex = findSubsequence(lines, targetLines);
        if (targetIndex < 0)
            return false;
        if (kindRaw === 'delete') {
            lines.splice(targetIndex, targetLines.length);
            return true;
        }
        const linesSource = changeRec.lines ??
            changeRec.text ??
            changeRec.content ??
            changeRec.body ??
            changeRec.replacement ??
            changeRec.newText ??
            changeRec.new_text ??
            changeRec.afterText ??
            changeRec.after_text;
        let replacements;
        if ((linesSource === null || linesSource === undefined) && typeof change.anchor === 'string' && typeof change.target === 'string') {
            replacements = splitTextIntoLines(String(change.anchor));
        }
        else {
            replacements = normalizeLines(linesSource, `changes[${index}].lines`);
        }
        lines.splice(targetIndex, targetLines.length, ...replacements);
        return true;
    }
    return false;
};
const detectIndentFromAnchor = (anchorLines, mode) => {
    const source = mode === 'first' ? anchorLines[0] ?? '' : anchorLines[anchorLines.length - 1] ?? '';
    const match = source.match(/^(\s*)/);
    return match ? match[1] ?? '' : '';
};
const applyAnchorIndent = (lines, anchorLines, position, enabled) => {
    if (!enabled) {
        return lines;
    }
    const indent = detectIndentFromAnchor(anchorLines, position);
    if (!indent) {
        return lines;
    }
    return lines.map((line) => {
        if (!line.trim()) {
            return line;
        }
        if (/^\s/.test(line)) {
            return line;
        }
        return `${indent}${line}`;
    });
};
export function buildStructuredPatch(payload) {
    if (!payload || typeof payload !== 'object') {
        throw new StructuredApplyPatchError('missing_payload', 'apply_patch arguments must be a JSON object');
    }
    if (!Array.isArray(payload.changes) || payload.changes.length === 0) {
        throw new StructuredApplyPatchError('missing_changes', 'apply_patch requires a non-empty "changes" array');
    }
    const topLevelFile = typeof payload.file === 'string' && payload.file.trim()
        ? normalizeFilePath(payload.file, 'file')
        : typeof payload.path === 'string' && String(payload.path).trim()
            ? normalizeFilePath(String(payload.path), 'path')
            : typeof payload.filepath === 'string' && String(payload.filepath).trim()
                ? normalizeFilePath(String(payload.filepath), 'filepath')
                : typeof payload.filename === 'string' && String(payload.filename).trim()
                    ? normalizeFilePath(String(payload.filename), 'filename')
                    : undefined;
    const sectionOrder = [];
    const fileSections = new Map();
    const ensureUpdateSection = (file) => {
        const existing = fileSections.get(file);
        if (existing) {
            if (existing.type !== 'update') {
                throw new StructuredApplyPatchError('invalid_change_sequence', `File "${file}" already marked as ${existing.type}`);
            }
            return existing;
        }
        const created = { type: 'update', hunks: [] };
        sectionOrder.push(file);
        fileSections.set(file, created);
        return created;
    };
    for (const [index, change] of payload.changes.entries()) {
        if (!change || typeof change !== 'object') {
            throw new StructuredApplyPatchError('invalid_change', `Change at index ${index} must be an object`);
        }
        const changeRec = change;
        const kindRaw = typeof change.kind === 'string' ? change.kind.trim().toLowerCase() : '';
        if (!kindRaw) {
            throw new StructuredApplyPatchError('invalid_change_kind', `Change at index ${index} is missing "kind"`);
        }
        if (!SUPPORTED_KINDS.includes(kindRaw)) {
            throw new StructuredApplyPatchError('invalid_change_kind', `Unsupported change kind "${change.kind}" at index ${index}`);
        }
        const fileSource = readStringish(changeRec.file) ??
            readStringish(changeRec.path) ??
            readStringish(changeRec.filepath) ??
            readStringish(changeRec.filename) ??
            readStringish(changeRec.file_path);
        const file = fileSource ? normalizeFilePath(fileSource, `changes[${index}].file`) : topLevelFile;
        if (!file) {
            throw new StructuredApplyPatchError('invalid_file', `Change at index ${index} is missing "file"`);
        }
        if (kindRaw === 'create_file') {
            const linesSource = changeRec.lines ?? changeRec.text ?? changeRec.content ?? changeRec.body ?? changeRec.replacement;
            const lines = normalizeLines(linesSource, `changes[${index}].lines`);
            const existing = fileSections.get(file);
            if (!existing) {
                sectionOrder.push(file);
                fileSections.set(file, { type: 'add', lines });
            }
            else if (existing.type === 'delete') {
                // Common model behavior: delete_file + create_file for same path → treat as replace.
                fileSections.set(file, { type: 'replace', lines });
            }
            else if (existing.type === 'add') {
                // Idempotent: last create wins.
                fileSections.set(file, { type: 'add', lines });
            }
            else if (existing.type === 'replace') {
                fileSections.set(file, { type: 'replace', lines });
            }
            else {
                // Existing updates imply file existed; represent as delete+add to keep a single executable path.
                fileSections.set(file, { type: 'replace', lines });
            }
            continue;
        }
        if (kindRaw === 'delete_file') {
            const existing = fileSections.get(file);
            if (!existing) {
                sectionOrder.push(file);
                fileSections.set(file, { type: 'delete' });
                continue;
            }
            if (existing.type === 'add') {
                // create_file then delete_file → net no-op; drop the section entirely.
                fileSections.delete(file);
                const idx = sectionOrder.indexOf(file);
                if (idx >= 0)
                    sectionOrder.splice(idx, 1);
                continue;
            }
            // update/replace/delete then delete_file → net delete.
            fileSections.set(file, { type: 'delete' });
            continue;
        }
        // Common shape: replace with lines but no target → treat as full-file replacement.
        if (kindRaw === 'replace') {
            const targetSource = change.target ??
                change.anchor ??
                change.context ??
                change.from ??
                change.old ??
                change.oldText ??
                change.old_text ??
                change.beforeText ??
                change.before_text;
            const hasTarget = typeof targetSource === 'string' && targetSource.trim().length > 0;
            const linesSource = changeRec.lines ??
                changeRec.text ??
                changeRec.content ??
                changeRec.body ??
                changeRec.replacement ??
                changeRec.newText ??
                changeRec.new_text ??
                changeRec.afterText ??
                changeRec.after_text;
            const hasReplacementBody = !(linesSource === null || linesSource === undefined);
            if (!hasTarget && hasReplacementBody) {
                const lines = normalizeLines(linesSource, `changes[${index}].lines`);
                const existing = fileSections.get(file);
                if (!existing) {
                    sectionOrder.push(file);
                }
                // Prefer a deterministic executable output: delete+add (replace).
                fileSections.set(file, { type: 'replace', lines });
                continue;
            }
        }
        const existing = fileSections.get(file);
        // Shape fix: allow create_file + subsequent structured edits by applying edits to the created content in-memory.
        if (existing && (existing.type === 'add' || existing.type === 'replace')) {
            const applied = tryApplyToFileLines(existing, kindRaw, change, changeRec, index);
            if (applied) {
                continue;
            }
            throw new StructuredApplyPatchError('invalid_change_sequence', `File "${file}" already marked as ${existing.type}`);
        }
        const section = ensureUpdateSection(file);
        switch (kindRaw) {
            case 'insert_after': {
                const anchorSource = change.anchor ??
                    change.target ??
                    change.context ??
                    change.from ??
                    change.old ??
                    change.oldText ??
                    change.old_text ??
                    change.beforeText ??
                    change.before_text;
                const anchor = toSafeString(anchorSource, `changes[${index}].anchor`);
                const anchorLines = splitTextIntoLines(anchor);
                const linesSource = changeRec.lines ??
                    changeRec.text ??
                    changeRec.content ??
                    changeRec.body ??
                    changeRec.replacement ??
                    changeRec.newText ??
                    changeRec.new_text ??
                    changeRec.afterText ??
                    changeRec.after_text;
                const additions = normalizeLines(linesSource, `changes[${index}].lines`);
                if (!additions.length) {
                    throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
                }
                const prepared = applyAnchorIndent(additions, anchorLines, 'last', change.use_anchor_indent);
                const hunkBody = [...buildContextLines(anchor), ...buildPrefixedLines(prepared, '+')];
                section.hunks.push(hunkBody);
                break;
            }
            case 'insert_before': {
                const anchorSource = change.anchor ??
                    change.target ??
                    change.context ??
                    change.from ??
                    change.old ??
                    change.oldText ??
                    change.old_text ??
                    change.beforeText ??
                    change.before_text;
                const anchor = toSafeString(anchorSource, `changes[${index}].anchor`);
                const anchorLines = splitTextIntoLines(anchor);
                const linesSource = changeRec.lines ??
                    changeRec.text ??
                    changeRec.content ??
                    changeRec.body ??
                    changeRec.replacement ??
                    changeRec.newText ??
                    changeRec.new_text ??
                    changeRec.afterText ??
                    changeRec.after_text;
                const additions = normalizeLines(linesSource, `changes[${index}].lines`);
                if (!additions.length) {
                    throw new StructuredApplyPatchError('invalid_lines', `changes[${index}].lines must include at least one line`);
                }
                const prepared = applyAnchorIndent(additions, anchorLines, 'first', change.use_anchor_indent);
                const hunkBody = [...buildPrefixedLines(prepared, '+'), ...buildContextLines(anchor)];
                section.hunks.push(hunkBody);
                break;
            }
            case 'replace': {
                // 兼容仅提供 anchor 的 replace 形态：将 anchor 视为 target 以尽可能保留用户意图。
                const targetSource = change.target ??
                    change.anchor ??
                    change.context ??
                    change.from ??
                    change.old ??
                    change.oldText ??
                    change.old_text ??
                    change.beforeText ??
                    change.before_text;
                const target = toSafeString(targetSource, `changes[${index}].target`);
                const linesSource = changeRec.lines ??
                    changeRec.text ??
                    changeRec.content ??
                    changeRec.body ??
                    changeRec.replacement ??
                    changeRec.newText ??
                    changeRec.new_text ??
                    changeRec.afterText ??
                    changeRec.after_text;
                let replacements;
                if ((linesSource === null || linesSource === undefined) && typeof change.anchor === 'string' && typeof change.target === 'string') {
                    // Common model mistake: provide { anchor: <new>, target: <old> } but omit lines.
                    // Treat anchor as replacement body (shape fix only).
                    replacements = splitTextIntoLines(String(change.anchor));
                }
                else {
                    replacements = normalizeLines(linesSource, `changes[${index}].lines`);
                }
                const hunkBody = [
                    ...buildPrefixedLines(splitTextIntoLines(target), '-'),
                    ...buildPrefixedLines(replacements, '+')
                ];
                section.hunks.push(hunkBody);
                break;
            }
            case 'delete': {
                const targetSource = change.target ??
                    change.anchor ??
                    change.context ??
                    change.from ??
                    change.old ??
                    change.oldText ??
                    change.old_text ??
                    change.beforeText ??
                    change.before_text;
                const target = toSafeString(targetSource, `changes[${index}].target`);
                const hunkBody = buildPrefixedLines(splitTextIntoLines(target), '-');
                section.hunks.push(hunkBody);
                break;
            }
            default: {
                throw new StructuredApplyPatchError('invalid_change_kind', `Unsupported change kind "${change.kind}" at index ${index}`);
            }
        }
    }
    if (!sectionOrder.length) {
        throw new StructuredApplyPatchError('missing_changes', 'apply_patch payload produced no file operations');
    }
    const lines = ['*** Begin Patch'];
    for (const file of sectionOrder) {
        const section = fileSections.get(file);
        if (!section)
            continue;
        if (section.type === 'add') {
            lines.push(`*** Add File: ${file}`);
            for (const line of section.lines) {
                lines.push(`+${line}`);
            }
        }
        else if (section.type === 'delete') {
            lines.push(`*** Delete File: ${file}`);
        }
        else if (section.type === 'replace') {
            lines.push(`*** Delete File: ${file}`);
            lines.push(`*** Add File: ${file}`);
            for (const line of section.lines) {
                lines.push(`+${line}`);
            }
        }
        else {
            lines.push(`*** Update File: ${file}`);
            const hunks = section.hunks || [];
            for (const hunk of hunks) {
                // 结构化补丁仅负责生成统一 diff 形态，不对多段 hunk 做逻辑裁剪；
                // 具体哪些 hunk 能成功应用由 apply_patch 客户端自行校验并返回错误信息。
                for (const entry of hunk) {
                    if (!entry.startsWith('@@')) {
                        lines.push(entry);
                    }
                }
            }
        }
    }
    lines.push('*** End Patch');
    return lines.join('\n');
}
export function isStructuredApplyPatchPayload(candidate) {
    if (!candidate || typeof candidate !== 'object') {
        return false;
    }
    const record = candidate;
    if (!Array.isArray(record.changes)) {
        return false;
    }
    return true;
}
