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 reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;

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.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * 与 AgentScope 1.x 文件写入 Schema 兼容的原子工作区工具。
 */
final class SafeWriteFileTool {

    private final WriteTextFileTool writeTextFileTool;
    private final InsertTextFileTool insertTextFileTool;

    /**
     * 创建文件写入工具组。
     *
     * @param pathGuard 路径保护器
     * @param quotaGuard 配额保护器
     */
    SafeWriteFileTool(WorkspacePathGuard pathGuard, WorkspaceQuotaGuard quotaGuard) {
        this.writeTextFileTool = new WriteTextFileTool(pathGuard, quotaGuard);
        this.insertTextFileTool = new InsertTextFileTool(pathGuard, quotaGuard);
    }

    /**
     * 获取写入文本文件工具。
     *
     * @return AgentScope 工具
     */
    AgentTool writeTextFileTool() {
        return writeTextFileTool;
    }

    /**
     * 获取插入文本文件工具。
     *
     * @return AgentScope 工具
     */
    AgentTool insertTextFileTool() {
        return insertTextFileTool;
    }

    /**
     * 新建、覆盖或范围替换文本文件。
     */
    private static final class WriteTextFileTool implements AgentTool {

        private final WorkspacePathGuard pathGuard;
        private final WorkspaceQuotaGuard quotaGuard;

        private WriteTextFileTool(WorkspacePathGuard pathGuard, WorkspaceQuotaGuard quotaGuard) {
            this.pathGuard = pathGuard;
            this.quotaGuard = quotaGuard;
        }

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

        /**
         * 获取工具描述。
         *
         * @return 工具描述
         */
        @Override
        public String getDescription() {
            return "Create, overwrite, or replace an inclusive line range in a UTF-8 workspace file.";
        }

        /**
         * 获取与 AgentScope 1.x 兼容的参数 Schema。
         *
         * @return JSON Schema
         */
        @Override
        public Map<String, Object> getParameters() {
            Map<String, Object> properties = new LinkedHashMap<>();
            properties.put("file_path", Map.of("type", "string", "description", "The target file path"));
            properties.put("content", Map.of("type", "string", "description", "The content to be written"));
            properties.put("ranges", Map.of(
                    "type", "string",
                    "description", "Optional inclusive replacement range such as '1,5'"));
            return Map.of(
                    "type", "object",
                    "properties", properties,
                    "required", List.of("file_path", "content"));
        }

        /**
         * 原子写入文件。
         *
         * @param param Tool 调用参数
         * @return Tool 结果
         */
        @Override
        public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
            return Mono.fromCallable(() -> write(param)).subscribeOn(Schedulers.boundedElastic());
        }

        private ToolResultBlock write(ToolCallParam param) {
            try {
                String filePath = requiredString(param, "file_path");
                String content = requiredStringAllowEmpty(param, "content");
                String ranges = optionalString(param, "ranges");
                Path target = pathGuard.resolveForWrite(filePath);
                byte[] bytes;
                if (ranges == null || ranges.isBlank() || !Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
                    bytes = content.getBytes(StandardCharsets.UTF_8);
                } else {
                    quotaGuard.validateFullRead(target);
                    List<String> lines = splitLines(WorkspaceTextFiles.readUtf8(target));
                    int[] range = parseReplacementRange(ranges, lines.size());
                    List<String> updated = new ArrayList<>();
                    updated.addAll(lines.subList(0, range[0] - 1));
                    updated.addAll(splitContentLines(content));
                    updated.addAll(lines.subList(range[1], lines.size()));
                    bytes = String.join("\n", updated).getBytes(StandardCharsets.UTF_8);
                }
                quotaGuard.validateWrite(target, bytes.length);
                WorkspaceTextFiles.atomicWrite(pathGuard, target, bytes);
                return ToolResultBlock.text("Write " + pathGuard.display(target) + " successfully.");
            } catch (AgentRuntimeException error) {
                return WorkspaceToolResults.error(error);
            } catch (RuntimeException error) {
                return WorkspaceToolResults.error(
                        new AgentRuntimeException("Unexpected workspace write failure.", error));
            }
        }
    }

    /**
     * 在指定 1-based 行号插入文本。
     */
    private static final class InsertTextFileTool implements AgentTool {

        private final WorkspacePathGuard pathGuard;
        private final WorkspaceQuotaGuard quotaGuard;

        private InsertTextFileTool(WorkspacePathGuard pathGuard, WorkspaceQuotaGuard quotaGuard) {
            this.pathGuard = pathGuard;
            this.quotaGuard = quotaGuard;
        }

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

        /**
         * 获取工具描述。
         *
         * @return 工具描述
         */
        @Override
        public String getDescription() {
            return "Insert UTF-8 content at a 1-based line number in an existing workspace file.";
        }

        /**
         * 获取与 AgentScope 1.x 兼容的参数 Schema。
         *
         * @return JSON Schema
         */
        @Override
        public Map<String, Object> getParameters() {
            Map<String, Object> properties = new LinkedHashMap<>();
            properties.put("file_path", Map.of("type", "string", "description", "The target file path"));
            properties.put("content", Map.of("type", "string", "description", "The content to be inserted"));
            properties.put("line_number", Map.of(
                    "type", "integer",
                    "description", "The 1-based line number where content is inserted"));
            return Map.of(
                    "type", "object",
                    "properties", properties,
                    "required", List.of("file_path", "content", "line_number"));
        }

        /**
         * 原子插入文件内容。
         *
         * @param param Tool 调用参数
         * @return Tool 结果
         */
        @Override
        public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
            return Mono.fromCallable(() -> insert(param)).subscribeOn(Schedulers.boundedElastic());
        }

        private ToolResultBlock insert(ToolCallParam param) {
            try {
                String filePath = requiredString(param, "file_path");
                String content = requiredStringAllowEmpty(param, "content");
                int lineNumber = requiredInteger(param, "line_number");
                Path target = pathGuard.resolveExistingFile(filePath);
                quotaGuard.validateFullRead(target);
                List<String> lines = splitLines(WorkspaceTextFiles.readUtf8(target));
                if (lineNumber < 1 || lineNumber > lines.size() + 1) {
                    throw new WorkspaceToolException("INVALID_ARGUMENT",
                            "line_number is outside the valid range [1, "
                                    + (lines.size() + 1) + "].", false);
                }
                List<String> updated = new ArrayList<>(lines);
                updated.addAll(lineNumber - 1, splitContentLines(content));
                byte[] bytes = String.join("\n", updated).getBytes(StandardCharsets.UTF_8);
                quotaGuard.validateWrite(target, bytes.length);
                WorkspaceTextFiles.atomicWrite(pathGuard, target, bytes);
                return ToolResultBlock.text("Insert content into " + pathGuard.display(target)
                        + " at line " + lineNumber + " successfully.");
            } catch (AgentRuntimeException error) {
                return WorkspaceToolResults.error(error);
            } catch (RuntimeException error) {
                return WorkspaceToolResults.error(
                        new AgentRuntimeException("Unexpected workspace insert failure.", error));
            }
        }
    }

    private static String requiredString(ToolCallParam param, String name) {
        String text = requiredStringAllowEmpty(param, name);
        if (text.isBlank()) {
            throw new WorkspaceToolException("INVALID_ARGUMENT",
                    "Missing required string parameter: " + name, false);
        }
        return text;
    }

    private static String requiredStringAllowEmpty(ToolCallParam param, String name) {
        Object value = param == null ? null : param.getInput().get(name);
        if (!(value instanceof String text)) {
            throw new WorkspaceToolException("INVALID_ARGUMENT",
                    "Missing required string parameter: " + name, false);
        }
        return text;
    }

    private static String optionalString(ToolCallParam param, String name) {
        Object value = param == null ? null : param.getInput().get(name);
        if (value == null) {
            return null;
        }
        if (!(value instanceof String text)) {
            throw new WorkspaceToolException("INVALID_ARGUMENT",
                    "Invalid string parameter: " + name, false);
        }
        return text;
    }

    private static int requiredInteger(ToolCallParam param, String name) {
        Object value = param == null ? null : param.getInput().get(name);
        if (!(value instanceof Number number)) {
            throw new WorkspaceToolException("INVALID_ARGUMENT",
                    "Missing required integer parameter: " + name, false);
        }
        return number.intValue();
    }

    private static int[] parseReplacementRange(String ranges, int lineCount) {
        String normalized = ranges.trim().replace("[", "").replace("]", "");
        String[] parts = normalized.split(",", -1);
        if (parts.length != 2) {
            throw new WorkspaceToolException("INVALID_ARGUMENT",
                    "Invalid range format. Expected 'start,end'.", false);
        }
        try {
            int start = Integer.parseInt(parts[0].trim());
            int end = Integer.parseInt(parts[1].trim());
            if (start < 1 || end < start || start > lineCount || end > lineCount) {
                throw new WorkspaceToolException("INVALID_ARGUMENT",
                        "Replacement range is outside the file.", false);
            }
            return new int[]{start, end};
        } catch (NumberFormatException error) {
            throw new WorkspaceToolException("INVALID_ARGUMENT",
                    "Invalid range format. Expected integer line numbers.", false, error);
        }
    }

    private static List<String> splitLines(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 List<String> splitContentLines(String content) {
        if (content.isEmpty()) {
            return List.of("");
        }
        return List.of(content.replace("\r\n", "\n").replace('\r', '\n').split("\n", -1));
    }
}
