package com.gzzm.lobster.api;

import com.gzzm.lobster.artifact.Artifact;
import com.gzzm.lobster.artifact.ArtifactService;
import com.gzzm.lobster.common.LobsterException;
import com.gzzm.lobster.common.UserVisibleThreadState;
import com.gzzm.lobster.identity.UserContext;
import com.gzzm.lobster.identity.UserContextHolder;
import com.gzzm.lobster.pending.PendingRequest;
import com.gzzm.lobster.pending.PendingRequestService;
import com.gzzm.lobster.thread.ThreadMessage;
import com.gzzm.lobster.thread.ThreadRoom;
import com.gzzm.lobster.thread.ThreadService;
import com.gzzm.lobster.workspace.WorkspaceResource;
import com.gzzm.lobster.workspace.ResourceMetadata;
import com.gzzm.lobster.workspace.WorkspaceService;
import net.cyan.arachne.HttpMethod;
import net.cyan.arachne.annotation.Service;
import net.cyan.commons.util.io.DownloadFile;
import net.cyan.nest.annotation.Inject;

import java.util.*;

/**
 * ThreadApi —— Thread 查询与管理 API / Thread queries and management.
 *
 * <p>- 创建 Thread
 * <p>- 列出 Thread
 * <p>- Thread 时间线
 * <p>- 工件列表
 * <p>- 待处理请求
 */
@Service
public class ThreadApi {

    @Inject private ThreadService threadService;
    @Inject private WorkspaceService workspaceService;
    @Inject private ArtifactService artifactService;
    @Inject private PendingRequestService pendingService;

    @Service(url = "/ai/api/threads/create", method = HttpMethod.post)
    public Map<String, Object> createThread(String title) throws Exception {
        UserContext user = UserContextHolder.require();
        ThreadRoom t = threadService.createThread(user, title);
        return toThreadMap(t, UserVisibleThreadState.idle);
    }

    /** 重命名 thread / Rename thread. Body / query: title */
    @Service(url = "/ai/api/threads/{$0}/rename", method = HttpMethod.post)
    public Map<String, Object> renameThread(String threadId, String title) throws Exception {
        UserContext user = UserContextHolder.require();
        ThreadRoom t = threadService.rename(user, threadId, title);
        return toThreadMap(t, UserVisibleThreadState.idle);
    }

    /** 软删除 thread（置 deleteTag=1）/ Soft-delete thread. */
    @Service(url = "/ai/api/threads/{$0}/delete", method = HttpMethod.post)
    public Map<String, Object> deleteThread(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        int n = threadService.delete(user, threadId);
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("deleted", n);
        out.put("threadId", threadId);
        return out;
    }

    @Service(url = "/ai/api/threads/list", method = HttpMethod.all)
    public Map<String, Object> listThreads(Integer offset, Integer limit) throws Exception {
        UserContext user = UserContextHolder.require();
        int o = offset == null ? 0 : Math.max(0, offset);
        int l = limit == null ? 20 : Math.max(1, Math.min(50, limit));
        List<ThreadRoom> rooms = threadService.listByUser(user.getUserId(), o, l);
        long total = threadService.countByUser(user.getUserId());
        List<Map<String, Object>> items = new ArrayList<>();
        for (ThreadRoom t : rooms) {
            UserVisibleThreadState state = pendingService.threadHasOpen(t.getThreadId())
                    ? UserVisibleThreadState.needs_attention
                    : UserVisibleThreadState.idle;
            items.add(toThreadMap(t, state));
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("items", items);
        out.put("offset", o);
        out.put("limit", l);
        out.put("total", total);
        out.put("hasMore", o + items.size() < total);
        return out;
    }

    @Service(url = "/ai/api/threads/{$0}", method = HttpMethod.all)
    public Map<String, Object> getThread(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        ThreadRoom t = threadService.requireOwnedThread(user, threadId);
        UserVisibleThreadState state = pendingService.threadHasOpen(t.getThreadId())
                ? UserVisibleThreadState.needs_attention
                : UserVisibleThreadState.idle;
        return toThreadMap(t, state);
    }

    @Service(url = "/ai/api/threads/{$0}/messages", method = HttpMethod.all)
    public Map<String, Object> listMessages(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        threadService.requireOwnedThread(user, threadId);
        List<ThreadMessage> messages = threadService.loadLightweightTranscript(threadId);
        List<Map<String, Object>> items = new ArrayList<>();
        for (ThreadMessage m : messages) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("messageId", m.getMessageId());
            row.put("role", m.getRole().name());
            row.put("content", m.getContent());
            row.put("runId", m.getRunId());
            row.put("toolCallId", m.getToolCallId());
            row.put("toolName", m.getToolName());
            // thinking-mode 模型的"思考过程"原文：仅 assistant 消息会有；前端按 ChatGPT 风格
            // 渲染成可折叠的灰色块。空字符串和 null 都不下发，避免无意义字段污染列表 payload.
            String rc = m.getReasoningContent();
            if (rc != null && !rc.isEmpty()) row.put("reasoningContent", rc);
            // 多模态：user 消息关联的图片 mediaId 列表（JSON）；前端 lobsterStore.rebuildMessagesFromTranscript
            // 解析后挂到 message.imageMediaIds，MessageItem 据此渲染历史缩略图.
            row.put("attachmentsJson", m.getAttachmentsJson());
            row.put("seq", m.getSeq());
            row.put("createTime", m.getCreateTime());
            items.add(row);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("messages", items);
        return out;
    }

    @Service(url = "/ai/api/threads/{$0}/artifacts", method = HttpMethod.all)
    public Map<String, Object> listArtifacts(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        threadService.requireOwnedThread(user, threadId);
        List<Artifact> artifacts = artifactService.listByThread(threadId);
        List<Map<String, Object>> items = new ArrayList<>();
        for (Artifact a : artifacts) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("artifactId", a.getArtifactId());
            row.put("title", a.getTitle());
            row.put("artifactType", a.getArtifactType().name());
            row.put("format", a.getFormat());
            row.put("sourceRunId", a.getSourceRunId());
            row.put("version", a.getVersion());
            row.put("status", a.getStatus() == null ? "active" : a.getStatus().name());
            row.put("createTime", a.getCreateTime());
            row.put("updateTime", a.getUpdateTime());
            items.add(row);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("artifacts", items);
        return out;
    }

    @Service(url = "/ai/api/threads/{$0}/resources", method = HttpMethod.all)
    public Map<String, Object> listResources(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        threadService.requireOwnedThread(user, threadId);
        List<WorkspaceResource> list = workspaceService.listResources(threadId, null, 0, 50);
        List<Map<String, Object>> items = new ArrayList<>();
        for (WorkspaceResource r : list) {
            Map<String, Object> row = new LinkedHashMap<>();
            row.put("resourceId", r.getResourceId());
            // sourceId：ARTIFACT → artifactId（预览用）；OA_FILE → OA fileId（后端预览/读取回退用）；
            // WORKSHOP_DOC → 工坊文档 id；USER_UPLOAD 用不到（下载/预览走 resourceId）。
            // 之前漏了这个字段，前端 onResourceOpen 里 r.sourceId 取到 undefined，
            // 点工件/OA 文件时 URL 变成 artifacts/undefined 直接 500。
            row.put("sourceId", r.getSourceId());
            row.put("displayName", r.getDisplayName());
            row.put("sourceType", r.getSourceType().name());
            String oaFileType = oaFileType(r);
            if (oaFileType != null) {
                row.put("oaFileType", oaFileType);
                row.put("oaFileTypeLabel", oaFileTypeLabel(r, oaFileType));
            }
            row.put("artifactType", r.getArtifactType());
            row.put("mimeType", r.getMimeType());
            row.put("status", r.getStatus() == null ? null : r.getStatus().name());
            // USER_UPLOAD 的 kind / sizeBytes / summary 都存在 metadataJson 里；
            // 前端侧按需解析即可（WorkspaceResourceList.vue 已 tolerant）。
            if (r.getMetadataJson() != null) row.put("metadataJson", r.getMetadataJson());
            row.putAll(ResourceMetadata.artifactLifecycle(r));
            // 资源在工作区登记的时间——前端侧栏列表用来排序 / 显示"3 分钟前".
            // 对各 sourceType 的语义：ARTIFACT/USER_UPLOAD = 创建时间；
            // OA_FILE = 挂入 thread 的时间（OA 自身的创建时间在 metadata 里，按需另读）.
            if (r.getCreateTime() != null) row.put("createTime", r.getCreateTime());
            items.add(row);
        }
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("resources", items);
        return out;
    }

    private String oaFileType(WorkspaceResource r) {
        if (r == null || r.getSourceType() != com.gzzm.lobster.common.ResourceSourceType.OA_FILE) return null;
        String type = ResourceMetadata.getOaFileType(r);
        if (type != null && !type.isEmpty()) return type;
        String origin = r.getOrigin();
        if (origin != null && origin.toLowerCase(Locale.ROOT).contains("mail")) return "MAIL";
        return null;
    }

    private String oaFileTypeLabel(WorkspaceResource r, String type) {
        String label = ResourceMetadata.getOaFileTypeLabel(r);
        if (label != null && !label.isEmpty()) return label;
        if ("MAIL".equalsIgnoreCase(type)) return "邮件";
        if ("DOCUMENT".equalsIgnoreCase(type)) return "公文";
        return type;
    }

    @Service(url = "/ai/api/threads/{$0}/pending-requests", method = HttpMethod.all)
    public Map<String, Object> listPending(String threadId) throws Exception {
        UserContext user = UserContextHolder.require();
        threadService.requireOwnedThread(user, threadId);
        List<PendingRequest> list = pendingService.listOpenByThread(threadId);
        List<Map<String, Object>> items = new ArrayList<>();
        for (PendingRequest p : list) items.add(toPendingMap(p));
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("pendingRequests", items);
        return out;
    }

    @Service(url = "/ai/api/artifacts/{$0}", method = HttpMethod.all)
    public Map<String, Object> getArtifact(String artifactId, Integer offset, Integer limit) throws Exception {
        UserContext user = UserContextHolder.require();
        Artifact a = artifactService.get(artifactId);
        if (a == null) throw new LobsterException("artifact.not_found", "Artifact not found");
        if (!a.getUserId().equals(user.getUserId())) {
            throw new LobsterException("artifact.forbidden", "Artifact not owned");
        }
        int off = offset == null ? 0 : offset;
        // limit 语义：
        //   null / 省略 → 16000 默认窗口（保持老调用兼容）
        //   <= 0         → 不截断，读全文（预览抽屉用这个值）
        //   > 0          → 上限 16000，防前端/LLM 拉太大
        Integer limParam = limit;
        boolean full = limParam != null && limParam <= 0;
        int lim = full ? 0 : (limParam == null ? 16000 : Math.min(16000, limParam));
        String content = artifactService.readArtifactContent(artifactId, off, lim);
        Map<String, Object> out = new LinkedHashMap<>();
        out.put("artifactId", a.getArtifactId());
        out.put("title", a.getTitle());
        out.put("content", content);
        out.put("artifactType", a.getArtifactType().name());
        out.put("format", a.getFormat());
        out.put("version", a.getVersion());
        // 全量模式不可能被截断；窗口模式按长度判断
        out.put("truncated", !full && content != null && content.length() >= lim);
        return out;
    }

    /**
     * 工件下载 / Download artifact raw bytes.
     *
     * <p>路由：{@code GET /ai/api/artifacts/{artifactId}/download}。
     * 文本工件也能走这里（按 format 给对应扩展名和 mime）；二进制工件（docx/xlsx/pdf/...）
     * 前端点预览时直接跳这里下载，避免在抽屉里 pre 标签展示一堆乱码。
     */
    @Service(url = "/ai/api/artifacts/{$0}/download")
    public DownloadFile downloadArtifact(String artifactId) throws Exception {
        UserContext user = UserContextHolder.require();
        Artifact a = artifactService.get(artifactId);
        if (a == null) throw new LobsterException("artifact.not_found", "Artifact not found");
        if (!a.getUserId().equals(user.getUserId())) {
            throw new LobsterException("artifact.forbidden", "Artifact not owned");
        }
        byte[] bytes = artifactService.readArtifactBytes(artifactId);
        if (bytes == null) bytes = new byte[0];
        String ext = a.getFormat() == null || a.getFormat().isEmpty() ? "bin" : a.getFormat();
        String baseTitle = a.getTitle() == null || a.getTitle().isEmpty() ? a.getArtifactId() : a.getTitle();
        String fileName = baseTitle.toLowerCase(Locale.ROOT).endsWith("." + ext) ? baseTitle : baseTitle + "." + ext;
        DownloadFile file = new DownloadFile(bytes, fileName, mimeOf(ext));
        file.setAttachment(true);
        return file;
    }

    /**
     * 工件内联预览 / Inline artifact preview (iframe / img 目标端点).
     *
     * <p>路由：{@code GET /ai/api/artifacts/{artifactId}/preview}。
     * 与 {@code /download} 的差别：
     * <ul>
     *   <li>Content-Disposition：inline（浏览器直接渲染，而非弹下载）</li>
     *   <li>格式白名单：仅 html/pdf/图片/纯文本可走，office 二进制强制回落 404
     *       （防 octet-stream 被 IE/老浏览器 sniff 成 HTML 执行）</li>
     *   <li>前端用 {@code <iframe sandbox="allow-scripts">} 包住，隔离 cookie / top-navigation</li>
     * </ul>
     *
     * <p>安全注意：HTML artifact 是用户自己生成的（或让 Claude 生成），与应用同 origin，
     * 脚本能读取当前登录态的非 HttpOnly cookie. 主防线由前端 {@code <iframe sandbox="allow-scripts">}
     * 承担——无 allow-same-origin 时 iframe 被视为 null origin，无法访问父 cookie / storage /
     * top-navigation. 后端仅格式白名单拦截（上面的 {@link #isInlinePreviewable}），阻止未知二进制
     * 被浏览器 MIME sniff 成 HTML 执行. 未来若 cyan DownloadFile 暴露 setHeader，
     * 再把 X-Content-Type-Options: nosniff / CSP sandbox 叠上去.
     */
    @Service(url = "/ai/api/artifacts/{$0}/preview")
    public DownloadFile previewArtifact(String artifactId) throws Exception {
        UserContext user = UserContextHolder.require();
        Artifact a = artifactService.get(artifactId);
        if (a == null) throw new LobsterException("artifact.not_found", "Artifact not found");
        if (!a.getUserId().equals(user.getUserId())) {
            throw new LobsterException("artifact.forbidden", "Artifact not owned");
        }
        String ext = a.getFormat() == null || a.getFormat().isEmpty() ? "bin" : a.getFormat();
        if (!isInlinePreviewable(ext)) {
            // 走到 preview 的二进制格式一般是前端误判；直接 404 而不是 fallthrough 下载，
            // 避免浏览器 Content-Type sniff 风险
            throw new LobsterException("artifact.preview_unsupported",
                    "Format not previewable inline: " + ext);
        }
        byte[] bytes = artifactService.readArtifactBytes(artifactId);
        if (bytes == null) bytes = new byte[0];
        String baseTitle = a.getTitle() == null || a.getTitle().isEmpty() ? a.getArtifactId() : a.getTitle();
        String fileName = baseTitle.toLowerCase(Locale.ROOT).endsWith("." + ext) ? baseTitle : baseTitle + "." + ext;
        DownloadFile file = new DownloadFile(bytes, fileName, mimeOf(ext));
        file.setAttachment(false);  // inline —— 浏览器直接渲染
        return file;
    }

    private static String mimeOf(String ext) {
        if (ext == null) return "application/octet-stream";
        switch (ext.toLowerCase(Locale.ROOT)) {
            case "txt": case "md": case "markdown": case "log": return "text/plain";
            case "csv": return "text/csv";
            case "json": return "application/json";
            case "html": case "htm": return "text/html";
            case "xml": return "application/xml";
            case "pdf": return "application/pdf";
            // 图片（preview 端点要用）
            case "png":  return "image/png";
            case "jpg": case "jpeg": return "image/jpeg";
            case "gif":  return "image/gif";
            case "webp": return "image/webp";
            case "svg":  return "image/svg+xml";
            case "bmp":  return "image/bmp";
            case "ico":  return "image/vnd.microsoft.icon";
            case "docx": return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
            case "doc":  return "application/msword";
            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";
            default: return "application/octet-stream";
        }
    }

    /**
     * 允许 iframe/img 内联渲染的工件格式白名单 / Whitelist of formats safe to serve inline.
     *
     * <p>preview 端点只对白名单放行。docx/xlsx/pptx 这类浏览器不会原生渲染的走 download
     * （或另起专门的 office preview），避免浏览器把 octet-stream 当 HTML 嗅探执行.
     */
    private static boolean isInlinePreviewable(String ext) {
        if (ext == null) return false;
        switch (ext.toLowerCase(Locale.ROOT)) {
            case "html": case "htm":
            case "pdf":
            case "png": case "jpg": case "jpeg": case "gif": case "webp": case "svg": case "bmp": case "ico":
            case "txt": case "md": case "markdown": case "log": case "csv": case "json": case "xml":
                return true;
            default:
                return false;
        }
    }

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

    private Map<String, Object> toThreadMap(ThreadRoom t, UserVisibleThreadState state) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("threadId", t.getThreadId());
        m.put("title", t.getTitle());
        m.put("state", state.name());
        m.put("type", t.getType());
        m.put("workspaceId", t.getWorkspaceId());
        m.put("lastActivityAt", t.getLastActivityAt());
        m.put("createTime", t.getCreateTime());
        return m;
    }

    private Map<String, Object> toPendingMap(PendingRequest p) {
        Map<String, Object> m = new LinkedHashMap<>();
        m.put("requestId", p.getRequestId());
        m.put("type", p.getType().name());
        m.put("title", p.getTitle());
        m.put("prompt", p.getPrompt());
        m.put("allowedActionsJson", p.getAllowedActionsJson());
        m.put("sourceRunId", p.getSourceRunId());
        // Keep the restored pending shape aligned with live SSE pending_request
        // events, where the frontend receives the source run as runId.
        m.put("runId", p.getSourceRunId());
        m.put("toolCallId", p.getToolCallId());
        // payload：ask_user 的 context / suggestedOptions；confirm_action 的上下文键值对等.
        // 之前只返 prompt 导致前端弹窗拿不到 context，用户看不到"问题背景".
        if (p.getPayloadJson() != null && !p.getPayloadJson().isEmpty()) {
            try {
                m.put("payload", com.gzzm.lobster.common.JsonUtil.fromJsonToMap(p.getPayloadJson()));
            } catch (Exception e) {
                // 解析不了就塞原始 JSON 字符串，至少前端能看到调试（极罕见：手工 SQL 改坏了）
                m.put("payload", p.getPayloadJson());
            }
        }
        m.put("status", p.getStatus().name());
        m.put("createTime", p.getCreateTime());
        return m;
    }
}
