package com.easyagents.agent.runtime.tool.operate;

import com.easyagents.agent.runtime.AgentRuntimeException;
import com.easyagents.agent.runtime.tool.operate.ApplyPatchTool.DiffLine;
import com.easyagents.agent.runtime.tool.operate.ApplyPatchTool.FilePatch;
import com.easyagents.agent.runtime.tool.operate.ApplyPatchTool.Hunk;
import com.easyagents.agent.runtime.tool.operate.ApplyPatchTool.PatchType;

import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * `*** Begin Patch` 和标准 unified diff 解析器。
 */
final class UnifiedPatchParser {

    private static final Pattern HUNK_HEADER = Pattern.compile(
            "^@@(?:\\s+-(\\d+)(?:,\\d+)?\\s+\\+\\d+(?:,\\d+)?\\s+@@.*)?$");

    private UnifiedPatchParser() {
    }

    /**
     * 解析补丁文本。
     *
     * @param patch 补丁文本
     * @return 有序文件补丁
     */
    static List<FilePatch> parse(String patch) {
        String normalized = patch.replace("\r\n", "\n").replace('\r', '\n');
        List<String> lines = List.of(normalized.split("\n", -1));
        if (!lines.isEmpty() && "*** Begin Patch".equals(lines.get(0))) {
            return parseEnvelope(lines);
        }
        return parseUnified(lines);
    }

    /**
     * 将单文件补丁应用到当前文本。
     *
     * @param patch 单文件补丁
     * @param current 当前 UTF-8 文本
     * @return 修改后文本
     */
    static String apply(FilePatch patch, String current) {
        boolean trailingNewline = patch.type() == PatchType.ADD || current.endsWith("\n") || current.endsWith("\r");
        List<String> content = splitDocument(current);
        if (patch.type() == PatchType.DELETE && patch.hunks().isEmpty()) {
            return "";
        }
        for (Hunk hunk : patch.hunks()) {
            List<String> oldLines = hunk.lines().stream()
                    .filter(line -> line.kind() != '+')
                    .map(DiffLine::text)
                    .toList();
            List<String> newLines = hunk.lines().stream()
                    .filter(line -> line.kind() != '-')
                    .map(DiffLine::text)
                    .toList();
            int position = locateUnique(content, oldLines, hunk.oldStart());
            for (int index = 0; index < oldLines.size(); index++) {
                if (!content.get(position + index).equals(oldLines.get(index))) {
                    throw new WorkspaceToolException("PATCH_CONFLICT",
                            "Patch hunk context does not match the target file.", false);
                }
            }
            content.subList(position, position + oldLines.size()).clear();
            content.addAll(position, newLines);
        }
        String result = String.join("\n", content);
        if (patch.type() == PatchType.DELETE && !result.isEmpty()) {
            throw new WorkspaceToolException("PATCH_CONFLICT",
                    "Delete patch does not match the complete target file.", false);
        }
        return trailingNewline && !content.isEmpty() ? result + "\n" : result;
    }

    private static List<FilePatch> parseEnvelope(List<String> lines) {
        List<FilePatch> patches = new ArrayList<>();
        Set<String> targets = new LinkedHashSet<>();
        int index = 1;
        while (index < lines.size()) {
            String line = lines.get(index);
            if ("*** End Patch".equals(line)) {
                return patches;
            }
            PatchType type;
            String path;
            if (line.startsWith("*** Add File: ")) {
                type = PatchType.ADD;
                path = line.substring("*** Add File: ".length()).trim();
            } else if (line.startsWith("*** Update File: ")) {
                type = PatchType.UPDATE;
                path = line.substring("*** Update File: ".length()).trim();
            } else if (line.startsWith("*** Delete File: ")) {
                type = PatchType.DELETE;
                path = line.substring("*** Delete File: ".length()).trim();
            } else if (line.isEmpty()) {
                index++;
                continue;
            } else {
                throw patchInvalid("Invalid patch section header.");
            }
            if (path.isBlank() || !targets.add(path)) {
                throw patchInvalid("Patch target is empty or duplicated.");
            }
            index++;
            List<String> body = new ArrayList<>();
            while (index < lines.size() && !lines.get(index).startsWith("*** ")) {
                body.add(lines.get(index++));
            }
            if (!body.isEmpty() && body.get(body.size() - 1).isEmpty()) {
                body.remove(body.size() - 1);
            }
            patches.add(buildFilePatch(type, path, body));
        }
        throw patchInvalid("Patch is missing *** End Patch.");
    }

    private static List<FilePatch> parseUnified(List<String> lines) {
        List<FilePatch> patches = new ArrayList<>();
        Set<String> targets = new LinkedHashSet<>();
        int index = 0;
        while (index < lines.size()) {
            if (!lines.get(index).startsWith("--- ")) {
                if (lines.get(index).isEmpty()) {
                    index++;
                    continue;
                }
                throw patchInvalid("Invalid unified diff: expected '---' header.");
            }
            String oldPath = headerPath(lines.get(index++).substring(4));
            if (index >= lines.size() || !lines.get(index).startsWith("+++ ")) {
                throw patchInvalid("Invalid unified diff: expected '+++' header.");
            }
            String newPath = headerPath(lines.get(index++).substring(4));
            PatchType type = "/dev/null".equals(oldPath) ? PatchType.ADD
                    : "/dev/null".equals(newPath) ? PatchType.DELETE : PatchType.UPDATE;
            String path = type == PatchType.DELETE ? stripPrefix(oldPath) : stripPrefix(newPath);
            if (path.isBlank() || !targets.add(path)) {
                throw patchInvalid("Patch target is empty or duplicated.");
            }
            List<String> body = new ArrayList<>();
            while (index < lines.size() && !lines.get(index).startsWith("--- ")) {
                body.add(lines.get(index++));
            }
            if (!body.isEmpty() && body.get(body.size() - 1).isEmpty()) {
                body.remove(body.size() - 1);
            }
            patches.add(buildFilePatch(type, path, body));
        }
        return patches;
    }

    private static FilePatch buildFilePatch(PatchType type, String path, List<String> body) {
        if (type == PatchType.DELETE && body.isEmpty()) {
            return new FilePatch(type, path, List.of(), 0, 0);
        }
        if (type == PatchType.ADD && body.stream().noneMatch(line -> line.startsWith("@@"))) {
            List<DiffLine> lines = new ArrayList<>();
            for (String line : body) {
                if (!line.startsWith("+")) {
                    throw patchInvalid("Added file lines must start with '+'.");
                }
                lines.add(new DiffLine('+', line.substring(1)));
            }
            return new FilePatch(type, path, List.of(new Hunk(1, lines)), lines.size(), 0);
        }
        List<Hunk> hunks = new ArrayList<>();
        List<DiffLine> current = null;
        Integer oldStart = null;
        int added = 0;
        int deleted = 0;
        for (String line : body) {
            Matcher header = HUNK_HEADER.matcher(line);
            if (header.matches()) {
                if (current != null) {
                    hunks.add(new Hunk(oldStart, List.copyOf(current)));
                }
                current = new ArrayList<>();
                oldStart = header.group(1) == null ? null : Integer.parseInt(header.group(1));
                continue;
            }
            if ("\\ No newline at end of file".equals(line)) {
                continue;
            }
            if (current == null) {
                throw patchInvalid("Patch hunk is missing an @@ header.");
            }
            if (line.isEmpty() || (line.charAt(0) != ' ' && line.charAt(0) != '+' && line.charAt(0) != '-')) {
                throw patchInvalid("Invalid patch hunk line.");
            }
            char kind = line.charAt(0);
            current.add(new DiffLine(kind, line.substring(1)));
            if (kind == '+') {
                added++;
            } else if (kind == '-') {
                deleted++;
            }
        }
        if (current != null) {
            hunks.add(new Hunk(oldStart, List.copyOf(current)));
        }
        if (hunks.isEmpty() && type != PatchType.DELETE) {
            throw patchInvalid("Patch file section does not contain a hunk.");
        }
        return new FilePatch(type, path, List.copyOf(hunks), added, deleted);
    }

    private static int locateUnique(List<String> content, List<String> oldLines, Integer declaredStart) {
        if (oldLines.isEmpty()) {
            if (declaredStart == null) {
                if (content.isEmpty()) {
                    return 0;
                }
                throw new WorkspaceToolException("PATCH_CONFLICT",
                        "Insertion hunk needs a line position or context.", false);
            }
            int position = Math.max(0, declaredStart - 1);
            if (position > content.size()) {
                throw new WorkspaceToolException("PATCH_CONFLICT",
                        "Insertion position is outside the target file.", false);
            }
            return position;
        }
        int match = -1;
        for (int start = 0; start + oldLines.size() <= content.size(); start++) {
            boolean equal = true;
            for (int offset = 0; offset < oldLines.size(); offset++) {
                if (!content.get(start + offset).equals(oldLines.get(offset))) {
                    equal = false;
                    break;
                }
            }
            if (equal) {
                if (match >= 0) {
                    throw new WorkspaceToolException("PATCH_CONFLICT",
                            "Patch hunk context is not unique.", false);
                }
                match = start;
            }
        }
        if (match < 0) {
            throw new WorkspaceToolException("PATCH_CONFLICT",
                    "Patch hunk context was not found.", false);
        }
        return match;
    }

    private static List<String> splitDocument(String content) {
        if (content.isEmpty()) {
            return new ArrayList<>();
        }
        String normalized = content.replace("\r\n", "\n").replace('\r', '\n');
        String[] values = normalized.split("\n", -1);
        int length = values.length;
        if (length > 0 && values[length - 1].isEmpty()) {
            length--;
        }
        List<String> lines = new ArrayList<>(length);
        for (int index = 0; index < length; index++) {
            lines.add(values[index]);
        }
        return lines;
    }

    private static String headerPath(String header) {
        String trimmed = header.trim();
        int tab = trimmed.indexOf('\t');
        return tab < 0 ? trimmed : trimmed.substring(0, tab);
    }

    private static String stripPrefix(String path) {
        if (path.startsWith("a/") || path.startsWith("b/")) {
            return path.substring(2);
        }
        return path;
    }

    private static WorkspaceToolException patchInvalid(String message) {
        return new WorkspaceToolException("PATCH_INVALID", message, false);
    }
}
