v3 · 37 files

ProjectElly/content/content.js 42 KB Raw
// content.js - Elly Website Blocker Content Script
// This script runs on all pages to monitor and block access
//
// NOTE: No ES module `import` here — content scripts must run as classic scripts on some
// Chromium/Opera builds even when manifest declares "type": "module", which throws
// "Cannot use import statement outside a module" on arbitrary sites. Keep in sync with
// lib/searchEngineHelpers.js (same logic as background).

/**
 * Detects common search-engine results pages so they can be allowed while destinations stay blocked.
 * Duplicated from lib/searchEngineHelpers.js — update both if this changes.
 * @param {string} url
 * @returns {boolean}
 */
function isSearchEngineResultsPage(url) {
    if (!url || typeof url !== 'string') {
        return false;
    }

    try {
        const u = new URL(url);
        if (u.protocol !== 'http:' && u.protocol !== 'https:') {
            return false;
        }

        const host = u.hostname.replace(/^www\./, '').toLowerCase();
        const path = u.pathname.toLowerCase();
        const hasQuery = Boolean(u.search && u.search.length > 1);

        if (host === 'google.com' || host.endsWith('.google.com')) {
            return path.startsWith('/search');
        }

        if (host.endsWith('bing.com')) {
            return path.startsWith('/search');
        }

        if (host === 'search.yahoo.com' || (host.endsWith('.yahoo.com') && path.includes('/search'))) {
            return true;
        }

        if (host === 'duckduckgo.com') {
            return path === '/' || path === '/lite' || path.startsWith('/?') || hasQuery;
        }

        if (host === 'ecosia.org') {
            return path.startsWith('/search');
        }

        if (host === 'search.brave.com') {
            return path.startsWith('/search') || hasQuery;
        }

        if (host === 'yandex.com' || host.endsWith('.yandex.com')) {
            return path.startsWith('/search');
        }

        if (host === 'startpage.com' || host === 'www.startpage.com') {
            return path.startsWith('/search') || path.startsWith('/sp/search');
        }

        if (host === 'ask.com') {
            return path.startsWith('/web');
        }

        if (host === 'baidu.com') {
            return path.startsWith('/s') || u.search.includes('wd=');
        }

        if (host === 'vrilsrc.com' || host === 'www.vrilsrc.com') {
            return path === '/' || path.startsWith('/search') || hasQuery;
        }

        return false;
    } catch {
        return false;
    }
}

// ============================================
// GLOBAL STATE & INITIALIZATION
// ============================================

let cachedSettings = {
    blockedSites: [],
    limitedSites: {},
    schedules: [],
    globalSettings: {},
    whitelist: []
};
let settingsLoaded = false;
let heartbeatInterval = null;
let isExtensionActive = true;

// ============================================
// SAFE CONSOLE UTILITIES (MUST BE FIRST)
// ============================================

// Safe console methods that won't throw errors
const safeConsole = {
    log: (...args) => {
        try {
            console.log(...args);
        } catch (e) {
            // Do nothing - console might not be available
        }
    },
    error: (...args) => {
        try {
            // Filter out extension context errors
            const firstArg = args[0];
            if (firstArg && typeof firstArg === 'string' &&
                (firstArg.includes('Extension context invalidated') ||
                    firstArg.includes('Could not establish connection'))) {
                return; // Suppress
            }
            console.error(...args);
        } catch (e) {
            // Do nothing
        }
    },
    warn: (...args) => {
        try {
            console.warn(...args);
        } catch (e) {
            // Do nothing
        }
    }
};

// Replace the global console with safe versions
(function () {
    const originalConsole = { ...console };
    console.log = safeConsole.log;
    console.error = safeConsole.error;
    console.warn = safeConsole.warn;
})();

// ============================================
// SAFE EXTENSION UTILITIES
// ============================================

function isExtensionContextValid() {
    if (typeof chrome === 'undefined' || !chrome.runtime) {
        return false;
    }
    try {
        // Accessing chrome.runtime.id can throw if context is invalidated
        return !!chrome.runtime.id;
    } catch (error) {
        return false;
    }
}

function safeExtractDomain(url) {
    try {
        return extractDomain(url);
    } catch (error) {
        safeConsole.log('Error extracting domain');
        return 'unknown';
    }
}

function safeIsUrlMatch(url, pattern) {
    try {
        return isUrlMatch(url, pattern);
    } catch (error) {
        safeConsole.log('Error matching URL');
        return false;
    }
}

function safeStorageGet(keys, callback) {
    if (!isExtensionContextValid()) {
        safeConsole.log('Extension context invalid, returning empty settings');
        isExtensionActive = false;
        callback({});
        return;
    }

    try {
        chrome.storage.sync.get(keys, (result) => {
            if (chrome.runtime.lastError) {
                safeConsole.log('Storage error');
                isExtensionActive = false;
                callback({});
                return;
            }
            callback(result || {});
        });
    } catch (error) {
        safeConsole.log('Storage access failed');
        isExtensionActive = false;
        callback({});
    }
}

function safeSendMessage(message, callback = () => { }) {
    // First check if extension is valid
    if (!isExtensionContextValid()) {
        safeConsole.log('Cannot send message - extension context invalid');
        isExtensionActive = false;
        disableExtensionFeatures();
        if (callback) callback(null);
        return false;
    }

    try {
        // Send message with proper error handling
        chrome.runtime.sendMessage(message, (response) => {
            // Check for errors in the callback
            if (chrome.runtime.lastError) {
                const error = chrome.runtime.lastError.message || '';
                if (error.includes('Extension context invalidated') ||
                    error.includes('Could not establish connection')) {
                    safeConsole.log('Extension context invalidated in callback');
                    isExtensionActive = false;
                    clearAllIntervals();
                    disableExtensionFeatures();
                }
                if (callback) callback(null);
                return;
            }
            if (callback) callback(response);
        });
        return true;
    } catch (error) {
        // Catch synchronous errors
        safeConsole.log('Sync error in safeSendMessage');
        if (error.message && (
            error.message.includes('Extension context invalidated') ||
            error.message.includes('Could not establish connection') ||
            error.message.includes('Extension') ||
            error.message.includes('context'))) {
            safeConsole.log('Extension context invalidated (sync catch)');
            isExtensionActive = false;
            clearAllIntervals();
            disableExtensionFeatures();
        }
        if (callback) callback(null);
        return false;
    }
}

// ============================================
// SETTINGS MANAGEMENT
// ============================================

function loadSettings() {
    return new Promise((resolve) => {
        safeStorageGet(['blockedSites', 'limitedSites', 'schedules', 'settings', 'whitelist'], (result) => {
            cachedSettings = {
                blockedSites: result.blockedSites || [],
                limitedSites: result.limitedSites || {},
                schedules: result.schedules || [],
                globalSettings: result.settings || {},
                whitelist: result.whitelist || []
            };

            settingsLoaded = true;
            safeConsole.log('Elly settings loaded');
            resolve(cachedSettings);
        });
    });
}

// ============================================
// BLOCK REASON LOGIC (NO CHROME APIS)
// ============================================

/**
 * Resolves limitedSites entry when the key was stored as full hostname (e.g. www.) or bare domain.
 * @param {string} url
 * @param {Record<string, unknown>} limitedSites
 * @returns {null | { limitMinutes: number }}
 */
function getLimitedSiteEntryForUrl(url, limitedSites) {
    if (!limitedSites || typeof limitedSites !== 'object') {
        return null;
    }
    try {
        const domain = safeExtractDomain(url);
        let host = '';
        try {
            host = new URL(url).hostname.toLowerCase();
        } catch (e) {
            host = '';
        }
        const bare = host.replace(/^www\./i, '');
        if (limitedSites[domain]) {
            return limitedSites[domain];
        }
        if (host && limitedSites[host]) {
            return limitedSites[host];
        }
        if (bare && limitedSites[bare]) {
            return limitedSites[bare];
        }
        for (const key of Object.keys(limitedSites)) {
            const k = String(key).toLowerCase();
            if (k === host || k === bare || k === domain) {
                return limitedSites[key];
            }
        }
    } catch (e) {
        safeConsole.log('getLimitedSiteEntryForUrl');
    }
    return null;
}

function updateBlockReasonWithSettings(url, settings) {
    if (!url || !settings) {
        return 'Elly says: Access restricted';
    }

    try {
        const domain = safeExtractDomain(url);

        // Check whitelist first - using safeIsUrlMatch
        if (settings.whitelist?.some(pattern => safeIsUrlMatch(url, pattern))) {
            return ''; // Not blocked - whitelisted
        }

        // Check blocked sites - using safeIsUrlMatch
        if (settings.blockedSites?.some(pattern => safeIsUrlMatch(url, pattern))) {
            return 'Elly says: Site is in your blocklist';
        }

        // Time limits must run before the search-engine SERP exception (otherwise SERP pages look "unblocked").
        const limitEntry = getLimitedSiteEntryForUrl(url, settings.limitedSites);
        if (limitEntry && typeof limitEntry.limitMinutes === 'number') {
            return `Elly says: Daily time limit reached (${limitEntry.limitMinutes} minutes)`;
        }

        // Check schedules
        const now = new Date();
        const currentDay = now.getDay();
        const currentTime = now.getHours() * 60 + now.getMinutes();

        for (const schedule of settings.schedules || []) {
            if (!schedule?.enabled) continue;

            const scheduleDays = schedule.days || [0, 1, 2, 3, 4, 5, 6];
            if (!scheduleDays.includes(currentDay)) continue;

            const startMinutes = timeToMinutes(schedule.startTime);
            const endMinutes = timeToMinutes(schedule.endTime);

            if (currentTime >= startMinutes && currentTime <= endMinutes) {
                if (schedule.type === 'block') {
                    const allowed = schedule.allowedSites || [];
                    if (!allowed.some(pattern => safeIsUrlMatch(url, pattern))) {
                        return `Elly says: Blocked by schedule (${schedule.name || 'Schedule'})`;
                    }
                } else if (schedule.type === 'allow') {
                    const blocked = schedule.blockedSites || [];
                    if (blocked.some(pattern => safeIsUrlMatch(url, pattern))) {
                        return `Elly says: Blocked by schedule (${schedule.name || 'Schedule'})`;
                    }
                }
            }
        }

        if (settings.globalSettings?.allowSearchEngineResults !== false && isSearchEngineResultsPage(url)) {
            return '';
        }

        return ''; // Not blocked

    } catch (error) {
        // Use safe console to avoid throwing more errors
        safeConsole.log('Error checking access');
        return 'Elly says: Error checking access';
    }
}

function isUrlMatch(url, pattern) {
    if (!url || !pattern) return false;

    try {
        const urlStr = String(url || '');
        const patternStr = String(pattern || '').trim();
        if (!patternStr) {
            return false;
        }

        const lowerUrl = urlStr.toLowerCase();
        const lowerPat = patternStr.toLowerCase();

        if (
            lowerPat.startsWith('chrome://')
            || lowerPat.startsWith('opera://')
            || lowerPat.startsWith('edge://')
            || lowerPat.startsWith('about:')
        ) {
            return lowerUrl.startsWith(lowerPat) || lowerUrl.includes(lowerPat);
        }

        if (patternStr.startsWith('*.')) {
            const domain = patternStr.slice(2).toLowerCase().replace(/\/.*$/, '');
            try {
                const host = new URL(urlStr).hostname.toLowerCase();
                const bareHost = host.replace(/^www\./, '');
                const bareDomain = domain.replace(/^www\./, '');
                return bareHost === bareDomain || bareHost.endsWith(`.${bareDomain}`);
            } catch {
                return false;
            }
        }

        if (patternStr.includes('://') || patternStr.includes('/')) {
            if (lowerUrl.startsWith(lowerPat) || lowerUrl.includes(lowerPat)) {
                return true;
            }
        }

        try {
            const host = new URL(urlStr).hostname.toLowerCase();
            if (!host) {
                return false;
            }
            let domainPart = lowerPat;
            if (domainPart.includes('://')) {
                try {
                    domainPart = new URL(patternStr).hostname.toLowerCase();
                } catch {
                    domainPart = lowerPat.replace(/^https?:\/\//, '').split('/')[0];
                }
            } else if (domainPart.includes('/')) {
                domainPart = domainPart.split('/')[0];
            }
            domainPart = domainPart.replace(/^\*\./, '');
            const bareHost = host.replace(/^www\./, '');
            const bareDomain = domainPart.replace(/^www\./, '');
            return bareHost === bareDomain || bareHost.endsWith(`.${bareDomain}`);
        } catch {
            return false;
        }
    } catch (error) {
        safeConsole.log('Error in isUrlMatch');
        return false;
    }
}

function extractDomain(url) {
    try {
        const urlObj = new URL(url);
        return urlObj.hostname.replace('www.', '');
    } catch {
        return url;
    }
}

function timeToMinutes(timeStr) {
    try {
        const [hours, minutes] = (timeStr || '00:00').split(':').map(Number);
        return hours * 60 + minutes;
    } catch {
        return 0;
    }
}

// ============================================
// HEARTBEAT & MONITORING
// ============================================

function startHeartbeat() {
    if (heartbeatInterval) {
        clearInterval(heartbeatInterval);
    }

    // Initial check
    if (!isExtensionContextValid()) {
        safeConsole.log('Extension not valid, stopping heartbeat');
        isExtensionActive = false;
        disableExtensionFeatures();
        return;
    }

    heartbeatInterval = setInterval(() => {
        // Check extension state before sending
        if (!isExtensionActive || !isExtensionContextValid()) {
            safeConsole.log('Extension inactive, clearing heartbeat');
            clearInterval(heartbeatInterval);
            heartbeatInterval = null;
            disableExtensionFeatures();
            return;
        }

        try {
            // Get current URL safely
            const currentUrl = window.location.href;
            const currentDomain = safeExtractDomain(currentUrl);

            // Use safeSendMessage instead of direct chrome.runtime.sendMessage
            const sent = safeSendMessage({
                action: 'heartbeat',
                url: currentUrl,
                timestamp: Date.now(),
                domain: currentDomain
            });

            // If message failed to send, extension is likely invalidated
            if (!sent) {
                safeConsole.log('Heartbeat failed to send');
                clearInterval(heartbeatInterval);
                heartbeatInterval = null;
                isExtensionActive = false;
                disableExtensionFeatures();
            }
        } catch (error) {
            safeConsole.log('Error in heartbeat');
            clearInterval(heartbeatInterval);
            heartbeatInterval = null;
            isExtensionActive = false;
            disableExtensionFeatures();
        }
    }, 30000); // 30 seconds
}

function clearAllIntervals() {
    if (heartbeatInterval) {
        clearInterval(heartbeatInterval);
        heartbeatInterval = null;
    }
}

function disableExtensionFeatures() {
    safeConsole.log('Disabling Elly extension features');
    clearAllIntervals();

    // Remove any UI elements
    const badge = document.querySelector('[data-elly-badge]');
    if (badge) badge.remove();

    // Clear any notifications
    const notifications = document.querySelectorAll('.elly-notification');
    notifications.forEach(notification => notification.remove());

    isExtensionActive = false;
}

// ============================================
// BLOCK PAGE HANDLING
// ============================================

/** @type {ReturnType<typeof setInterval> | null} */
let blockPageCountdownIntervalId = null;

/**
 * Display-only countdown for the block page. Does not navigate away (that caused redirect loops with
 * time limits / SERP handling and blocked PIN entry when the interval kept firing after 0).
 * @param {number} seconds
 * @returns {ReturnType<typeof setInterval> | null}
 */
function startCountdownTimer(seconds = 900) {
    if (blockPageCountdownIntervalId != null) {
        clearInterval(blockPageCountdownIntervalId);
        blockPageCountdownIntervalId = null;
    }

    let remaining = seconds;

    function updateTimer() {
        const s = Math.max(0, remaining);
        const minutes = Math.floor(s / 60);
        const rem = s % 60;
        const timerElement = document.getElementById('countdownTimer');
        if (timerElement) {
            timerElement.textContent =
                `${minutes.toString().padStart(2, '0')}:${rem.toString().padStart(2, '0')}`;
        }

        if (remaining <= 0) {
            if (blockPageCountdownIntervalId != null) {
                clearInterval(blockPageCountdownIntervalId);
                blockPageCountdownIntervalId = null;
            }
            return;
        }
        remaining--;
    }

    blockPageCountdownIntervalId = setInterval(updateTimer, 1000);
    updateTimer();
    return blockPageCountdownIntervalId;
}

function getOriginalUrlFromStorage() {
    return sessionStorage.getItem('ellyOriginalUrl') || null;
}

// ============================================
// PIN INPUT HANDLING (for block page)
// ============================================

function setupPinInput() {
    const el = document.getElementById('overridePinEntry');
    if (!el) {
        return;
    }
    el.addEventListener('input', (e) => {
        e.target.value = e.target.value.replace(/\D/g, '').slice(0, 12);
    });
    el.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            e.preventDefault();
            submitPin();
        }
    });
}

/**
 * Resolves the current tab id for the block page (query param or chrome.tabs.getCurrent).
 * @returns {Promise<number|null>}
 */
async function resolveTabId() {
    const params = new URLSearchParams(window.location.search);
    const fromQuery = params.get('tabId');
    if (fromQuery) {
        const n = parseInt(fromQuery, 10);
        if (Number.isFinite(n)) {
            return n;
        }
    }
    try {
        const tab = await chrome.tabs.getCurrent();
        return tab?.id ?? null;
    } catch (error) {
        safeConsole.log('resolveTabId failed');
        return null;
    }
}

/**
 * Scrolls the Parent PIN section into view and focuses the field.
 * Focus must run synchronously inside the click handler — deferring with
 * queueMicrotask / setTimeout drops user activation and browsers ignore focus().
 * @returns {void}
 */
function focusPinSection() {
    const section = document.getElementById('ellyPinSection');
    const inp = /** @type {HTMLInputElement|null} */ (document.getElementById('overridePinEntry'));
    const strict = !!cachedSettings.globalSettings?.strictMode;

    if (strict || (section && section.hidden) || (inp && inp.closest('[hidden]'))) {
        void showEllyContentAlertModal(
            'Strict Mode is on — temporary PIN overrides are disabled. A parent must remove this site from the Blocklist in Elly Settings.',
            { title: 'Elly says' }
        );
        return;
    }

    if (!section || !inp) {
        void showEllyContentAlertModal(
            'PIN field is not available on this page. Reload the block page and try again.',
            { title: 'Elly says' }
        );
        return;
    }

    section.hidden = false;
    section.scrollIntoView({ behavior: 'smooth', block: 'center' });
    // Synchronous focus while still inside the user-gesture stack.
    inp.focus({ preventScroll: true });
    try {
        inp.select();
    } catch {
        /* ignore — some engines reject select() on password inputs */
    }
    section.classList.remove('elly-pin-flash');
    // Force reflow so the flash animation restarts on repeated clicks.
    void section.offsetWidth;
    section.classList.add('elly-pin-flash');
    window.setTimeout(() => section.classList.remove('elly-pin-flash'), 1200);
}

function clearPinField() {
    const inp = document.getElementById('overridePinEntry');
    if (inp) {
        inp.value = '';
    }
}

/**
 * Tabler-style modal (replaces window.alert) for block page / content-script UI.
 * @param {string} message
 * @param {{ title?: string }} [opts]
 * @returns {Promise<void>}
 */
function showEllyContentAlertModal(message, opts = {}) {
    const title = opts.title || 'Elly';
    return new Promise((resolve) => {
        const existing = document.getElementById('elly-content-alert-overlay');
        if (existing) {
            existing.remove();
        }
        const overlay = document.createElement('div');
        overlay.id = 'elly-content-alert-overlay';
        overlay.className = 'elly-content-alert-overlay';
        overlay.setAttribute('role', 'dialog');
        overlay.setAttribute('aria-modal', 'true');
        overlay.setAttribute('aria-labelledby', 'elly-content-alert-heading');

        const card = document.createElement('div');
        card.className = 'elly-content-alert-card';

        const h3 = document.createElement('h3');
        h3.id = 'elly-content-alert-heading';
        h3.className = 'elly-content-alert-title';
        const icon = document.createElement('i');
        icon.className = 'ti ti-info-circle';
        h3.appendChild(icon);
        h3.appendChild(document.createTextNode(` ${title}`));

        const p = document.createElement('p');
        p.className = 'elly-content-alert-msg';
        p.textContent = message;

        const ok = document.createElement('button');
        ok.type = 'button';
        ok.className = 'elly-content-alert-ok';
        ok.textContent = 'OK';

        const finish = () => {
            ok.removeEventListener('click', onOk);
            overlay.removeEventListener('click', onBackdrop);
            document.removeEventListener('keydown', onKey);
            overlay.remove();
            resolve();
        };
        const onOk = () => finish();
        const onBackdrop = (e) => {
            if (e.target === overlay) {
                finish();
            }
        };
        const onKey = (e) => {
            if (e.key === 'Escape') {
                e.preventDefault();
                finish();
            }
        };

        ok.addEventListener('click', onOk);
        overlay.addEventListener('click', onBackdrop);
        document.addEventListener('keydown', onKey);

        card.appendChild(h3);
        card.appendChild(p);
        card.appendChild(ok);
        overlay.appendChild(card);
        document.body.appendChild(overlay);
        ok.focus();
    });
}

/**
 * Loads blocked URL from storage, sets sessionStorage for redirect, and fills #reasonText.
 * @returns {Promise<void>}
 */
async function hydrateBlockPageUi() {
    const reasonEl = document.getElementById('reasonText');
    if (!reasonEl) {
        return;
    }

    const tabId = await resolveTabId();
    if (!tabId || !isExtensionContextValid()) {
        reasonEl.textContent = 'Elly says: Access restricted';
        return;
    }

    const storageData = await new Promise((resolve) => {
        chrome.storage.local.get(['originalUrls'], (result) => {
            if (chrome.runtime.lastError) {
                safeConsole.log('hydrateBlockPageUi storage error');
                resolve({});
                return;
            }
            resolve(result || {});
        });
    });

    const originalUrls = storageData.originalUrls || {};
    const originalUrl = originalUrls[tabId] ?? originalUrls[String(tabId)];

    if (originalUrl) {
        try {
            sessionStorage.setItem('ellyOriginalUrl', originalUrl);
        } catch (error) {
            safeConsole.log('sessionStorage set failed');
        }
    }

    const reason = updateBlockReasonWithSettings(originalUrl || '', cachedSettings);
    const text = reason && String(reason).trim() !== '' ? reason : 'Elly says: Access restricted';
    reasonEl.textContent = text;
}

async function submitPin() {
    const raw = document.getElementById('overridePinEntry')?.value?.trim() || '';
    if (!/^\d{8,12}$/.test(raw)) {
        await showEllyContentAlertModal('PIN must be 8 to 12 digits', { title: 'Elly says' });
        return;
    }
    const pin = raw;

    if (!isExtensionContextValid()) {
        await showEllyContentAlertModal('Extension reloaded - please refresh the page', { title: 'Elly says' });
        return;
    }

    const tabId = await resolveTabId();
    if (!tabId) {
        await showEllyContentAlertModal('Could not determine tab — reload the block page', { title: 'Elly says' });
        return;
    }

    try {
        // Create a promise wrapper with error handling
        const response = await new Promise((resolve) => {
            const sent = safeSendMessage({
                action: 'overrideBlock',
                pin: pin,
                tabId: tabId
            }, (response) => {
                if (response === null) {
                    // Error case handled by safeSendMessage
                    resolve({ success: false, error: 'Extension context invalid' });
                } else {
                    resolve(response || { success: false });
                }
            });

            // If message couldn't be sent at all
            if (!sent) {
                resolve({ success: false, error: 'Cannot communicate with extension' });
            }
        });

        if (response?.success) {
            clearPinField();

            let originalUrl = getOriginalUrlFromStorage();
            if (!originalUrl) {
                const data = await new Promise((resolve) => {
                    chrome.storage.local.get(['originalUrls'], (result) => {
                        if (chrome.runtime.lastError) {
                            resolve({});
                            return;
                        }
                        resolve(result || {});
                    });
                });
                const ou = data.originalUrls || {};
                originalUrl = ou[tabId] ?? ou[String(tabId)] ?? null;
                if (originalUrl) {
                    try {
                        sessionStorage.setItem('ellyOriginalUrl', originalUrl);
                    } catch (error) {
                        safeConsole.log('sessionStorage set failed after override');
                    }
                }
            }
            if (originalUrl) {
                window.location.href = originalUrl;
            } else {
                await showEllyContentAlertModal(
                    'Override active for 15 minutes. Open the site from history or bookmarks.',
                    { title: 'Elly says' }
                );
            }
        } else {
            await showEllyContentAlertModal(response?.error || 'Incorrect PIN', { title: 'Elly says' });
            clearPinInputs();
        }
    } catch (error) {
        safeConsole.log('PIN submission error');
        await showEllyContentAlertModal('Error processing PIN - please refresh', { title: 'Elly says' });
    }
}

function clearPinInputs() {
    const el = document.getElementById('overridePinEntry');
    if (el) {
        el.value = '';
        el.focus();
    }
}

// ============================================
// NOTIFICATIONS & UI
// ============================================

function showFocusModeNotification() {
    if (!isExtensionActive) return;

    showNotification(
        '🎯 Focus Mode Active',
        'Distracting sites are temporarily blocked',
        'Stay productive!',
        'linear-gradient(135deg, #667eea 0%, #764ba2 100%)'
    );
}

function showTimeLimitWarning(domain, remainingMinutes) {
    if (!isExtensionActive) return;

    showNotification(
        '⏰ Time Limit Warning',
        `You have ${remainingMinutes} minutes remaining on ${domain}`,
        'Consider taking a break soon',
        'linear-gradient(135deg, #ff7e5f 0%, #feb47b 100%)',
        'pulseWarning'
    );
}

function showNotification(title, message, subtext, gradient, animation = 'slideIn') {
    if (!isExtensionActive) return;

    const notification = document.createElement('div');
    notification.className = 'elly-notification';
    notification.style.cssText = `
        position: fixed;
        top: 20px;
        right: 20px;
        background: ${gradient};
        color: white;
        padding: 15px 20px;
        border-radius: 10px;
        box-shadow: 0 10px 30px rgba(0,0,0,0.3);
        z-index: 10000;
        font-family: Arial, sans-serif;
        max-width: 300px;
        animation: ${animation} 0.5s ease-out;
    `;

    notification.innerHTML = `
        <div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
        <div style="font-size: 12px; opacity: 0.9;">${message}</div>
        <div style="margin-top: 10px; font-size: 11px; opacity: 0.7;">${subtext}</div>
    `;

    document.body.appendChild(notification);

    setTimeout(() => {
        notification.style.animation = 'slideOut 0.5s ease-in';
        setTimeout(() => notification.remove(), 500);
    }, 5000);
}

function addNotificationStyles() {
    const style = document.createElement('style');
    style.textContent = `
        @keyframes slideIn {
            from { transform: translateX(100%); opacity: 0; }
            to { transform: translateX(0); opacity: 1; }
        }
        @keyframes slideOut {
            from { transform: translateX(0); opacity: 1; }
            to { transform: translateX(100%); opacity: 0; }
        }
        @keyframes pulseWarning {
            0%, 100% { box-shadow: 0 10px 30px rgba(255, 126, 95, 0.5); }
            50% { box-shadow: 0 10px 40px rgba(255, 126, 95, 0.8); }
        }
    `;

    if (document.head) {
        document.head.appendChild(style);
    } else if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', () => {
            if (document.head) document.head.appendChild(style);
        });
    }
}

// ============================================
// PAGE MONITORING
// ============================================

function monitorPageChanges() {
    let lastUrl = window.location.href;

    const urlCheckInterval = setInterval(() => {
        if (!isExtensionActive) {
            clearInterval(urlCheckInterval);
            return;
        }

        if (window.location.href !== lastUrl) {
            lastUrl = window.location.href;
            checkCurrentPage();
        }
    }, 1000);

    // Monitor link clicks
    document.addEventListener('click', (e) => {
        if (!isExtensionActive) return;

        if (e.target.tagName === 'A' && e.target.href) {
            const reason = updateBlockReasonWithSettings(e.target.href, cachedSettings);
            if (reason) {
                e.preventDefault();
                showWarning(reason, e.target.href);
            }
        }
    }, true);
}

function checkCurrentPage() {
    if (!settingsLoaded || !isExtensionActive) return;

    const reason = updateBlockReasonWithSettings(window.location.href, cachedSettings);
    if (reason) {
        safeConsole.log('Current page should be blocked:', reason);
        // Could redirect to block page here
    }
}

function showWarning(reason, url) {
    if (!isExtensionActive) return;

    const warning = document.createElement('div');
    warning.style.cssText = `
        position: fixed;
        top: 20px;
        left: 50%;
        transform: translateX(-50%);
        background: linear-gradient(135deg, #ef4444, #dc2626);
        color: white;
        padding: 16px 24px;
        border-radius: 12px;
        font-weight: 600;
        font-size: 14px;
        z-index: 100000;
        box-shadow: 0 10px 40px rgba(239, 68, 68, 0.4);
        max-width: 400px;
        text-align: center;
        animation: slideIn 0.3s ease;
    `;

    warning.innerHTML = `
        <div style="margin-bottom: 8px;"><strong>${reason}</strong></div>
        <div style="font-size: 12px; opacity: 0.9; margin-bottom: 12px;">${url}</div>
        <button style="
            background: white;
            color: #dc2626;
            border: none;
            padding: 6px 16px;
            border-radius: 6px;
            font-weight: 600;
            cursor: pointer;
            font-size: 12px;
        " onclick="this.parentElement.remove()">OK</button>
    `;

    document.body.appendChild(warning);

    setTimeout(() => {
        if (warning.parentElement) {
            warning.style.opacity = '0';
            warning.style.transition = 'opacity 0.3s';
            setTimeout(() => warning.remove(), 300);
        }
    }, 5000);
}

// ============================================
// MAIN INITIALIZATION
// ============================================

function initializeContentScript() {
    safeConsole.log('Initializing Elly content script...');

    // Initialize extension state
    isExtensionActive = isExtensionContextValid();

    if (!isExtensionActive) {
        safeConsole.log('Elly: Extension not available or reloaded');
        return;
    }

    // Check if we're on the block page or regular page
    if (window.location.href.includes('block.html')) {
        initializeBlockPage();
    } else {
        initializeRegularPage();
    }
}

/**
 * Wires block page controls. Inline onclick is blocked by extension-page CSP (script-src 'self').
 * Call before awaiting storage so Focus PIN / Go Back work even if settings load is slow.
 * @returns {void}
 */
function wireBlockPageControls() {
    const goBack = () => {
        if (window.history.length > 1) {
            window.history.back();
        } else {
            window.close();
        }
    };

    const btnOverride = document.getElementById('ellyBtnOverride');
    const btnGoBack = document.getElementById('ellyBtnGoBack');
    const btnSubmit = document.getElementById('ellyPinSubmit');
    const btnCancel = document.getElementById('ellyPinCancel');

    if (btnOverride && !btnOverride.dataset.ellyWired) {
        btnOverride.dataset.ellyWired = '1';
        btnOverride.addEventListener('click', (e) => {
            e.preventDefault();
            focusPinSection();
        });
    }
    if (btnGoBack && !btnGoBack.dataset.ellyWired) {
        btnGoBack.dataset.ellyWired = '1';
        btnGoBack.addEventListener('click', (e) => {
            e.preventDefault();
            goBack();
        });
    }
    if (btnSubmit && !btnSubmit.dataset.ellyWired) {
        btnSubmit.dataset.ellyWired = '1';
        btnSubmit.addEventListener('click', (e) => {
            e.preventDefault();
            void submitPin();
        });
    }
    if (btnCancel && !btnCancel.dataset.ellyWired) {
        btnCancel.dataset.ellyWired = '1';
        btnCancel.addEventListener('click', (e) => {
            e.preventDefault();
            clearPinField();
        });
    }
}

function initializeBlockPage() {
    // Wire immediately — do not wait on storage (Focus PIN must respond on first click).
    wireBlockPageControls();
    setupPinInput();
    addNotificationStyles();

    loadSettings().then(async () => {
        await hydrateBlockPageUi();
        applyStrictModeBlockPageUi();
        startCountdownTimer();
    });
}

/**
 * Strict Mode disables temporary 15-minute overrides on the block page.
 */
function applyStrictModeBlockPageUi() {
    const strict = !!cachedSettings.globalSettings?.strictMode;
    const pinSection = document.getElementById('ellyPinSection');
    const btnOverride = document.getElementById('ellyBtnOverride');
    if (pinSection) {
        pinSection.hidden = strict;
    }
    if (btnOverride) {
        btnOverride.hidden = strict;
    }
    if (strict) {
        let note = document.getElementById('ellyStrictModeNote');
        if (!note) {
            note = document.createElement('p');
            note.id = 'ellyStrictModeNote';
            note.className = 'pin-hint';
            note.style.marginTop = '12px';
            note.textContent =
                'Strict Mode is on — temporary overrides are disabled. A parent must remove this site from the Blocklist (PIN required).';
            const controls = document.querySelector('.controls');
            if (controls && controls.parentNode) {
                controls.parentNode.insertBefore(note, controls);
            } else {
                document.querySelector('.container')?.appendChild(note);
            }
        }
        note.hidden = false;
    } else {
        const note = document.getElementById('ellyStrictModeNote');
        if (note) {
            note.hidden = true;
        }
    }
}

function initializeRegularPage() {
    // Load settings and start monitoring
    loadSettings().then(() => {
        safeConsole.log('Elly content script initialized');

        // Start monitoring
        startHeartbeat();
        monitorPageChanges();
        checkCurrentPage();

        // Add notification styles
        addNotificationStyles();

        // Add Elly badge if enabled
        if (cachedSettings.globalSettings?.showBadge !== false) {
            addEllyBadge();
        }
    }).catch(error => {
        safeConsole.log('Failed to load settings');
    });
}

function addEllyBadge() {
    if (!isExtensionActive) return;

    // Remove existing badge
    const existingBadge = document.querySelector('[data-elly-badge]');
    if (existingBadge) existingBadge.remove();

    const badge = document.createElement('div');
    badge.setAttribute('data-elly-badge', 'true');
    badge.style.cssText = `
        position: fixed;
        bottom: 20px;
        right: 20px;
        background: linear-gradient(135deg, #6366f1, #8b5cf6);
        color: white;
        padding: 8px 16px;
        border-radius: 20px;
        font-size: 12px;
        font-weight: 600;
        z-index: 99999;
        box-shadow: 0 4px 20px rgba(99, 102, 241, 0.3);
        display: flex;
        align-items: center;
        gap: 6px;
        cursor: pointer;
        opacity: 0.9;
        transition: opacity 0.3s;
    `;

    badge.innerHTML = `<span style="font-size: 14px;">👁</span><span>Elly Active</span>`;

    badge.addEventListener('click', () => {
        if (isExtensionContextValid()) {
            const sent = safeSendMessage({ action: 'openDashboard' });
            if (!sent) {
                badge.innerHTML = `<span style="font-size: 14px;">⚠️</span><span>Extension Reloaded</span>`;
                badge.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)';
            }
        } else {
            badge.innerHTML = `<span style="font-size: 14px;">⚠️</span><span>Extension Reloaded</span>`;
            badge.style.background = 'linear-gradient(135deg, #f59e0b, #d97706)';
        }
    });

    badge.addEventListener('mouseenter', () => badge.style.opacity = '1');
    badge.addEventListener('mouseleave', () => badge.style.opacity = '0.9');

    document.body.appendChild(badge);
}

// ============================================
// ERROR HANDLING & STARTUP
// ============================================

// Check extension context immediately
if (!isExtensionContextValid()) {
    safeConsole.log('Elly: Extension context invalidated - content script will not run');
    safeConsole.log('Note: This is normal when reloading the extension in developer mode');
} else {
    // Start initialization
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initializeContentScript);
    } else {
        initializeContentScript();
    }
}

// Cleanup on page unload
window.addEventListener('beforeunload', () => {
    clearAllIntervals();
});

// Log startup
try {
    console.log('%c🔒 Elly Content Script Loaded',
        'color: #6366f1; font-weight: bold; font-size: 14px;');
} catch (e) {
    // Ignore console errors
}