// ChatUI: handles DOM rendering and server-event driven updates
class ChatUI {
    constructor(service) {
        this.lastBubble = null;
        this.service = service;
        this.display = {};
        this.buffer = {};
        this.loadingTimers = {};
        this.earliestId = null;   // track the cursor
        this.loadingHistory = false;
        this.hasMoreHistory = true;
        this.statusBubbleId = null;
    }

    // --- streaming/render state ---
    RENDER_INTERVAL = 120;      // ms: throttle re-render during streaming
    HIGHLIGHT_INTERVAL = 600;   // ms: throttle highlighting while streaming
    STICKY_THRESHOLD = 32;      // px: how close to bottom to auto-stick

    renderTimer = null;
    highlightTimer = null;
    scrollRaf = null;
    streaming = false;

    init() {
        this.basePath = $('#pathPrefix').val();
        this.service.basePath = this.basePath;

        // bind events
        [
            'created', 'in_progress', 'completed',
            'output_item.added', 'output_item.done',
            'content_part.added', 'output_text.delta',
            'function_call_arguments.delta', 'function_call_arguments.done'
        ].forEach(evt => this.service.on(`response.${evt}`, e => this[`on${evt.replace(/\./g, '_')}`](e)));
        // bind events
        [
            'function_call.started', 'function_call.completed',
        ].forEach(evt => this.service.on(`${evt}`, e => this[`on${evt.replace(/\./g, '_')}`](e)));

        // on‐scroll listener
        const box = $('#chatBox');
        this.loadMoreHistory().then(() => {
            // After initial load, scroll to bottom
            box.scrollTop(box[0].scrollHeight);
            // Bind scroll event to load more when near top
            setTimeout(() => {
                box.on('scroll', () => {
                    if (box.scrollTop() < 50 && !this.loadingHistory && this.hasMoreHistory) {
                        this.loadMoreHistory();
                    }
                });
            }, 200);
        });
    }
    async loadMoreHistory() {
        this.loadingHistory = true;
        const ref = $('#sessionReference').val();
        // pass earliestId for paging, omit on first load
        const json = await this.service.fetchHistory(ref, this.earliestId, 10);
        if (!json.success) {
            this.loadingHistory = false;
            return;
        }
        const messages = json.data.output;
        if (messages.length < 10) this.hasMoreHistory = false;

        // remember current scroll position & height
        const box = $('#chatBox');
        const oldScrollHeight = box[0].scrollHeight;

        // prepend them in reverse chronological order
        messages.reverse().forEach(msg => {
            this.prependChat(msg.content, msg.role, msg.response_id);
        });
        if(messages.length === 0) {
            this.prependChat('This is a new chat! Ask me anything.', 'system');
            this.loadingHistory = false;
            return;
        }
        // update cursor to the new oldest
        this.earliestId = messages[messages.length - 1].response_id;

        // restore scroll so the user doesn’t see a jump
        const newScrollHeight = box[0].scrollHeight;
        // Temporarily disable CSS smooth scroll to prevent flicker
        const boxEl = box.get(0);
        const prevBehavior = boxEl.style.scrollBehavior;
        boxEl.style.scrollBehavior = 'auto';
        box.scrollTop(newScrollHeight - oldScrollHeight);
        // Restore original scroll behavior
        boxEl.style.scrollBehavior = prevBehavior;

        this.loadingHistory = false;
    }


    oncreated(e) {
        //this.createChat(e.response_id);
    }
    onin_progress() {
        //this.toggleInput(false);
    }
    onoutput_item_added(e) {
        this.showOutput(e.item.id);
    }
    oncontent_part_added(e) {
        this.updateOutput(e.part.text);
    }
    onoutput_text_delta(e) { this.updateOutput(e.delta); }
    onoutput_item_done(e) {

    }
    onfunction_call_started(e) {
       this.updateFunction(`Calling ${e.name}...`);
    }
    onfunction_call_completed(e) {
       this.updateFunction(' done.<br>');
    }
    onfunction_call_arguments_delta(e) {
        const id = e.itemId;
        if(typeof this.loadingTimers[id] === 'undefined') {
            this.loadingTimers[id] = true;
            this.updateFunction('Generating function call arguments...');
        }
    }
    onfunction_call_arguments_done(e) {
        const id = e.itemId;

        if (this.loadingTimers[id]) {
            delete this.loadingTimers[id];
        }
        this.updateFunction(' done <br>');
    }
    oncompleted(e) {
        console.log('Response completed:', e);
        const respId = e.response_id;
        // fetch final full response
        this.service.fetchResponse(respId)
            .then(json => {
                if (json.success) {
                    const bubble = $(`.system-message[data-response-id="${respId}"]`).find('.system-bubble');
                    const fullHtml = this.md(json.data.output);
                    
                    const id = this.statusBubbleId;
                    const b = this.lastBubble;
                    const output = b.find('.output-text');

                    // cancel streaming timers
                    if (this.renderTimer) { clearTimeout(this.renderTimer); this.renderTimer = null; }
                    if (this.highlightTimer) { clearTimeout(this.highlightTimer); this.highlightTimer = null; }

                    const before = this.measureScroll();
                    this.buffer[id] = fullHtml;
                    output.html(fullHtml).show();
                    this.sanitizeLinks(output);
                    output.find('pre, code').each((_, e2) => hljs.highlightElement(e2));
                    this.preserveOrStick(before);

                    /*this.buffer[id] = fullHtml;
                    output.html(this.md(this.buffer[id])).show();
                    this.sanitizeLinks(output);
                    this.scroll();*/
                }
            })
            .finally(() => this.clearStatus());
    }

    updateStatus(text) {
        const b = this.lastBubble;
        b.find('.status-updates').text(text).show();
        b.find('.function-updates,.output-text').hide();
    }
    updateFunction(text) {
        const b = this.lastBubble;
        b.find('.function-updates').append(text).show();
    }
    updateOutput(text) {
        const id = this.statusBubbleId;
        const b  = this.lastBubble;
        if (!id || !b) return;

        this.streaming = true;
        this.buffer[id] = (this.buffer[id] || '') + text;

        // Throttle renders to avoid reflow thrash
        this.scheduleRender();
    }
    finishOutput() {
        const id = this.statusBubbleId;
        const b  = this.lastBubble;
        if (!id || !b) return;

        // stop pending timers
        if (this.renderTimer) { clearTimeout(this.renderTimer); this.renderTimer = null; }
        if (this.highlightTimer) { clearTimeout(this.highlightTimer); this.highlightTimer = null; }

        this.streaming = false;

        const output = b.find('.output-text');
        const before = this.measureScroll();

        output.html(this.mdFinal(this.buffer[id] || '')).show();
        this.sanitizeLinks(output);
        output.find('pre, code').each((_, e) => hljs.highlightElement(e));

        this.preserveOrStick(before);
        this.buffer[id] = null;
    }
    showOutput(id) {
        const b = this.lastBubble;
        b.find('.status-updates,.function-updates').hide();
        b.find('.output-text').show().html('');
        this.display[id] = b.find('.output-text');
        this.scroll();
    }
    // Call this before sending
    startStatus() {
        const id = 'status_'+Date.now();
        this.statusBubbleId = id;
        this.createChat(id);
        this.updateStatus('Thinking...');
        this.toggleInput(false);
        this.scroll();
    }
    // Remove status bubble when done
    clearStatus() {
        if (this.statusBubbleId) {
            this.finishOutput();
            //this.remove(this.statusBubbleId, true);
            this.statusBubbleId = null;
        }
        this.toggleInput(true);
    }

    // Create skeleton bubble with three regions
    createChat(bubbleId) {
        const container = $(`
        <div class="mb-2 text-start system-message" data-response-id="${bubbleId}">
            <div class="system-bubble">
            <div class="status-updates" style="display:none;color:var(--muted-text);"></div>
            <div class="function-updates" style="display:none;color:var(--primary-color);"></div>
            <div class="output-text" style="display:none;"></div>
            </div>
        </div>
        `);
        $('#chatBox').append(container);
        this.lastBubble = container.find('.system-bubble');
        this.scroll(container);
    }
    
    prependChat(text, sender, id = '') {
        const align = sender === 'user' ? 'text-end' : 'text-start';
        const div = $(`
      <div class="mb-2 ${align} ${sender}-message" data-response-id="${id}">
        <div class="${sender}-bubble" data-response-id="${id}">
          <div class="output-text">${this.md(text)}</div>
        </div>
      </div>
    `);
        $('#chatBox').prepend(div);
    }
    addChat(text, sender, id = '') {
        const align = sender === 'user' ? 'text-end' : 'text-start';
        const div = $(`<div class="mb-2 ${align} ${sender}-message" data-response-id="${id}"><div class="${sender}-bubble" data-response-id="${id}"><div class="output-item"><div class="output-text">${this.md(text)}</div></div></div></div>`);
        $('#chatBox').append(div);
        this.scroll(div);
        this.sanitizeLinks(div);
    }

    //finish(id, idx, txt) { this.display[id].html(this.md(txt)); this.sanitizeLinks(this.display[id]); this.scroll(); }
    remove(id, animate = false) {
        // find the .system-message that contains our temporary item
        const msg = $(`.system-message`).has(`#${id}`);
        if (msg.length === 0) return; // nothing to remove

        if (animate) {
            msg
                .css('overflow', 'hidden')  // ensure contents don't overflow
                .animate(
                    { opacity: 0, height: 0, margin: 0, padding: 0 },
                    700,                    // duration in ms
                    () => msg.remove()      // callback once animation finishes
                );
        } else msg.remove();                // remove bubble + wrapper
    }
    sanitizeLinks(div) {
        div.find('a').each((_, a) => {
            const href = $(a).attr('href');
            if (href && !href.startsWith('javascript:') && !href.startsWith('#')) {
                $(a).attr('target', '__blank');
                $(a).attr('rel', 'noopener noreferrer');
            }
        });
    }
    md(t) {
        if (t === null) return t;
        if (typeof t !== 'string') return t;
        if (t.length === 0) return t;
        marked.setOptions({ breaks: true, gfm: true, highlight: (c, l) => hljs.highlightAuto(c, [l]).value });
        return DOMPurify.sanitize(marked.parse(t));
    }
    scroll(div) { const box = $('#chatBox'); box.scrollTop(box[0].scrollHeight); const root = div || box; root.find('pre, code').each((_, e) => hljs.highlightElement(e)); }
    toggleInput(en) { $('#sendMessage').prop('disabled', !en); $('#userMessage').prop('disabled', !en).prop('readonly', !en); }


    atBottom() {
        const box = $('#chatBox')[0];
        if (!box) return true;
        const distance = box.scrollHeight - box.scrollTop - box.clientHeight;
        return distance <= this.STICKY_THRESHOLD;
    }

    // keep user's relative position if they scrolled up
    preserveOrStick(before) {
        const box = $('#chatBox');
        const el = box[0];
        if (!el) return;

        const wasAtBottom = before?.wasAtBottom ?? this.atBottom();
        const distanceFromBottom = before?.distanceFromBottom ??
            (el.scrollHeight - el.scrollTop - el.clientHeight);

        if (wasAtBottom) {
            this.scheduleScroll();
        } else {
            // maintain relative position
            const target = el.scrollHeight - el.clientHeight - distanceFromBottom;
            el.scrollTop = Math.max(0, target);
        }
    }

    measureScroll() {
        const el = $('#chatBox')[0];
        if (!el) return { wasAtBottom: true, distanceFromBottom: 0 };
        const wasAtBottom = this.atBottom();
        const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
        return { wasAtBottom, distanceFromBottom };
    }

    scheduleScroll() {
        if (this.scrollRaf) return;
        this.scrollRaf = requestAnimationFrame(() => {
            this.scrollRaf = null;
            const box = $('#chatBox');
            const el = box[0];
            if (!el) return;
            el.scrollTop = el.scrollHeight;
        });
    }

    // FAST markdown (no HLJS) for streaming
    mdFast(t) {
        if (t == null || typeof t !== 'string' || t.length === 0) return t;
        marked.setOptions({ breaks: true, gfm: true, highlight: null });
        return DOMPurify.sanitize(marked.parse(t));
    }

    // FINAL markdown (with HLJS) for completion
    mdFinal(t) {
        if (t == null || typeof t !== 'string' || t.length === 0) return t;
        marked.setOptions({ breaks: true, gfm: true, highlight: (c, l) => hljs.highlightAuto(c, [l]).value });
        return DOMPurify.sanitize(marked.parse(t));
    }

    // Throttled render during streaming
    scheduleRender() {
        if (this.renderTimer) return;
        this.renderTimer = setTimeout(() => {
            this.renderTimer = null;
            this.performRender();
        }, this.RENDER_INTERVAL);
    }

    performRender() {
        const id = this.statusBubbleId;
        const b = this.lastBubble;
        if (!id || !b) return;

        const output = b.find('.output-text');
        const before = this.measureScroll();
        const html = this.mdFast(this.buffer[id] || '');
        output.html(html).show();
        this.sanitizeLinks(output);
        this.preserveOrStick(before);

        // schedule light highlighting (throttled) while streaming
        clearTimeout(this.highlightTimer);
        this.highlightTimer = setTimeout(() => {
            this.highlightTimer = null;
            output.find('pre, code').each((_, e) => hljs.highlightElement(e));
        }, this.HIGHLIGHT_INTERVAL);
    }
}