package com.gzzm.lobster.tool.builtin;

import com.gzzm.lobster.artifact.Artifact;
import com.gzzm.lobster.artifact.ArtifactService;
import com.gzzm.lobster.common.*;
import com.gzzm.lobster.identity.UserContext;
import com.gzzm.lobster.oa.OaFileClient;
import com.gzzm.lobster.pending.PendingRequest;
import com.gzzm.lobster.pending.PendingRequestService;
import com.gzzm.lobster.thread.ThreadRoom;
import com.gzzm.lobster.thread.ThreadService;
import com.gzzm.lobster.tool.*;
import com.gzzm.lobster.workspace.ResourceMetadata;
import com.gzzm.lobster.workspace.WorkspaceResource;
import com.gzzm.lobster.workspace.WorkspaceService;
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.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
 * WorkspaceResourceTools —— Layer 1 工作区资源工具 /
 * Layer 1 workspace resource tools: list_files / search_files / read_file /
 * write_file / save_to_oa.
 */
public class WorkspaceResourceTools {

    @Inject private ThreadService threadService;
    @Inject private WorkspaceService workspaceService;
    @Inject private ArtifactService artifactService;
    @Inject private PendingRequestService pendingRequestService;
    @Inject private OaFileClient oaFileClient;

    public void registerTo(ToolRegistry registry) {
        registry.register(listFilesDef(), this::listFiles);
        registry.register(searchFilesDef(), this::searchFiles);
        registry.register(readFileDef(), this::readFile);
        registry.register(writeFileDef(), new ToolExecutor() {
            @Override
            public ToolResult execute(ToolContext ctx, Map<String, Object> args) throws Exception {
                return writeFile(ctx, args);
            }

            @Override
            public Map<String, Object> redactAuditDetail(Map<String, Object> args, ToolResult result) {
                return redactWriteFileAuditDetail(args, result);
            }
        });
        registry.register(saveToOaDef(), this::saveToOa);
        registry.register(saveResourceSummaryDef(), this::saveResourceSummary);
    }

    // -------------- list_files --------------

    private BuiltinToolDefinition listFilesDef() {
        return BuiltinToolDefinition.builder()
                .name("list_files")
                .displayName("列出工作区资源")
                .description("列出当前工作区中的资源（OA 文件引用 / 工件 / 文档工坊文档）。"
                        + "可按 sourceType 和 artifactType 筛选。只返回薄索引，不返回原文；"
                        + "需要获取资源区文件文本内容时，使用 read_file 并传 resourceId。")
                .category(ToolCategory.WORKSPACE)
                .risk(ToolRiskLevel.READ_ONLY)
                .inputSchema(SchemaBuilder.obj()
                        .propEnum("sourceType", "资源来源类型筛选", "OA_FILE", "ARTIFACT", "WORKSHOP_DOC", "ALL")
                        .prop("artifactType", "string", "工件类型筛选（可选，仅 sourceType=ARTIFACT 时生效）")
                        .propInt("page", "页码，从 1 开始")
                        .propInt("pageSize", "每页数量，默认 20，上限 50")
                        .build())
                .build();
    }

    private ToolResult listFiles(ToolContext ctx, Map<String, Object> args) throws Exception {
        String sourceType = asStr(args.get("sourceType"));
        int page = Math.max(1, asInt(args.get("page"), 1));
        int pageSize = clamp(asInt(args.get("pageSize"), 20), 1, 50);
        ThreadRoom thread = threadService.requireOwnedThread(ctx.getUserContext(), ctx.getThreadId());
        ResourceSourceType filter = null;
        if (sourceType != null && !sourceType.isEmpty() && !"ALL".equalsIgnoreCase(sourceType)) {
            try { filter = ResourceSourceType.valueOf(sourceType); } catch (Exception e) { /* ignore */ }
        }
        List<WorkspaceResource> list = workspaceService.listResources(thread.getThreadId(), filter,
                (page - 1) * pageSize, pageSize);
        long total = workspaceService.countResources(thread.getThreadId());
        List<Map<String, Object>> items = new ArrayList<>();
        for (WorkspaceResource r : list) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("resourceId", r.getResourceId());
            row.put("displayName", r.getDisplayName());
            row.put("sourceType", r.getSourceType().name());
            addOaFileType(row, r);
            row.put("artifactType", r.getArtifactType());
            row.put("mimeType", r.getMimeType());
            row.put("status", r.getStatus() == null ? null : r.getStatus().name());
            row.put("updatedAt", r.getUpdateTime());
            // summary 可能是之前 assistant 通过 save_resource_summary 写入的语义摘要；
            // 没有就返回 null，LLM 自己决定要不要 read_file 补上.
            String summary = ResourceMetadata.getSummary(r);
            if (summary != null) row.put("summary", summary);
            items.add(row);
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("items", items);
        data.put("total", total);
        data.put("hasMore", (long) page * pageSize < total);
        return ToolResult.okData(data);
    }

    // -------------- search_files --------------

    private BuiltinToolDefinition searchFilesDef() {
        return BuiltinToolDefinition.builder()
                .name("search_files")
                .displayName("按标题搜索工作区资源")
                .description("在当前工作区按关键词搜索文件和工件的【标题】和元信息。"
                        + "不搜索文件正文全文。需要看正文时请用 read_file。")
                .category(ToolCategory.WORKSPACE)
                .risk(ToolRiskLevel.READ_ONLY)
                .inputSchema(SchemaBuilder.obj()
                        .prop("query", "string", "关键词")
                        .propInt("maxResults", "最大返回数，默认 10")
                        .required("query")
                        .build())
                .build();
    }

    private ToolResult searchFiles(ToolContext ctx, Map<String, Object> args) throws Exception {
        String query = asStr(args.get("query"));
        int maxResults = clamp(asInt(args.get("maxResults"), 10), 1, 20);
        ThreadRoom thread = threadService.requireOwnedThread(ctx.getUserContext(), ctx.getThreadId());
        List<WorkspaceResource> hits = workspaceService.searchByTitle(thread.getThreadId(), query, maxResults);
        List<Map<String, Object>> items = new ArrayList<>();
        for (WorkspaceResource r : hits) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("resourceId", r.getResourceId());
            row.put("displayName", r.getDisplayName());
            row.put("sourceType", r.getSourceType().name());
            addOaFileType(row, r);
            row.put("artifactType", r.getArtifactType());
            items.add(row);
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("items", items);
        data.put("total", items.size());
        return ToolResult.okData(data);
    }

    // -------------- read_file --------------

    private BuiltinToolDefinition readFileDef() {
        return BuiltinToolDefinition.builder()
                .name("read_file")
                .displayName("读取资源内容")
                .description("读取工作区资源区中某个文件或工件的文本内容。"
                        + "适用于 USER_UPLOAD / OA_FILE / ARTIFACT / WORKSHOP_DOC；"
                        + "传入资源列表中的 resourceId，不要传 OA fileId。大文件自动分段，返回截断信息。")
                .category(ToolCategory.WORKSPACE)
                .risk(ToolRiskLevel.READ_ONLY)
                .inputSchema(SchemaBuilder.obj()
                        .prop("resourceId", "string", "Resource ID")
                        .propInt("offset", "起始字符索引，默认 0")
                        .propInt("limit", "最大读取字符数，默认 8000，上限 16000")
                        .required("resourceId")
                        .build())
                .build();
    }

    private ToolResult readFile(ToolContext ctx, Map<String, Object> args) throws Exception {
        String resourceId = asStr(args.get("resourceId"));
        int offset = Math.max(0, asInt(args.get("offset"), 0));
        int limit = clamp(asInt(args.get("limit"), 8000), 1, 16000);
        threadService.requireOwnedThread(ctx.getUserContext(), ctx.getThreadId());
        String content = workspaceService.readResource(ctx.getUserContext(), ctx.getThreadId(),
                resourceId, offset, limit);
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("resourceId", resourceId);
        data.put("content", content == null ? "" : content);
        data.put("offset", offset);
        data.put("limit", limit);
        data.put("truncated", content != null && content.length() >= limit);
        return ToolResult.okData(data);
    }

    // -------------- write_file --------------

    private BuiltinToolDefinition writeFileDef() {
        return BuiltinToolDefinition.builder()
                .name("write_file")
                .displayName("创建/覆盖资源")
                .description("在 workspace 内创建或覆盖一个资源，也用于写代码文件。"
                        + "当需要让 code_exec 执行较长脚本、生成 PPT/Word/Excel/PDF/HTML 的完整脚本，"
                        + "或需要反复修改调试脚本时，必须用本工具创建 artifactType=CODE_SCRIPT，"
                        + "随后把返回的 artifactId 传给 code_exec.code_ref；"
                        + "仅在 workspace 内操作，不直接修改 OA 原文件。需要出站到 OA 请用 save_to_oa。")
                .category(ToolCategory.WORKSPACE)
                .risk(ToolRiskLevel.WRITE)
                .inputSchema(SchemaBuilder.obj()
                        .prop("displayName", "string", "显示名；写脚本时使用清晰文件名，如 generate_ppt.js 或 process_data.py")
                        .prop("content", "string", "写入的文本内容；artifactType=CODE_SCRIPT 时这里是完整源码")
                        .prop("mimeType", "string", "可选，默认 text/plain；脚本建议 text/x-python 或 application/javascript")
                        .prop("targetResourceId", "string", "可选；提供则覆盖已有资源")
                        .propEnum("artifactType", "可选的工件类型声明；CODE_SCRIPT 表示代码脚本，创建后用 artifactId 作为 code_exec.code_ref",
                                "GENERATED_DOCUMENT", "WORKING_COPY", "EXTRACTED_TABLE",
                                "TASK_LEDGER", "SUMMARY_SNAPSHOT", "MERGED_RESULT",
                                "PENDING_ACTION_OBJECT", "CODE_SCRIPT")
                        .prop("sourceResourceId", "string", "可选；创建 WORKING_COPY 时的源资源 ID")
                        .required("displayName", "content")
                        .build())
                .build();
    }

    @SuppressWarnings("unchecked")
    private ToolResult writeFile(ToolContext ctx, Map<String, Object> args) throws Exception {
        String displayName = asStr(args.get("displayName"));
        String content = asStr(args.get("content"));
        String mimeType = asStr(args.get("mimeType"));
        String targetResourceId = asStr(args.get("targetResourceId"));
        String artifactTypeStr = asStr(args.get("artifactType"));
        String sourceResourceId = asStr(args.get("sourceResourceId"));
        ThreadRoom thread = threadService.requireOwnedThread(ctx.getUserContext(), ctx.getThreadId());
        UserContext user = ctx.getUserContext();

        ArtifactType artifactType = ArtifactType.GENERATED_DOCUMENT;
        if (artifactTypeStr != null && !artifactTypeStr.isEmpty()) {
            try { artifactType = ArtifactType.valueOf(artifactTypeStr); } catch (Exception e) { /* ignore */ }
        }

        if (targetResourceId != null && !targetResourceId.isEmpty()) {
            // 覆盖已有资源 —— targetResourceId 可能是 res_xxx (WorkspaceResource) 或 art_xxx (Artifact).
            // 与 save_to_oa 同样的 ID 混用问题：res_xxx 要先 getResource 取出真实 artifactId，
            // 不能直接 overwrite(resourceId). 统一解析 + 归属校验.
            String resolvedResourceId = null;
            String artifactId;
            if (targetResourceId.startsWith("res_")) {
                WorkspaceResource r = workspaceService.getResource(targetResourceId);
                if (r == null) return ToolResult.error("resource not found: " + targetResourceId);
                if (!thread.getThreadId().equals(r.getThreadId())
                        || !user.getUserId().equals(r.getUserId())) {
                    return ToolResult.error("resource not owned by current thread/user");
                }
                if (r.getSourceType() != ResourceSourceType.ARTIFACT
                        && r.getSourceType() != ResourceSourceType.WORKSHOP_DOC) {
                    return ToolResult.error("write_file overwrite only supports ARTIFACT / WORKSHOP_DOC; got "
                            + r.getSourceType());
                }
                resolvedResourceId = targetResourceId;
                artifactId = r.getSourceId();
            } else {
                // art_xxx 或其它 —— 直接按 artifactId 走
                artifactId = targetResourceId;
            }
            Artifact updated = artifactService.overwrite(artifactId, user, content, displayName);
            Map<String, Object> data = new LinkedHashMap<>();
            // resourceId 与 artifactId 是两回事，分别填；向后兼容仍保留 resourceId 字段.
            data.put("resourceId", resolvedResourceId != null ? resolvedResourceId : updated.getArtifactId());
            data.put("artifactId", updated.getArtifactId());
            data.put("sourceType", "ARTIFACT");
            data.put("artifactType", updated.getArtifactType().name());
            data.put("action", "OVERWRITTEN");
            data.put("version", updated.getVersion());
            return ToolResult.ok("已覆盖资源", data);
        }

        // 创建新 artifact
        String format = guessFormat(mimeType);
        Artifact a = artifactService.create(thread, user, artifactType, displayName, content, format,
                null, ctx.getRunId(), sourceResourceId);
        WorkspaceResource r = workspaceService.registerArtifact(thread, user, a);
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("resourceId", r.getResourceId());
        data.put("artifactId", a.getArtifactId());
        data.put("sourceType", r.getSourceType().name());
        data.put("artifactType", a.getArtifactType().name());
        data.put("action", "CREATED");
        return ToolResult.ok("已创建资源", data);
    }

    private Map<String, Object> redactWriteFileAuditDetail(Map<String, Object> args, ToolResult result) {
        Map<String, Object> d = new LinkedHashMap<>();
        if (args != null) {
            putIfPresent(d, "displayName", args.get("displayName"));
            putIfPresent(d, "mimeType", args.get("mimeType"));
            putIfPresent(d, "targetResourceId", args.get("targetResourceId"));
            putIfPresent(d, "artifactType", args.get("artifactType"));
            putIfPresent(d, "sourceResourceId", args.get("sourceResourceId"));
            String content = asStr(args.get("content"));
            if (content != null) {
                d.put("contentLength", Integer.valueOf(content.length()));
                d.put("contentSha256", sha256(content));
            }
        }
        if (result != null) {
            d.put("resultStatus", result.getStatus() == null ? null : result.getStatus().name());
            Map<String, Object> data = result.getData();
            if (data != null) {
                putIfPresent(d, "resultResourceId", data.get("resourceId"));
                putIfPresent(d, "resultArtifactId", data.get("artifactId"));
                putIfPresent(d, "resultAction", data.get("action"));
                putIfPresent(d, "resultVersion", data.get("version"));
            }
        }
        return d;
    }

    // -------------- save_to_oa --------------

    private BuiltinToolDefinition saveToOaDef() {
        return BuiltinToolDefinition.builder()
                .name("save_to_oa")
                .displayName("回写 OA 文件库")
                .description("将工作区中一个资源回写到 OA 文件库。NEW_FILE 直接新建；"
                        + "OVERWRITE_ORIGINAL / SUBMIT_FOR_APPROVAL 会生成 pending_request 请用户确认。")
                .category(ToolCategory.WORKSPACE)
                .risk(ToolRiskLevel.WRITE)
                .inputSchema(SchemaBuilder.obj()
                        .prop("resourceId", "string", "Workspace 内要回写的资源 ID")
                        .propEnum("saveMode", "回写模式",
                                "NEW_FILE", "OVERWRITE_ORIGINAL", "SUBMIT_FOR_APPROVAL")
                        .prop("displayName", "string", "保存为新文件时的文件名（可选）")
                        .required("resourceId", "saveMode")
                        .build())
                .build();
    }

    private ToolResult saveToOa(ToolContext ctx, Map<String, Object> args) throws Exception {
        String ref = asStr(args.get("resourceId"));
        String saveMode = asStr(args.get("saveMode"));
        String displayName = asStr(args.get("displayName"));
        ThreadRoom thread = threadService.requireOwnedThread(ctx.getUserContext(), ctx.getThreadId());
        UserContext user = ctx.getUserContext();

        // ref 可能是：
        //   res_xxx (WorkspaceResource.resourceId，code_exec.produced 返回的标准形态)
        //   art_xxx (Artifact.artifactId，兼容老调用方)
        // 统一解析成 (resourceId?, artifactId) —— 真实 artifact + workspace 归属校验双路径.
        String resourceId = null;
        String artifactId;
        if (ref != null && ref.startsWith("res_")) {
            WorkspaceResource r = workspaceService.getResource(ref);
            if (r == null) return ToolResult.error("resource not found: " + ref);
            if (!thread.getThreadId().equals(r.getThreadId())
                    || !user.getUserId().equals(r.getUserId())) {
                return ToolResult.error("resource not owned by current thread/user");
            }
            if (r.getSourceType() != com.gzzm.lobster.common.ResourceSourceType.ARTIFACT
                    && r.getSourceType() != com.gzzm.lobster.common.ResourceSourceType.WORKSHOP_DOC) {
                return ToolResult.error("save_to_oa only supports ARTIFACT / WORKSHOP_DOC; got "
                        + r.getSourceType());
            }
            resourceId = ref;
            artifactId = r.getSourceId();
        } else {
            artifactId = ref;
        }
        Artifact a = artifactService.get(artifactId);
        if (a == null) return ToolResult.error("artifact not found: " + ref);
        if (!a.getUserId().equals(user.getUserId())) return ToolResult.error("artifact not owned");
        boolean isBinary = isBinaryFormat(a.getFormat());

        if ("NEW_FILE".equalsIgnoreCase(saveMode)) {
            String name = (displayName == null || displayName.isEmpty()) ? a.getTitle() : displayName;
            String oaFileId;
            if (isBinary) {
                byte[] bytes = artifactService.readArtifactBytes(artifactId);
                oaFileId = oaFileClient.writeNewBytes(user, "/", name,
                        bytes == null ? new byte[0] : bytes, guessMime(a.getFormat()));
            } else {
                String content = artifactService.readArtifactContent(artifactId, 0, 0);
                oaFileId = oaFileClient.writeNew(user, "/", name, content, guessMime(a.getFormat()));
            }
            Map<String, Object> data = new LinkedHashMap<>();
            data.put("oaFileId", oaFileId);
            data.put("action", "SAVED_AS_NEW");
            return ToolResult.ok("已保存到 OA 文件库", data);
        }

        // 覆盖或审批：走 pending request，等待用户确认
        Map<String, Object> payload = new LinkedHashMap<>();
        payload.put("artifactId", artifactId);
        if (resourceId != null) payload.put("resourceId", resourceId);
        payload.put("saveMode", saveMode);
        payload.put("sourceOaFileId", a.getSourceOaFileId());
        payload.put("binary", isBinary);
        payload.put("displayName", (displayName == null || displayName.isEmpty()) ? a.getTitle() : displayName);
        payload.put("mimeType", guessMime(a.getFormat()));
        List<String> actions = new ArrayList<>(java.util.Arrays.asList("CONFIRM", "CANCEL"));
        PendingRequest pr = pendingRequestService.create(thread, user,
                PendingRequestType.confirm_action,
                "OVERWRITE_ORIGINAL".equalsIgnoreCase(saveMode) ? "覆盖原 OA 文件" : "提交审批回写",
                "请确认将工件「" + a.getTitle() + "」" + ("OVERWRITE_ORIGINAL".equalsIgnoreCase(saveMode) ? "覆盖到原 OA 文件" : "提交审批流程"),
                payload, actions, artifactId, ctx.getRunId(), ctx.getToolCallId());
        return ToolResult.pending(pr.getRequestId(), PendingRequestType.confirm_action,
                pr.getTitle(),
                "已发出确认请求，等待用户响应");
    }

    // -------------- save_resource_summary --------------

    private BuiltinToolDefinition saveResourceSummaryDef() {
        return BuiltinToolDefinition.builder()
                .name("save_resource_summary")
                .displayName("回写资源摘要")
                .description("读完一个 workspace 资源后，把要点提炼成一句话摘要存回该资源。"
                        + "用途：让后续 workspace 索引段和 list_files 都能直接看到这份文件讲了什么，"
                        + "避免每次都重新 read_file。"
                        + "\n触发时机：调用 read_file / oa_read_file 拿到正文后，你认为这份文件值得被记住。"
                        + "\n适用于：USER_UPLOAD / OA_FILE / ARTIFACT 任何一种资源。"
                        + "\n约束：单条 ≤ 200 字；用客观陈述，不要带主观评价；同一资源重复写入会覆盖旧摘要.")
                .category(ToolCategory.WORKSPACE)
                .risk(ToolRiskLevel.WRITE)
                .inputSchema(SchemaBuilder.obj()
                        .prop("resourceId", "string", "资源 ID")
                        .prop("summary", "string", "一句话摘要，≤ 200 字")
                        .required("resourceId", "summary")
                        .build())
                .build();
    }

    private ToolResult saveResourceSummary(ToolContext ctx, Map<String, Object> args) throws Exception {
        String resourceId = asStr(args.get("resourceId"));
        String summary = asStr(args.get("summary"));
        if (resourceId == null || resourceId.isEmpty()) {
            return ToolResult.error("resourceId required");
        }
        if (summary == null || summary.trim().isEmpty()) {
            return ToolResult.error("summary required");
        }
        String trimmed = summary.trim();
        if (trimmed.length() > 200) trimmed = trimmed.substring(0, 200);
        // 归属校验在 WorkspaceService 内；传 user.getUserId() 让它拦截跨用户写.
        WorkspaceResource updated = workspaceService.updateResourceSummary(
                resourceId, ctx.getUserId(), trimmed, "assistant");
        if (updated == null) {
            return ToolResult.error("resource not found or not owned");
        }
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("resourceId", resourceId);
        data.put("summary", trimmed);
        data.put("chars", trimmed.length());
        return ToolResult.ok("摘要已保存", data);
    }

    // -------- helpers --------

    private String asStr(Object o) { return o == null ? null : String.valueOf(o); }

    private void putIfPresent(Map<String, Object> out, String key, Object value) {
        if (value != null && !String.valueOf(value).isEmpty()) out.put(key, value);
    }

    private String sha256(String text) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            byte[] bytes = digest.digest((text == null ? "" : text).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) {
            return "unavailable";
        }
    }

    private void addOaFileType(Map<String, Object> row, WorkspaceResource r) {
        if (r == null || r.getSourceType() != ResourceSourceType.OA_FILE) return;
        String type = ResourceMetadata.getOaFileType(r);
        if ((type == null || type.isEmpty()) && r.getOrigin() != null
                && r.getOrigin().toLowerCase().contains("mail")) {
            type = "MAIL";
        }
        if (type == null || type.isEmpty()) return;
        String label = ResourceMetadata.getOaFileTypeLabel(r);
        if (label == null || label.isEmpty()) {
            if ("MAIL".equalsIgnoreCase(type)) label = "邮件";
            else if ("DOCUMENT".equalsIgnoreCase(type)) label = "公文";
            else label = type;
        }
        row.put("oaFileType", type);
        row.put("oaFileTypeLabel", label);
    }

    private int asInt(Object o, int def) {
        if (o == null) return def;
        if (o instanceof Number) return ((Number) o).intValue();
        try { return Integer.parseInt(String.valueOf(o)); } catch (Exception e) { return def; }
    }

    private int clamp(int v, int min, int max) { return Math.max(min, Math.min(max, v)); }

    private String guessFormat(String mimeType) {
        if (mimeType == null) return "txt";
        if (mimeType.contains("html")) return "html";
        if (mimeType.contains("json")) return "json";
        if (mimeType.contains("docx") || mimeType.contains("wordprocessingml")) return "docx";
        return "txt";
    }

    private String guessMime(String format) {
        return com.gzzm.lobster.common.MimeTypes.fromFormat(format);
    }

    /** 是否为二进制格式 —— 决定 save_to_oa 走字节路径还是字符串路径. */
    static boolean isBinaryFormat(String format) {
        if (format == null) return false;
        switch (format.toLowerCase()) {
            case "docx":
            case "xlsx":
            case "pptx":
            case "pdf":
            case "png":
            case "jpg":
            case "jpeg":
            case "zip":
                return true;
            default:
                return false;
        }
    }
}
