package com.gzzm.lobster.api;

import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.ResourceSourceType;
import com.gzzm.lobster.config.LobsterConfig;
import com.gzzm.lobster.identity.UserContext;
import com.gzzm.lobster.identity.UserContextHolder;
import com.gzzm.lobster.oa.OaFileClient;
import com.gzzm.lobster.parse.UploadService;
import com.gzzm.lobster.storage.FileSystemContentStore;
import com.gzzm.lobster.thread.ThreadRoom;
import com.gzzm.lobster.thread.ThreadService;
import com.gzzm.lobster.workspace.ResourceMetadata;
import com.gzzm.lobster.workspace.WorkspaceResource;
import com.gzzm.lobster.workspace.WorkspaceService;
import net.cyan.arachne.HttpMethod;
import net.cyan.arachne.annotation.Service;
import net.cyan.commons.util.InputFile;
import net.cyan.commons.util.io.DownloadFile;
import net.cyan.nest.annotation.Inject;

import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * UploadApi —— 用户文件上传入口 / User-file upload endpoint.
 *
 * <p>路由：{@code POST /ai/api/uploads/create}。
 * multipart/form-data：字段名 {@code uploadFile}；表单参数 {@code threadId} 必填。
 *
 * <p>响应示例：
 * <pre>{@code
 *   {
 *     "resourceId": "res_xxx",
 *     "uploadId":   "up_xxx",
 *     "kind":       "docx",
 *     "originalName": "报告.docx",
 *     "sizeBytes":  15400,
 *     "parseError": null,
 *     "summary": { "title":..., "topSections":[...] },
 *     "preview":    "--- ... 前 1200 字 markdown ..."
 *   }
 * }</pre>
 *
 * <p>前端拿到 {@code resourceId} 即可在 workspace 侧显示附件气泡；Agent 会通过
 * workspace 索引段看到新资源并按需 {@code read_file} / {@code read_outline} 深读。
 */
@Service
public class UploadApi {

    @Inject private UploadService uploadService;
    @Inject private ThreadService threadService;
    @Inject private WorkspaceService workspaceService;
    @Inject private FileSystemContentStore contentStore;
    @Inject private OaFileClient oaFileClient;

    /** Arachne multipart 注入点. 每次请求 @Service bean 为一次性实例（框架行为），线程安全. */
    private InputFile uploadFile;

    public InputFile getUploadFile() { return uploadFile; }
    public void setUploadFile(InputFile uploadFile) { this.uploadFile = uploadFile; }

    @Service(url = "/ai/api/uploads/create", method = HttpMethod.post)
    public Map<String, Object> create(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        if (threadId == null || threadId.isEmpty()) {
            throw new LobsterException("upload.invalid", "threadId is required");
        }
        if (uploadFile == null) {
            throw new LobsterException("upload.invalid", "uploadFile is required");
        }

        ThreadRoom thread = threadService.requireOwnedThread(user, threadId);

        // 优先拒绝超大文件：先看 InputFile.size() 再读流，避免无谓地撑内存
        long max = LobsterConfig.getUploadMaxBytes();
        try {
            long declared = uploadFile.size();
            if (declared > max) {
                throw new LobsterException("upload.too_large",
                        "Upload exceeds " + max + " bytes: " + declared);
            }
        } catch (LobsterException e) {
            throw e;
        } catch (Throwable ignore) { /* size() 个别 InputFile 实现会抛，交给 readAll 的兜底 */ }

        String name = uploadFile.getName();
        String declaredType = null;
        try { declaredType = uploadFile.getType(); } catch (Throwable ignore) { /* 老实现可能没有 */ }
        String mime = (declaredType != null && !declaredType.isEmpty()) ? declaredType : guessMime(name);

        byte[] bytes = readAll(uploadFile.getInputStream(), max);

        UploadService.UploadResult res = uploadService.handle(thread, user, name, mime, bytes);

        Map<String, Object> out = new LinkedHashMap<>();
        out.put("resourceId", res.resourceId);
        out.put("uploadId", res.uploadId);
        out.put("kind", res.kind);
        out.put("originalName", res.originalName);
        out.put("sizeBytes", res.sizeBytes);
        if (res.parseError != null) out.put("parseError", res.parseError);
        if (res.outlineSummary != null) out.put("summary", res.outlineSummary);
        if (res.markdownPreview != null) out.put("preview", res.markdownPreview);
        return out;
    }

    /**
     * 下载上传原件 / Download original uploaded file.
     *
     * <p>路由：{@code GET /ai/api/uploads/{resourceId}/download}。只对
     * {@link ResourceSourceType#USER_UPLOAD} / {@link ResourceSourceType#OA_FILE}
     * 有效；其它类型走各自专属预览入口。
     *
     * <p>鉴权：resource 的 userId 必须匹配当前登录用户。不做 thread 归属额外校验——
     * resource 建立时就绑定到 userId。
     */
    @Service(url = "/ai/api/uploads/{$0}/download")
    public DownloadFile download(String resourceId) throws Exception {
        UserContext user = UserContextHolder.require();
        if (resourceId == null || resourceId.isEmpty()) {
            throw new LobsterException("upload.invalid", "resourceId is required");
        }

        WorkspaceResource r = workspaceService.getResource(resourceId);
        if (r == null) {
            throw new LobsterException("upload.not_found", "Resource not found: " + resourceId);
        }
        if (!user.getUserId().equals(r.getUserId())) {
            throw new LobsterException("upload.forbidden", "No permission to download: " + resourceId);
        }
        if (!isResourceOriginalSupported(r)) {
            throw new LobsterException("upload.invalid_type",
                    "Download is only supported for USER_UPLOAD/OA_FILE, got " + r.getSourceType());
        }

        Map<String, Object> meta = metadata(r);
        byte[] bytes = readOriginalBytes(user, r, meta);
        String fileName = fileName(resourceId, r, meta);
        String mime = firstNonEmpty(r.getMimeType(), guessMime(fileName));

        DownloadFile file = new DownloadFile(bytes, fileName, mime);
        file.setAttachment(true); // 强制 Content-Disposition: attachment，浏览器下载不内联渲染
        return file;
    }

    /**
     * 内联返回原始文件——主要给 {@code <img src="...">} 渲染图片附件用 /
     * Inline original file (for direct rendering by &lt;img&gt; in chat bubbles).
     *
     * <p>路由：{@code GET /ai/api/uploads/{resourceId}/inline}。鉴权同 {@link #download}；
     * 对 {@link ResourceSourceType#USER_UPLOAD} / {@link ResourceSourceType#OA_FILE}
     * 有效。和 {@code /download} 唯一区别：
     * 不强制 attachment，浏览器按 mimeType 内联渲染（图片直接显示，pdf 内嵌预览等）。
     *
     * <p>用于多模态场景：用户上传图片 → 前端 MessageItem 拿 resourceId 拼成
     * {@code /ai/api/uploads/{rid}/inline} 当 img.src，浏览器 GET 即显示缩略图.
     */
    @Service(url = "/ai/api/uploads/{$0}/inline")
    public DownloadFile inline(String resourceId) throws Exception {
        UserContext user = UserContextHolder.require();
        if (resourceId == null || resourceId.isEmpty()) {
            throw new LobsterException("upload.invalid", "resourceId is required");
        }
        WorkspaceResource r = workspaceService.getResource(resourceId);
        if (r == null) {
            throw new LobsterException("upload.not_found", "Resource not found: " + resourceId);
        }
        if (!user.getUserId().equals(r.getUserId())) {
            throw new LobsterException("upload.forbidden", "No permission to view: " + resourceId);
        }
        if (!isResourceOriginalSupported(r)) {
            throw new LobsterException("upload.invalid_type",
                    "Inline view is only supported for USER_UPLOAD/OA_FILE, got " + r.getSourceType());
        }
        Map<String, Object> meta = metadata(r);
        byte[] bytes = readOriginalBytes(user, r, meta);
        String fileName = fileName(resourceId, r, meta);
        String mime = firstNonEmpty(r.getMimeType(), guessMime(fileName));

        DownloadFile file = new DownloadFile(bytes, fileName, mime);
        file.setAttachment(false); // 内联：浏览器按 mimeType 自动渲染
        return file;
    }

    private boolean isResourceOriginalSupported(WorkspaceResource r) {
        return r != null && (r.getSourceType() == ResourceSourceType.USER_UPLOAD
                || r.getSourceType() == ResourceSourceType.OA_FILE);
    }

    private Map<String, Object> metadata(WorkspaceResource r) {
        return ResourceMetadata.readMap(r.getMetadataJson());
    }

    private byte[] readOriginalBytes(UserContext user, WorkspaceResource r, Map<String, Object> meta) throws Exception {
        String origRef = asString(meta.get("origRef"));
        if (origRef != null && !origRef.isEmpty()) {
            byte[] bytes = contentStore.readBinary(origRef);
            if (bytes == null) {
                throw new LobsterException("upload.origin_missing", "Original file missing on disk: " + origRef);
            }
            return bytes;
        }
        if (r.getSourceType() == ResourceSourceType.OA_FILE) {
            byte[] bytes = oaFileClient.downloadBytes(user, r.getSourceId());
            if (bytes == null) {
                throw new LobsterException("upload.origin_missing", "OA file bytes missing: " + r.getSourceId());
            }
            return bytes;
        }
        throw new LobsterException("upload.no_origin", "No original file for " + r.getResourceId());
    }

    private String fileName(String resourceId, WorkspaceResource r, Map<String, Object> meta) {
        return firstNonEmpty(asString(meta.get("originalName")), r.getDisplayName(), resourceId);
    }

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

    private static String firstNonEmpty(String... vals) {
        if (vals == null) return null;
        for (String v : vals) {
            if (v != null && !v.isEmpty()) return v;
        }
        return null;
    }

    /**
     * 读完输入流，带硬上限——防止客户端在 Content-Length 骗报后继续灌流量撑内存.
     * 超过 cap 时抛 {@link LobsterException}。
     */
    private static byte[] readAll(InputStream in, long cap) throws Exception {
        ByteArrayOutputStream buf = new ByteArrayOutputStream();
        byte[] chunk = new byte[16 * 1024];
        int n;
        long total = 0;
        while ((n = in.read(chunk)) > 0) {
            total += n;
            if (total > cap) {
                throw new LobsterException("upload.too_large",
                        "Upload exceeds " + cap + " bytes (streamed)");
            }
            buf.write(chunk, 0, n);
        }
        return buf.toByteArray();
    }

    /**
     * 粗略根据扩展名猜 mime；真实 mime 由客户端 Content-Type 决定最权威。
     * 第一期只做够用的映射，政务文档覆盖 docx/doc/pdf/xlsx/xls/pptx/ppt/txt/md/csv.
     */
    private static String guessMime(String name) {
        if (name == null) return "application/octet-stream";
        String lower = name.toLowerCase();
        int dot = lower.lastIndexOf('.');
        if (dot < 0) return "application/octet-stream";
        String ext = lower.substring(dot + 1);
        switch (ext) {
            case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
            case "doc":  return "application/msword";
            case "pdf":  return "application/pdf";
            case "xlsx": return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
            case "xls":  return "application/vnd.ms-excel";
            case "pptx": return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
            case "ppt":  return "application/vnd.ms-powerpoint";
            case "txt":  return "text/plain";
            case "md":   return "text/markdown";
            case "csv":  return "text/csv";
            case "json": return "application/json";
            default:     return "application/octet-stream";
        }
    }
}
