package com.gzzm.lobster.tool;

import com.gzzm.lobster.common.IdGenerator;
import com.gzzm.lobster.common.JsonUtil;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.PendingRequestType;
import com.gzzm.lobster.common.ToolCategory;
import com.gzzm.lobster.common.ToolResultStatus;
import com.gzzm.lobster.common.ToolRiskLevel;
import com.gzzm.lobster.llm.ToolCall;
import com.gzzm.platform.commons.Tools;
import net.cyan.nest.annotation.Inject;

import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

/**
 * ToolExecutorDispatcher —— 统一工具执行入口 / Single entry-point dispatcher.
 *
 * <p>职责：
 * <ol>
 * <li>按 toolName 查工具定义；不存在则返回标准化错误给 LLM 修正</li>
 * <li>权限校验</li>
 * <li>速率限制（按风险分层的默认值）</li>
 * <li>参数解析（JSON → Map）</li>
 * <li>调用 executor + 记录耗时</li>
 * <li>写审计</li>
 * </ol>
 *
 * <p>V2 新增（2026-04）：
 * <ul>
 *   <li><b>timeout</b>：若 {@link BuiltinToolDefinition#getDefaultTimeoutMs()} &gt; 0，
 *       工具执行会被包进 {@code Future.get(timeout)}；超时直接 {@code cancel(true)}
 *       并返回 {@code tool.cancelled} 错误</li>
 *   <li><b>cancel 传播</b>：调度前 check {@link ToolContext#isCancelled()}；
 *       命中直接返回 {@code tool.cancelled} 错误，不进 executor</li>
 *   <li><b>不</b>做幂等键、<b>不</b>做自动重试、<b>不</b>做补偿事务 —— 故意避免过度设计</li>
 * </ul>
 */
public class ToolExecutorDispatcher {

    @Inject
    private ToolRegistry toolRegistry;

    @Inject
    private ToolPermissionChecker permissionChecker;

    @Inject
    private ToolRateLimiter rateLimiter;

    @Inject
    private ToolAuditLogger auditLogger;

    @Inject
    private ToolExecutionRecordDao executionRecordDao;

    private String startToolExecution(ToolContext ctx, ToolCall call, BuiltinToolDefinition def,
                                      ToolExecutor executor, Map<String, Object> args) {
        if (ctx == null || call == null || def == null) return null;
        try {
            ToolExecutionRecordDao dao = executionRecordDao();
            if (dao == null) return null;
            ToolExecutionRecord r = new ToolExecutionRecord();
            r.setExecutionId(IdGenerator.prefixed("tex_"));
            r.setRunId(ctx.getRunId());
            r.setThreadId(ctx.getThreadId());
            r.setToolCallId(ctx.getToolCallId());
            r.setToolName(call.getName());
            r.setSideEffect(def.getSideEffect() == null ? null : def.getSideEffect().name());
            r.setStatus("started");
            r.setArgumentsJson(recordedArgumentsJson(executor, args));
            r.setStartedAt(new Date());
            dao.save(r);
            return r.getExecutionId();
        } catch (Throwable t) {
            try { Tools.log("[ToolExecutionRecord] start failed", t); } catch (Throwable ignore) { /* ignore */ }
            return null;
        }
    }

    private void finishToolExecution(String executionId, String status, String resultJson,
                                     String errorMessage, long durationMs) {
        if (executionId == null || executionId.isEmpty()) return;
        try {
            ToolExecutionRecordDao dao = executionRecordDao();
            if (dao == null) return;
            dao.finish(status, resultJson, truncate(errorMessage, 2000),
                    durationMs < 0 ? null : Long.valueOf(durationMs), new Date(), executionId);
        } catch (Throwable t) {
            try { Tools.log("[ToolExecutionRecord] finish failed: " + executionId, t); }
            catch (Throwable ignore) { /* ignore */ }
        }
    }

    private ToolExecutionRecordDao executionRecordDao() {
        try {
            ToolExecutionRecordDao d = Tools.getBean(ToolExecutionRecordDao.class);
            if (d != null) return d;
        } catch (Throwable ignore) { /* fallback */ }
        return executionRecordDao;
    }

    private static String truncate(String s, int max) {
        if (s == null || max <= 0 || s.length() <= max) return s;
        return s.substring(0, max);
    }

    /**
     * 工具执行线程池 / Per-tool executor.
     *
     * <p>仅在 {@code defaultTimeoutMs > 0} 的工具走 Future.get(timeout) 路径时用。
     * 零超时工具仍在调用线程同步跑，避免不必要的线程切换。
     */
    private final ExecutorService toolPool = Executors.newCachedThreadPool(r -> {
        Thread t = new Thread(r, "lobster-tool-exec");
        t.setDaemon(true);
        return t;
    });

    public ToolResult dispatch(ToolContext ctx, ToolCall call) {
        long start = System.currentTimeMillis();

        // —— 调度前取消检查 —— //
        if (ctx != null && ctx.isCancelled()) {
            auditLogger.record(ctx, call.getName(), Collections.<String, Object>emptyMap(),
                    "cancelled", 0);
            return ToolResult.error("tool cancelled before dispatch");
        }

        BuiltinToolDefinition def = toolRegistry.find(call.getName());
        if (def == null) {
            ToolResult err = ToolResult.error("tool not found: " + call.getName());
            auditLogger.record(ctx, call.getName(), Collections.<String, Object>emptyMap(),
                    "error.not_found", (int) (System.currentTimeMillis() - start));
            return err;
        }
        Map<String, Object> args;
        try {
            args = JsonUtil.fromJsonToMap(call.getArgumentsJson());
        } catch (Throwable t) {
            ToolResult err = ToolResult.error("invalid arguments JSON: " + t.getMessage());
            auditLogger.record(ctx, call.getName(), Collections.<String, Object>emptyMap(),
                    "error.bad_args", (int) (System.currentTimeMillis() - start));
            return err;
        }
        // —— args 解析完立即取 executor —— //
        // 脱敏钩子必须在所有 audit 路径（denied / rate_limit / ok / error / cancelled / exception）
        // 生效。之前 denied/rate_limit 直接 record(args) 会把 code_exec.code 原文落审计，违反红线.
        ToolExecutor executor = toolRegistry.executor(call.getName());
        ToolResult permissionDenied = checkMcpReplayPermission(ctx, call, def, executor, args, start);
        if (permissionDenied != null) return permissionDenied;
        ToolResult recorded = replayRecordedToolResult(ctx, call, def, executor, args);
        if (recorded != null) {
            String replayAudit = recorded.getStatus() == ToolResultStatus.error
                    ? "error.replay_blocked" : "replayed";
            auditLogger.record(ctx, call.getName(), Collections.<String, Object>emptyMap(),
                    replayAudit, (int) (System.currentTimeMillis() - start));
            return recorded;
        }
        String executionId = startToolExecution(ctx, call, def, executor, args);
        if (executionId == null && !isReadOnly(def)) {
            ToolResult err = ToolResult.error("tool execution record unavailable; side-effectful tool was not executed");
            auditLogger.record(ctx, call.getName(), Collections.<String, Object>emptyMap(),
                    "error.record_unavailable", (int) (System.currentTimeMillis() - start));
            return err;
        }

        try {
            permissionChecker.check(def, ctx.getUserContext());
        } catch (LobsterException e) {
            Map<String, Object> auditArgs = redactOrDefault(executor, args, null);
            auditLogger.record(ctx, call.getName(), auditArgs, "denied", (int) (System.currentTimeMillis() - start));
            finishToolExecution(executionId, "denied", null, e.getMessage(), System.currentTimeMillis() - start);
            return ToolResult.error(e.getMessage());
        }
        try {
            Integer override = def.getRateLimitPerMinute();
            int perMinute = (override != null && override > 0) ? override : rateLimitFor(def.getRiskLevel());
            rateLimiter.acquire(ctx.getUserId(), def.getToolName(), perMinute);
        } catch (LobsterException e) {
            Map<String, Object> auditArgs = redactOrDefault(executor, args, null);
            auditLogger.record(ctx, call.getName(), auditArgs, "rate_limit", (int) (System.currentTimeMillis() - start));
            finishToolExecution(executionId, "rate_limit", null, e.getMessage(), System.currentTimeMillis() - start);
            return ToolResult.error(e.getMessage());
        }

        // —— 实际执行：按定义是否声明 timeout 走两条路径 —— //
        final ToolContext finalCtx = ctx;
        final Map<String, Object> finalArgs = args;
        Callable<ToolResult> task = () -> executor.execute(finalCtx, finalArgs);

        long effectiveTimeoutMs = effectiveTimeoutMs(def, ctx);
        try {
            ToolResult result;
            if (effectiveTimeoutMs > 0) {
                result = runWithTimeout(task, effectiveTimeoutMs, call.getName());
            } else {
                result = task.call();
            }
            if (result == null) result = ToolResult.error("tool returned null");
            String auditResult = result.getStatus() == ToolResultStatus.ok ? "ok"
                    : (result.getStatus() == ToolResultStatus.pending ? "pending" : "error");
            Map<String, Object> auditArgs = redactOrDefault(executor, args, result);
            auditLogger.record(ctx, call.getName(), auditArgs, auditResult, (int) (System.currentTimeMillis() - start));
            finishToolExecution(executionId, auditResult, result.toToolMessageContent(), null,
                    System.currentTimeMillis() - start);
            return result;
        } catch (ToolCancelledException tc) {
            Map<String, Object> auditArgs = redactOrDefault(executor, args, null);
            auditLogger.record(ctx, call.getName(), auditArgs, "cancelled",
                    (int) (System.currentTimeMillis() - start));
            finishToolExecution(executionId, "cancelled", null, tc.getMessage(), System.currentTimeMillis() - start);
            return ToolResult.error(tc.getMessage() == null ? "tool cancelled" : tc.getMessage());
        } catch (Throwable t) {
            try {
                Tools.log("[ToolExecutor] " + call.getName() + " failed", t);
            } catch (Throwable ignore) { /* ignore */ }
            Map<String, Object> auditArgs = redactOrDefault(executor, args, null);
            auditLogger.record(ctx, call.getName(), auditArgs, "exception",
                    (int) (System.currentTimeMillis() - start));
            finishToolExecution(executionId, "exception", null,
                    t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage(),
                    System.currentTimeMillis() - start);
            return ToolResult.error("tool " + call.getName() + " failed: " + (t.getMessage() == null ? t.getClass().getSimpleName() : t.getMessage()));
        }
    }

    /**
     * 调 executor.redactAuditDetail.
     * <p><b>Fail-closed</b>：如果 redact 实现抛异常或调不到 executor，不再回落到原始 args
     * —— 某些工具 args 本身就含敏感正文（code_exec 的 code 是典型例子），一次实现 bug
     * 不能让原文落审计. 退路：抹掉已知敏感字段、只留安全元信息.
     *
     * @return 永不返 null；要么是 executor 给的脱敏 map，要么是强制剥敏的 fallback.
     */
    private Map<String, Object> redactOrDefault(ToolExecutor executor, Map<String, Object> args, ToolResult result) {
        if (executor != null) {
            try {
                Map<String, Object> d = executor.redactAuditDetail(args, result);
                if (d != null) return d;
            } catch (Throwable t) {
                try { Tools.log("[ToolExecutor] redactAuditDetail failed — falling back to hard-redact", t); }
                catch (Throwable ignore) { /* ignore */ }
                return hardRedact(args);
            }
        }
        // executor.redactAuditDetail 返 null → 默认策略仍把敏感字段抹掉
        return hardRedact(args);
    }

    private ToolResult checkMcpReplayPermission(ToolContext ctx, ToolCall call,
                                                BuiltinToolDefinition def,
                                                ToolExecutor executor,
                                                Map<String, Object> args,
                                                long start) {
        if (def == null || def.getCategory() != ToolCategory.MCP) return null;
        try {
            permissionChecker.check(def, ctx == null ? null : ctx.getUserContext());
            return null;
        } catch (LobsterException e) {
            Map<String, Object> auditArgs = redactOrDefault(executor, args, null);
            auditLogger.record(ctx, call.getName(), auditArgs, "denied",
                    (int) (System.currentTimeMillis() - start));
            return ToolResult.error(e.getMessage());
        }
    }

    private ToolResult replayRecordedToolResult(ToolContext ctx, ToolCall call,
                                                BuiltinToolDefinition def,
                                                ToolExecutor executor,
                                                Map<String, Object> args) {
        if (ctx == null || call == null || ctx.getRunId() == null || ctx.getToolCallId() == null) return null;
        try {
            ToolExecutionRecordDao dao = executionRecordDao();
            if (dao == null) return null;
            List<ToolExecutionRecord> records = dao.listByRunAndCall(ctx.getRunId(), ctx.getToolCallId());
            if (records == null || records.isEmpty()) return null;
            ToolExecutionRecord latest = records.get(0);
            if (latest == null || latest.getStatus() == null) return null;
            if (!matchesRecordedToolCall(latest, call, executor, args)) {
                if ("READ".equals(latest.getSideEffect())) return null;
                return ToolResult.error("tool execution identity mismatch after backend recovery; not re-executed to avoid duplicate side effects");
            }
            String status = latest.getStatus();
            if ("started".equals(status)) {
                if ("READ".equals(latest.getSideEffect())) return null;
                return ToolResult.error("tool execution state is unknown after backend recovery; not re-executed to avoid duplicate side effects");
            }
            return resultFromRecord(latest);
        } catch (Throwable t) {
            try { Tools.log("[ToolExecutionRecord] replay lookup failed", t); } catch (Throwable ignore) { /* ignore */ }
            if (!isReadOnly(def)) {
                return ToolResult.error("tool execution record lookup failed; side-effectful tool was not executed to avoid duplicate side effects");
            }
            return null;
        }
    }

    private boolean matchesRecordedToolCall(ToolExecutionRecord record, ToolCall call,
                                            ToolExecutor executor, Map<String, Object> args) {
        if (record == null || call == null) return false;
        if (!Objects.equals(record.getToolName(), call.getName())) return false;
        RecordedArguments recorded = parseRecordedArguments(record.getArgumentsJson());
        if (recorded.fingerprint != null) {
            return Objects.equals(recorded.fingerprint, fingerprintArguments(args));
        }
        if (containsSensitiveKey(args)) return false;
        if (!recorded.legacyComparable) return false;
        return Objects.equals(canonicalJson(recorded.redactedArgs),
                canonicalJson(redactOrDefault(executor, args, null)));
    }

    private String recordedArgumentsJson(ToolExecutor executor, Map<String, Object> args) {
        Map<String, Object> record = new LinkedHashMap<>();
        record.put("_recordVersion", Integer.valueOf(2));
        record.put("argumentsFingerprint", fingerprintArguments(args));
        record.put("redactedArgs", redactOrDefault(executor, args, null));
        return JsonUtil.toJson(record);
    }

    private String fingerprintArguments(Map<String, Object> args) {
        String canonical = canonicalJson(args == null ? Collections.<String, Object>emptyMap() : args);
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] bytes = digest.digest(canonical.getBytes(StandardCharsets.UTF_8));
            StringBuilder sb = new StringBuilder(bytes.length * 2);
            for (byte b : bytes) sb.append(String.format("%02x", b & 0xff));
            return sb.toString();
        } catch (Throwable t) {
            throw new LobsterException("tool.args_fingerprint_failed",
                    "Failed to fingerprint tool arguments", t);
        }
    }

    private String canonicalJson(Object value) {
        return JsonUtil.toJson(normalizeForFingerprint(value));
    }

    @SuppressWarnings("unchecked")
    private RecordedArguments parseRecordedArguments(String rawJson) {
        if (rawJson == null || rawJson.isEmpty()) {
            return new RecordedArguments(null, Collections.<String, Object>emptyMap(), true);
        }
        try {
            Map<String, Object> parsed = JsonUtil.fromJsonToMap(rawJson);
            if (parsed == null) {
                return new RecordedArguments(null, Collections.<String, Object>emptyMap(), true);
            }
            Object fp = parsed.get("argumentsFingerprint");
            Object redacted = parsed.get("redactedArgs");
            if (fp != null && redacted instanceof Map) {
                return new RecordedArguments(String.valueOf(fp),
                        (Map<String, Object>) redacted, false);
            }
            boolean comparable = !containsRedactedMarker(parsed);
            return new RecordedArguments(null, parsed, comparable);
        } catch (Throwable ignore) {
            return new RecordedArguments(null, Collections.<String, Object>emptyMap(), false);
        }
    }

    private boolean containsRedactedMarker(Object value) {
        if (value instanceof Map) {
            for (Object v : ((Map<?, ?>) value).values()) {
                if (containsRedactedMarker(v)) return true;
            }
            return false;
        }
        if (value instanceof Iterable) {
            for (Object item : (Iterable<?>) value) {
                if (containsRedactedMarker(item)) return true;
            }
            return false;
        }
        return value instanceof String && ((String) value).startsWith("<redacted:");
    }

    private boolean containsSensitiveKey(Object value) {
        if (value instanceof Map) {
            for (Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet()) {
                Object key = e.getKey();
                if (key != null && SENSITIVE_KEYS.contains(String.valueOf(key).toLowerCase())) {
                    return true;
                }
                if (containsSensitiveKey(e.getValue())) return true;
            }
            return false;
        }
        if (value instanceof Iterable) {
            for (Object item : (Iterable<?>) value) {
                if (containsSensitiveKey(item)) return true;
            }
        }
        return false;
    }

    private static final class RecordedArguments {
        final String fingerprint;
        final Map<String, Object> redactedArgs;
        final boolean legacyComparable;

        RecordedArguments(String fingerprint, Map<String, Object> redactedArgs,
                          boolean legacyComparable) {
            this.fingerprint = fingerprint;
            this.redactedArgs = redactedArgs == null
                    ? Collections.<String, Object>emptyMap() : redactedArgs;
            this.legacyComparable = legacyComparable;
        }
    }

    @SuppressWarnings("unchecked")
    private Object normalizeForFingerprint(Object value) {
        if (value instanceof Map) {
            TreeMap<String, Object> sorted = new TreeMap<>();
            for (Map.Entry<?, ?> e : ((Map<?, ?>) value).entrySet()) {
                if (e.getKey() != null) {
                    sorted.put(String.valueOf(e.getKey()), normalizeForFingerprint(e.getValue()));
                }
            }
            return sorted;
        }
        if (value instanceof Iterable) {
            List<Object> out = new ArrayList<>();
            for (Object item : (Iterable<?>) value) out.add(normalizeForFingerprint(item));
            return out;
        }
        return value;
    }

    @SuppressWarnings("unchecked")
    private ToolResult resultFromRecord(ToolExecutionRecord record) {
        String status = record.getStatus();
        String raw = record.getResultJson();
        Map<String, Object> body = Collections.emptyMap();
        if (raw != null && !raw.isEmpty()) {
            try {
                Map<String, Object> parsed = JsonUtil.fromJsonToMap(raw);
                if (parsed != null) body = parsed;
            } catch (Throwable ignore) { /* fall back below */ }
        }
        String message = str(body.get("message"), record.getErrorMessage());
        Object dataObj = body.get("data");
        Map<String, Object> data = dataObj instanceof Map
                ? (Map<String, Object>) dataObj : Collections.<String, Object>emptyMap();
        if ("ok".equals(status)) {
            List<String> artifactIds = stringList(body.get("artifactIds"));
            return artifactIds.isEmpty()
                    ? ToolResult.ok(message, data)
                    : ToolResult.okWithArtifacts(message, data, artifactIds);
        }
        if ("pending".equals(status)) {
            String pendingId = str(body.get("pendingRequestId"), str(data.get("pendingRequestId"), null));
            String typeName = str(data.get("type"), null);
            PendingRequestType type = PendingRequestType.confirm_action;
            if (typeName != null && !typeName.isEmpty()) {
                try { type = PendingRequestType.valueOf(typeName); } catch (Throwable ignore) { /* default */ }
            }
            String title = str(data.get("title"), message);
            return ToolResult.pending(pendingId, type, title, message);
        }
        return data.isEmpty() ? ToolResult.error(message) : ToolResult.errorData(message, data);
    }

    private boolean isReadOnly(BuiltinToolDefinition def) {
        return def == null || def.getSideEffect() == null || def.getSideEffect() == SideEffectLevel.READ;
    }

    private static String str(Object value, String fallback) {
        if (value == null) return fallback;
        String s = String.valueOf(value);
        return s.isEmpty() ? fallback : s;
    }

    private static List<String> stringList(Object value) {
        if (!(value instanceof Iterable)) return Collections.emptyList();
        List<String> out = new ArrayList<>();
        for (Object item : (Iterable<?>) value) {
            if (item != null) out.add(String.valueOf(item));
        }
        return out;
    }

    /**
     * 强制剥敏：对已知敏感字段（code / password / api_key / token / secret）直接替换为
     * {@code <redacted:N>}（N = 原长度），其它字段原样. 兜底路径，宁可审计信息不全，
     * 也不能让原文泄露.
     */
    private static final java.util.Set<String> SENSITIVE_KEYS =
            new java.util.HashSet<>(java.util.Arrays.asList(
                    "code", "content", "password", "passwd", "api_key", "apikey", "token",
                    "access_token", "refresh_token", "secret", "secret_key",
                    "private_key", "authorization"));

    private static Map<String, Object> hardRedact(Map<String, Object> args) {
        if (args == null || args.isEmpty()) return java.util.Collections.emptyMap();
        Map<String, Object> out = new java.util.LinkedHashMap<>(args.size());
        for (Map.Entry<String, Object> e : args.entrySet()) {
            String k = e.getKey();
            Object v = e.getValue();
            if (k != null && SENSITIVE_KEYS.contains(k.toLowerCase())) {
                String s = v == null ? "" : String.valueOf(v);
                out.put(k, "<redacted:" + s.length() + ">");
            } else {
                out.put(k, v);
            }
        }
        return out;
    }

    /**
     * 计算实际 timeout / Compute effective timeout.
     *
     * <p>取 {@link BuiltinToolDefinition#getDefaultTimeoutMs()} 与 run deadline 的最小值。
     * 两者都未设置返回 0（不限时）。
     */
    private long effectiveTimeoutMs(BuiltinToolDefinition def, ToolContext ctx) {
        long toolTo = def == null ? 0 : def.getDefaultTimeoutMs();
        long runRemain = 0;
        if (ctx != null && ctx.getDeadlineAtMs() > 0) {
            runRemain = Math.max(0, ctx.getDeadlineAtMs() - System.currentTimeMillis());
        }
        if (toolTo <= 0) return runRemain;
        if (runRemain <= 0) return toolTo;
        return Math.min(toolTo, runRemain);
    }

    /**
     * 带超时的工具执行 / Run tool with timeout via Future.
     *
     * <p>超时时 {@code future.cancel(true)} 发出 interrupt 信号；工具是否响应 interrupt
     * 由工具自身决定（Claude Code 同样不做更强保证）。不响应 interrupt 的工具会在
     * 后台线程继续跑到结束 —— dispatcher 已经返回 {@code tool.cancelled}，资源泄漏可控。
     */
    private ToolResult runWithTimeout(Callable<ToolResult> task, long timeoutMs, String toolName) throws Exception {
        Future<ToolResult> future = toolPool.submit(task);
        try {
            return future.get(timeoutMs, TimeUnit.MILLISECONDS);
        } catch (TimeoutException te) {
            future.cancel(true);
            throw new ToolCancelledException(com.gzzm.lobster.llm.CancelReason.TIMEOUT,
                    "tool " + toolName + " timed out after " + timeoutMs + "ms");
        } catch (java.util.concurrent.ExecutionException ee) {
            Throwable cause = ee.getCause();
            if (cause instanceof Exception) throw (Exception) cause;
            if (cause instanceof Error) throw (Error) cause;
            throw ee;
        }
    }

    /** 按风险等级映射默认速率限额；生产可在 QuotaPolicy 中精细覆盖。 */
    private int rateLimitFor(ToolRiskLevel risk) {
        if (risk == null) return 60;
        switch (risk) {
            case READ_ONLY: return 120;
            case WRITE: return 60;
            case BATCH_WRITE: return 20;
            case DESTRUCTIVE: return 10;
            default: return 60;
        }
    }
}
