import { EVENTS, PROTOCOL_VERSION, OFFICIAL_SERVER_URL, OFFICIAL_SERVER_TOKEN, EPISODE_LOBBY_TIMEOUT, FORCE_SYNC_TIMEOUT } from './shared/constants.js';
import { generateUsername } from './shared/names.js';
import { loadLocale, getMessage, getSystemLanguage } from './i18n.js';


// --- State Management ---
let socket = null;
let isConnecting = false;
let peerId = null; // initialized via getPeerId()
let currentRoom = null;
let currentTabId = null;
let currentTabTitle = null; // New: for Smart Matching
let logs = [];
let history = []; // New: for Action History
let storageInitialized = false;
let pendingLogs = [];
let pendingHistory = [];
let eventQueue = [];
let isNamespaceJoined = false;
let lastActionState = { action: null, senderId: null, timestamp: 0, acks: [] };
let localSeq = 0;                         // Monotonically increasing command sequence for this peer
const lastSeqBySender = {};               // senderId → last received seq (stale command guard)
const activePorts = new Set();            // New: track active content ports for keep-alive
let expectedAcksCount = 0;                // Snapshot of peerCount when initiating Force Sync

// --- Keep-Alive Port Listener ---
chrome.runtime.onConnect.addListener((port) => {
    if (port.name === 'keepAlive') {
        activePorts.add(port);
        port.onDisconnect.addListener(() => {
            activePorts.delete(port);
        });
    }
});

function _persistLastSeq() {
    if (storageInitialized) chrome.storage.session.set({ lastSeqBySender });
}

// --- Boot Sequence Lock ---
let restorationTask = null;

function ensureState() {
    if (!restorationTask) {
        restorationTask = new Promise(resolve => {
            let resolved = false;
            const done = () => { if (!resolved) { resolved = true; resolve(); } };

            const storageTimeout = setTimeout(() => {
                addLog('Storage restoration timed out, continuing with defaults', 'warn');
                storageInitialized = true;
                done();
            }, 10000);

            chrome.storage.session.get([
                'logs', 'history', 'currentRoom', 'lastActionState', 
                'eventQueue', 'isForceSyncInitiator', 'forceSyncAcks', 
                'forceSyncDeadline', 'reconnectFailed', 'reconnectStartTime', 'reconnectAttempts', 'currentTabId', 'currentTabTitle',
                'episodeLobby', 'localSeq', 'lastSeqBySender', 'expectedAcksCount', 'roomIdleSince', 'lastContentHeartbeatAt'
            ], (data) => {
                clearTimeout(storageTimeout);
                if (data.expectedAcksCount !== undefined) expectedAcksCount = data.expectedAcksCount;
                if (data.currentTabId !== undefined) currentTabId = data.currentTabId;
                if (data.currentTabTitle !== undefined) currentTabTitle = data.currentTabTitle;
                // Merge data from storage with any early-arriving state
                // New entries (added during boot) must stay at the top (index 0)
                if (data.logs) logs = [...logs, ...data.logs].slice(0, 200);
                if (data.history) history = [...history, ...data.history].slice(0, 20);
                if (data.currentRoom) currentRoom = data.currentRoom;
                if (data.lastActionState) lastActionState = data.lastActionState;
                
                if (data.eventQueue) eventQueue = [...eventQueue, ...data.eventQueue].slice(0, 50);
                if (data.isForceSyncInitiator !== undefined && isForceSyncInitiator === false) {
                    isForceSyncInitiator = data.isForceSyncInitiator;
                }
                if (data.forceSyncAcks) {
                    const mergedAcks = new Set([...forceSyncAcks, ...data.forceSyncAcks]);
                    forceSyncAcks = mergedAcks;
                }
                if (data.reconnectFailed !== undefined) reconnectFailed = data.reconnectFailed;
                if (data.reconnectStartTime) reconnectStartTime = data.reconnectStartTime;
                if (data.reconnectAttempts !== undefined) reconnectAttempts = data.reconnectAttempts;
                if (data.roomIdleSince !== undefined) roomIdleSince = data.roomIdleSince;
                if (data.lastContentHeartbeatAt !== undefined) lastContentHeartbeatAt = data.lastContentHeartbeatAt;

                // Recover Force Sync Timeout
                if (data.forceSyncDeadline) {
                    const remaining = data.forceSyncDeadline - Date.now();
                    if (remaining > 0 && isForceSyncInitiator) {
                        forceSyncTimeout = setTimeout(() => {
                            if (isForceSyncInitiator) {
                                addLog('Force Sync: Recovered timeout triggered, executing...', 'warn');
                                executeForceSync();
                            }
                        }, remaining);
                    } else if (remaining <= 0 && isForceSyncInitiator) {
                        executeForceSync();
                    }
                }

                // Recover Episode Lobby
                if (data.episodeLobby && !episodeLobby) {
                    episodeLobby = data.episodeLobby;
                    const lobbyRemaining = (episodeLobby.createdAt + EPISODE_LOBBY_TIMEOUT) - Date.now();
                    if (lobbyRemaining > 0) {
                        episodeLobbyTimeout = setTimeout(() => cancelEpisodeLobby('Timeout'), lobbyRemaining);
                    } else {
                        cancelEpisodeLobby('Timeout (recovered)');
                    }
                }

                if (data.localSeq !== undefined && !isNaN(data.localSeq)) localSeq = data.localSeq;
                if (data.lastSeqBySender && typeof data.lastSeqBySender === 'object') Object.assign(lastSeqBySender, data.lastSeqBySender);

                storageInitialized = true;
                
                // Process any early logs/history that weren't captured in the spread
                if (pendingLogs.length > 0) {
                    logs = [...pendingLogs, ...logs].slice(0, 200);
                    chrome.storage.session.set({ logs });
                    pendingLogs = [];
                }
                if (pendingHistory.length > 0) {
                    history = [...pendingHistory, ...history].slice(0, 20);
                    chrome.storage.session.set({ history });
                    pendingHistory = [];
                }

                done();
            });
        });
    }
    return restorationTask;
}

// Start restoration immediately
ensureState();

let reconnectTimer = null;
let reconnectStartTime = null;
let reconnectFailed = false;
let reconnectAttempts = 0;
let currentServerUrl = null;
let roomIdleSince = null;
let lastContentHeartbeatAt = null;
const MAX_RECONNECT_ATTEMPTS = 20;
const _RECONNECT_BASE_DELAY = 500;
const _RECONNECT_MAX_DELAY = 5000;
const ROOM_IDLE_AUTO_LEAVE_MS = 2 * 60 * 60 * 1000;

// Force Sync Coordination
let isForceSyncInitiator = false;
let forceSyncAcks = new Set();
let forceSyncTimeout = null;

// Episode Auto-Sync Lobby
let episodeLobby = null; // { expectedTitle, initiatorPeerId, readyPeers: [], createdAt }
let episodeLobbyTimeout = null;

// --- Episode Title Extraction (synced with content.js) ---
function extractEpisodeId(title) {
    if (!title || typeof title !== 'string') return null;
    const se = title.match(/S(?:eason\s*)?(\d+)[\s\-\.]*E(?:pisode\s*)?(\d+)/i);
    if (se) return `S${String(se[1]).padStart(2, '0')}E${String(se[2]).padStart(2, '0')}`;
    const ep = title.match(/(?:Episode|Folge|Ep\.?|#)\s*(\d+)/i);
    if (ep) return `EP${String(ep[1]).padStart(3, '0')}`;
    return null;
}

function sameEpisode(titleA, titleB) {
    if (!titleA && !titleB) return true; // Both unknown → assume same (backward compat)
    if (!titleA || !titleB) return false; // One unknown, one known → different
    const idA = extractEpisodeId(titleA);
    const idB = extractEpisodeId(titleB);
    if (idA && idB) return idA === idB; // Both have parseable IDs → compare IDs
    if (idA || idB) return false;       // One has ID, other doesn't → different
    return titleA === titleB;            // Neither has ID → exact string match
}

// --- Storage Utils ---

/**
 * Canonical peer data factory. All peer object construction must go through
 * here to guarantee a consistent shape with predictable null defaults.
 * @param {object} raw - Raw data from server event or heartbeat payload.
 * @returns {object} Normalized peer data object.
 */
function createPeerData(raw) {
    return {
        peerId:        raw.peerId        || null,
        username:      raw.username      || null,
        tabTitle:      raw.tabTitle      || null,
        mediaTitle:    raw.mediaTitle    || null,
        playbackState: raw.playbackState || null,
        currentTime:   raw.currentTime   != null ? raw.currentTime : null,
        volume:        raw.volume        != null ? raw.volume       : null,
        muted:         raw.muted         != null ? raw.muted        : null,
        lastHeartbeat: Date.now()
    };
}

/**
 * Updates properties of a peer in the room and instantly broadcasts the changes to the popup UI.
 * Also tracks lastReactiveUpdate to guard against older heartbeats in transit overwriting state.
 */
function updateLocalPeerState(targetPeerId, updates) {
    if (!currentRoom || !Array.isArray(currentRoom.peers)) return;
    const peer = currentRoom.peers.find(p => typeof p === 'object' ? p.peerId === targetPeerId : p === targetPeerId);
    if (peer && typeof peer === 'object') {
        Object.keys(updates).forEach(key => {
            if (updates[key] !== undefined && updates[key] !== null) {
                peer[key] = updates[key];
            }
        });
        peer.lastReactiveUpdate = Date.now(); // Race condition guard lock
        if (updates.currentTime !== undefined && updates.currentTime !== null) {
            peer.lastHeartbeat = Date.now(); // reset time interpolation baseline
        }
        if (storageInitialized) chrome.storage.session.set({ currentRoom });
        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
    }
}

async function getPeerId() {
    const data = await chrome.storage.local.get(['peerId']);
    if (data.peerId) return data.peerId;
    const newId = self.crypto.randomUUID().substring(0, 8);
    await chrome.storage.local.set({ peerId: newId });
    return newId;
}

async function getSettings() {
    // Try local (per-device) first, fall back to sync for migration
    let data = await chrome.storage.local.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']);
    let migrated = false;
    if (!data.username) {
        const syncData = await chrome.storage.sync.get(['serverUrl', 'useCustomServer', 'roomId', 'password', 'username']);
        if (syncData.username || syncData.roomId) {
            data = syncData;
            migrated = true;
        }
    }
    let username = data.username;
    if (!username) {
        username = generateUsername();
    }
    if (migrated) {
        await chrome.storage.local.set({ 
            serverUrl: data.serverUrl || '',
            useCustomServer: data.useCustomServer || false,
            roomId: data.roomId || '',
            password: data.password || '',
            username
        });
    } else if (!data.username) {
        await chrome.storage.local.set({ username });
    }
    return {
        serverUrl: data.serverUrl || '',
        useCustomServer: data.useCustomServer || false,
        roomId: data.roomId || '',
        password: data.password || '',
        username
    };
}

function addLog(message, type = 'info') {
    const log = {
        timestamp: new Date().toISOString(),
        message,
        type
    };
    if (!storageInitialized) {
        pendingLogs.unshift(log);
    } else {
        logs.unshift(log);
        if (logs.length > 200) logs.pop();
        chrome.storage.session.set({ logs });
    }
    chrome.runtime.sendMessage({ type: 'LOG_UPDATE', log }).catch(() => {});
}

// --- WebSocket Client ---
function resolveServerUrl(settings) {
    return (settings.serverUrl && settings.useCustomServer) ? settings.serverUrl : OFFICIAL_SERVER_URL;
}

function forceDisconnect() {
    if (reconnectTimer) {
        clearTimeout(reconnectTimer);
        reconnectTimer = null;
    }
    if (episodeLobbyTimeout) {
        clearTimeout(episodeLobbyTimeout);
        episodeLobbyTimeout = null;
    }
    episodeLobby = null;
    if (forceSyncTimeout) {
        clearTimeout(forceSyncTimeout);
        forceSyncTimeout = null;
    }
    if (socket) {
        socket.onopen = null;
        socket.onmessage = null;
        socket.onclose = null;
        socket.onerror = null;
        socket.close();
        socket = null;
    }
    currentServerUrl = null;
    isConnecting = false;
    isNamespaceJoined = false;
    isForceSyncInitiator = false;
    expectedAcksCount = 0;
    roomIdleSince = null;
    lastContentHeartbeatAt = null;
    forceSyncAcks.clear();
    eventQueue = [];
    chrome.storage.session.set({
        isForceSyncInitiator: false,
        forceSyncAcks: [],
        forceSyncDeadline: null,
        expectedAcksCount: 0,
        eventQueue: [],
        episodeLobby: null,
        roomIdleSince: null,
        lastContentHeartbeatAt: null
    }).catch(() => {});
    if (currentRoom) {
        currentRoom.peers = [];
        if (storageInitialized) chrome.storage.session.set({ currentRoom });
        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
    }
    broadcastConnectionStatus('disconnected');
}

function persistRoomIdleState() {
    chrome.storage.session.set({ roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
}

function markRoomUseful() {
    roomIdleSince = null;
    lastContentHeartbeatAt = Date.now();
    persistRoomIdleState();
}

function markRoomPotentiallyIdle() {
    if (!currentRoom) {
        roomIdleSince = null;
        lastContentHeartbeatAt = null;
        persistRoomIdleState();
        return;
    }
    if (!roomIdleSince) {
        roomIdleSince = Date.now();
        persistRoomIdleState();
    }
}

function clearTargetTabForIdle() {
    currentTabId = null;
    currentTabTitle = null;
    lastContentHeartbeatAt = null;
    if (currentRoom) {
        roomIdleSince = Date.now();
    }
    chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt }).catch(() => {});
    updateBadgeStatus();
}

async function leaveRoomAfterIdleGrace(reason) {
    if (!currentRoom) return;
    emit(EVENTS.LEAVE_ROOM, { peerId });
    currentRoom = null;
    currentTabId = null;
    currentTabTitle = null;
    roomIdleSince = null;
    lastContentHeartbeatAt = null;
    clearEpisodeLobbyState();
    await chrome.storage.session.set({
        currentRoom: null,
        currentTabId: null,
        currentTabTitle: null,
        roomIdleSince: null,
        lastContentHeartbeatAt: null,
        episodeLobby: null
    }).catch(() => {});
    await chrome.storage.local.set({ roomId: '', password: '' }).catch(() => {});
    addLog(reason, 'info');
    chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
    updateBadgeStatus();
}

async function connect() {
    if (isConnecting) return;
    isConnecting = true;

    let finalUrl = '';
    try {
        // --- Phase 1: Storage ---
        let settings;
        try {
            if (!peerId) peerId = await getPeerId();
            settings = await getSettings();
        } catch (e) {
            throw new Error(`[Storage Error] ${e.message}`);
        }

        // --- Phase 2: Connection Guard ---
        if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) {
            if (isNamespaceJoined) {
                isConnecting = false;
                return;
            }
            socket.onopen = null;
            socket.onmessage = null;
            socket.onclose = null;
            socket.onerror = null;
            socket.close();
        }

        if (!navigator.onLine) {
            addLog('Browser is offline. Waiting...', 'warn');
            broadcastConnectionStatus('offline');
            isConnecting = false;
            scheduleReconnect();
            return;
        }

        broadcastConnectionStatus('reconnecting');
        const isCustomServer = settings.serverUrl && settings.useCustomServer;
        finalUrl = isCustomServer ? settings.serverUrl : OFFICIAL_SERVER_URL;

        // --- Phase 3: URL Validation ---
        try {
            if (isCustomServer) {
                finalUrl = finalUrl.trim();
                if (!finalUrl.includes('://')) {
                    finalUrl = 'ws://' + finalUrl;
                }
                const urlObj = new URL(finalUrl);
                const isLocal = urlObj.hostname === 'localhost' || urlObj.hostname === '127.0.0.1';
                if (urlObj.protocol !== 'wss:' && !isLocal) {
                    urlObj.protocol = 'wss:';
                    finalUrl = urlObj.toString();
                    addLog('Security: Upgraded to wss:// for remote host.', 'warn');
                }
            }
        } catch (e) {
            throw new Error(`[URL Error] ${e.message}`);
        }

        addLog(`Connecting to ${isCustomServer ? finalUrl : 'Official Server'}... (attempt ${reconnectAttempts + 1})`, 'info');

        currentServerUrl = finalUrl;

        // --- Phase 4: WebSocket Init ---
        try {
            const url = new URL(finalUrl);
            url.pathname = '/socket.io/';
            url.searchParams.set('EIO', '4');
            url.searchParams.set('transport', 'websocket');
            url.searchParams.set('version', chrome.runtime.getManifest().version);
            url.searchParams.set('token', OFFICIAL_SERVER_TOKEN);

            socket = new WebSocket(url.toString());
        } catch (e) {
            throw new Error(`[Connection Error] ${e.message}`);
        }

        // --- Phase 5: Event Listeners ---
        socket.onopen = () => {
            reconnectAttempts = 0;
            reconnectStartTime = null;
            reconnectFailed = false;
            addLog('WebSocket Connection Opened', 'success');
            chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null }).catch(() => {});
            isNamespaceJoined = false;
            socket.send('40');
        };

        socket.onmessage = async (event) => {
            await ensureState();
            const msg = event.data;
            if (msg === '2') {
                socket.send('3');
                return;
            }
            if (msg.startsWith('0')) {
                addLog(`Socket.IO Handshake: ${msg}`, 'info');
            } else if (msg.startsWith('40')) {
                isConnecting = false;
                isNamespaceJoined = true;
                broadcastConnectionStatus('connected');
                addLog('Joined Namespace /', 'success');
                const settings = await getSettings();
                if (settings.roomId) {
                    emit(EVENTS.JOIN_ROOM, { 
                        roomId: settings.roomId, 
                        password: settings.password,
                        peerId,
                        username: settings.username,
                        tabTitle: currentTabTitle,
                        protocolVersion: PROTOCOL_VERSION
                    });
                }
                while (eventQueue.length > 0) {
                    const queuedMsg = eventQueue.shift();
                    emit(queuedMsg.event, queuedMsg.data);
                }
                eventQueue = [];
                chrome.storage.session.set({ eventQueue: [] });
            } else if (msg.startsWith('42')) {
                try {
                    const payload = JSON.parse(msg.substring(2));
                    try {
                        handleServerEvent(payload[0], payload[1]);
                    } catch (handlerErr) {
                        addLog(`Handler error for ${payload[0]}: ${handlerErr.message}`, 'error');
                    }
                } catch (_e) {
                    addLog(`Failed to parse message: ${msg}`, 'error');
                }
            }
        };

        socket.onclose = () => {
            isConnecting = false;
            isNamespaceJoined = false;
            
            isForceSyncInitiator = false;
            forceSyncAcks.clear();
            if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
            chrome.storage.session.set({ 
                isForceSyncInitiator: false, 
                forceSyncAcks: [], 
                forceSyncDeadline: null 
            });

            
            if (currentRoom) {
                currentRoom.peers = [];
                if (storageInitialized) chrome.storage.session.set({ currentRoom });
                chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
            }
            broadcastConnectionStatus('disconnected');
            addLog('Disconnected. Scheduling reconnect...', 'warn');
            socket = null;
            scheduleReconnect();
        };

        socket.onerror = () => {
            broadcastConnectionStatus('disconnected');
            const logType = reconnectAttempts > 1 ? 'error' : 'warn';
            addLog('WebSocket Error: Connection failed', logType);
        };

    } catch (e) {
        isConnecting = false;
        const logType = reconnectAttempts > 1 ? 'error' : 'warn';
        const errMsg = (e && e.message) ? e.message : String(e || 'Unknown connection error');
        addLog(errMsg, logType);
        broadcastConnectionStatus('disconnected');
        scheduleReconnect();
    }
}


function broadcastConnectionStatus(status) {
    chrome.runtime.sendMessage({ type: 'CONNECTION_STATUS', status }).catch(() => {});
    updateBadgeStatus();
}

function updateBadgeStatus() {
    const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined;
    const isReconnecting = !isConnected && reconnectAttempts > 0;
    const status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : (isReconnecting ? 'reconnecting' : 'disconnected'));

    if (status === 'reconnecting') {
        chrome.action.setBadgeText({ text: '...' });
        chrome.action.setBadgeBackgroundColor({ color: '#f59e0b' });
    } else if (status === 'connecting') {
        chrome.action.setBadgeText({ text: '...' });
        chrome.action.setBadgeBackgroundColor({ color: '#fbbf24' });
    } else if (status === 'connected' && currentRoom && currentTabId) {
        chrome.action.setBadgeText({ text: 'ON' });
        chrome.action.setBadgeBackgroundColor({ color: '#22c55e' });
    } else {
        chrome.action.setBadgeText({ text: '' });
    }
}

function showNotification(senderName, action) {
    chrome.storage.sync.get(['browserNotifications', 'locale'], async (settings) => {
        if (!settings.browserNotifications) return;

        const lang = settings.locale || getSystemLanguage();
        await loadLocale(lang);

        let labelKey = '';
        if (action === 'play') labelKey = 'NOTIF_PLAY';
        else if (action === 'pause') labelKey = 'NOTIF_PAUSE';
        else if (action === 'seek') labelKey = 'NOTIF_SEEK';
        else if (action === 'force_sync_prepare') labelKey = 'NOTIF_FORCE_PREPARE';
        else if (action === 'force_sync_execute') labelKey = 'NOTIF_FORCE_EXECUTE';

        const label = labelKey ? getMessage(labelKey) : action;

        let displayName = senderName || 'A peer';
        if (currentRoom && Array.isArray(currentRoom.peers)) {
            const peer = currentRoom.peers.find(p => (p.peerId || p) === senderName);
            if (peer && peer.username) displayName = peer.username;
        }

        if (displayName === 'You' || displayName === 'YOU') {
            displayName = getMessage('LABEL_YOU') || 'YOU';
        }

        const message = getMessage('TOAST_PEER_ACTION', { name: displayName, action: label }) + '.';

        chrome.notifications.create(`sync_${Date.now()}`, {
            type: 'basic',
            iconUrl: 'icons/icon128.png',
            title: 'KoalaSync',
            message: message,
            priority: 1
        });
    });
}

function scheduleReconnect() {
    if (reconnectTimer) return;

    if (!reconnectStartTime) reconnectStartTime = Date.now();

    const elapsed = Date.now() - reconnectStartTime;
    reconnectAttempts++;

    if (!reconnectFailed && (elapsed > 300000 || reconnectAttempts > MAX_RECONNECT_ATTEMPTS)) {
        reconnectFailed = true;
        addLog('Switching to slow reconnect mode (every 5 minutes)', 'warn');
    }

    const delay = reconnectFailed
        ? 300000
        : Math.min(_RECONNECT_BASE_DELAY * Math.pow(1.5, reconnectAttempts - 1), _RECONNECT_MAX_DELAY);

    if (reconnectFailed) {
        addLog(`Slow reconnect in 5min (attempt ${reconnectAttempts})`, 'info');
    } else {
        addLog(`Reconnect in ${Math.round(delay)}ms (attempt ${reconnectAttempts})`, 'warn');
    }

    chrome.storage.session.set({ reconnectFailed, reconnectAttempts, reconnectStartTime }).catch(() => {});

    reconnectTimer = setTimeout(() => {
        reconnectTimer = null;
        connect();
    }, delay);
}

// Slow reconnect logic is now handled in the keepAlive alarm

function emit(event, data) {
    if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) {
        const msg = `42${JSON.stringify([event, data])}`;
        socket.send(msg);
    } else {
        eventQueue.push({ event, data });
        if (eventQueue.length > 50) {
            eventQueue.shift();
            addLog('Event queue cap reached, dropping oldest event', 'warn');
        }
        chrome.storage.session.set({ eventQueue });
    }
}

function addToHistory(action, senderId) {
    const historyEntry = {
        action,
        senderId: senderId || 'You',
        timestamp: new Date().toISOString()
    };
    if (!storageInitialized) {
        pendingHistory.unshift(historyEntry);
    } else {
        history.unshift(historyEntry);
        if (history.length > 20) history.pop();
        chrome.storage.session.set({ history });
    }
    chrome.runtime.sendMessage({ type: 'HISTORY_UPDATE', history }).catch(() => {});
}

// --- Event Handlers ---
function handleServerEvent(event, data) {
    if (!data) {
        addLog(`Ignored server event ${event} due to empty payload`, 'warn');
        return;
    }
    switch (event) {
        case EVENTS.ROOM_DATA:
            currentRoom = data;
            markRoomPotentiallyIdle();
            if (currentRoom && Array.isArray(currentRoom.peers)) {
                currentRoom.peers = currentRoom.peers.map(p => typeof p === 'object' ? createPeerData(p) : { peerId: p, username: null, tabTitle: null, mediaTitle: null, playbackState: null, currentTime: null, volume: null, muted: null, lastHeartbeat: Date.now() });
                
                // Clear sequence tracking for peers that are no longer in the room
                const activePeerIds = new Set(currentRoom.peers.map(p => typeof p === 'object' ? p.peerId : p));
                Object.keys(lastSeqBySender).forEach(pId => {
                    if (!activePeerIds.has(pId)) {
                        delete lastSeqBySender[pId];
                    }
                });
                _persistLastSeq();
            } else if (currentRoom) {
                currentRoom.peers = [];
            }

            // Recover server-tracked active Episode Lobby if present
            if (data && data.activeLobby && !episodeLobby) {
                episodeLobby = {
                    expectedTitle: data.activeLobby.expectedTitle,
                    initiatorPeerId: data.activeLobby.initiatorPeerId,
                    readyPeers: data.activeLobby.readyPeers,
                    createdAt: Date.now()
                };
                persistEpisodeLobby();
                broadcastLobbyUpdate();
                addLog(`Recovered active episode lobby from server: "${episodeLobby.expectedTitle}"`, 'info');

                // Notify content script to start polling
                if (currentTabId) {
                    const tabId = parseInt(currentTabId);
                    if (!isNaN(tabId)) {
                        chrome.tabs.sendMessage(tabId, {
                            type: 'EPISODE_LOBBY',
                            expectedTitle: episodeLobby.expectedTitle
                        }).catch(() => {});
                    }
                }

                // Schedule timeout if we don't already have one
                if (!episodeLobbyTimeout) {
                    episodeLobbyTimeout = setTimeout(() => cancelEpisodeLobby('Timeout'), EPISODE_LOBBY_TIMEOUT);
                }
            }
            if (storageInitialized) chrome.storage.session.set({ currentRoom });
            addLog(`Joined Room: ${data?.roomId || 'unknown'}`, 'success');
            chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: data.peers }).catch(() => {});
                        
            // Inform Website Bridge & Popup
            const joinStatusMsg = { type: 'JOIN_STATUS', success: true, message: 'Joined' };
            chrome.runtime.sendMessage(joinStatusMsg).catch(() => {});
            chrome.tabs.query({}, (tabs) => {
                tabs.forEach(tab => {
                    chrome.tabs.sendMessage(tab.id, joinStatusMsg).catch(() => {});
                });
            });
            break;
        case EVENTS.ROOM_LIST:
            chrome.runtime.sendMessage({ type: 'ROOM_LIST', rooms: data.rooms }).catch(() => {});
            break;
        case EVENTS.ERROR:
            isConnecting = false;
            broadcastConnectionStatus('disconnected');
            addLog(`Server Error: ${data.message}`, 'error');
            chrome.storage.sync.get(['locale'], async (settings) => {
                const lang = settings.locale || getSystemLanguage();
                await loadLocale(lang);
                chrome.notifications.create(`error_${Date.now()}`, {
                    type: 'basic',
                    iconUrl: 'icons/icon128.png',
                    title: getMessage('NOTIF_ERROR_TITLE') || 'KoalaSync Error',
                    message: data.message
                });
            });
            // Inform Website Bridge & Popup
            const errStatusMsg = { type: 'JOIN_STATUS', success: false, message: data.message };
            chrome.runtime.sendMessage(errStatusMsg).catch(() => {});
            chrome.tabs.query({}, (tabs) => {
                tabs.forEach(tab => {
                    chrome.tabs.sendMessage(tab.id, errStatusMsg).catch(() => {});
                });
            });
            break;
        case EVENTS.PLAY:
        case EVENTS.PAUSE:
        case EVENTS.SEEK:
        case EVENTS.FORCE_SYNC_PREPARE:
            if (data.senderId && typeof data.seq === 'number') {
                const lastSeq = lastSeqBySender[data.senderId];
                if (lastSeq !== undefined && data.seq <= lastSeq) {
                    addLog(`Ignored stale ${event} from ${data.senderId} (seq ${data.seq} <= ${lastSeq})`, 'warn');
                    break;
                }
                lastSeqBySender[data.senderId] = data.seq;
                _persistLastSeq();
            }
            if (data.senderId) {
                addToHistory(event, data.senderId);
                showNotification(data.senderId, event);
                updateLastAction(event, data.senderId);
                lastActionState.targetTime = data.targetTime !== undefined ? data.targetTime : data.currentTime;
                if (storageInitialized) chrome.storage.session.set({ lastActionState });

                // Remote Reactive Update
                updateLocalPeerState(data.senderId, {
                    playbackState: event === EVENTS.PLAY ? 'playing' : (event === EVENTS.PAUSE ? 'paused' : undefined),
                    currentTime: data.currentTime !== undefined ? data.currentTime : (data.targetTime !== undefined ? data.targetTime : undefined)
                });
            }
            routeToContent(event, data);
            break;
        case EVENTS.FORCE_SYNC_ACK:
            if (data.senderId && typeof data.seq === 'number') {
                const lastSeq = lastSeqBySender[data.senderId];
                if (lastSeq !== undefined && data.seq <= lastSeq) break;
                lastSeqBySender[data.senderId] = data.seq;
                _persistLastSeq();
            }
            if (isForceSyncInitiator) {
                forceSyncAcks.add(data.senderId);
                chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) });
                addLog(`Received ACK from ${data.senderId} (${forceSyncAcks.size})`, 'info');
                
                // Update UI state for buffering progress
                if (lastActionState && lastActionState.action === EVENTS.FORCE_SYNC_PREPARE) {
                    if (!Array.isArray(lastActionState.acks)) lastActionState.acks = [];
                    if (!lastActionState.acks.includes(data.senderId)) {
                        lastActionState.acks.push(data.senderId);
                        if (storageInitialized) chrome.storage.session.set({ lastActionState });
                        chrome.runtime.sendMessage({ type: 'ACTION_UPDATE', state: lastActionState }).catch(() => {});
                    }

                    // Force Sync ACK Reactive Update
                    updateLocalPeerState(data.senderId, {
                        playbackState: 'paused', // Preparing for force sync always pauses the player
                        currentTime: lastActionState.targetTime
                    });
                }

                // Check if all peers responded using the snapshot count
                const targetCount = expectedAcksCount > 0 ? expectedAcksCount : (currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1);
                if (forceSyncAcks.size >= targetCount) {
                    executeForceSync();
                }
            }
            break;
        case EVENTS.FORCE_SYNC_EXECUTE:
            if (data?.senderId && typeof data.seq === 'number') {
                const lastSeq = lastSeqBySender[data.senderId];
                if (lastSeq !== undefined && data.seq <= lastSeq) break;
                lastSeqBySender[data.senderId] = data.seq;
                _persistLastSeq();
            }
            if (data?.senderId) {
                addToHistory(event, data.senderId);
                showNotification(data.senderId, event);

                // (The sender's state is updated below with everyone else)
            }

            // Force Sync Execute Remote Reactive Update:
            // Set all peers to playing and apply a reactive lock to block stale heartbeats
            if (currentRoom && Array.isArray(currentRoom.peers)) {
                currentRoom.peers.forEach(peer => {
                    if (peer && typeof peer === 'object') {
                        peer.playbackState = 'playing';
                        peer.lastReactiveUpdate = Date.now();
                    }
                });
                if (storageInitialized) chrome.storage.session.set({ currentRoom });
                chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
            }

            routeToContent(event, data);
            break;
        case EVENTS.EVENT_ACK:
            if (lastActionState && lastActionState.action && data?.senderId) {
                // Correlation Check: Only accept ACK if it matches our current action's timestamp
                if (data.actionTimestamp === lastActionState.timestamp) {
                    if (!Array.isArray(lastActionState.acks)) lastActionState.acks = [];
                    if (!lastActionState.acks.includes(data.senderId)) {
                        lastActionState.acks.push(data.senderId);
                        if (storageInitialized) chrome.storage.session.set({ lastActionState });
                        chrome.runtime.sendMessage({ type: 'ACTION_UPDATE', state: lastActionState }).catch(() => {});

                        // ACK Reactive Update
                        updateLocalPeerState(data.senderId, {
                            playbackState: lastActionState.action === EVENTS.PLAY ? 'playing' : (lastActionState.action === EVENTS.PAUSE ? 'paused' : undefined),
                            currentTime: (lastActionState.action === EVENTS.SEEK || lastActionState.action === EVENTS.FORCE_SYNC_PREPARE) ? lastActionState.targetTime : undefined
                        });
                    }
                }
            }
            break;
        case EVENTS.PEER_STATUS:
            if (currentRoom) {
                if (!Array.isArray(currentRoom.peers)) currentRoom.peers = [];
                if (data.status === 'joined') {
                    if (!currentRoom.peers.find(p => (p.peerId || p) === data.peerId)) {
                        delete lastSeqBySender[data.peerId];
                        _persistLastSeq();
                        
                        currentRoom.peers.push(createPeerData(data));
                        if (storageInitialized) chrome.storage.session.set({ currentRoom });
                        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});

                        if (episodeLobby && episodeLobby.initiatorPeerId === peerId) {
                            emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: episodeLobby.expectedTitle });
                        }
                    }
                } else if (data.status === 'left') {
                    currentRoom.peers = currentRoom.peers.filter(p => (p.peerId || p) !== data.peerId);
                    if (storageInitialized) chrome.storage.session.set({ currentRoom });
                    chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});

                    if (episodeLobby) {
                        checkEpisodeLobbyPeerDeparture();
                    }

                    if (isForceSyncInitiator) {
                        forceSyncAcks.delete(data.peerId);
                        chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) });
                        expectedAcksCount = Math.max(1, currentRoom.peers ? currentRoom.peers.length : 1);
                        chrome.storage.session.set({ expectedAcksCount });
                        if (forceSyncAcks.size >= expectedAcksCount) {
                            executeForceSync();
                        }
                    }
                } else {
                    const peer = currentRoom.peers.find(p => (typeof p === 'object' ? p.peerId : p) === data.peerId);
                    if (peer) {
                        if (typeof peer === 'object') {
                            peer.tabTitle = data.tabTitle;
                            peer.username = data.username;
                            peer.mediaTitle = data.mediaTitle !== undefined ? data.mediaTitle : peer.mediaTitle;
                            peer.volume = data.volume !== undefined ? data.volume : peer.volume;
                            peer.muted = data.muted !== undefined ? data.muted : peer.muted;

                            const timeSinceReactive = peer.lastReactiveUpdate ? (Date.now() - peer.lastReactiveUpdate) : Infinity;
                            const ignoreStatus = timeSinceReactive < 300;

                            if (!ignoreStatus) {
                                peer.playbackState = data.playbackState !== undefined ? data.playbackState : peer.playbackState;
                                peer.currentTime = data.currentTime !== undefined ? data.currentTime : peer.currentTime;
                                if (data.playbackState !== undefined || data.currentTime !== undefined) {
                                    peer.lastHeartbeat = Date.now();
                                }
                            }
                        } else {
                            // Migration: replace string peer with normalized object
                            const idx = currentRoom.peers.indexOf(peer);
                            currentRoom.peers[idx] = createPeerData(data);
                        }
                        if (storageInitialized) chrome.storage.session.set({ currentRoom });
                        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
                    }
                }
            }
            break;
        case EVENTS.EPISODE_LOBBY:
            if (data.senderId && data.expectedTitle) {
                addLog(`Episode lobby from ${data.senderId}: "${data.expectedTitle}"`, 'info');
                // If we already have a lobby for this same title, treat as dedup
                if (episodeLobby && sameEpisode(episodeLobby.expectedTitle, data.expectedTitle)) {
                    break; // Already tracking this lobby
                }
                // Cancel any existing lobby before starting a new one
                if (episodeLobby) clearEpisodeLobbyState();
                
                episodeLobby = {
                    expectedTitle: data.expectedTitle,
                    initiatorPeerId: data.senderId,
                    readyPeers: [data.senderId], // Initiator is already ready
                    createdAt: Date.now()
                };
                persistEpisodeLobby();
                broadcastLobbyUpdate();

                // Start timeout
                episodeLobbyTimeout = setTimeout(() => cancelEpisodeLobby('Timeout'), EPISODE_LOBBY_TIMEOUT);

                // Forward to content script to start polling
                if (currentTabId) {
                    const tabId = parseInt(currentTabId);
                    if (!isNaN(tabId)) {
                        chrome.tabs.sendMessage(tabId, {
                            type: 'EPISODE_LOBBY',
                            expectedTitle: data.expectedTitle
                        }).catch(() => {});
                    }
                }
            }
            break;
        case EVENTS.EPISODE_READY:
            if (episodeLobby && data.senderId) {
                if (!episodeLobby.readyPeers.includes(data.senderId)) {
                    episodeLobby.readyPeers.push(data.senderId);
                    persistEpisodeLobby();
                    broadcastLobbyUpdate();
                    addLog(`Episode ready from ${data.senderId} (${episodeLobby.readyPeers.length})`, 'info');
                    checkEpisodeLobbyCompletion();
                }
            }
            break;
        case EVENTS.EPISODE_LOBBY_CANCEL:
            if (episodeLobby) {
                const title = episodeLobby.expectedTitle;
                clearEpisodeLobbyState();
                addLog(`Episode lobby for "${title}" cancelled by ${data.senderId || 'peer'}`, 'warn');
            }
            break;
        default:
            addLog(`Received unknown event from server: ${event}`, 'warn');
            break;
    }
}

function executeForceSync() {
    if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
    isForceSyncInitiator = false;
    forceSyncAcks.clear();
    expectedAcksCount = 0;
    chrome.storage.session.set({ 
        isForceSyncInitiator: false, 
        forceSyncAcks: [], 
        forceSyncDeadline: null,
        expectedAcksCount: 0
    });

    // Set all peers to playing and apply a reactive lock to block stale heartbeats
    if (currentRoom && Array.isArray(currentRoom.peers)) {
        currentRoom.peers.forEach(peer => {
            if (peer && typeof peer === 'object') {
                peer.playbackState = 'playing';
                peer.lastReactiveUpdate = Date.now();
            }
        });
        if (storageInitialized) chrome.storage.session.set({ currentRoom });
        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
    }

    const executionTimestamp = Date.now();
    updateLastAction(EVENTS.FORCE_SYNC_EXECUTE, 'You', executionTimestamp);

    localSeq++;
    chrome.storage.session.set({ localSeq });

    emit(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp, seq: localSeq });
    routeToContent(EVENTS.FORCE_SYNC_EXECUTE, { actionTimestamp: executionTimestamp, seq: localSeq });
    addLog('Force Sync Executed', 'success');
}

// --- Episode Auto-Sync Lobby Functions ---
function persistEpisodeLobby() {
    if (storageInitialized) chrome.storage.session.set({ episodeLobby });
}

function broadcastLobbyUpdate() {
    chrome.runtime.sendMessage({ type: 'LOBBY_UPDATE', lobby: episodeLobby }).catch(() => {});
}

function clearEpisodeLobbyState() {
    if (episodeLobbyTimeout) clearTimeout(episodeLobbyTimeout);
    episodeLobbyTimeout = null;
    episodeLobby = null;
    if (storageInitialized) chrome.storage.session.set({ episodeLobby: null });
    broadcastLobbyUpdate();

    // Notify content script to stop polling
    if (currentTabId) {
        const tabId = parseInt(currentTabId);
        if (!isNaN(tabId)) {
            chrome.tabs.sendMessage(tabId, { type: 'EPISODE_LOBBY_CANCEL' }).catch(() => {});
        }
    }
}

function cancelEpisodeLobby(reason) {
    if (!episodeLobby) return;
    const title = episodeLobby.expectedTitle;
    
    // Broadcast cancellation to room
    emit(EVENTS.EPISODE_LOBBY_CANCEL, { peerId });

    clearEpisodeLobbyState();
    addLog(`Episode lobby cancelled: ${reason} for "${title}"`, 'warn');

    const reasonKeys = {
        'Timeout': 'LOBBY_CANCEL_TIMEOUT',
        'Timeout (recovered)': 'LOBBY_CANCEL_TIMEOUT_RECOVERED',
        'All other peers left': 'LOBBY_CANCEL_PEERS_LEFT',
        'Timeout — not all peers loaded the episode': 'LOBBY_CANCEL_TIMEOUT_PEERS_LOAD',
        'Cancelled by user': 'LOBBY_CANCEL_USER'
    };

    // Chrome notification on failure (per Q2: only notify on failure)
    chrome.storage.sync.get(['browserNotifications', 'locale'], async (settings) => {
        if (!settings.browserNotifications) return;

        const lang = settings.locale || getSystemLanguage();
        await loadLocale(lang);

        const reasonKey = reasonKeys[reason];
        const localizedReason = reasonKey ? getMessage(reasonKey) : reason;

        const titleText = getMessage('NOTIF_LOBBY_CANCEL_TITLE') || 'KoalaSync — Episode Sync Failed';
        const messageText = getMessage('NOTIF_LOBBY_CANCEL_MSG', { reason: localizedReason }) || `Auto-sync cancelled: ${localizedReason}. You may need to manually sync.`;

        chrome.notifications.create(`episode_${Date.now()}`, {
            type: 'basic',
            iconUrl: 'icons/icon128.png',
            title: titleText,
            message: messageText,
            priority: 1
        });
    });
}

function executeEpisodeLobby() {
    if (!episodeLobby) return;
    const title = episodeLobby.expectedTitle;
    clearEpisodeLobbyState();
    addLog(`Episode lobby complete: Starting "${title}" via Force Sync`, 'success');

    isForceSyncInitiator = true;
    forceSyncAcks.clear();
    expectedAcksCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1;
    const deadline = Date.now() + FORCE_SYNC_TIMEOUT;
    const timestamp = Date.now();
    updateLastAction(EVENTS.FORCE_SYNC_PREPARE, 'You', timestamp);
    lastActionState.targetTime = 0.0;
    if (storageInitialized) chrome.storage.session.set({ lastActionState });
    chrome.storage.session.set({ 
        isForceSyncInitiator: true, 
        forceSyncAcks: [], 
        forceSyncDeadline: deadline,
        expectedAcksCount: expectedAcksCount
    });

    const syncPayload = { targetTime: 0.0 };
    localSeq++;
    chrome.storage.session.set({ localSeq });
    emit(EVENTS.FORCE_SYNC_PREPARE, { ...syncPayload, peerId, actionTimestamp: timestamp, seq: localSeq });
    routeToContent(EVENTS.FORCE_SYNC_PREPARE, { ...syncPayload, actionTimestamp: timestamp, seq: localSeq });

    forceSyncTimeout = setTimeout(() => {
        if (isForceSyncInitiator) {
            addLog('Force Sync (Episode): Timeout waiting for ACKs, executing anyway...', 'warn');
            executeForceSync();
        }
    }, FORCE_SYNC_TIMEOUT);
}

function checkEpisodeLobbyCompletion() {
    if (!episodeLobby || !currentRoom) return;
    const peerCount = currentRoom.peers ? currentRoom.peers.length : 1;
    if (episodeLobby.readyPeers.length >= peerCount) {
        executeEpisodeLobby();
    }
}

function checkEpisodeLobbyPeerDeparture() {
    if (!episodeLobby || !currentRoom) return;
    if (!Array.isArray(currentRoom.peers)) return;
    const remainingPeerIds = currentRoom.peers.map(p => typeof p === 'object' ? p.peerId : p);
    
    // If only we remain, cancel the lobby
    if (remainingPeerIds.length <= 1) {
        cancelEpisodeLobby('All other peers left');
        return;
    }

    // Filter readyPeers to only include peers still in the room
    episodeLobby.readyPeers = episodeLobby.readyPeers.filter(id => remainingPeerIds.includes(id));
    persistEpisodeLobby();
    broadcastLobbyUpdate();

    // Re-check if all remaining peers are now ready
    checkEpisodeLobbyCompletion();
}

function updateLastAction(action, senderId, timestamp = Date.now()) {
    lastActionState = {
        action,
        senderId,
        timestamp,
        acks: []
    };
    if (storageInitialized) chrome.storage.session.set({ lastActionState });
    chrome.runtime.sendMessage({ type: 'ACTION_UPDATE', state: lastActionState }).catch(() => {});
}

async function routeToContent(action, payload) {
    if (!currentTabId) return;

    const tabId = parseInt(currentTabId);
    if (isNaN(tabId)) return;

    const actionTimestamp = payload?.actionTimestamp || Date.now();
    const commandSenderId = payload?.senderId || null;

    _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, 0);
}

function _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries) {
    chrome.tabs.sendMessage(tabId, { 
        type: 'SERVER_COMMAND',
        action,
        payload,
        actionTimestamp,
        commandSenderId
    }).catch(err => {
        if (retries >= 3) {
            addLog(`Content Script not responding in tab ${tabId} after ${retries} retries`, 'warn');
            clearTargetTabForIdle();
            return;
        }
        if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
            chrome.scripting.executeScript({
                target: { tabId },
                files: ['content.js']
            }).then(() => {
                setTimeout(() => _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries + 1), 500);
            }).catch(_err => {
                addLog(`Auto-reinject failed for tab ${tabId}`, 'warn');
            });
        } else {
            addLog(`Content Script not responding in tab ${tabId}`, 'warn');
            clearTargetTabForIdle();
        }
    });
}

// --- Keep-Alive Mechanism ---
chrome.alarms.create('keepAlive', { periodInMinutes: 0.5 });
chrome.alarms.onAlarm.addListener(async (alarm) => {
    await ensureState();
    if (alarm.name === 'keepAlive') {
        chrome.storage.session.get('keepAlive', () => {});
        if (!socket || socket.readyState !== WebSocket.OPEN) {
            if (!reconnectFailed) {
                connect();
            }
        } else if (currentRoom) {
            const now = Date.now();
            const heartbeatAge = lastContentHeartbeatAt ? (now - lastContentHeartbeatAt) : Infinity;
            if (!currentTabId || heartbeatAge > 45000) {
                markRoomPotentiallyIdle();
            }
            if (roomIdleSince && Date.now() - roomIdleSince >= ROOM_IDLE_AUTO_LEAVE_MS) {
                await leaveRoomAfterIdleGrace('Left room after 2 hours without a selected video heartbeat.');
                return;
            }
            // Heartbeat Logic: Always include identity metadata
            const settings = await getSettings();
            emit(EVENTS.PEER_STATUS, { 
                peerId, 
                status: 'heartbeat',
                username: settings.username,
                tabTitle: currentTabTitle
            });
        }
    }
});

function leaveOldRoomIfSwitching(newRoomId) {
    if (currentRoom && currentRoom.roomId !== newRoomId) {
        addLog(`Switching rooms: leaving ${currentRoom.roomId} to join ${newRoomId}`, 'info');
        if (socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined) {
            emit(EVENTS.LEAVE_ROOM, { peerId });
        }
        currentRoom = null;
        if (storageInitialized) chrome.storage.session.set({ currentRoom: null });
        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});

        // Reset force sync states
        isForceSyncInitiator = false;
        forceSyncAcks.clear();
        expectedAcksCount = 0;
        if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
        chrome.storage.session.set({ 
            isForceSyncInitiator: false, 
            forceSyncAcks: [], 
            forceSyncDeadline: null,
            expectedAcksCount: 0
        });

        // Cancel any active episode lobby
        clearEpisodeLobbyState();
    }
}

// --- Extension Message Listeners ---
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
    handleAsyncMessage(message, sender, sendResponse);
    return true; // Keep channel open for async responses
});

async function handleAsyncMessage(message, sender, sendResponse) {
    if (!message) return;
    await ensureState();

    if (message.type === 'CONNECT') {
        reconnectFailed = false;
        reconnectStartTime = null;
        reconnectAttempts = 0;
        chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null });
        const settings = await getSettings();
        if (settings.roomId) {
            leaveOldRoomIfSwitching(settings.roomId);
        }
        const desiredUrl = resolveServerUrl(settings);
        if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) {
            if (desiredUrl !== currentServerUrl) forceDisconnect();
            connect();
        } else if (settings.roomId) {
            emit(EVENTS.JOIN_ROOM, { 
                roomId: settings.roomId, 
                password: settings.password,
                peerId,
                username: settings.username,
                tabTitle: currentTabTitle,
                protocolVersion: PROTOCOL_VERSION
            });
        }
        sendResponse({ status: 'ok' });
    } else if (message.type === 'RETRY_CONNECT') {
        reconnectFailed = false;
        reconnectStartTime = null;
        reconnectAttempts = 0;
        chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null });
        forceDisconnect();
        connect();
        sendResponse({ status: 'ok' });
    } else if (message.type === 'GET_STATUS') {
        const isConnected = socket && socket.readyState === WebSocket.OPEN && isNamespaceJoined;
        const isReconnecting = !isConnected && reconnectAttempts > 0;
        let status = isConnected ? 'connected' : (isConnecting || (socket && socket.readyState === WebSocket.CONNECTING) ? 'connecting' : (isReconnecting ? 'reconnecting' : 'disconnected'));
        sendResponse({ 
            status, 
            peerId, 
            peers: currentRoom ? currentRoom.peers : [],
            lastActionState,
            targetTabId: currentTabId,
            episodeLobby: episodeLobby,
            reconnectAttempts,
            reconnectSlowMode: reconnectFailed,
            roomId: currentRoom ? currentRoom.roomId : null,
            serverUrl: currentServerUrl,
            version: chrome.runtime.getManifest().version,
            protocolVersion: PROTOCOL_VERSION,
            roomPassword: currentRoom ? currentRoom.password : null
        });
    } else if (message.type === 'LEAVE_ROOM') {
        emit(EVENTS.LEAVE_ROOM, { peerId });
        currentRoom = null;
        currentTabId = null;
        currentTabTitle = null;
        roomIdleSince = null;
        lastContentHeartbeatAt = null;

        updateBadgeStatus();
        
        isForceSyncInitiator = false;
        forceSyncAcks.clear();
        expectedAcksCount = 0;
        if (forceSyncTimeout) clearTimeout(forceSyncTimeout);

        // Cancel any active episode lobby
        clearEpisodeLobbyState();

        chrome.storage.session.set({ 
            currentRoom: null,
            currentTabId: null,
            currentTabTitle: null,
            roomIdleSince: null,
            lastContentHeartbeatAt: null,
            isForceSyncInitiator: false,
            forceSyncAcks: [],
            forceSyncDeadline: null,
            episodeLobby: null,
            expectedAcksCount: 0
        });
        addLog('Left Room', 'info');
        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: [] }).catch(() => {});
        sendResponse({ status: 'ok' });
    } else if (message.type === 'CLEAR_LOGS') {
        logs = [];
        sendResponse({ status: 'ok' });
    } else if (message.type === 'GET_LOGS') {
        sendResponse(logs);
    } else if (message.type === 'GET_HISTORY') {
        sendResponse(history);
    } else if (message.type === 'GET_ROOM_LIST') {
        emit(EVENTS.GET_ROOMS, {});
        sendResponse({ status: 'ok' });
    } else if (message.type === 'WEB_JOIN_REQUEST') {
        const { roomId: rawRoomId, password, useCustomServer, serverUrl } = message;
        const roomId = typeof rawRoomId === 'string' ? rawRoomId.replace(/[^a-zA-Z0-9\-]/g, '') : '';
        chrome.storage.local.set({ 
            roomId, 
            password,
            useCustomServer: !!useCustomServer,
            serverUrl: serverUrl || ''
        }, async () => {
            reconnectFailed = false;
            reconnectStartTime = null;
            reconnectAttempts = 0;
            chrome.storage.session.set({ reconnectFailed: false, reconnectAttempts: 0, reconnectStartTime: null });
            broadcastConnectionStatus('connecting');
            leaveOldRoomIfSwitching(roomId);
            const settings = await getSettings();
            const desiredUrl = resolveServerUrl(settings);
            if (desiredUrl !== currentServerUrl || !socket || socket.readyState !== WebSocket.OPEN || !isNamespaceJoined) {
                if (desiredUrl !== currentServerUrl) forceDisconnect();
                connect();
            } else {
                emit(EVENTS.JOIN_ROOM, { 
                    roomId, 
                    password,
                    peerId,
                    username: settings.username,
                    tabTitle: currentTabTitle,
                    protocolVersion: PROTOCOL_VERSION
                });
            }
            addLog(`Joining room via link: ${roomId}`, 'info');
            sendResponse({ status: 'ok' });
        });
    } else if (message.type === 'REGENERATE_ID') {
        const newId = self.crypto.randomUUID().substring(0, 8);
        chrome.storage.local.set({ peerId: newId }, () => {
            peerId = newId;
            addLog(`Identity regenerated: ${newId}`, 'success');
            if (socket) socket.close(); // Force reconnect with new ID
            sendResponse({ peerId: newId });
        });
    } else if (message.type === 'GET_VIDEO_STATE') {
        const { tabId } = message;
        if (!tabId) {
            sendResponse({ error: 'No tabId provided' });
            return;
        }
        chrome.tabs.sendMessage(tabId, { type: 'GET_VIDEO_STATE' }, (res) => {
            if (chrome.runtime.lastError) {
                sendResponse({ error: chrome.runtime.lastError.message });
            } else {
                sendResponse(res);
            }
        });
    } else if (message.type === 'CONTENT_EVENT') {
        const processEvent = () => {
            const timestamp = Date.now();
            localSeq++;
            chrome.storage.session.set({ localSeq });
            updateLastAction(message.action, 'You', timestamp);
            
            const payload = message.payload || {};
            lastActionState.targetTime = payload.targetTime !== undefined ? payload.targetTime : payload.currentTime;
            if (storageInitialized) chrome.storage.session.set({ lastActionState });
            
            payload.actionTimestamp = timestamp;
            payload.seq = localSeq;
            message.payload = payload;
            
            // Local Reactive Update
            updateLocalPeerState(peerId, {
                playbackState: message.action === EVENTS.PLAY ? 'playing' : (message.action === EVENTS.PAUSE ? 'paused' : undefined),
                currentTime: payload.currentTime !== undefined ? payload.currentTime : (payload.targetTime !== undefined ? payload.targetTime : undefined)
            });

            if (message.action === EVENTS.FORCE_SYNC_PREPARE) {
                isForceSyncInitiator = true;
                forceSyncAcks.clear();
                expectedAcksCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1;
                const deadline = Date.now() + FORCE_SYNC_TIMEOUT;
                chrome.storage.session.set({ 
                    isForceSyncInitiator: true, 
                    forceSyncAcks: [], 
                    forceSyncDeadline: deadline,
                    expectedAcksCount: expectedAcksCount
                });
                addLog('Initiating Force Sync...', 'info');
                
                routeToContent(EVENTS.FORCE_SYNC_PREPARE, message.payload);
     
                if (forceSyncTimeout) clearTimeout(forceSyncTimeout);
                forceSyncTimeout = setTimeout(() => {
                    if (isForceSyncInitiator) {
                        addLog('Force Sync: Timeout waiting for ACKs, executing anyway...', 'warn');
                        executeForceSync();
                    }
                }, FORCE_SYNC_TIMEOUT);
            }
            addToHistory(message.action, 'You');
            
            const isNonEssentialEvent = message.action === EVENTS.PLAY || message.action === EVENTS.PAUSE || message.action === EVENTS.SEEK;
            const otherCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.filter(p => (typeof p === 'object' ? p.peerId : p) !== peerId).length : 0;
            const hasOtherPeers = otherCount > 0;
            
            if (isNonEssentialEvent && !hasOtherPeers) {
                sendResponse({ status: 'ok_solo' });
                return;
            }
            
            emit(message.action, { ...message.payload, peerId });
            sendResponse({ status: 'ok' });
        };

        if (sender.tab) {
            const senderTabId = sender.tab.id;
            
            if (!currentTabId || currentTabId !== senderTabId) {
                sendResponse({ status: 'ignored_unselected_tab' });
                return;
            }
            
            currentTabTitle = sender.tab.title ? sender.tab.title.substring(0, 50) : null;
            chrome.storage.session.set({ currentTabTitle });
            updateBadgeStatus();
            processEvent();
        } else {
            routeToContent(message.action, message.payload);
            processEvent();
        }
    } else if (message.type === 'FORCE_SYNC_ACK') {
        if (isForceSyncInitiator) {
            forceSyncAcks.add(peerId);
            chrome.storage.session.set({ forceSyncAcks: Array.from(forceSyncAcks) });
            addLog(`Local ACK received (${forceSyncAcks.size})`, 'info');

            // Local Force Sync ACK Reactive Update
            if (lastActionState && lastActionState.action === EVENTS.FORCE_SYNC_PREPARE) {
                updateLocalPeerState(peerId, {
                    playbackState: 'paused',
                    currentTime: lastActionState.targetTime
                });
            }

            const peerCount = currentRoom && Array.isArray(currentRoom.peers) ? currentRoom.peers.length : 1;
            if (forceSyncAcks.size >= peerCount) {
                executeForceSync();
            }
        } else {
            localSeq++;
            chrome.storage.session.set({ localSeq });
            emit(EVENTS.FORCE_SYNC_ACK, { peerId, seq: localSeq });
        }
        sendResponse({ status: 'ok' });
    } else if (message.type === 'CMD_ACK') {
        const commandSenderId = message.commandSenderId;
        if (commandSenderId && commandSenderId !== peerId) {
            emit(EVENTS.EVENT_ACK, { 
                senderId: peerId, 
                targetId: commandSenderId,
                actionTimestamp: message.actionTimestamp 
            });
        }
        sendResponse({ status: 'ok' });
    } else if (message.type === 'HEARTBEAT') {
        if (sender.tab) {
            const senderTabId = sender.tab.id;
            
            if (!currentTabId || currentTabId !== senderTabId) {
                sendResponse({ status: 'ignored_unselected_tab' });
                return;
            }
            
            currentTabTitle = sender.tab.title ? sender.tab.title.substring(0, 50) : null;
            chrome.storage.session.set({ currentTabTitle });
            updateBadgeStatus();
        }

        markRoomUseful();
        getSettings().then(settings => {
            const statusPayload = { ...message.payload, peerId, username: settings.username, tabTitle: currentTabTitle };
            emit(EVENTS.PEER_STATUS, statusPayload);

            if (currentRoom && Array.isArray(currentRoom.peers)) {
                const me = currentRoom.peers.find(p => (p.peerId || p) === peerId);
                if (me && typeof me === 'object') {
                    me.tabTitle = currentTabTitle;
                    me.username = settings.username;
                    me.mediaTitle = message.payload?.mediaTitle;
                    me.playbackState = message.payload?.playbackState;
                    me.currentTime = message.payload?.currentTime;
                    me.volume = message.payload?.volume;
                    me.muted = message.payload?.muted;
                    me.lastHeartbeat = Date.now();
                    if (storageInitialized) chrome.storage.session.set({ currentRoom });
                    chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
                }
            }
            sendResponse({ status: 'ok' });
        }).catch(err => {
            addLog('Heartbeat settings error: ' + err.message, 'error');
            sendResponse({ status: 'ok' });
        });
    } else if (message.type === 'SET_TARGET_TAB') {
        currentTabId = message.tabId;
        currentTabTitle = message.tabTitle;
        lastContentHeartbeatAt = null;
        if (currentRoom) {
            roomIdleSince = Date.now();
        }
        chrome.storage.session.set({ currentTabId, currentTabTitle, roomIdleSince, lastContentHeartbeatAt });
        updateBadgeStatus();
        
        if (currentTabId) {
            chrome.scripting.executeScript({
                target: { tabId: currentTabId },
                files: ['content.js']
            }).catch(err => {
                addLog(`Failed to inject into tab: ${err.message}`, 'warn');
            });
        }
        
        sendResponse({ status: 'ok' });
    } else if (message.type === 'LOG') {
        addLog(`[Content] ${message.message}`, message.level || 'info');
        sendResponse({ status: 'ok' });
    } else if (message.type === 'EPISODE_CHANGED') {
        // Content script detected an episode transition
        if (sender.tab) {
            const senderTabId = sender.tab.id;
            if (!currentTabId || currentTabId !== senderTabId) {
                sendResponse({ status: 'ignored_unselected_tab' });
                return;
            }
        }

        const newTitle = message.payload && message.payload.newTitle;
        if (!newTitle) {
            sendResponse({ status: 'no_title' });
            return;
        }

        // Check setting
        const epSettings = await chrome.storage.sync.get(['autoSyncNextEpisode']);
        if (epSettings.autoSyncNextEpisode === false) {
            addLog(`Episode change detected ("${newTitle}") but Auto-Sync is disabled.`, 'info');
            sendResponse({ status: 'disabled' });
            return;
        }

        // If lobby already exists for this title, just mark self ready
        if (episodeLobby && sameEpisode(episodeLobby.expectedTitle, newTitle)) {
            if (!episodeLobby.readyPeers.includes(peerId)) {
                episodeLobby.readyPeers.push(peerId);
                persistEpisodeLobby();
                broadcastLobbyUpdate();
                emit(EVENTS.EPISODE_READY, { peerId, title: newTitle });
                checkEpisodeLobbyCompletion();
            }
            sendResponse({ status: 'ready_sent' });
            return;
        }

        // Cancel any existing lobby for a different episode
        if (episodeLobby) clearEpisodeLobbyState();

        // Create new lobby
        episodeLobby = {
            expectedTitle: newTitle,
            initiatorPeerId: peerId,
            readyPeers: [peerId], // We are already ready
            createdAt: Date.now()
        };
        persistEpisodeLobby();
        broadcastLobbyUpdate();
        addLog(`Episode lobby created: "${newTitle}"`, 'info');

        // Tell content script to pause the video and start polling
        // (This is the only place we pause — after confirming the feature is enabled)
        if (sender.tab && sender.tab.id) {
            chrome.tabs.sendMessage(sender.tab.id, {
                type: 'PAUSE_FOR_LOBBY',
                expectedTitle: newTitle
            }).catch(() => {});
        }

        // Broadcast to room
        emit(EVENTS.EPISODE_LOBBY, { peerId, expectedTitle: newTitle });

        // Start timeout (Q1: Option B — cancel on timeout)
        episodeLobbyTimeout = setTimeout(() => cancelEpisodeLobby('Timeout — not all peers loaded the episode'), EPISODE_LOBBY_TIMEOUT);

        // Immediate check — maybe we're the only one in the room
        checkEpisodeLobbyCompletion();

        sendResponse({ status: 'lobby_created' });
    } else if (message.type === 'EPISODE_READY_LOCAL') {
        // Content script confirmed it loaded the lobby episode
        if (episodeLobby && message.payload && sameEpisode(message.payload.title, episodeLobby.expectedTitle)) {
            if (!episodeLobby.readyPeers.includes(peerId)) {
                episodeLobby.readyPeers.push(peerId);
                persistEpisodeLobby();
                broadcastLobbyUpdate();
                emit(EVENTS.EPISODE_READY, { peerId, title: message.payload.title });
                addLog(`Local episode ready: "${message.payload.title}"`, 'success');
                checkEpisodeLobbyCompletion();
            }
        }
        sendResponse({ status: 'ok' });
    } else if (message.type === 'CONTENT_BOOT') {
        // Content script re-injected, check if there's an active lobby
        if (episodeLobby) {
            sendResponse({ lobbyActive: true, expectedTitle: episodeLobby.expectedTitle });
        } else {
            sendResponse({ lobbyActive: false });
        }
    } else if (message.type === 'CANCEL_EPISODE_LOBBY') {
        if (episodeLobby) {
            cancelEpisodeLobby('Cancelled by user');
            sendResponse({ status: 'ok' });
        } else {
            sendResponse({ error: 'No active lobby' });
        }
    } else {
        // Final fallback to prevent channel hanging
        sendResponse({ error: 'unhandled_message' });
    }
}

// Tab removal listener
chrome.tabs.onRemoved.addListener(async (tabId) => {
    await ensureState();
    if (tabId === currentTabId) {
        const wasInRoom = !!currentRoom;
        currentTabId = null;
        currentTabTitle = null;
        lastContentHeartbeatAt = null;
        roomIdleSince = Date.now();
        chrome.storage.session.set({ currentTabId: null, currentTabTitle: null, roomIdleSince, lastContentHeartbeatAt });
        updateBadgeStatus();
        addLog('Target tab closed.', 'warn');

        if (wasInRoom) {
            const roomAtClose = currentRoom;
            getSettings().then(settings => {
                if (currentRoom !== roomAtClose) return;

                emit(EVENTS.PEER_STATUS, {
                    peerId,
                    playbackState: 'paused',
                    currentTime: null,
                    mediaTitle: null,
                    username: settings.username,
                    tabTitle: null
                });

                if (currentRoom && Array.isArray(currentRoom.peers)) {
                    const me = currentRoom.peers.find(p => (p.peerId || p) === peerId);
                    if (me && typeof me === 'object') {
                        me.playbackState = 'paused';
                        me.currentTime = null;
                        me.mediaTitle = null;
                        me.tabTitle = null;
                        me.lastHeartbeat = Date.now();
                        if (storageInitialized) chrome.storage.session.set({ currentRoom });
                        chrome.runtime.sendMessage({ type: 'PEER_UPDATE', peers: currentRoom.peers }).catch(() => {});
                    }
                }
            }).catch(() => {});
        }
    }
});

// Re-inject on full page refresh
chrome.tabs.onUpdated.addListener(async (tabId, changeInfo, _tab) => {
    await ensureState();
    if (currentTabId && tabId === parseInt(currentTabId) && changeInfo.status === 'complete') {
        chrome.scripting.executeScript({
            target: { tabId },
            files: ['content.js']
        }).catch(() => {});
    }
});

// Initial Connect
connect();
