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

import com.easyagents.agent.runtime.AgentRuntimeException;
import io.agentscope.core.message.ToolResultBlock;
import io.agentscope.core.tool.AgentTool;
import io.agentscope.core.tool.ToolCallParam;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 有界 unified diff / context hunk 工作区补丁工具。
 */
final class ApplyPatchTool implements AgentTool {

    private static final Logger logger = LoggerFactory.getLogger(ApplyPatchTool.class);

    private final WorkspacePathGuard pathGuard;
    private final WorkspaceQuotaGuard quotaGuard;
    private final long maxPatchSize;
    private final int maxFiles;
    private final long maxAffectedBytes;

    /**
     * 创建补丁工具。
     *
     * @param pathGuard 路径保护器
     * @param quotaGuard 配额保护器
     * @param maxPatchSize Patch 输入最大字节数
     * @param maxFiles 单次最大影响文件数
     * @param maxAffectedBytes 原内容与新内容合计最大字节数
     */
    ApplyPatchTool(WorkspacePathGuard pathGuard,
                   WorkspaceQuotaGuard quotaGuard,
                   long maxPatchSize,
                   int maxFiles,
                   long maxAffectedBytes) {
        if (maxPatchSize <= 0 || maxFiles <= 0 || maxAffectedBytes <= 0) {
            throw new WorkspaceToolException("WORKSPACE_CONFIG_INVALID", "Patch limits must be positive.", false);
        }
        this.pathGuard = pathGuard;
        this.quotaGuard = quotaGuard;
        this.maxPatchSize = maxPatchSize;
        this.maxFiles = maxFiles;
        this.maxAffectedBytes = maxAffectedBytes;
    }

    /**
     * 获取工具名。
     *
     * @return `apply_patch`
     */
    @Override
    public String getName() {
        return AgentOperateToolAdapter.APPLY_PATCH_TOOL;
    }

    /**
     * 获取工具描述。
     *
     * @return 工具描述
     */
    @Override
    public String getDescription() {
        return "Apply a bounded unified diff to workspace-relative UTF-8 text files atomically per file.";
    }

    /**
     * 获取参数 Schema。
     *
     * @return JSON Schema
     */
    @Override
    public Map<String, Object> getParameters() {
        return Map.of(
                "type", "object",
                "properties", Map.of("patch", Map.of(
                        "type", "string",
                        "description", "Unified diff or *** Begin Patch context patch")),
                "required", List.of("patch"));
    }

    /**
     * 解析、预检并应用补丁。
     *
     * @param param Tool 调用参数
     * @return Tool 结果
     */
    @Override
    public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
        return Mono.fromCallable(() -> apply(param)).subscribeOn(Schedulers.boundedElastic());
    }

    private ToolResultBlock apply(ToolCallParam param) {
        try {
            Object value = param == null ? null : param.getInput().get("patch");
            if (!(value instanceof String patch) || patch.isBlank()) {
                throw new WorkspaceToolException("PATCH_INVALID", "Missing required string parameter: patch.", false);
            }
            if (patch.getBytes(StandardCharsets.UTF_8).length > maxPatchSize) {
                throw new WorkspaceToolException("WORKSPACE_QUOTA_EXCEEDED",
                        "Patch input exceeds the configured maximum size.", false);
            }
            List<FilePatch> patches = UnifiedPatchParser.parse(patch);
            if (patches.isEmpty()) {
                throw new WorkspaceToolException("PATCH_INVALID", "Patch does not contain file changes.", false);
            }
            if (patches.size() > maxFiles) {
                throw new WorkspaceToolException("WORKSPACE_QUOTA_EXCEEDED",
                        "Patch affects too many files.", false);
            }
            PatchPlan plan = prepare(patches);
            commit(plan);
            return ToolResultBlock.text("Patch applied successfully: " + plan.changes().size()
                    + " file(s), " + plan.addedLines() + " insertion(s), "
                    + plan.deletedLines() + " deletion(s).");
        } catch (AgentRuntimeException error) {
            return WorkspaceToolResults.error(error);
        } catch (RuntimeException error) {
            return WorkspaceToolResults.error(
                    new AgentRuntimeException("Unexpected patch execution failure.", error));
        }
    }

    private PatchPlan prepare(List<FilePatch> patches) {
        Map<Path, byte[]> originals = new LinkedHashMap<>();
        Map<Path, byte[]> desired = new LinkedHashMap<>();
        Map<Path, Long> resultingSizes = new LinkedHashMap<>();
        long affectedBytes = 0;
        int addedLines = 0;
        int deletedLines = 0;
        for (FilePatch patch : patches) {
            Path target = patch.type() == PatchType.ADD
                    ? pathGuard.resolveForWrite(patch.path()) : pathGuard.resolveExistingFile(patch.path());
            if (originals.containsKey(target)) {
                throw new WorkspaceToolException("PATCH_INVALID", "Patch contains a duplicate target.", false);
            }
            byte[] original = null;
            if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
                quotaGuard.validateFullRead(target);
                original = readBytes(target);
            }
            if (patch.type() == PatchType.ADD && original != null) {
                throw new WorkspaceToolException("PATCH_CONFLICT", "Patch add target already exists.", false);
            }
            String current = original == null ? "" : WorkspaceTextFiles.decodeUtf8(original);
            String updated = UnifiedPatchParser.apply(patch, current);
            byte[] next = null;
            if (patch.type() != PatchType.DELETE) {
                next = updated.getBytes(StandardCharsets.UTF_8);
            }
            affectedBytes = addBounded(affectedBytes, original == null ? 0 : original.length);
            affectedBytes = addBounded(affectedBytes, next == null ? 0 : next.length);
            originals.put(target, original);
            desired.put(target, next);
            resultingSizes.put(target, next == null ? -1L : (long) next.length);
            addedLines += patch.addedLines();
            deletedLines += patch.deletedLines();
        }
        quotaGuard.validateBatch(resultingSizes);
        return new PatchPlan(originals, desired, List.copyOf(desired.keySet()), addedLines, deletedLines);
    }

    private long addBounded(long left, long right) {
        long value;
        try {
            value = Math.addExact(left, right);
        } catch (ArithmeticException error) {
            throw new WorkspaceToolException("WORKSPACE_QUOTA_EXCEEDED",
                    "Patch affected content exceeds the configured maximum size.", false, error);
        }
        if (value > maxAffectedBytes) {
            throw new WorkspaceToolException("WORKSPACE_QUOTA_EXCEEDED",
                    "Patch affected content exceeds the configured maximum size.", false);
        }
        return value;
    }

    private void commit(PatchPlan plan) {
        List<Path> committed = new ArrayList<>();
        try {
            for (Path target : plan.changes()) {
                byte[] next = plan.desired().get(target);
                pathGuard.revalidate(target);
                if (next == null) {
                    Files.delete(target);
                } else {
                    WorkspaceTextFiles.atomicWrite(pathGuard, target, next);
                }
                committed.add(target);
            }
        } catch (Exception commitError) {
            Collections.reverse(committed);
            Exception rollbackError = null;
            for (Path target : committed) {
                try {
                    byte[] original = plan.originals().get(target);
                    if (original == null) {
                        Files.deleteIfExists(target);
                    } else {
                        WorkspaceTextFiles.atomicWrite(pathGuard, target, original);
                    }
                } catch (Exception error) {
                    if (rollbackError == null) {
                        rollbackError = error;
                    } else {
                        rollbackError.addSuppressed(error);
                    }
                }
            }
            if (rollbackError != null) {
                commitError.addSuppressed(rollbackError);
                logger.error("Patch commit and rollback failed; workspace requires inspection", commitError);
                throw new WorkspaceToolException("PATCH_ROLLBACK_FAILED",
                        "Patch commit and rollback failed; workspace requires inspection.", false, commitError);
            }
            logger.error("Patch commit failed and was rolled back", commitError);
            throw new WorkspaceToolException("WORKSPACE_IO_FAILED",
                    "Patch commit failed and all changes were rolled back.", true, commitError);
        }
    }

    private byte[] readBytes(Path target) {
        return WorkspaceTextFiles.readUtf8(target).getBytes(StandardCharsets.UTF_8);
    }

    /**
     * 补丁事务计划。
     *
     * @param originals 提交前原内容
     * @param desired 提交后内容，null 表示删除
     * @param changes 有序目标列表
     * @param addedLines 新增行数
     * @param deletedLines 删除行数
     */
    private record PatchPlan(Map<Path, byte[]> originals,
                             Map<Path, byte[]> desired,
                             List<Path> changes,
                             int addedLines,
                             int deletedLines) {
    }

    /**
     * 文件变更类型。
     */
    enum PatchType {
        /** 新增文件。 */
        ADD,
        /** 更新文件。 */
        UPDATE,
        /** 删除文件。 */
        DELETE
    }

    /**
     * 单文件补丁。
     *
     * @param type 变更类型
     * @param path 工作区相对路径
     * @param hunks 上下文块
     * @param addedLines 新增行数
     * @param deletedLines 删除行数
     */
    record FilePatch(PatchType type,
                     String path,
                     List<Hunk> hunks,
                     int addedLines,
                     int deletedLines) {
    }

    /**
     * 单个上下文块。
     *
     * @param oldStart unified diff 声明的原起始行，可空
     * @param lines 上下文行
     */
    record Hunk(Integer oldStart, List<DiffLine> lines) {
    }

    /**
     * 上下文行。
     *
     * @param kind 空格表示上下文，减号表示删除，加号表示新增
     * @param text 行内容
     */
    record DiffLine(char kind, String text) {
    }
}
