(function () {

    const NEW_CHAT_VALUE = "__new__";
    const CHAT_PAGE_SIZE = 25;
    const MESSAGE_PAGE_SIZE = 30;
    const STALE_CHECK_THROTTLE_MS = 15000;

    let ctx = null;
    let workItemId = null;

    let chats = [];
    let activeChatId = null;
    let messages = [];
    let stagedFiles = [];
    let sending = false;
    let summarizing = false;
    let loadRequestId = 0;

    let summary = {
        status: "loading",
        text: "",
        lastUpdated: null,
        origin: "",
        error: ""
    };

    let summaryPromise = null;
    let summaryEnabled = true;
    let lastStaleCheck = 0;
    let disposed = false;

    let inputHistoryIndex = null;
    let inputHistoryDraft = "";

    const el = {};

    async function init() {
        disposed = false;
        sending = false;
        summarizing = false;
        cacheDom();
        bindEvents();

        try {
            await window.ensureSDKReady();
            ctx = window.QAContext;
            workItemId = ctx?.workItemId;

            if (!workItemId) {
                throw new Error("No work item open");
            }
        } catch (err) {
            renderFatal(err.message || "Failed to initialize Cleo");
            return;
        }

        startNewChat();

        summaryPromise = ensureSummary();
        summaryPromise.catch(e => console.error("[Cleo] summary failed", e));
        loadChats().catch(e => console.error("[Cleo] chats failed", e));
    }

    function dispose() {
        disposed = true;
        window.removeEventListener("qa-workitem-saved", onWorkItemSaved);
        document.removeEventListener("visibilitychange", onVisibilityChange);
    }

    function cacheDom() {
        el.root = document.getElementById("qa-cleo-root");
        el.select = document.getElementById("qa-cleo-chat-select");
        el.newChat = document.getElementById("qa-cleo-new-chat");
        el.messages = document.getElementById("qa-cleo-messages");
        el.suggestions = document.getElementById("qa-cleo-suggestions");
        el.input = document.getElementById("qa-cleo-input");
        el.send = document.getElementById("qa-cleo-send");
        el.attach = document.getElementById("qa-cleo-attach");
        el.file = document.getElementById("qa-cleo-file");
        el.attachments = document.getElementById("qa-cleo-attachments");
    }

    function bindEvents() {
        el.select.addEventListener("change", onSelectChange);
        el.newChat.addEventListener("click", () => {
            startNewChat();
            el.input.focus();
        });

        el.attach.addEventListener("click", () => el.file.click());
        el.file.addEventListener("change", onFilesSelected);

        el.send.addEventListener("click", handleSend);
        el.suggestions.addEventListener("click", onSuggestionClick);
        el.input.addEventListener("input", () => {
            inputHistoryIndex = null;
            autoResizeInput();
            saveInputDraft();
        });
        el.input.addEventListener("keydown", (e) => {
            if (e.key === "Enter" && !e.shiftKey) {
                e.preventDefault();
                handleSend();
                return;
            }

            if ((e.key === "ArrowUp" || e.key === "ArrowDown") &&
                !e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey) {
                const recalled = e.key === "ArrowUp" ? recallHistoryUp() : recallHistoryDown();
                if (recalled) e.preventDefault();
            }
        });

        window.addEventListener("qa-workitem-saved", onWorkItemSaved);
        document.addEventListener("visibilitychange", onVisibilityChange);
    }

    function onWorkItemSaved() {
        checkForStaleSummary();
    }

    function onVisibilityChange() {
        if (document.visibilityState === "visible") {
            checkForStaleSummary();
        }
    }

    /* ============================ SUMMARY ============================ */

    async function ensureSummary() {
        if (!(await hasSummaryLicense())) {
            summaryEnabled = false;
            removeSummaryMessage();
            return;
        }

        setSummaryStatus("loading");

        let stored = null;
        let snapshot = null;

        try {
            const [storedRes, snap] = await Promise.all([
                fetchStoredSummary(),
                readWorkItemSnapshot()
            ]);
            stored = storedRes;
            snapshot = snap;
        } catch (err) {
            setSummaryError(err.message || "Failed to load summary");
            return;
        }

        if (!stored) {
            await generate(snapshot, "generated");
            return;
        }

        if (isStale(stored, snapshot)) {
            applySummary(stored.summary, stored.lastUpdated, "reused");
            await generate(snapshot, "updated");
            return;
        }

        rememberHash(snapshot.hash);
        applySummary(stored.summary, stored.lastUpdated, "reused");
    }

    async function checkForStaleSummary() {
        if (disposed || !summaryEnabled || !workItemId) return;
        if (summary.status === "loading") return;

        const now = Date.now();
        if (now - lastStaleCheck < STALE_CHECK_THROTTLE_MS) return;
        lastStaleCheck = now;

        try {
            const snapshot = await readWorkItemSnapshot();
            const stored = { summary: summary.text, lastUpdated: summary.lastUpdated };

            if (summary.status === "error" || isStale(stored, snapshot)) {
                summaryPromise = generate(snapshot, "updated");
                await summaryPromise;
            }
        } catch (e) {
            console.warn("[Cleo] stale check failed", e);
        }
    }

    async function regenerate() {
        if (!summaryEnabled) return;
        try {
            const snapshot = await readWorkItemSnapshot();
            summaryPromise = generate(snapshot, "updated");
            await summaryPromise;
        } catch (err) {
            setSummaryError(err.message || "Failed to regenerate summary");
        }
    }

    async function generate(snapshot, origin) {
        setSummaryStatus("loading");

        try {
            const data = await requestSummary(snapshot);
            rememberHash(snapshot.hash);
            applySummary(data.summary, data.lastUpdated, origin);
        } catch (err) {
            setSummaryError(err.message || "Failed to generate summary");
        }
    }

    async function requestSummary(snapshot) {
        const res = await IndexLibrary.PostRequest({
            baseAPIUrl: ctx.baseAPIUrl,
            endpoint: `/api/WorkItemSummary/Generate`,
            azureAuthenticationHeader: ctx.accessToken,
            organizationName: ctx.organizationName,
            organizationId: ctx.organizationId,
            body: {
                organizationId: ctx.organizationId,
                devOpsProjectId: ctx.projectId,
                devOpsProjectName: ctx.projectName,
                workItemId,
                workItemFields: snapshot.summarizeFields,
                createdBy: ctx.userEmail
            }
        });

        if (!res.ok) {
            const text = await res.text();
            throw new Error(text || "Failed to generate summary");
        }

        return res.json();
    }

    async function fetchStoredSummary() {
        const res = await IndexLibrary.GetRequest({
            baseAPIUrl: ctx.baseAPIUrl,
            endpoint: `/api/WorkItemSummary/Get/${workItemId}`,
            azureAuthenticationHeader: ctx.accessToken,
            organizationName: ctx.organizationName,
            organizationId: ctx.organizationId
        });

        if (res.status === 404) return null;
        if (!res.ok) throw new Error("Failed to load summary");

        return res.json();
    }

    async function readWorkItemSnapshot() {
        const cfgRes = await IndexLibrary.GetRequest({
            baseAPIUrl: ctx.baseAPIUrl,
            endpoint: `/api/Settings/GetSummarizeConfigurations`,
            azureAuthenticationHeader: ctx.accessToken,
            organizationName: ctx.organizationName,
            organizationId: ctx.organizationId
        });

        if (!cfgRes.ok) throw new Error("Failed to fetch summarize configuration");

        const cfg = await cfgRes.json();
        const fields =
            cfg.summarizeFields
                ?.split(",")
                .map(f => f.trim())
                .filter(Boolean) ||
            ["System.Id", "System.Title", "System.Description", "System.State"];

        const trackingFields = ["System.ChangedDate", "System.CommentCount"];
        const requested = fields.concat(trackingFields.filter(f => !fields.includes(f)));

        let wi;
        try {
            wi = await IndexLibrary.GetWorkItemWithFields(Number(workItemId), requested);
        } catch (e) {
            wi = await IndexLibrary.GetWorkItemWithFields(
                Number(workItemId),
                fields.concat(fields.includes("System.ChangedDate") ? [] : ["System.ChangedDate"])
            );
        }

        const summarizeFields = {};
        fields.forEach(f => {
            summarizeFields[f] = wi.fields?.[f];
        });

        return {
            summarizeFields,
            changedDate: wi.fields?.["System.ChangedDate"] || null,
            hash: hashSnapshot(summarizeFields, wi.fields?.["System.CommentCount"])
        };
    }

    function isStale(stored, snapshot) {
        if (!stored || !stored.summary) return true;

        const known = readHash();
        if (known) return known !== snapshot.hash;

        if (snapshot.changedDate && stored.lastUpdated) {
            const changed = new Date(snapshot.changedDate).getTime();
            const generated = new Date(stored.lastUpdated).getTime();
            if (!isNaN(changed) && !isNaN(generated)) {
                return changed > generated;
            }
        }

        return false;
    }

    function hashKey() {
        return `qa_summary_hash_${ctx?.organizationId}_${workItemId}`;
    }

    function readHash() {
        try {
            return localStorage.getItem(hashKey());
        } catch (e) {
            return null;
        }
    }

    function rememberHash(hash) {
        try {
            localStorage.setItem(hashKey(), hash);
        } catch (e) { /* storage unavailable */ }
    }

    function hashSnapshot(fields, commentCount) {
        const payload = JSON.stringify(fields) + "|" + String(commentCount ?? "");
        let hash = 5381;
        for (let i = 0; i < payload.length; i++) {
            hash = ((hash << 5) + hash + payload.charCodeAt(i)) | 0;
        }
        return String(hash >>> 0);
    }

    async function hasSummaryLicense() {
        try {
            const features = ctx?.features || {};
            if (features.summary && features.summary !== "enable" && features.summary !== true) {
                return false;
            }

            if (typeof loadProductLicenseType !== "function") return true;

            const userType = await loadProductLicenseType(ctx, "summary", "tsUserType");
            return userType !== null;
        } catch (e) {
            return true;
        }
    }

    function applySummary(text, lastUpdated, origin) {
        summary = {
            status: "ready",
            text: text || "",
            lastUpdated,
            origin,
            error: ""
        };
        paintSummaryMessage();
    }

    function setSummaryStatus(status) {
        summary.status = status;
        paintSummaryMessage();
    }

    function setSummaryError(message) {
        summary.status = "error";
        summary.error = message;
        paintSummaryMessage();
    }

    function getLiveSummaryMessage() {
        return messages.find(m => m.kind === "summary" && m.live) || null;
    }

    function removeSummaryMessage() {
        const live = getLiveSummaryMessage();
        if (!live) return;
        messages = messages.filter(m => m !== live);
        renderMessages();
    }

    function paintSummaryMessage() {
        const live = getLiveSummaryMessage();
        if (!live) return;

        live.content = summary.text;

        const node = el.messages?.querySelector('[data-summary="live"]');
        if (node) {
            node.replaceWith(buildMessageNode(live));
            scrollToBottom();
        } else {
            renderMessages();
        }
    }

    /* ============================= CHATS ============================= */

    async function loadChats() {
        try {
            const res = await IndexLibrary.GetRequest({
                baseAPIUrl: ctx.baseAPIUrl,
                endpoint: `/api/InChat?pageno=1&pagesize=${CHAT_PAGE_SIZE}` +
                    `&createdBy=${encodeURIComponent(ctx.userEmail || "")}` +
                    `&projectId=${encodeURIComponent(ctx.projectId || "")}` +
                    `&workItemId=${encodeURIComponent(workItemId)}`,
                azureAuthenticationHeader: ctx.accessToken,
                organizationName: ctx.organizationName,
                organizationId: ctx.organizationId
            });

            if (!res.ok) throw new Error(`API Error: ${res.status}`);

            const apiChats = await res.json();
            const fetched = (apiChats || []).map(c => ({
                id: String(c.id ?? c.ID),
                title: c.name || c.Name || "Untitled chat"
            }));

            const fetchedIds = new Set(fetched.map(c => c.id));
            chats = chats.filter(c => !fetchedIds.has(c.id)).concat(fetched);
        } catch (e) {
            console.error("[Cleo] failed to load chats", e);
        } finally {
            renderChatSelect();
        }
    }

    function renderChatSelect() {
        el.select.innerHTML = "";

        const newOpt = document.createElement("option");
        newOpt.value = NEW_CHAT_VALUE;
        newOpt.textContent = "New chat";
        el.select.appendChild(newOpt);

        chats.forEach(chat => {
            const opt = document.createElement("option");
            opt.value = chat.id;
            opt.textContent = chat.title;
            el.select.appendChild(opt);
        });

        el.select.value = activeChatId || NEW_CHAT_VALUE;
    }

    function onSelectChange() {
        const value = el.select.value;

        if (value === NEW_CHAT_VALUE) {
            startNewChat();
            el.input.focus();
            return;
        }

        openChat(value);
    }

    function startNewChat() {
        loadRequestId++;
        inputHistoryIndex = null;
        inputHistoryDraft = "";
        activeChatId = null;
        messages = summaryEnabled
            ? [{ role: "assistant", kind: "summary", live: true, content: summary.text }]
            : [];
        clearStagedAttachments();
        renderMessages();
        if (el.select) el.select.value = NEW_CHAT_VALUE;
        restoreInputDraft();
    }

    async function openChat(chatId) {
        const requestId = ++loadRequestId;

        inputHistoryIndex = null;
        inputHistoryDraft = "";
        activeChatId = chatId;
        messages = [];
        clearStagedAttachments();
        restoreInputDraft();
        el.messages.innerHTML = `
            <div class="qa-cleo-messages-loading">
                <span class="qa-inline-spinner"></span> Loading conversation…
            </div>`;

        try {
            const res = await IndexLibrary.GetRequest({
                baseAPIUrl: ctx.baseAPIUrl,
                endpoint: `/api/InChat/${chatId}/messages?pageno=1&pagesize=${MESSAGE_PAGE_SIZE}` +
                    `&createdBy=${encodeURIComponent(ctx.userEmail || "")}`,
                azureAuthenticationHeader: ctx.accessToken,
                organizationName: ctx.organizationName,
                organizationId: ctx.organizationId
            });

            if (!res.ok) throw new Error(`API Error: ${res.status}`);

            const apiMessages = await res.json();
            if (requestId !== loadRequestId) return;

            messages = (apiMessages || []).map(m => ({
                role: (m.isAIResponse ?? m.IsAIResponse) ? "assistant" : "user",
                content: m.messageContext || m.MessageContext || "",
                attachments: m.attachments || m.Attachments || []
            })).reverse();

            const isWholeConversation = (apiMessages || []).length < MESSAGE_PAGE_SIZE;
            if (isWholeConversation && messages.length && messages[0].role === "assistant") {
                messages[0].kind = "summary";
                messages[0].live = false;
            }

            renderMessages();
        } catch (e) {
            if (requestId !== loadRequestId) return;
            el.messages.innerHTML =
                `<div class="qa-cleo-error">Couldn't load this conversation.</div>`;
        }
    }

    /* ========================== SUGGESTIONS ========================== */

    function onSuggestionClick(e) {
        const btn = e.target.closest(".qa-cleo-pill");
        if (!btn || btn.disabled || sending || summarizing) return;

        switch (btn.dataset.suggestion) {
            case "summarize":
                handleSummarize();
                break;
            case "status":
                sendSuggestion("Give me a status update on this work item.");
                break;
            case "rootcause":
                sendSuggestion("Provide a root cause analysis for this work item.");
                break;
        }
    }

    function sendSuggestion(text) {
        if (sending) return;
        el.input.value = text;
        autoResizeInput();
        handleSend();
    }

    async function handleSummarize() {
        if (sending || summarizing) return;

        if (!summaryEnabled) {
            messages.push({
                role: "assistant",
                content: "A summary isn't available for your current license."
            });
            renderMessages();
            return;
        }

        summarizing = true;
        setInputEnabled(false);
        showTyping();

        try {
            const snapshot = await readWorkItemSnapshot();
            const data = await requestSummary(snapshot);
            rememberHash(snapshot.hash);

            hideTyping();
            messages.push({
                role: "assistant",
                kind: "summary",
                live: false,
                content: data.summary,
                lastUpdated: data.lastUpdated
            });
            renderMessages();

            const chatId = await ensureChatForSummary();
            if (chatId) {
                await persistSummaryMessage(chatId, data.summary);
            }
        } catch (err) {
            console.error("[Cleo] summarize failed", err);
            messages.push({
                role: "error",
                content: err.message || "Failed to generate summary"
            });
        } finally {
            hideTyping();
            summarizing = false;
            setInputEnabled(true);
            renderChatSelect();
            renderMessages();
        }
    }

    async function ensureChatForSummary() {
        if (activeChatId) return activeChatId;

        try {
            await waitForSummary();

            const created = await createChatOnApi("Summary");
            activeChatId = created.id;
            chats = [{ id: created.id, title: created.title }]
                .concat(chats.filter(c => c.id !== created.id));

            await finalizeAutoSummary(activeChatId);

            return activeChatId;
        } catch (e) {
            console.warn("[Cleo] failed to create chat for summary", e);
            return null;
        }
    }

    /* =========================== MESSAGING =========================== */

    async function handleSend() {
        const text = el.input.value.trim();

        // An attachment on its own is a valid turn — "what's wrong here?" with just a screenshot.
        if ((!text && stagedFiles.length === 0) || sending || summarizing) return;

        sending = true;
        setInputEnabled(false);
        el.input.value = "";
        clearInputDraft();
        inputHistoryIndex = null;
        inputHistoryDraft = "";
        autoResizeInput();

        const isNewChat = !activeChatId;

        // Taken and cleared before the upload so the composer empties on send, as the text does.
        const filesToSend = stagedFiles.slice();
        clearStagedAttachments();

        const userMessage = { role: "user", content: text, attachments: filesToSend.map(buildLocalPreview) };
        messages.push(userMessage);
        renderMessages();
        showTyping();

        try {
            if (isNewChat) {
                await waitForSummary();

                const created = await createChatOnApi(text);
                activeChatId = created.id;
                chats = [{ id: created.id, title: created.title }]
                    .concat(chats.filter(c => c.id !== created.id));

                await finalizeAutoSummary(activeChatId);

                renderChatSelect();
                renderMessages();
                showTyping();
            }

            // Uploaded here, not at compose time: the upload ticket is scoped to the chat id, which a
            // brand new conversation only has once it has been created above.
            const uploaded = await IndexLibrary.UploadAttachments(attachmentContext(), activeChatId, filesToSend);

            const result = await sendMessageApi(activeChatId, text, uploaded);

            if (result.attachments && result.attachments.length) {
                userMessage.attachments.forEach(a => a.localUrl && URL.revokeObjectURL(a.localUrl));
                userMessage.attachments = result.attachments;
            }

            messages.push({ role: "assistant", content: result.content });
        } catch (err) {
            console.error("[Cleo] send failed", err);
            messages.push({
                role: "error",
                content: err.message || "Unable to get a response. Please try again.",
                retry: text
            });
        } finally {
            hideTyping();
            sending = false;
            setInputEnabled(true);
            renderMessages();
            el.input.focus();
        }
    }

    async function waitForSummary() {
        if (!summaryEnabled || !summaryPromise) return;
        try {
            await summaryPromise;
        } catch (e) { /* a failed summary must not block the message */ }
    }

    async function finalizeAutoSummary(chatId) {
        const live = getLiveSummaryMessage();
        if (!live) return;

        live.live = false;
        live.content = summary.text;

        if (summary.status === "ready" && summary.text) {
            await persistSummaryMessage(chatId, summary.text);
        } else {
            messages = messages.filter(m => m !== live);
        }
    }

    async function persistSummaryMessage(chatId, text) {
        try {
            const res = await IndexLibrary.PostRequest({
                baseAPIUrl: ctx.baseAPIUrl,
                endpoint: `/api/InChat/${chatId}/messages/assistant`,
                azureAuthenticationHeader: ctx.accessToken,
                organizationName: ctx.organizationName,
                organizationId: ctx.organizationId,
                headers: { "UserEmail": ctx.userEmail || "" },
                body: { MessageContext: text }
            });

            if (!res.ok) {
                console.warn("[Cleo] failed to persist summary message", res.status);
            }
        } catch (e) {
            console.warn("[Cleo] failed to persist summary message", e);
        }
    }

    async function createChatOnApi(initialContext) {
        const res = await IndexLibrary.PostRequest({
            baseAPIUrl: ctx.baseAPIUrl,
            endpoint: `/api/InChat/new`,
            azureAuthenticationHeader: ctx.accessToken,
            organizationName: ctx.organizationName,
            organizationId: ctx.organizationId,
            headers: { "UserEmail": ctx.userEmail || "" },
            body: {
                OrganizationId: ctx.organizationId,
                ProjectId: ctx.projectId,
                WorkItemId: Number(workItemId),
                InitialContext: initialContext
            }
        });

        if (!res.ok) throw new Error(`API Error: ${res.status}`);

        const apiChat = await res.json();
        return {
            id: String(apiChat.chatId || apiChat.id || apiChat.chatID || apiChat.ID),
            title: apiChat.chatName || apiChat.name || apiChat.title || "New chat"
        };
    }

    async function sendMessageApi(chatId, message, attachments = []) {
        const res = await IndexLibrary.PostRequest({
            baseAPIUrl: ctx.baseAPIUrl,
            endpoint: `/api/InChat/${chatId}/chat`,
            azureAuthenticationHeader: ctx.accessToken,
            organizationName: ctx.organizationName,
            organizationId: ctx.organizationId,
            headers: { "UserEmail": ctx.userEmail || "" },
            body: {
                MessageContext: message,
                azureDevOpsOrgName: ctx.organizationName,
                azureAccessToken: ctx.accessToken,
                attachments: attachments
            }
        });

        let data = null;
        try {
            data = await res.json();
        } catch (e) { /* non JSON error body */ }

        if (!res.ok) {
            throw new Error(data?.errorMessage || data?.message || `API Error: ${res.status}`);
        }
        if (data?.isError) {
            throw new Error(data.errorMessage || "Unable to get a response. Please try again.");
        }

        const content = data?.aiResponse?.messageContext || data?.aiResponse?.MessageContext;
        if (!content) throw new Error("Invalid API response format");

        return {
            content,
            attachments: data?.userMessage?.attachments || data?.userMessage?.Attachments || []
        };
    }

    /* ========================= ATTACHMENTS ========================== */

    function attachmentContext() {
        return {
            baseAPIUrl: ctx.baseAPIUrl,
            azureAuthenticationHeader: ctx.accessToken,
            organizationName: ctx.organizationName,
            organizationId: ctx.organizationId,
            userEmail: ctx.userEmail || ""
        };
    }

    function clearStagedAttachments() {
        stagedFiles = [];
        renderStagedAttachments();
    }

    function onFilesSelected(e) {
        const files = Array.from(e.target.files || []);
        el.file.value = "";

        for (const file of files) {
            if (!IndexLibrary.IsAcceptedAttachment(file)) {
                IndexLibrary.ShowToast("error", `${file.name} is not a supported type (.md or an image).`);
                continue;
            }

            if (stagedFiles.length >= IndexLibrary.MAX_ATTACHMENTS_PER_MESSAGE) {
                IndexLibrary.ShowToast("error",
                    `You can attach at most ${IndexLibrary.MAX_ATTACHMENTS_PER_MESSAGE} files to a message.`);
                break;
            }

            stagedFiles.push(file);
        }

        renderStagedAttachments();
    }

    function renderStagedAttachments() {
        if (!el.attachments) return;

        el.attachments.innerHTML = "";

        if (stagedFiles.length === 0) {
            el.attachments.classList.add("hidden");
            if (el.attach) el.attach.disabled = false;
            return;
        }

        el.attachments.classList.remove("hidden");

        // Deliberately not re-enabling mid-send: setInputEnabled owns that state.
        if (el.attach && !sending) {
            el.attach.disabled = stagedFiles.length >= IndexLibrary.MAX_ATTACHMENTS_PER_MESSAGE;
        }

        stagedFiles.forEach(file => {
            const node = document.createElement("span");
            node.className = "qa-cleo-chip";

            const icon = document.createElement("i");
            icon.className = IndexLibrary.IsImageAttachment(file.name) ? "bi bi-image" : "bi bi-file-earmark-text";
            node.appendChild(icon);

            const label = document.createElement("span");
            label.className = "qa-cleo-chip-name";
            label.textContent = file.name;
            label.title = file.name;
            node.appendChild(label);

            const remove = document.createElement("button");
            remove.type = "button";
            remove.className = "qa-cleo-chip-remove";
            remove.innerHTML = '<i class="bi bi-x"></i>';
            remove.title = "Remove attachment";
            remove.addEventListener("click", () => {
                stagedFiles = stagedFiles.filter(f => f !== file);
                renderStagedAttachments();
            });
            node.appendChild(remove);

            el.attachments.appendChild(node);
        });
    }

    /** A thumbnail backed by the local file, shown until the server confirms the message. */
    function buildLocalPreview(file) {
        return {
            localUrl: URL.createObjectURL(file),
            fileName: file.name,
            isImage: IndexLibrary.IsImageAttachment(file.name)
        };
    }

    /**
     * The blob URL is only requested once the tile is near the viewport, so reopening a long
     * conversation does not fire a signing request for every image it ever contained.
     */
    function buildAttachmentTile(attachment, uniform) {
        const fileName = attachment.fileName || attachment.FileName || "attachment";
        const isImage = attachment.isImage ?? attachment.IsImage ?? IndexLibrary.IsImageAttachment(fileName);

        if (!isImage) {
            const file = document.createElement("span");
            file.className = "qa-cleo-attachment-file";
            file.innerHTML = '<i class="bi bi-file-earmark-text"></i>';
            const name = document.createElement("span");
            name.textContent = fileName;
            file.appendChild(name);
            file.title = fileName;
            return file;
        }

        const tile = document.createElement("button");
        tile.type = "button";
        tile.className = "qa-cleo-attachment-tile";
        tile.title = fileName;

        const width = attachment.width ?? attachment.Width;
        const height = attachment.height ?? attachment.Height;
        if (!uniform && width && height) tile.style.aspectRatio = `${width} / ${height}`;

        const img = document.createElement("img");
        img.loading = "lazy";
        img.decoding = "async";
        img.alt = fileName;
        tile.appendChild(img);

        const show = (url) => {
            img.src = url;
            tile.classList.add("loaded");
        };

        if (attachment.localUrl) {
            show(attachment.localUrl);
            tile.addEventListener("click", () => openAttachmentLightbox(attachment.localUrl, fileName));
            return tile;
        }

        tile.addEventListener("click", () => {
            if (img.src) openAttachmentLightbox(img.src, fileName);
        });

        const id = attachment.id ?? attachment.Id;

        const cachedUrl = IndexLibrary.GetCachedAttachmentUrl(activeChatId, id);
        if (cachedUrl) {
            show(cachedUrl);
            return tile;
        }

        let requested = false;

        const load = async () => {
            if (requested) return;
            requested = true;

            try {
                const url = await IndexLibrary.GetAttachmentUrl(attachmentContext(), activeChatId, id);
                if (url) {
                    show(url);
                } else {
                    tile.classList.add("failed");
                }
            } catch (e) {
                console.warn("[Cleo] could not load attachment", fileName, e);
                tile.classList.add("failed");
            }
        };

        if (typeof IntersectionObserver === "function") {
            const observer = new IntersectionObserver(entries => {
                if (entries.some(entry => entry.isIntersecting)) {
                    observer.disconnect();
                    load();
                }
            }, { rootMargin: "400px" });
            observer.observe(tile);
        } else {
            load();
        }

        return tile;
    }

    function buildAttachmentGrid(attachments) {
        if (!attachments || attachments.length === 0) return null;

        const images = attachments.filter(a =>
            a.isImage ?? a.IsImage ?? IndexLibrary.IsImageAttachment(a.fileName || a.FileName || ""));
        const files = attachments.filter(a => !images.includes(a));

        const grid = document.createElement("div");
        const count = Math.min(attachments.length, IndexLibrary.MAX_ATTACHMENTS_PER_MESSAGE);
        grid.className = `qa-cleo-attachment-grid count-${count}`;

        const uniform = images.length > 1;

        images.forEach(attachment => grid.appendChild(buildAttachmentTile(attachment, uniform)));
        files.forEach(attachment => grid.appendChild(buildAttachmentTile(attachment, uniform)));

        return grid;
    }

    function openAttachmentLightbox(url, fileName) {
        const overlay = document.createElement("div");
        overlay.className = "qa-cleo-lightbox";

        const image = document.createElement("img");
        image.src = url;
        image.alt = fileName;
        overlay.appendChild(image);

        const close = () => {
            overlay.remove();
            document.removeEventListener("keydown", onKeyDown);
        };

        const onKeyDown = (e) => {
            if (e.key === "Escape") close();
        };

        overlay.addEventListener("click", close);
        document.addEventListener("keydown", onKeyDown);
        document.body.appendChild(overlay);
    }

    /* =========================== RENDERING =========================== */

    function renderMessages() {
        if (!el.messages) return;

        el.messages.innerHTML = "";
        const fragment = document.createDocumentFragment();

        messages.forEach(msg => fragment.appendChild(buildMessageNode(msg)));

        el.messages.appendChild(fragment);
        scrollToBottom();
    }

    function buildMessageNode(msg) {
        if (msg.kind === "summary") {
            return buildSummaryNode(msg);
        }

        const wrapper = document.createElement("div");
        wrapper.className = `qa-cleo-msg ${msg.role}`;

        const content = document.createElement("div");
        content.className = "qa-cleo-msg-content";

        if (msg.role === "assistant") {
            content.classList.add("qa-cleo-markdown");
            content.innerHTML = renderMarkdown(msg.content);
        } else if (msg.role === "error") {
            content.textContent = msg.content;

            const retry = document.createElement("button");
            retry.type = "button";
            retry.className = "qa-cleo-retry";
            retry.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Retry';
            retry.addEventListener("click", () => {
                const index = messages.indexOf(msg);
                if (index === -1) return;
                messages.splice(index, 1);
                const previous = messages[messages.length - 1];
                if (previous && previous.role === "user") messages.pop();
                renderMessages();
                el.input.value = msg.retry || "";
                handleSend();
            });
            content.appendChild(retry);
        } else {
            content.textContent = msg.content;
        }

        // Stacked in a column so the thumbnails sit above the bubble rather than beside it; the
        // wrapper itself is a flex row that handles left/right alignment of the whole message.
        const grid = buildAttachmentGrid(msg.attachments);
        if (grid) {
            const stack = document.createElement("div");
            stack.className = "qa-cleo-msg-stack";
            stack.appendChild(grid);
            if (msg.content) stack.appendChild(content);
            wrapper.appendChild(stack);
            return wrapper;
        }

        wrapper.appendChild(content);
        return wrapper;
    }

    function buildSummaryNode(msg) {
        const wrapper = document.createElement("div");
        wrapper.className = "qa-cleo-msg assistant";
        if (msg.live) wrapper.dataset.summary = "live";

        const content = document.createElement("div");
        content.className = "qa-cleo-msg-content qa-cleo-summary";

        const head = document.createElement("div");
        head.className = "qa-cleo-summary-head";
        head.innerHTML = '<i class="bi bi-stars"></i><span>AI Summary</span>';

        const meta = document.createElement("span");
        meta.className = "qa-cleo-summary-meta";
        head.appendChild(meta);
        content.appendChild(head);

        const body = document.createElement("div");
        body.className = "qa-cleo-markdown";

        if (!msg.live) {
            body.innerHTML = renderMarkdown(msg.content);
            content.appendChild(body);
            wrapper.appendChild(content);
            return wrapper;
        }

        if (summary.status === "loading") {
            meta.innerHTML = '<span class="qa-inline-spinner"></span>';
            body.classList.add("qa-cleo-summary-status");
            body.textContent = summary.text
                ? "The work item changed — updating the summary…"
                : "Summarizing this work item…";
        } else if (summary.status === "error") {
            body.classList.add("qa-cleo-summary-status", "qa-cleo-summary-failed");
            body.textContent = summary.error || "Couldn't generate the summary.";

            const retry = document.createElement("button");
            retry.type = "button";
            retry.className = "qa-cleo-retry";
            retry.innerHTML = '<i class="bi bi-arrow-clockwise"></i> Try again';
            retry.addEventListener("click", regenerate);
            body.appendChild(retry);
        } else {
            const stamp = summary.lastUpdated
                ? IndexLibrary.convertUtcToLocal(summary.lastUpdated)
                : "";
            meta.textContent = summary.origin === "reused"
                ? `Up to date · ${stamp}`
                : `Updated · ${stamp}`;
            body.innerHTML = renderMarkdown(summary.text);
        }

        content.appendChild(body);
        wrapper.appendChild(content);
        return wrapper;
    }

    function renderMarkdown(text) {
        try {
            if (typeof DOMPurify === "undefined") {
                return escapeHtml(text);
            }
            const parsed = marked.parse(text || "");
            const temp = document.createElement("div");
            temp.innerHTML = DOMPurify.sanitize(parsed, { USE_PROFILES: { html: true } });
            temp.querySelectorAll("a").forEach(a => {
                a.setAttribute("target", "_blank");
                a.setAttribute("rel", "noopener noreferrer");
            });
            return temp.innerHTML;
        } catch (e) {
            return escapeHtml(text);
        }
    }

    function renderFatal(message) {
        if (!el.messages) return;
        el.messages.innerHTML = `<div class="qa-cleo-error">${escapeHtml(message)}</div>`;
        setInputEnabled(false);
    }

    function showTyping() {
        hideTyping();
        const wrapper = document.createElement("div");
        wrapper.id = "qa-cleo-typing";
        wrapper.className = "qa-cleo-msg assistant";
        wrapper.innerHTML = `
            <div class="qa-cleo-msg-content qa-cleo-typing">
                <span>Thinking</span>
                <span class="qa-cleo-dots"><span></span><span></span><span></span></span>
            </div>`;
        el.messages.appendChild(wrapper);
        scrollToBottom();
    }

    function hideTyping() {
        const node = document.getElementById("qa-cleo-typing");
        if (node) node.remove();
    }

    function scrollToBottom() {
        requestAnimationFrame(() => {
            el.messages.scrollTop = el.messages.scrollHeight;
        });
    }

    function setInputEnabled(enabled) {
        el.send.disabled = !enabled;
        el.input.disabled = !enabled;
        setSuggestionsEnabled(enabled);
        if (el.attach) el.attach.disabled = !enabled;
    }

    function setSuggestionsEnabled(enabled) {
        el.suggestions?.querySelectorAll(".qa-cleo-pill")
            .forEach(btn => { btn.disabled = !enabled; });
    }

    function autoResizeInput() {
        el.input.style.height = "auto";
        el.input.style.height = Math.min(el.input.scrollHeight, 110) + "px";
    }

    function getInputHistory() {
        return messages
            .filter(m => m.role === "user" && typeof m.content === "string")
            .map(m => m.content);
    }

    function setInputValue(value) {
        el.input.value = value;
        autoResizeInput();
        const end = value.length;
        el.input.setSelectionRange(end, end);
        saveInputDraft();
    }

    function draftKey() {
        return `qa_cleo_draft_${ctx?.organizationId}_${workItemId}_${activeChatId ?? "new"}`;
    }

    function saveInputDraft() {
        try {
            localStorage.setItem(draftKey(), el.input.value);
        } catch (e) { /* storage unavailable */ }
    }

    function restoreInputDraft() {
        let text = "";
        try {
            text = localStorage.getItem(draftKey()) || "";
        } catch (e) { /* storage unavailable */ }
        el.input.value = text;
        autoResizeInput();
    }

    function clearInputDraft() {
        try {
            localStorage.removeItem(draftKey());
        } catch (e) { /* storage unavailable */ }
    }

    function recallHistoryUp() {
        const history = getInputHistory();
        if (history.length === 0) return false;

        if (inputHistoryIndex === null) {
            inputHistoryDraft = el.input.value;
            inputHistoryIndex = history.length - 1;
        } else if (inputHistoryIndex > 0) {
            inputHistoryIndex--;
        } else {
            return false;
        }

        setInputValue(history[inputHistoryIndex]);
        return true;
    }

    function recallHistoryDown() {
        if (inputHistoryIndex === null) return false;

        const history = getInputHistory();
        if (inputHistoryIndex < history.length - 1) {
            inputHistoryIndex++;
            setInputValue(history[inputHistoryIndex]);
        } else {
            const draft = inputHistoryDraft;
            inputHistoryIndex = null;
            inputHistoryDraft = "";
            setInputValue(draft);
        }
        return true;
    }

    function escapeHtml(text) {
        return String(text ?? "")
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#39;");
    }

    ScreenManager.register("cleo", { init, dispose });
})();
