// SlothApp - Google Chat DOM observer.
// Reads what the user is already seeing in chat.google.com / mail.google.com/chat
// and forwards new messages to the background worker (and onward to the native app).
// No external requests are made.

(() => {
    const SOURCE = 'google_chat';
    const MAX_MSG_TEXT = 500; // mirror ChatSnippet.maxTextLength
    const seenIds = new Set();
    let initialScanComplete = false;

    // Selectors live here so we can update them centrally when Google changes the DOM.
    // These target stable attributes where possible; class names are fallbacks.
    const SELECTORS = {
        messageNodes: [
            '[data-topic-id]',                          // top-level thread/message containers
            '[data-message-id]',
            '[role="listitem"]',
        ],
        author: [
            '[data-name]',
            '[data-hovercard-id]',
            'span[data-self-name]',
        ],
        text: [
            '[data-message-text]',
            '[data-text]',
            '[jsname="bgckF"]',
            'div[dir="auto"]',
        ],
        timestamp: [
            'span[data-absolute-timestamp]',
            '[data-time]',
            'time',
        ],
        roomName: [
            'h1[role="heading"]',
            '[data-room-name]',
        ],
        participantList: [
            '[data-member-name]',
            '[data-hovercard-id]',
        ],
    };

    function pickFirst(node, selectors) {
        for (const sel of selectors) {
            const el = node.querySelector ? node.querySelector(sel) : null;
            if (el) return el;
        }
        return null;
    }

    function extractMessage(node, { allowMissingTimestamp = false } = {}) {
        if (!node || !node.querySelector) return null;
        const id = node.getAttribute('data-message-id')
            || node.getAttribute('data-topic-id')
            || node.id
            || null;
        if (!id || seenIds.has(id)) return null;

        const authorEl = pickFirst(node, SELECTORS.author);
        const textEl = pickFirst(node, SELECTORS.text);
        const tsEl = pickFirst(node, SELECTORS.timestamp);
        if (!textEl) return null;

        const text = (textEl.innerText || '').trim();
        if (!text) return null;

        let messageTimestamp = null;
        if (tsEl) {
            const abs = tsEl.getAttribute('data-absolute-timestamp')
                || tsEl.getAttribute('datetime')
                || tsEl.getAttribute('title');
            if (abs) {
                const t = new Date(abs);
                if (!isNaN(t.getTime())) messageTimestamp = t.toISOString();
            }
        }
        if (!messageTimestamp && !allowMissingTimestamp) {
            seenIds.add(id);
            return null;
        }

        const author = authorEl
            ? (authorEl.getAttribute('data-name') || authorEl.getAttribute('data-hovercard-id') || authorEl.innerText || '').trim()
            : null;

        seenIds.add(id);
        // Cap snippet length on the JS side too.
        const truncated = text.length > MAX_MSG_TEXT ? text.slice(0, MAX_MSG_TEXT) + '…' : text;

        return {
            id,
            author: author || 'unknown',
            messageText: truncated,
            messageTimestamp: messageTimestamp || new Date().toISOString(),
        };
    }

    function currentRoom() {
        // Title: the document title is the most reliable. Google Chat uses
        // formats like "Chat - <Room Name>" or "<Person Name> - Chat".
        let title = null;
        const docTitle = (document.title || '').trim();
        if (docTitle) {
            // Strip leading/trailing "Chat - " or " - Chat" / " - Google Chat"
            title = docTitle
                .replace(/\s*-\s*Google Chat\s*$/i, '')
                .replace(/\s*-\s*Chat\s*$/i, '')
                .replace(/^\s*Chat\s*-\s*/i, '')
                .trim();
        }
        // Fallbacks via DOM
        if (!title) {
            const headers = [
                document.querySelector('main h1'),
                document.querySelector('main [role="heading"][aria-level="1"]'),
                document.querySelector('[data-topic-id] [role="heading"]'),
            ].filter(Boolean);
            if (headers[0]) title = (headers[0].innerText || '').trim();
        }

        // Participants: collect distinct author names from currently-rendered
        // messages. This is the most reliable across Google's DOM changes.
        const authorSet = new Set();
        for (const el of document.querySelectorAll('[data-name], [data-hovercard-id]')) {
            const n = (el.getAttribute('data-name') || el.getAttribute('data-hovercard-id') || el.innerText || '').trim();
            if (!n) continue;
            // Skip obvious ids (numeric / email-like long strings handled separately)
            if (/^[0-9a-f-]{16,}$/i.test(n)) continue;
            authorSet.add(n);
        }
        const participants = Array.from(authorSet).slice(0, 30);

        // ThreadId: chat URLs look like
        //   chat.google.com/u/0/dm/<id>
        //   chat.google.com/u/0/room/<id>
        //   chat.google.com/u/0/app/chat/<id>
        // Use the longest stable-looking path segment as the threadId.
        const path = location.pathname || '';
        const segs = path.split('/').filter(Boolean);
        let threadId = null;
        for (const s of segs.reverse()) {
            if (s.length >= 8 && /^[A-Za-z0-9_-]+$/.test(s)) { threadId = s; break; }
        }
        if (!threadId) threadId = (location.hash || '').replace(/^#/, '') || null;

        return { roomName: title, participants, threadId };
    }

    // ---- Extension lifecycle guards ----
    // The page outlives the extension's content-script context whenever the
    // user reloads/updates the extension. Anything we leave running (timers,
    // MutationObserver, callbacks accessing `chrome.*`) will throw
    // "Extension context invalidated" until we shut everything down.
    let urlPollInterval = null;
    let cleanedUp = false;

    function isExtAlive() {
        try { return !!(chrome && chrome.runtime && chrome.runtime.id); }
        catch (_) { return false; }
    }

    function cleanup() {
        if (cleanedUp) return;
        cleanedUp = true;
        try { observer.disconnect(); } catch (_) {}
        if (urlPollInterval) { clearInterval(urlPollInterval); urlPollInterval = null; }
        console.log('[SlothApp] Google Chat collector stopped (extension context gone).');
    }

    function safeSend(payload) {
        if (cleanedUp) return;
        if (!isExtAlive()) { cleanup(); return; }
        try {
            chrome.runtime.sendMessage(payload, () => {
                // Accessing lastError can itself throw after invalidation.
                try { void chrome.runtime.lastError; } catch (_) {}
            });
        } catch (_) {
            cleanup();
        }
    }

    function reportMessage(msg) {
        const room = currentRoom();
        safeSend({
            type: 'chatMessage',
            chat: {
                threadId: room.threadId,
                roomName: room.roomName,
                participants: room.participants,
                author: msg.author,
                messageText: msg.messageText,
                messageTimestamp: msg.messageTimestamp,
                url: location.href,
            },
        });
    }

    function scanAll(options = {}) {
        for (const sel of SELECTORS.messageNodes) {
            const nodes = document.querySelectorAll(sel);
            nodes.forEach((n) => {
                const m = extractMessage(n, options);
                if (m) reportMessage(m);
            });
        }
    }

    function reportRoomOpen() {
        const room = currentRoom();
        if (!room.roomName) return;
        safeSend({
            type: 'chatRoomOpened',
            chat: {
                threadId: room.threadId,
                roomName: room.roomName,
                participants: room.participants,
                url: location.href,
            },
        });
    }

    // Observe DOM for added messages.
    const observer = new MutationObserver(() => {
        if (cleanedUp) return;
        if (!isExtAlive()) { cleanup(); return; }
        if (!initialScanComplete) return;
        try { scanAll({ allowMissingTimestamp: true }); }
        catch (e) {
            if (!isExtAlive()) cleanup();
            else console.warn('[SlothApp] scanAll threw', e);
        }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // Watch URL changes (SPA routing).
    let lastHref = location.href;
    urlPollInterval = setInterval(() => {
        if (cleanedUp) return;
        if (!isExtAlive()) { cleanup(); return; }
        if (location.href !== lastHref) {
            lastHref = location.href;
            seenIds.clear();
            initialScanComplete = false;
            setTimeout(() => {
                if (cleanedUp || !isExtAlive()) { cleanup(); return; }
                try {
                    reportRoomOpen();
                    scanAll({ allowMissingTimestamp: false });
                    initialScanComplete = true;
                } catch (_) { if (!isExtAlive()) cleanup(); }
            }, 800);
        }
    }, 1500);

    // Initial pass after the SPA settles.
    setTimeout(() => {
        if (cleanedUp || !isExtAlive()) { cleanup(); return; }
        try {
            reportRoomOpen();
            scanAll({ allowMissingTimestamp: false });
            initialScanComplete = true;
        } catch (_) { if (!isExtAlive()) cleanup(); }
    }, 1500);

    // Safety net: when the page navigates away or closes, tear down our timers
    // and observer so nothing stays alive in an invalidated context.
    window.addEventListener('pagehide', cleanup, { once: true });

    console.log('[SlothApp] Google Chat collector active.');
})();
