v3 · 37 files

ProjectElly/options/options.js 102 KB Raw
import '../lib/presets.js';
import {
    getApiBase,
    getPremiumPlansPageUrl,
    getLegalPagesBaseUrl,
    getEllyLicenseUrl,
    loginVrilsoft,
    verifyVrilsoftMfa,
    registerVrilsoftAccount,
    fetchAmIPremium,
    protectPlaintext,
    getJwt,
    setJwt,
    getLastLoginEmail,
    getProtectedCredentialPresence,
    EllyStorageKeys
} from './ellyPremium.js';
import { getTopBlockedSitesForUi, tryPostQuietStatsReport } from '../lib/ellyQuietStats.js';
import {
    LEGACY_DEFAULT_PARENT_PIN,
    isAcceptableNewAccessPin,
    isAcceptableNewParentPin,
    isForbiddenAccessPin,
    isForbiddenParentPin,
    normalizePinDigits,
    stripSecretsFromStorageSnapshot,
    importContainsPlaintextParentPin
} from '../lib/ellyAccessPin.js';

/** Show plaintext fields even when encrypted blobs exist (after Replace). */
const ellyKeyReveal = { apiKey: false, secret: false };

/** Pending MFA ticket from login (session memory only). */
let ellyPendingMfaTicket = null;
/** Email used for the pending MFA login (greeting after verify). */
let ellyPendingMfaEmail = '';

// Initialize global charts object
window.charts = window.charts || {
    dailyBlocks: null,
    timeSaved: null,
    category: null
};

// Footer helper for Version 
// Update version from manifest
function updateVersionFromManifest() {
    const manifest = chrome.runtime.getManifest();
    const version = manifest.version;

    // Find and update version element in footer
    const footerInfo = document.querySelector('.footer-info');
    if (footerInfo) {
        const versionSpan = footerInfo.querySelector('span:first-child');
        if (versionSpan) {
            versionSpan.innerHTML = `<i class="ti ti-shield-lock"></i> Elly v${version}`;
        }
    }
}

// Helper function for chart management
function createOrUpdateChart(canvasId, chartType, data, options) {
    const canvas = document.getElementById(canvasId);
    if (!canvas) {
        console.error(`Canvas element with ID '${canvasId}' not found`);
        return null;
    }

    const ctx = canvas.getContext('2d');

    // Check if chart already exists and destroy it
    if (window.charts[canvasId]) {
        try {
            window.charts[canvasId].destroy();
        } catch (e) {
            console.log('Error destroying chart:', e);
        }
        window.charts[canvasId] = null;
    }

    // Also check Chart.js instances
    const existingChart = Chart.getChart(canvasId);
    if (existingChart) {
        try {
            existingChart.destroy();
        } catch (e) {
            console.log('Error destroying existing chart:', e);
        }
    }

    // Create new chart
    window.charts[canvasId] = new Chart(ctx, {
        type: chartType,
        data: data,
        options: options
    });

    return window.charts[canvasId];
}

const DM_SESSION_KEY = 'ellyDmUnlocked';
const BLOCKLIST_SESSION_KEY = 'ellyBlocklistUnlocked';
const LOCAL_PARENT_PIN_KEY = 'ellyParentPinConfigured';
/** Parent / override PIN (block page, settings, data tools). Not the 8-digit extension access PIN. */
const PARENT_PIN_MIN_LEN = 8;
const PARENT_PIN_MAX_LEN = 12;

/**
 * Applies the visible tab without PIN checks (caller must authorize first when needed).
 * @param {string} tabId
 */
function switchTab(tabId) {
    const ellyPremiumPanel = document.getElementById('ellyPremiumPanel');
    if (ellyPremiumPanel) {
        ellyPremiumPanel.hidden = true;
    }

    console.log('Switching to tab:', tabId);

    // Update active button
    document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
    document.querySelector(`.tab-btn[data-tab="${tabId}"]`)?.classList.add('active');

    // Show selected tab
    document.querySelectorAll('.tab-content').forEach(content => {
        content.classList.remove('active');
        if (content.id === `${tabId}-tab`) {
            content.classList.add('active');
        }
    });

    // Destroy charts when leaving stats tab
    if (tabId !== 'stats') {
        // Safely destroy all charts
        Object.keys(window.charts).forEach(chartKey => {
            if (window.charts[chartKey]) {
                try {
                    window.charts[chartKey].destroy();
                } catch (e) {
                    console.log('Error destroying chart:', e);
                }
                window.charts[chartKey] = null;
            }
        });

        // Also clean up any Chart.js instances
        if (typeof Chart !== 'undefined' && Chart.instances) {
            Object.values(Chart.instances).forEach(instance => {
                try {
                    instance.destroy();
                } catch (e) {
                    console.log('Error destroying Chart.js instance:', e);
                }
            });
        }
    }

    // Load data for the tab if needed
    if (tabId === 'stats') {
        loadStatistics();
    }
}

/**
 * @param {string} tabId
 * @returns {Promise<boolean>}
 */
async function requestSwitchTab(tabId) {
    if (tabId === 'blocklist') {
        const ok = await ensureBlocklistAccess(
            'Enter your Parent PIN to open the Blocklist.',
            'Incorrect Parent PIN. Blocklist stays locked.'
        );
        if (!ok) {
            return false;
        }
    }
    switchTab(tabId);
    return true;
}

function initializeTabs() {
    const tabButtons = document.querySelectorAll('.tab-btn');

    tabButtons.forEach(button => {
        button.addEventListener('click', () => {
            const tabId = button.dataset.tab;
            if (tabId) {
                requestSwitchTab(tabId).catch((err) => {
                    console.error('Tab switch failed:', err);
                });
            }
        });
    });
}

/**
 * @param {string} pin
 * @returns {boolean}
 */
function isValidParentPin(pin) {
    return typeof pin === 'string' && /^\d{8,12}$/.test(pin);
}

/**
 * @param {{ blocklistPinLock?: boolean, strictMode?: boolean } | null | undefined} settings
 * @returns {boolean}
 */
function isBlocklistPinLockActive(settings) {
    return !!(settings?.blocklistPinLock || settings?.strictMode);
}

/**
 * Grants a short-lived background token so storage shrinks of blockedSites are not reverted.
 * @param {string} [pin] Required when PIN lock / Strict Mode is active.
 * @returns {Promise<boolean>}
 */
async function grantBlocklistMutationAuth(pin) {
    try {
        const response = await chrome.runtime.sendMessage({
            action: 'authorizeBlocklistMutation',
            pin: pin || null
        });
        return !!response?.success;
    } catch (err) {
        console.error('authorizeBlocklistMutation failed:', err);
        return false;
    }
}

/**
 * Revokes background mutation auth so further removals need a fresh Parent PIN.
 * @returns {Promise<void>}
 */
async function revokeBlocklistMutationAuth() {
    try {
        await chrome.runtime.sendMessage({ action: 'revokeBlocklistMutationAuth' });
    } catch (err) {
        console.error('revokeBlocklistMutationAuth failed:', err);
    }
}

/**
 * Updates the lock icon on the Blocklist nav button.
 * @param {boolean} locked
 */
function updateBlocklistTabLockIcon(locked) {
    const icon = document.getElementById('blocklistTabLockIcon');
    if (icon) {
        icon.hidden = !locked;
    }
}

/**
 * True when sensitive areas currently require a Parent PIN (lock setting on + session not unlocked).
 * @returns {Promise<boolean>}
 */
async function isSensitiveAreasLockedNow() {
    const data = await chrome.storage.sync.get(['settings']);
    if (!isBlocklistPinLockActive(data.settings)) {
        return false;
    }
    if (sessionStorage.getItem(BLOCKLIST_SESSION_KEY) !== '1') {
        return true;
    }
    try {
        const status = await chrome.runtime.sendMessage({ action: 'hasBlocklistMutationAuth' });
        return !status?.active;
    } catch {
        return true;
    }
}

/**
 * Syncs the header Lock / Unlock button with the current lock state.
 * @returns {Promise<void>}
 */
async function updateHeaderLockButton() {
    const btn = document.getElementById('btnHeaderLock');
    const icon = document.getElementById('btnHeaderLockIcon');
    const label = document.getElementById('btnHeaderLockLabel');
    if (!btn || !icon || !label) {
        return;
    }

    const lockedNow = await isSensitiveAreasLockedNow();
    const data = await chrome.storage.sync.get(['settings']);
    const pinLockOn = isBlocklistPinLockActive(data.settings);

    btn.classList.toggle('is-locked', lockedNow);
    btn.classList.toggle('is-unlocked', !lockedNow);
    btn.setAttribute('aria-pressed', lockedNow ? 'true' : 'false');

    if (lockedNow) {
        icon.className = 'ti ti-lock';
        label.textContent = 'Locked';
        btn.title = 'Click to unlock sensitive areas with your Parent PIN';
    } else if (pinLockOn) {
        icon.className = 'ti ti-lock-open';
        label.textContent = 'Lock';
        btn.title = 'Lock sensitive areas again (requires PIN to reopen)';
    } else {
        icon.className = 'ti ti-lock-open';
        label.textContent = 'Lock';
        btn.title = 'Lock down Blocklist, whitelist, and data tools behind your Parent PIN';
    }

    updateBlocklistTabLockIcon(pinLockOn);
}

/**
 * Locks sensitive areas: enables PIN lock, clears session unlocks, revokes mutation auth.
 * @returns {Promise<void>}
 */
async function lockSensitiveAreasNow() {
    const data = await chrome.storage.sync.get(['settings']);
    const settings = { ...(data.settings || {}) };
    settings.blocklistPinLock = true;
    await chrome.storage.sync.set({ settings });

    sessionStorage.removeItem(BLOCKLIST_SESSION_KEY);
    sessionStorage.removeItem(DM_SESSION_KEY);
    await revokeBlocklistMutationAuth();

    updateSecurityToggleUi(settings);
    updateDataMgmtLockUI();

    if (document.getElementById('blocklist-tab')?.classList.contains('active')) {
        switchTab('settings');
    }

    await updateHeaderLockButton();
    showEllyToast('Locked. Parent PIN is required for Blocklist and other sensitive areas.', 'success');
}

/**
 * Unlocks sensitive areas for this options session after Parent PIN verification.
 * @returns {Promise<void>}
 */
async function unlockSensitiveAreasWithPin() {
    const pin = await requireParentPin(
        'Enter your Parent PIN to unlock sensitive areas for this session.',
        'Incorrect Parent PIN. Still locked.'
    );
    if (pin == null) {
        return;
    }
    const granted = await grantBlocklistMutationAuth(pin);
    if (!granted) {
        showEllyToast('Could not unlock. Try again.', 'warning');
        return;
    }
    sessionStorage.setItem(BLOCKLIST_SESSION_KEY, '1');
    sessionStorage.setItem(DM_SESSION_KEY, '1');
    updateDataMgmtLockUI();
    await updateHeaderLockButton();
    showEllyToast('Unlocked for this session. Use Lock when you step away.', 'success');
}

/**
 * Header Lock button: lock immediately, or unlock with Parent PIN.
 * @returns {Promise<void>}
 */
async function onHeaderLockButtonClick() {
    const lockedNow = await isSensitiveAreasLockedNow();
    if (lockedNow) {
        await unlockSensitiveAreasWithPin();
        return;
    }
    await lockSensitiveAreasNow();
}

/**
 * When PIN lock / Strict Mode is on and this session is not unlocked, leave Blocklist for Settings.
 * @returns {Promise<void>}
 */
async function enforceBlocklistTabOnLoad() {
    const data = await chrome.storage.sync.get(['settings']);
    const locked = isBlocklistPinLockActive(data.settings);
    updateBlocklistTabLockIcon(locked);
    updateSecurityToggleUi(data.settings || {});
    await updateHeaderLockButton();

    if (!locked) {
        return;
    }
    if (sessionStorage.getItem(BLOCKLIST_SESSION_KEY) === '1') {
        return;
    }

    const blocklistTab = document.getElementById('blocklist-tab');
    if (blocklistTab?.classList.contains('active')) {
        switchTab('settings');
        showEllyToast('Blocklist is PIN-locked. Enter your Parent PIN from the Blocklist tab or use Unlock.', 'info');
    }
}

/**
 * @param {{ blocklistPinLock?: boolean, strictMode?: boolean }} settings
 */
function updateSecurityToggleUi(settings) {
    const pinLockEl = /** @type {HTMLInputElement|null} */ (document.getElementById('blocklistPinLock'));
    const strictEl = /** @type {HTMLInputElement|null} */ (document.getElementById('strictMode'));
    if (!pinLockEl || !strictEl) {
        return;
    }
    const strict = !!settings.strictMode;
    pinLockEl.checked = strict ? true : !!settings.blocklistPinLock;
    pinLockEl.disabled = strict;
    strictEl.checked = strict;
    updateBlocklistTabLockIcon(isBlocklistPinLockActive({
        blocklistPinLock: pinLockEl.checked,
        strictMode: strict
    }));
}

/**
 * @param {string} promptMessage
 * @param {string} wrongPinToast
 * @returns {Promise<boolean>}
 */
async function ensureBlocklistAccess(promptMessage, wrongPinToast) {
    const data = await chrome.storage.sync.get(['settings']);
    if (!isBlocklistPinLockActive(data.settings)) {
        await grantBlocklistMutationAuth();
        return true;
    }
    if (sessionStorage.getItem(BLOCKLIST_SESSION_KEY) === '1') {
        try {
            const status = await chrome.runtime.sendMessage({ action: 'hasBlocklistMutationAuth' });
            if (status?.active) {
                await grantBlocklistMutationAuth();
                return true;
            }
        } catch (err) {
            console.error('hasBlocklistMutationAuth failed:', err);
        }
        sessionStorage.removeItem(BLOCKLIST_SESSION_KEY);
    }
    const pin = await requireParentPin(promptMessage, wrongPinToast);
    if (pin == null) {
        return false;
    }
    const granted = await grantBlocklistMutationAuth(pin);
    if (!granted) {
        showEllyToast('Could not authorize Blocklist changes. Try again.', 'warning');
        return false;
    }
    sessionStorage.setItem(BLOCKLIST_SESSION_KEY, '1');
    await updateHeaderLockButton();
    return true;
}

document.addEventListener('DOMContentLoaded', () => {
    initializeTabs();
    setupEventListeners();
    initEllyPremiumUi();

    ensureAccessPinGate()
        .then((ok) => {
            if (!ok) {
                return;
            }
            return loadAllData().then(() => enforceBlocklistTabOnLoad());
        })
        .catch((err) => {
            console.error('Initial load failed:', err);
        });

    // Auto-save every 30 seconds (only meaningful after unlock)
    setInterval(() => {
        if (sessionStorage.getItem('ellyAccessUnlocked') === '1') {
            saveAllSettings();
        }
    }, 30000);
});

window.addEventListener('pagehide', () => {
    try {
        chrome.runtime.sendMessage({ action: 'lockAccessSession' }).catch(() => {});
    } catch {
        /* ignore */
    }
});

/**
 * Shows or hides an options modal overlay.
 * @param {string} id
 * @param {boolean} visible
 * @returns {void}
 */
function setModalVisible(id, visible) {
    const modal = document.getElementById(id);
    if (!modal) {
        return;
    }
    modal.classList.toggle('modal-overlay--visible', visible);
}

/**
 * Blocks main options UI until Access PIN is configured and verified for this SW session.
 * @returns {Promise<boolean>}
 */
async function ensureAccessPinGate() {
    const status = await chrome.runtime.sendMessage({ action: 'getAppStatus' });
    const configured = !!(status?.configured ?? status?.pinSet);
    const verified = !!status?.verified;

    if (!configured) {
        setModalVisible('accessPinUnlockModal', false);
        setModalVisible('accessPinSetupModal', true);
        return false;
    }
    if (!verified) {
        setModalVisible('accessPinSetupModal', false);
        setModalVisible('accessPinUnlockModal', true);
        return false;
    }
    setModalVisible('accessPinSetupModal', false);
    setModalVisible('accessPinUnlockModal', false);
    sessionStorage.setItem('ellyAccessUnlocked', '1');
    return true;
}

/**
 * Saves a new Access PIN from the setup modal (unconfigured installs).
 * @returns {Promise<void>}
 */
async function saveAccessPinSetupFromModal() {
    const errEl = document.getElementById('accessPinSetupError');
    const a = normalizePinDigits(document.getElementById('accessPinSetupNew')?.value);
    const b = normalizePinDigits(document.getElementById('accessPinSetupConfirm')?.value);
    if (errEl) {
        errEl.hidden = true;
        errEl.textContent = '';
    }
    if (!isAcceptableNewAccessPin(a)) {
        if (errEl) {
            errEl.textContent = isForbiddenAccessPin(a)
                ? 'That PIN is not allowed. Choose a different 8-digit PIN.'
                : 'Enter an 8-digit Access PIN.';
            errEl.hidden = false;
        }
        return;
    }
    if (a !== b) {
        if (errEl) {
            errEl.textContent = 'PINs do not match.';
            errEl.hidden = false;
        }
        return;
    }
    const response = await chrome.runtime.sendMessage({ action: 'setNewPIN', newPin: a });
    if (!response?.success) {
        if (errEl) {
            errEl.textContent = response?.error || 'Could not save Access PIN';
            errEl.hidden = false;
        }
        return;
    }
    const n = /** @type {HTMLInputElement|null} */ (document.getElementById('accessPinSetupNew'));
    const c = /** @type {HTMLInputElement|null} */ (document.getElementById('accessPinSetupConfirm'));
    if (n) n.value = '';
    if (c) c.value = '';
    setModalVisible('accessPinSetupModal', false);
    sessionStorage.setItem('ellyAccessUnlocked', '1');
    await loadAllData();
    await enforceBlocklistTabOnLoad();
}

/**
 * Unlocks options with verifyAccessPin.
 * @returns {Promise<void>}
 */
async function unlockAccessPinFromModal() {
    const errEl = document.getElementById('accessPinUnlockError');
    const pin = normalizePinDigits(document.getElementById('accessPinUnlockInput')?.value);
    if (errEl) {
        errEl.hidden = true;
        errEl.textContent = '';
    }
    const response = await chrome.runtime.sendMessage({ action: 'verifyAccessPin', pin });
    if (!response?.success) {
        if (errEl) {
            errEl.textContent = response?.error || 'Incorrect Access PIN';
            errEl.hidden = false;
        }
        return;
    }
    const input = /** @type {HTMLInputElement|null} */ (document.getElementById('accessPinUnlockInput'));
    if (input) {
        input.value = '';
    }
    setModalVisible('accessPinUnlockModal', false);
    sessionStorage.setItem('ellyAccessUnlocked', '1');
    await loadAllData();
    await enforceBlocklistTabOnLoad();
}

/**
 * Changes Access PIN via resetPIN (current + new + confirm).
 * @returns {Promise<void>}
 */
async function changeAccessPinFromSecurity() {
    const errEl = document.getElementById('accessPinChangeError');
    const oldPin = normalizePinDigits(document.getElementById('accessPinCurrent')?.value);
    const newPin = normalizePinDigits(document.getElementById('accessPinNew')?.value);
    const confirm = normalizePinDigits(document.getElementById('accessPinConfirm')?.value);
    if (errEl) {
        errEl.hidden = true;
        errEl.textContent = '';
    }
    if (!isAcceptableNewAccessPin(newPin)) {
        if (errEl) {
            errEl.textContent = isForbiddenAccessPin(newPin)
                ? 'That PIN is not allowed. Choose a different 8-digit PIN.'
                : 'New Access PIN must be 8 digits.';
            errEl.hidden = false;
        }
        return;
    }
    if (newPin !== confirm) {
        if (errEl) {
            errEl.textContent = 'New PINs do not match.';
            errEl.hidden = false;
        }
        return;
    }
    const response = await chrome.runtime.sendMessage({
        action: 'resetPIN',
        oldPin,
        newPin
    });
    if (!response?.success) {
        if (errEl) {
            errEl.textContent = response?.error || 'Could not change Access PIN';
            errEl.hidden = false;
        }
        showEllyToast(response?.error || 'Could not change Access PIN', 'warning');
        return;
    }
    ['accessPinCurrent', 'accessPinNew', 'accessPinConfirm'].forEach((id) => {
        const el = /** @type {HTMLInputElement|null} */ (document.getElementById(id));
        if (el) {
            el.value = '';
        }
    });
    showEllyToast('Access PIN updated.', 'success');
}

/**
 * Opera search-page access instruction is plain text (no navigable extensions link).
 * @returns {void}
 */
function setupOperaExtensionsDetailsLink() {
    /* no-op: do not open chrome://extensions from Options */
}

async function loadAllData() {
    try {
        const data = await chrome.storage.sync.get(null);
        // Update version from manifest first
        updateVersionFromManifest();
        // Load blocklist
        loadBlocklist(data.blockedSites || []);
        loadWhitelist(data.whitelist || []);

        // Load time limits
        loadTimeLimits(data.limitedSites || {});

        // Load schedules
        loadSchedules(data.schedules || []);

        // Load settings
        loadSettings(data.settings || {});

        // Load focus sessions
        if (data.focusSessions) {
            updateFocusSessionUI(data.focusSessions);
        }

        // Update last sync time
        document.getElementById('lastSync').textContent =
            `Last sync: ${new Date().toLocaleTimeString()}`;

        await initDataMgmtAccess();

    } catch (error) {
        console.error('Error loading data:', error);
        await openEllyAlertModal({
            title: 'Could not load settings',
            message: 'Error loading settings. Please try again.'
        });
    }
}

async function migrateParentPinConfigured() {
    const local = await chrome.storage.local.get([LOCAL_PARENT_PIN_KEY]);
    if (local[LOCAL_PARENT_PIN_KEY]) {
        return;
    }
    try {
        const status = await chrome.runtime.sendMessage({ action: 'getAppStatus' });
        if (status?.parentPinConfigured) {
            await chrome.storage.local.set({ [LOCAL_PARENT_PIN_KEY]: true });
        }
    } catch (err) {
        console.warn('Elly: parent PIN configured check failed', err);
    }
}

function updateBlockThemePreview() {
    const sel = document.getElementById('themeSelect');
    if (!sel) {
        return;
    }
    const v = sel.value;
    sel.setAttribute('data-block-theme', v);
    document.querySelectorAll('.preview-chip').forEach((chip) => {
        chip.classList.toggle('preview-chip--active', chip.dataset.theme === v);
    });
}

function updateSyncFrequencyHint() {
    const sel = document.getElementById('syncFrequency');
    const hint = document.getElementById('syncFrequencyHint');
    if (!sel || !hint) {
        return;
    }
    const map = {
        instant: 'Changes propagate as soon as your browser syncs (typically within minutes).',
        hourly: 'About once per hour across your signed-in devices.',
        daily: 'About once per day across your signed-in devices.'
    };
    hint.textContent = map[sel.value] || '';
}

function updateDataMgmtLockUI() {
    const unlocked = sessionStorage.getItem(DM_SESSION_KEY) === '1';
    ['exportData', 'importData', 'resetData', 'syncFrequency'].forEach((id) => {
        const el = document.getElementById(id);
        if (el) {
            el.disabled = !unlocked;
        }
    });
    const unlockBtn = document.getElementById('unlockDataMgmt');
    if (unlockBtn) {
        unlockBtn.style.display = unlocked ? 'none' : 'inline-flex';
    }
}

async function initDataMgmtAccess() {
    await migrateParentPinConfigured();
    const local = await chrome.storage.local.get([LOCAL_PARENT_PIN_KEY]);
    const modal = document.getElementById('initialPinModal');
    if (!local[LOCAL_PARENT_PIN_KEY] && modal) {
        modal.classList.add('modal-overlay--visible');
        updateDataMgmtLockUI();
        return;
    }
    updateDataMgmtLockUI();
}

async function assertDataMgmtUnlocked() {
    if (sessionStorage.getItem(DM_SESSION_KEY) === '1') {
        return true;
    }
    await openEllyAlertModal({
        title: 'Data Management locked',
        message: 'Unlock Data Management with your Parent PIN first (button in that section).'
    });
    return false;
}

async function saveInitialParentPin() {
    const errEl = document.getElementById('initialPinError');
    const a = document.getElementById('initialPinNew')?.value?.trim() || '';
    const b = document.getElementById('initialPinConfirm')?.value?.trim() || '';

    if (errEl) {
        errEl.hidden = true;
        errEl.textContent = '';
    }

    if (!isAcceptableNewParentPin(a)) {
        if (errEl) {
            errEl.textContent = isForbiddenParentPin(a)
                ? `Choose a PIN other than the default ${LEGACY_DEFAULT_PARENT_PIN}.`
                : `Enter ${PARENT_PIN_MIN_LEN}–${PARENT_PIN_MAX_LEN} numeric digits only.`;
            errEl.hidden = false;
        }
        return;
    }
    if (a !== b) {
        if (errEl) {
            errEl.textContent = 'PINs do not match.';
            errEl.hidden = false;
        }
        return;
    }

    try {
        const response = await chrome.runtime.sendMessage({
            action: 'setParentPin',
            currentPin: '',
            newPin: a
        });
        if (!response?.success) {
            if (errEl) {
                errEl.textContent = response?.error || 'Could not save Parent PIN.';
                errEl.hidden = false;
            }
            return;
        }
    } catch (err) {
        if (errEl) {
            errEl.textContent = err?.message || 'Could not save Parent PIN.';
            errEl.hidden = false;
        }
        return;
    }

    await chrome.storage.local.set({ [LOCAL_PARENT_PIN_KEY]: true });
    sessionStorage.setItem(DM_SESSION_KEY, '1');

    const pinField = document.getElementById('overridePin');
    if (pinField) {
        pinField.value = '';
        pinField.placeholder = 'unchanged';
    }

    const modal = document.getElementById('initialPinModal');
    if (modal) {
        modal.classList.remove('modal-overlay--visible');
    }
    updateDataMgmtLockUI();
}

/**
 * Blocklist label for parental preset: real URL stays in storage; UI shows prohibited-N only.
 * @param {string} storedPattern
 * @returns {string}
 */
function getBlocklistDisplayLabel(storedPattern) {
    const presets = typeof window !== 'undefined' && window.ELLY_PRESETS && Array.isArray(window.ELLY_PRESETS.prohibited)
        ? window.ELLY_PRESETS.prohibited
        : [];
    const idx = presets.indexOf(storedPattern);
    if (idx >= 0) {
        return `prohibited-${idx + 1}`;
    }
    return storedPattern;
}

function loadBlocklist(blockedSites) {
    const blocklistElement = document.getElementById('blocklist');
    const blockCount = document.getElementById('blockCount');

    blocklistElement.innerHTML = '';
    blockCount.textContent = blockedSites.length;

    blockedSites.forEach((site) => {
        const li = document.createElement('li');
        const span = document.createElement('span');
        span.className = 'blocklist-site-label';
        const label = getBlocklistDisplayLabel(site);
        span.textContent = label;
        if (label !== site) {
            span.title = 'Parental block (label hidden)';
        }
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'remove-btn';
        btn.dataset.site = site;
        btn.textContent = 'Remove';
        li.appendChild(span);
        li.appendChild(btn);
        blocklistElement.appendChild(li);
    });

    document.querySelectorAll('#blocklist .remove-btn').forEach((btn) => {
        btn.addEventListener('click', async (e) => {
            const site = e.currentTarget.dataset.site;
            if (site) {
                await removeFromBlocklist(site);
            }
        });
    });
}

function loadWhitelist(whitelist) {
    const whitelistElement = document.getElementById('whitelist');
    const countEl = document.getElementById('whitelistCount');
    if (countEl) {
        countEl.textContent = whitelist.length;
    }
    whitelistElement.replaceChildren();

    whitelist.forEach((site) => {
        const li = document.createElement('li');
        const span = document.createElement('span');
        span.textContent = site;
        const btn = document.createElement('button');
        btn.type = 'button';
        btn.className = 'remove-btn';
        btn.dataset.site = site;
        btn.textContent = 'Remove';
        li.appendChild(span);
        li.appendChild(btn);
        whitelistElement.appendChild(li);
    });

    document.querySelectorAll('#whitelist .remove-btn').forEach((btn) => {
        btn.addEventListener('click', async (e) => {
            const site = e.currentTarget.dataset.site;
            if (site) {
                await removeFromWhitelist(site);
            }
        });
    });
}

async function removeFromBlocklist(site) {
    const ok = await ensureBlocklistAccess(
        'Enter your Parent PIN to remove a site from the Blocklist.',
        'Incorrect Parent PIN. Site was not removed.'
    );
    if (!ok) {
        return;
    }
    const data = await chrome.storage.sync.get(['blockedSites']);
    const blockedSites = (data.blockedSites || []).filter(s => s !== site);
    await chrome.storage.sync.set({ blockedSites });
    loadBlocklist(blockedSites);
}

async function removeFromWhitelist(site) {
    const ok = await ensureBlocklistAccess(
        'Enter your Parent PIN to change the whitelist (it bypasses the Blocklist).',
        'Incorrect Parent PIN. Whitelist was not changed.'
    );
    if (!ok) {
        return;
    }
    const data = await chrome.storage.sync.get(['whitelist']);
    const whitelist = (data.whitelist || []).filter(s => s !== site);
    await chrome.storage.sync.set({ whitelist });
    loadWhitelist(whitelist);
}

function loadTimeLimits(limitedSites) {
    const limitsList = document.getElementById('limitsList');
    const limitsCount = document.getElementById('limitsCount');

    limitsList.replaceChildren();
    limitsCount.textContent = Object.keys(limitedSites).length;

    for (const [domain, limit] of Object.entries(limitedSites)) {
        const limitCard = document.createElement('div');
        limitCard.className = 'limit-card';

        const header = document.createElement('div');
        header.className = 'limit-header';
        const domainEl = document.createElement('span');
        domainEl.className = 'limit-domain';
        domainEl.textContent = domain;
        const timeEl = document.createElement('span');
        timeEl.className = 'limit-time';
        timeEl.textContent = `${limit.limitMinutes} min/day`;
        header.appendChild(domainEl);
        header.appendChild(timeEl);

        const progressContainer = document.createElement('div');
        progressContainer.className = 'progress-container';
        const progressBar = document.createElement('div');
        progressBar.className = 'progress-bar';
        progressBar.style.width = '50%';
        progressContainer.appendChild(progressBar);

        const usageInfo = document.createElement('div');
        usageInfo.className = 'usage-info';
        const used = document.createElement('span');
        used.textContent = 'Used: 0 min';
        const sep = document.createElement('span');
        sep.textContent = ' | ';
        const remaining = document.createElement('span');
        remaining.textContent = `Remaining: ${limit.limitMinutes} min`;
        usageInfo.appendChild(used);
        usageInfo.appendChild(sep);
        usageInfo.appendChild(remaining);

        const actions = document.createElement('div');
        actions.className = 'limit-actions';
        const editBtn = document.createElement('button');
        editBtn.type = 'button';
        editBtn.className = 'btn-secondary';
        editBtn.dataset.domain = domain;
        editBtn.textContent = 'Edit';
        const removeBtn = document.createElement('button');
        removeBtn.type = 'button';
        removeBtn.className = 'btn-danger';
        removeBtn.dataset.domain = domain;
        removeBtn.textContent = 'Remove';
        actions.appendChild(editBtn);
        actions.appendChild(removeBtn);

        limitCard.appendChild(header);
        limitCard.appendChild(progressContainer);
        limitCard.appendChild(usageInfo);
        limitCard.appendChild(actions);
        limitsList.appendChild(limitCard);
    }

    document.querySelectorAll('.limit-actions .btn-danger').forEach((btn) => {
        btn.addEventListener('click', async (e) => {
            const domain = e.currentTarget.dataset.domain;
            if (domain) {
                await removeTimeLimit(domain);
            }
        });
    });
}

async function removeTimeLimit(domain) {
    const data = await chrome.storage.sync.get(['limitedSites', 'settings']);
    if (isBlocklistPinLockActive(data.settings)) {
        const ok = await requireParentPinOk(
            'Enter your Parent PIN to remove this time limit.',
            'Incorrect Parent PIN. Time limit was not removed.'
        );
        if (!ok) {
            return;
        }
    }
    const limitedSites = { ...(data.limitedSites || {}) };
    delete limitedSites[domain];
    await chrome.storage.sync.set({ limitedSites });
    loadTimeLimits(limitedSites);
}

function loadSchedules(schedules) {
    const schedulesList = document.getElementById('schedulesList');
    schedulesList.innerHTML = '';

    schedules.forEach(schedule => {
        const scheduleCard = createScheduleCard(schedule);
        schedulesList.appendChild(scheduleCard);
    });
}

function createScheduleCard(schedule) {
    const card = document.createElement('div');
    card.className = 'schedule-card';
    card.dataset.id = schedule.id;

    const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];

    const header = document.createElement('div');
    header.className = 'schedule-header';
    const nameInput = document.createElement('input');
    nameInput.type = 'text';
    nameInput.className = 'schedule-name';
    nameInput.value = schedule.name || 'New Schedule';
    const toggleLabel = document.createElement('label');
    toggleLabel.className = 'toggle';
    const enabledInput = document.createElement('input');
    enabledInput.type = 'checkbox';
    enabledInput.className = 'schedule-enabled';
    enabledInput.checked = !!schedule.enabled;
    const slider = document.createElement('span');
    slider.className = 'toggle-slider';
    toggleLabel.appendChild(enabledInput);
    toggleLabel.appendChild(slider);
    header.appendChild(nameInput);
    header.appendChild(toggleLabel);

    const body = document.createElement('div');
    body.className = 'schedule-body';

    const timeRange = document.createElement('div');
    timeRange.className = 'time-range';
    const timeLabel = document.createElement('label');
    timeLabel.textContent = 'Time Range:';
    const startInput = document.createElement('input');
    startInput.type = 'time';
    startInput.className = 'schedule-start';
    startInput.value = schedule.startTime || '09:00';
    const toSpan = document.createElement('span');
    toSpan.textContent = 'to';
    const endInput = document.createElement('input');
    endInput.type = 'time';
    endInput.className = 'schedule-end';
    endInput.value = schedule.endTime || '17:00';
    timeRange.appendChild(timeLabel);
    timeRange.appendChild(startInput);
    timeRange.appendChild(toSpan);
    timeRange.appendChild(endInput);

    const daysSelector = document.createElement('div');
    daysSelector.className = 'days-selector';
    const daysLabel = document.createElement('label');
    daysLabel.textContent = 'Days:';
    const daysButtons = document.createElement('div');
    daysButtons.className = 'days-buttons';
    for (let day = 0; day < 7; day++) {
        const dayBtn = document.createElement('button');
        dayBtn.type = 'button';
        dayBtn.className = 'day-btn';
        if (schedule.days?.includes(day)) {
            dayBtn.classList.add('active');
        }
        dayBtn.dataset.day = String(day);
        dayBtn.textContent = dayNames[day];
        daysButtons.appendChild(dayBtn);
    }
    daysSelector.appendChild(daysLabel);
    daysSelector.appendChild(daysButtons);

    const scheduleType = document.createElement('div');
    scheduleType.className = 'schedule-type';
    const blockLabel = document.createElement('label');
    const blockRadio = document.createElement('input');
    blockRadio.type = 'radio';
    blockRadio.name = `type_${schedule.id}`;
    blockRadio.value = 'block';
    blockRadio.checked = schedule.type === 'block';
    blockLabel.appendChild(blockRadio);
    blockLabel.appendChild(document.createTextNode(' Block all except:'));
    const allowLabel = document.createElement('label');
    const allowRadio = document.createElement('input');
    allowRadio.type = 'radio';
    allowRadio.name = `type_${schedule.id}`;
    allowRadio.value = 'allow';
    allowRadio.checked = schedule.type === 'allow';
    allowLabel.appendChild(allowRadio);
    allowLabel.appendChild(document.createTextNode(' Allow all except:'));
    const urlsArea = document.createElement('textarea');
    urlsArea.className = 'schedule-urls';
    const urlList = (schedule.type === 'block' ? schedule.allowedSites : schedule.blockedSites) || [];
    urlsArea.value = Array.isArray(urlList) ? urlList.join('\n') : '';
    scheduleType.appendChild(blockLabel);
    scheduleType.appendChild(allowLabel);
    scheduleType.appendChild(urlsArea);

    const actions = document.createElement('div');
    actions.className = 'schedule-actions';
    const saveBtn = document.createElement('button');
    saveBtn.type = 'button';
    saveBtn.className = 'btn-save';
    saveBtn.textContent = 'Save';
    const deleteBtn = document.createElement('button');
    deleteBtn.type = 'button';
    deleteBtn.className = 'btn-delete';
    deleteBtn.textContent = 'Delete';
    actions.appendChild(saveBtn);
    actions.appendChild(deleteBtn);

    body.appendChild(timeRange);
    body.appendChild(daysSelector);
    body.appendChild(scheduleType);
    body.appendChild(actions);
    card.appendChild(header);
    card.appendChild(body);

    saveBtn.addEventListener('click', () => saveSchedule(card));
    deleteBtn.addEventListener('click', () => deleteSchedule(schedule.id));

    card.querySelectorAll('.day-btn').forEach((btn) => {
        btn.addEventListener('click', () => {
            btn.classList.toggle('active');
        });
    });

    return card;
}

/**
 * @returns {Promise<boolean>}
 */
async function requireEllySchedulePremiumOrBlock() {
    const pr = await fetchAmIPremium();
    if (pr.isPremium) {
        return true;
    }
    showEllyToast('Schedules are a Premium feature. Sign in on Premium & API and subscribe to save.', 'warning');
    const dlg = document.getElementById('ellyUpgradeModal');
    const plansUrl = await getPremiumPlansPageUrl();
    const link = document.getElementById('ellyUpgradeModalLink');
    if (link) {
        link.href = plansUrl;
    }
    if (dlg && typeof dlg.showModal === 'function') {
        dlg.showModal();
    } else {
        await openEllyAlertModal({
            title: 'Premium required',
            message: 'Schedules require Elly Premium. Open Premium & API in the header, then subscribe on Vrilsoft.'
        });
    }
    return false;
}

/**
 * @param {string} message
 * @param {'info'|'success'|'warning'} kind
 */
function showEllyToast(message, kind = 'info') {
    const host = document.getElementById('ellyToastHost');
    if (!host) {
        return;
    }
    const el = document.createElement('div');
    el.className = `elly-toast elly-toast--${kind}`;
    el.textContent = message;
    host.appendChild(el);
    requestAnimationFrame(() => {
        el.classList.add('elly-toast--show');
    });
    setTimeout(() => {
        el.classList.remove('elly-toast--show');
        setTimeout(() => el.remove(), 400);
    }, 5200);
}

/**
 * Tabler-style informational modal (replaces window.alert).
 * @param {{ title?: string, message: string, confirmLabel?: string }} opts
 * @returns {Promise<void>}
 */
function openEllyAlertModal(opts) {
    const title = opts.title ?? 'Notice';
    const message = opts.message ?? '';
    const confirmLabel = opts.confirmLabel ?? 'OK';
    const dlg = document.getElementById('ellyAlertModal');
    const titleEl = document.getElementById('ellyAlertModalTitle');
    const textEl = document.getElementById('ellyAlertModalText');
    const okBtn = document.getElementById('ellyAlertModalOk');
    if (!dlg || !titleEl || !textEl || !okBtn) {
        return Promise.resolve();
    }
    titleEl.textContent = '';
    const titleIcon = document.createElement('i');
    titleIcon.className = 'ti ti-info-circle';
    titleEl.appendChild(titleIcon);
    titleEl.appendChild(document.createTextNode(` ${title}`));
    textEl.textContent = message;
    okBtn.replaceChildren();
    const okIcon = document.createElement('i');
    okIcon.className = 'ti ti-check';
    okBtn.appendChild(okIcon);
    okBtn.appendChild(document.createTextNode(` ${confirmLabel}`));

    return new Promise((resolve) => {
        let done = false;
        const cleanup = () => {
            okBtn.removeEventListener('click', onOk);
            dlg.removeEventListener('cancel', onCancel);
        };
        const finish = () => {
            if (done) {
                return;
            }
            done = true;
            dlg.close();
            cleanup();
            resolve();
        };
        const onOk = () => finish();
        const onCancel = (e) => {
            e.preventDefault();
            finish();
        };
        okBtn.addEventListener('click', onOk);
        dlg.addEventListener('cancel', onCancel);
        dlg.showModal();
    });
}

/**
 * Tabler-style confirm modal (replaces window.confirm).
 * @param {{ title?: string, message: string, confirmLabel?: string, cancelLabel?: string, danger?: boolean }} opts
 * @returns {Promise<boolean>}
 */
function openEllyConfirmModal(opts) {
    const title = opts.title ?? 'Confirm';
    const message = opts.message ?? '';
    const confirmLabel = opts.confirmLabel ?? 'OK';
    const cancelLabel = opts.cancelLabel ?? 'Cancel';
    const danger = !!opts.danger;
    const dlg = document.getElementById('ellyConfirmModal');
    const titleEl = document.getElementById('ellyConfirmModalTitle');
    const textEl = document.getElementById('ellyConfirmModalText');
    const okBtn = document.getElementById('ellyConfirmModalOk');
    const cancelBtn = document.getElementById('ellyConfirmModalCancel');
    if (!dlg || !titleEl || !textEl || !okBtn || !cancelBtn) {
        return Promise.resolve(false);
    }
    titleEl.textContent = '';
    const titleIcon = document.createElement('i');
    titleIcon.className = danger ? 'ti ti-alert-triangle' : 'ti ti-help';
    titleEl.appendChild(titleIcon);
    titleEl.appendChild(document.createTextNode(` ${title}`));
    textEl.textContent = message;
    okBtn.className = danger ? 'btn-danger' : 'btn-primary';
    okBtn.replaceChildren();
    const okIcon = document.createElement('i');
    okIcon.className = 'ti ti-check';
    okBtn.appendChild(okIcon);
    okBtn.appendChild(document.createTextNode(` ${confirmLabel}`));
    cancelBtn.textContent = cancelLabel;

    return new Promise((resolve) => {
        let done = false;
        const cleanup = () => {
            okBtn.removeEventListener('click', onOk);
            cancelBtn.removeEventListener('click', onCancel);
            dlg.removeEventListener('cancel', onCancelEsc);
        };
        const finish = (/** @type {boolean} */ v) => {
            if (done) {
                return;
            }
            done = true;
            dlg.close();
            cleanup();
            resolve(v);
        };
        const onOk = () => finish(true);
        const onCancel = () => finish(false);
        const onCancelEsc = (e) => {
            e.preventDefault();
            finish(false);
        };
        okBtn.addEventListener('click', onOk);
        cancelBtn.addEventListener('click', onCancel);
        dlg.addEventListener('cancel', onCancelEsc);
        dlg.showModal();
    });
}

/**
 * Shows sign-in fields or the signed-in greeting block based on JWT.
 * @returns {Promise<void>}
 */
async function syncEllyAuthPanels() {
    const signedOut = document.getElementById('ellyAuthSignedOut');
    const signedIn = document.getElementById('ellyAuthSignedIn');
    const greetingLine = document.getElementById('ellyGreetingLine');
    if (!signedOut || !signedIn) {
        return;
    }
    const jwt = await getJwt();
    const email = await getLastLoginEmail();
    if (jwt) {
        signedOut.hidden = true;
        signedIn.hidden = false;
        if (greetingLine) {
            greetingLine.textContent = email
                ? `You're signed in as ${email}.`
                : "You're signed in to your Vril One account.";
        }
    } else {
        signedOut.hidden = false;
        signedIn.hidden = true;
        if (greetingLine) {
            greetingLine.textContent = '';
        }
    }
}

/**
 * Hides plaintext API key/secret inputs when encrypted blobs exist; Replace reveals a field again.
 * Enables inputs only when JWT is present. Key block stays visible when Premium panel is open.
 * @returns {Promise<void>}
 */
async function syncEllyKeyRowsVisibility() {
    const jwt = await getJwt();
    const signedIn = !!jwt;
    const { hasApiKey, hasSecret } = await getProtectedCredentialPresence();
    const keyIn = document.getElementById('ellyApiKeyRow');
    const keyStored = document.getElementById('ellyApiKeyStoredRow');
    const secIn = document.getElementById('ellyApiSecretRow');
    const secStored = document.getElementById('ellyApiSecretStoredRow');
    const keyPlain = /** @type {HTMLInputElement|null} */ (document.getElementById('ellyApiKeyPlain'));
    const secretPlain = /** @type {HTMLInputElement|null} */ (document.getElementById('ellyApiSecretPlain'));
    const saveKeys = /** @type {HTMLButtonElement|null} */ (document.getElementById('ellyBtnSaveKeys'));
    const hint = document.getElementById('ellyKeySignInHint');
    const keyBlock = document.getElementById('ellyKeyBlock');

    if (keyBlock) {
        keyBlock.hidden = false;
    }
    if (hint) {
        hint.hidden = signedIn;
    }

    const showKeyInput = !signedIn || !hasApiKey || ellyKeyReveal.apiKey;
    const showSecretInput = !signedIn || !hasSecret || ellyKeyReveal.secret;
    if (keyIn) {
        keyIn.hidden = !showKeyInput;
    }
    if (keyStored) {
        keyStored.hidden = !signedIn || showKeyInput;
    }
    if (secIn) {
        secIn.hidden = !showSecretInput;
    }
    if (secStored) {
        secStored.hidden = !signedIn || showSecretInput;
    }
    if (keyPlain) {
        keyPlain.disabled = !signedIn;
    }
    if (secretPlain) {
        secretPlain.disabled = !signedIn;
    }
    if (saveKeys) {
        saveKeys.disabled = !signedIn;
    }
}

/**
 * Shows or hides the MFA code UI after password login.
 * @param {boolean} visible
 * @returns {void}
 */
function setEllyMfaUiVisible(visible) {
    const block = document.getElementById('ellyMfaBlock');
    if (block) {
        block.hidden = !visible;
    }
    if (!visible) {
        const codeEl = /** @type {HTMLInputElement|null} */ (document.getElementById('ellyMfaCode'));
        if (codeEl) {
            codeEl.value = '';
        }
        ellyPendingMfaTicket = null;
        ellyPendingMfaEmail = '';
    }
}

async function initEllyPremiumUi() {
    const baseInput = document.getElementById('ellyApiBase');
    const panel = document.getElementById('ellyPremiumPanel');
    const btnTop = document.getElementById('btnEllyPremium');
    const upgrade = document.getElementById('ellyLinkUpgrade');
    const btnSignIn = document.getElementById('ellyBtnSignIn');
    const btnSignOut = document.getElementById('ellyBtnSignOut');
    const statusLine = document.getElementById('ellyPremiumStatusLine');
    const keyBlock = document.getElementById('ellyKeyBlock');
    const saveKeys = document.getElementById('ellyBtnSaveKeys');
    const modalClose = document.getElementById('ellyUpgradeModalClose');

    const base = await getApiBase();
    const plansUrlInit = await getPremiumPlansPageUrl();
    if (baseInput) {
        baseInput.value = base;
    }
    if (upgrade) {
        upgrade.href = plansUrlInit;
    }
    const mlink = document.getElementById('ellyUpgradeModalLink');
    if (mlink) {
        mlink.href = plansUrlInit;
    }

    const legalBase = await getLegalPagesBaseUrl();
    document.getElementById('ellyRegLinkTos')?.setAttribute('href', `${legalBase}/Legal/Terms`);
    document.getElementById('ellyRegLinkRefund')?.setAttribute('href', `${legalBase}/Legal/Refund`);
    document.getElementById('ellyRegLinkPrivacy')?.setAttribute('href', `${legalBase}/Legal/Privacy`);
    const licenseUrl = await getEllyLicenseUrl();
    document.getElementById('ellyRegLinkLicense')?.setAttribute('href', licenseUrl);
    document.getElementById('ellyLicenseFooterLink')?.setAttribute('href', licenseUrl);

    const regModal = /** @type {HTMLDialogElement|null} */ (document.getElementById('ellyRegisterModal'));
    const regErr = document.getElementById('ellyRegisterError');
    document.getElementById('ellyBtnRegister')?.addEventListener('click', () => {
        if (regErr) {
            regErr.hidden = true;
            regErr.textContent = '';
        }
        regModal?.showModal();
    });
    document.getElementById('ellyRegisterCancel')?.addEventListener('click', () => regModal?.close());
    document.getElementById('ellyRegisterSubmit')?.addEventListener('click', async () => {
        const realName = document.getElementById('ellyRegRealName')?.value?.trim() || '';
        const userName = document.getElementById('ellyRegUserName')?.value?.trim() || '';
        const email = document.getElementById('ellyRegEmail')?.value?.trim() || '';
        const mobile = document.getElementById('ellyRegMobile')?.value?.trim() || '';
        const password = document.getElementById('ellyRegPassword')?.value || '';
        const confirmPassword = document.getElementById('ellyRegPassword2')?.value || '';
        const acceptTos = document.getElementById('ellyRegTos')?.checked === true;
        const acceptRefund = document.getElementById('ellyRegRefund')?.checked === true;
        const acceptPrivacy = document.getElementById('ellyRegPrivacy')?.checked === true;
        const acceptLicense = document.getElementById('ellyRegLicense')?.checked === true;
        if (regErr) {
            regErr.hidden = true;
        }
        if (!acceptLicense) {
            if (regErr) {
                regErr.textContent = 'You must accept the Elly Software License.';
                regErr.hidden = false;
            }
            return;
        }
        const r = await registerVrilsoftAccount({
            realName,
            userName,
            email,
            mobile,
            password,
            confirmPassword,
            acceptTos,
            acceptRefund,
            acceptPrivacy
        });
        if (!r.ok) {
            if (regErr) {
                regErr.textContent = r.error || 'Registration failed.';
                regErr.hidden = false;
            }
            return;
        }
        showEllyToast(r.message || 'Account created. Check your email to activate.', 'success');
        ['ellyRegRealName', 'ellyRegUserName', 'ellyRegEmail', 'ellyRegMobile', 'ellyRegPassword', 'ellyRegPassword2'].forEach((id) => {
            const el = document.getElementById(id);
            if (el) {
                el.value = '';
            }
        });
        const cTos = document.getElementById('ellyRegTos');
        const cRef = document.getElementById('ellyRegRefund');
        const cPrv = document.getElementById('ellyRegPrivacy');
        if (cTos) {
            cTos.checked = false;
        }
        if (cRef) {
            cRef.checked = false;
        }
        if (cPrv) {
            cPrv.checked = false;
        }
        regModal?.close();
    });

    btnTop?.addEventListener('click', async () => {
        const ok = await requireParentPinForPremiumPanel();
        if (!ok) {
            return;
        }
        if (panel) {
            panel.hidden = !panel.hidden;
        }
        await refreshEllyPremiumLine(statusLine, keyBlock);
    });

    btnSignIn?.addEventListener('click', async () => {
        const email = document.getElementById('ellyLoginEmail')?.value?.trim() || '';
        const password = document.getElementById('ellyLoginPass')?.value || '';
        if (!email || !password) {
            showEllyToast('Enter email and password to sign in.', 'warning');
            return;
        }
        setEllyMfaUiVisible(false);
        const r = await loginVrilsoft(email, password);
        if (r.requiresMfaEnrollment) {
            showEllyToast(
                'This account requires authenticator MFA enrollment. Enable MFA on your Vrilsoft / Vril One account, then sign in again.',
                'warning'
            );
            if (statusLine) {
                statusLine.textContent =
                    'MFA enrollment required — open Vrilsoft / Vril One account security to enable authenticator MFA, then return here.';
            }
            return;
        }
        if (r.requiresMfa && r.mfaTicket) {
            ellyPendingMfaTicket = r.mfaTicket;
            ellyPendingMfaEmail = email;
            setEllyMfaUiVisible(true);
            showEllyToast('Enter your authenticator code to finish signing in.', 'info');
            if (statusLine) {
                statusLine.textContent = 'MFA required — enter the code from your authenticator app.';
            }
            return;
        }
        if (!r.ok) {
            showEllyToast(r.error || 'Login failed', 'warning');
            return;
        }
        showEllyToast('Signed in to your Vril One account.', 'success');
        const passEl = document.getElementById('ellyLoginPass');
        if (passEl) {
            passEl.value = '';
        }
        await syncEllyAuthPanels();
        await refreshEllyPremiumLine(statusLine, keyBlock);
    });

    document.getElementById('ellyBtnMfaVerify')?.addEventListener('click', async () => {
        const code = document.getElementById('ellyMfaCode')?.value?.trim() || '';
        if (!ellyPendingMfaTicket) {
            showEllyToast('Sign in with email and password first to start MFA.', 'warning');
            return;
        }
        if (!code) {
            showEllyToast('Enter your authenticator code.', 'warning');
            return;
        }
        const r = await verifyVrilsoftMfa(ellyPendingMfaTicket, code, ellyPendingMfaEmail);
        if (!r.ok) {
            showEllyToast(r.error || 'MFA verification failed', 'warning');
            return;
        }
        setEllyMfaUiVisible(false);
        showEllyToast('Signed in to your Vril One account.', 'success');
        const passEl = document.getElementById('ellyLoginPass');
        if (passEl) {
            passEl.value = '';
        }
        await syncEllyAuthPanels();
        await refreshEllyPremiumLine(statusLine, keyBlock);
    });

    btnSignOut?.addEventListener('click', async () => {
        ellyKeyReveal.apiKey = false;
        ellyKeyReveal.secret = false;
        setEllyMfaUiVisible(false);
        await setJwt(null);
        const passEl = document.getElementById('ellyLoginPass');
        if (passEl) {
            passEl.value = '';
        }
        showEllyToast('Signed out from your Vril One account.', 'info');
        await syncEllyAuthPanels();
        await refreshEllyPremiumLine(statusLine, keyBlock);
    });

    document.getElementById('ellyBtnReplaceApiKey')?.addEventListener('click', async () => {
        ellyKeyReveal.apiKey = true;
        await syncEllyKeyRowsVisibility();
    });
    document.getElementById('ellyBtnReplaceSecret')?.addEventListener('click', async () => {
        ellyKeyReveal.secret = true;
        await syncEllyKeyRowsVisibility();
    });

    saveKeys?.addEventListener('click', async () => {
        if (!(await getJwt())) {
            showEllyToast('Sign in above to encrypt & store keys.', 'warning');
            return;
        }
        const k = document.getElementById('ellyApiKeyPlain')?.value || '';
        const s = document.getElementById('ellyApiSecretPlain')?.value || '';
        if (!k && !s) {
            showEllyToast('Enter at least an API key or secret.', 'warning');
            return;
        }
        const out = {};
        if (k) {
            const pk = await protectPlaintext(k);
            if (!pk.ok) {
                showEllyToast(pk.error || 'Could not protect API key', 'warning');
                return;
            }
            out[EllyStorageKeys.encApiKey] = pk.protectedPayload;
        }
        if (s) {
            const ps = await protectPlaintext(s);
            if (!ps.ok) {
                showEllyToast(ps.error || 'Could not protect API secret', 'warning');
                return;
            }
            out[EllyStorageKeys.encSecret] = ps.protectedPayload;
        }
        await chrome.storage.local.set(out);
        showEllyToast('Encrypted credentials saved in this extension.', 'success');
        const pkInput = document.getElementById('ellyApiKeyPlain');
        const psInput = document.getElementById('ellyApiSecretPlain');
        if (pkInput) {
            pkInput.value = '';
        }
        if (psInput) {
            psInput.value = '';
        }
        ellyKeyReveal.apiKey = false;
        ellyKeyReveal.secret = false;
        await syncEllyKeyRowsVisibility();
    });

    modalClose?.addEventListener('click', () => {
        const dlg = document.getElementById('ellyUpgradeModal');
        if (dlg && typeof dlg.close === 'function') {
            dlg.close();
        }
    });

    await syncEllyAuthPanels();
    await syncEllyKeyRowsVisibility();
}

/**
 * @param {HTMLElement | null} statusLine
 * @param {HTMLElement | null} keyBlock
 */
async function refreshEllyPremiumLine(statusLine, keyBlock) {
    try {
        let jwt = await getJwt();
        if (keyBlock) {
            keyBlock.hidden = false;
        }
        if (!statusLine) {
            return;
        }
        if (!jwt) {
            statusLine.textContent = 'Not signed in. Sign in above to encrypt & store API keys.';
            return;
        }
        const pr = await fetchAmIPremium();
        jwt = await getJwt();
        if (keyBlock) {
            keyBlock.hidden = false;
        }
        if (!jwt) {
            statusLine.textContent = 'Session expired — sign in again.';
            return;
        }
        if (pr.error === 'not_logged_in') {
            statusLine.textContent = 'Not signed in.';
            return;
        }
        if (pr.error) {
            statusLine.textContent = `Could not load premium status: ${pr.error}`;
            return;
        }
        const r = /** @type {Record<string, unknown>} */ (pr.raw || {});
        const planLabel =
            (typeof r.tierDisplayName === 'string' && r.tierDisplayName.trim()) ||
            (typeof r.planKey === 'string' && r.planKey.trim()) ||
            'unknown';
        statusLine.textContent = pr.isPremium
            ? `Premium active — plan: ${planLabel}${r.isPastDue ? ' (payment past due — update billing on Vrilsoft)' : ''}.`
            : 'Signed in — no active premium plan detected for this account (cart purchase or Stripe subscription).';
    } finally {
        await syncEllyAuthPanels();
        await syncEllyKeyRowsVisibility();
    }
}

/**
 * @param {number} n
 * @returns {number}
 */
function clampPinBoxes(n) {
    return Math.max(1, Math.min(12, Number.isFinite(n) ? n : 4));
}

/**
 * Opens the reusable Tabler-style PIN modal and returns entered digits.
 * @param {{ title: string, message: string, pinLength: number, minLength?: number, maxLength?: number, confirmText?: string }} opts
 * @returns {Promise<string|null>}
 */
async function promptPinWithModal(opts) {
    const dialog = /** @type {HTMLDialogElement|null} */ (document.getElementById('ellyPinPromptModal'));
    const titleEl = document.getElementById('ellyPinPromptTitle');
    const msgEl = document.getElementById('ellyPinPromptText');
    const wrap = document.getElementById('ellyPinPromptInputs');
    const errEl = document.getElementById('ellyPinPromptError');
    const btnCancel = document.getElementById('ellyPinPromptCancel');
    const btnConfirm = document.getElementById('ellyPinPromptConfirm');
    if (!dialog || !titleEl || !msgEl || !wrap || !errEl || !btnCancel || !btnConfirm) {
        return null;
    }

    const pinLen = clampPinBoxes(opts.pinLength);
    const minLen = Math.max(1, Math.min(pinLen, opts.minLength ?? pinLen));
    const maxLen = Math.max(minLen, Math.min(pinLen, opts.maxLength ?? pinLen));
    titleEl.innerHTML = `<i class="ti ti-key"></i> ${opts.title}`;
    msgEl.textContent = opts.message;
    btnConfirm.innerHTML = opts.confirmText
        ? `<i class="ti ti-check"></i> ${opts.confirmText}`
        : '<i class="ti ti-check"></i> Confirm';
    errEl.hidden = true;
    errEl.textContent = '';
    wrap.innerHTML = '';
    wrap.style.setProperty('--pin-len', String(pinLen));

    /**
     * @returns {HTMLInputElement[]}
     */
    function inputs() {
        return Array.from(wrap.querySelectorAll('.elly-pin-digit'));
    }

    /**
     * @returns {string}
     */
    function readPin() {
        return inputs().map(i => i.value || '').join('');
    }

    /**
     * @param {number} fromIndex
     * @param {string} chars
     */
    function applyChars(fromIndex, chars) {
        const list = inputs();
        const clean = (chars || '').replace(/\D/g, '');
        if (!clean) {
            return;
        }
        let idx = fromIndex;
        for (const ch of clean) {
            if (idx >= list.length) {
                break;
            }
            list[idx].value = ch;
            idx++;
        }
        const next = list[Math.min(idx, list.length - 1)];
        next?.focus();
        next?.select();
    }

    for (let i = 0; i < pinLen; i++) {
        const box = document.createElement('input');
        box.type = 'password';
        box.inputMode = 'numeric';
        box.autocomplete = 'off';
        box.maxLength = 1;
        box.className = 'elly-pin-digit';
        box.setAttribute('aria-label', `PIN digit ${i + 1}`);
        box.addEventListener('keydown', (e) => {
            if (e.key === 'Backspace') {
                e.preventDefault();
                box.value = '';
                const list = inputs();
                const prev = list[i - 1];
                if (prev) {
                    prev.value = '';
                    prev.focus();
                    prev.select();
                }
                return;
            }
            if (e.key === 'ArrowLeft') {
                e.preventDefault();
                const prev = inputs()[i - 1];
                prev?.focus();
                prev?.select();
                return;
            }
            if (e.key === 'ArrowRight') {
                e.preventDefault();
                const next = inputs()[i + 1];
                next?.focus();
                next?.select();
                return;
            }
            if (e.key.length === 1 && !/\d/.test(e.key)) {
                e.preventDefault();
            }
        });
        box.addEventListener('input', () => {
            const val = (box.value || '').replace(/\D/g, '');
            if (!val) {
                box.value = '';
                return;
            }
            if (val.length > 1) {
                box.value = '';
                applyChars(i, val);
                return;
            }
            box.value = val[0];
            const next = inputs()[i + 1];
            if (next) {
                next.focus();
                next.select();
            }
        });
        box.addEventListener('paste', (e) => {
            e.preventDefault();
            const text = e.clipboardData?.getData('text') || '';
            applyChars(i, text);
        });
        wrap.appendChild(box);
    }

    const first = inputs()[0];
    const settle = () => {
        first?.focus();
        first?.select();
    };

    return await new Promise((resolve) => {
        let settled = false;
        const finish = (value) => {
            if (settled) return;
            settled = true;
            cleanup();
            if (dialog.open) {
                dialog.close();
            }
            resolve(value);
        };
        const onCancelBtn = () => finish(null);
        const onDialogCancel = (e) => {
            e.preventDefault();
            finish(null);
        };
        const onConfirm = () => {
            const entered = readPin();
            if (entered.length < minLen || entered.length > maxLen || !/^\d+$/.test(entered)) {
                errEl.textContent = minLen === maxLen
                    ? `Enter all ${pinLen} digits to continue.`
                    : `Enter ${minLen}–${maxLen} digits to continue.`;
                errEl.hidden = false;
                return;
            }
            errEl.hidden = true;
            finish(entered);
        };
        const onWrapKeyDown = (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                onConfirm();
            }
        };
        const cleanup = () => {
            btnCancel.removeEventListener('click', onCancelBtn);
            btnConfirm.removeEventListener('click', onConfirm);
            dialog.removeEventListener('cancel', onDialogCancel);
            wrap.removeEventListener('keydown', onWrapKeyDown);
        };

        btnCancel.addEventListener('click', onCancelBtn);
        btnConfirm.addEventListener('click', onConfirm);
        dialog.addEventListener('cancel', onDialogCancel);
        wrap.addEventListener('keydown', onWrapKeyDown);

        if (!dialog.open) {
            dialog.showModal();
        }
        requestAnimationFrame(settle);
    });
}

/**
 * @param {string} promptMessage
 * @param {string} wrongPinToast
 * @returns {Promise<string|null>} Entered PIN on success; null if cancelled or wrong.
 */
async function requireParentPin(promptMessage, wrongPinToast) {
    const entered = await promptPinWithModal({
        title: 'Parent PIN Required',
        message: promptMessage,
        pinLength: PARENT_PIN_MAX_LEN,
        minLength: PARENT_PIN_MIN_LEN,
        maxLength: PARENT_PIN_MAX_LEN,
        confirmText: 'Verify'
    });
    if (entered == null) {
        return null;
    }
    const trimmed = normalizePinDigits(entered);
    if (!isValidParentPin(trimmed) || isForbiddenParentPin(trimmed)) {
        showEllyToast(wrongPinToast, 'warning');
        return null;
    }
    try {
        const verified = await chrome.runtime.sendMessage({
            action: 'verifyParentPinForBlocklist',
            pin: trimmed
        });
        if (!verified?.success) {
            showEllyToast(verified?.error || wrongPinToast, 'warning');
            return null;
        }
        return trimmed;
    } catch {
        showEllyToast(wrongPinToast, 'warning');
        return null;
    }
}

/**
 * @param {string} promptMessage
 * @param {string} wrongPinToast
 * @returns {Promise<boolean>}
 */
async function requireParentPinOk(promptMessage, wrongPinToast) {
    const pin = await requireParentPin(promptMessage, wrongPinToast);
    return pin != null;
}

/**
 * @returns {Promise<boolean>}
 */
async function requireParentPinForPremiumPanel() {
    return requireParentPinOk(
        'Enter your parent override PIN to open Premium & API settings.',
        'Incorrect parent PIN.'
    );
}

/**
 * @param {Event} e
 * @returns {Promise<void>}
 */
async function onBlocklistPinLockChange(e) {
    const el = /** @type {HTMLInputElement} */ (e.target);
    const data = await chrome.storage.sync.get(['settings']);
    const settings = { ...(data.settings || {}) };

    if (settings.strictMode) {
        el.checked = true;
        showEllyToast('Strict Mode keeps PIN lock on. Turn off Strict Mode first.', 'warning');
        return;
    }

    if (!el.checked) {
        const pin = await requireParentPin(
            'Enter your Parent PIN to disable Blocklist PIN lock.',
            'Incorrect Parent PIN. PIN lock stays on.'
        );
        if (pin == null) {
            el.checked = true;
            return;
        }
        sessionStorage.removeItem(BLOCKLIST_SESSION_KEY);
        const response = await chrome.runtime.sendMessage({
            action: 'updateSecuritySettings',
            pin,
            settings: { blocklistPinLock: false }
        });
        if (!response?.success) {
            el.checked = true;
            showEllyToast(response?.error || 'Could not authorize that change. Try again.', 'warning');
            return;
        }
        updateSecurityToggleUi({ ...settings, blocklistPinLock: false });
        await updateHeaderLockButton();
        showEllyToast('Blocklist PIN lock disabled.', 'success');
        return;
    }

    const response = await chrome.runtime.sendMessage({
        action: 'updateSecuritySettings',
        settings: { blocklistPinLock: true }
    });
    if (!response?.success) {
        el.checked = false;
        showEllyToast(response?.error || 'Could not enable PIN lock.', 'warning');
        return;
    }
    sessionStorage.removeItem(BLOCKLIST_SESSION_KEY);
    if (document.getElementById('blocklist-tab')?.classList.contains('active')) {
        switchTab('settings');
    }
    updateSecurityToggleUi({ ...settings, blocklistPinLock: true });
    await updateHeaderLockButton();
    showEllyToast('Blocklist PIN lock enabled.', 'success');
}

/**
 * @param {Event} e
 * @returns {Promise<void>}
 */
async function onStrictModeChange(e) {
    const el = /** @type {HTMLInputElement} */ (e.target);
    const data = await chrome.storage.sync.get(['settings']);
    const settings = { ...(data.settings || {}) };

    if (!el.checked) {
        const pin = await requireParentPin(
            'Enter your Parent PIN to turn off Strict Mode.',
            'Incorrect Parent PIN. Strict Mode stays on.'
        );
        if (pin == null) {
            el.checked = true;
            return;
        }
        const response = await chrome.runtime.sendMessage({
            action: 'updateSecuritySettings',
            pin,
            settings: { strictMode: false }
        });
        if (!response?.success) {
            el.checked = true;
            showEllyToast(response?.error || 'Could not authorize that change. Try again.', 'warning');
            return;
        }
        updateSecurityToggleUi({ ...settings, strictMode: false });
        await updateHeaderLockButton();
        showEllyToast('Strict Mode disabled.', 'success');
        return;
    }

    const response = await chrome.runtime.sendMessage({
        action: 'updateSecuritySettings',
        settings: { strictMode: true, blocklistPinLock: true }
    });
    if (!response?.success) {
        el.checked = false;
        showEllyToast(response?.error || 'Could not enable Strict Mode.', 'warning');
        return;
    }
    sessionStorage.removeItem(BLOCKLIST_SESSION_KEY);
    if (document.getElementById('blocklist-tab')?.classList.contains('active')) {
        switchTab('settings');
    }
    updateSecurityToggleUi({ ...settings, strictMode: true, blocklistPinLock: true });
    await updateHeaderLockButton();
    showEllyToast('Strict Mode enabled. Temporary overrides are off; Blocklist stays PIN-locked.', 'success');
}

async function saveSchedule(cardElement) {
    const allow = await requireEllySchedulePremiumOrBlock();
    if (!allow) {
        return;
    }

    const dataPre = await chrome.storage.sync.get(['settings']);
    if (isBlocklistPinLockActive(dataPre.settings)) {
        const pinOk = await requireParentPinOk(
            'Enter your Parent PIN to save this schedule.',
            'Incorrect Parent PIN. Schedule was not saved.'
        );
        if (!pinOk) {
            return;
        }
    }

    const scheduleId = cardElement.dataset.id;
    const name = cardElement.querySelector('.schedule-name').value;
    const enabled = cardElement.querySelector('.schedule-enabled').checked;
    const startTime = cardElement.querySelector('.schedule-start').value;
    const endTime = cardElement.querySelector('.schedule-end').value;
    const type = cardElement.querySelector(`input[name="type_${scheduleId}"]:checked`).value;
    const urls = cardElement.querySelector('.schedule-urls').value
        .split('\n')
        .map(url => url.trim())
        .filter(url => url);

    const days = Array.from(cardElement.querySelectorAll('.day-btn.active'))
        .map(btn => parseInt(btn.dataset.day));

    const schedule = {
        id: scheduleId,
        name,
        enabled,
        startTime,
        endTime,
        type,
        days: days.length > 0 ? days : [0, 1, 2, 3, 4, 5, 6],
        [type === 'block' ? 'allowedSites' : 'blockedSites']: urls
    };

    const data = await chrome.storage.sync.get(['schedules']);
    const schedules = data.schedules || [];
    const index = schedules.findIndex(s => s.id === scheduleId);

    if (index >= 0) {
        schedules[index] = schedule;
    } else {
        schedules.push(schedule);
    }

    await chrome.storage.sync.set({ schedules });
    showEllyToast('Schedule saved.', 'success');
}

async function deleteSchedule(scheduleId) {
    const ok = await openEllyConfirmModal({
        title: 'Delete schedule',
        message: 'Are you sure you want to delete this schedule?',
        confirmLabel: 'Delete',
        danger: true
    });
    if (!ok) {
        return;
    }
    const data = await chrome.storage.sync.get(['schedules', 'settings']);
    if (isBlocklistPinLockActive(data.settings)) {
        const pinOk = await requireParentPinOk(
            'Enter your Parent PIN to delete this schedule.',
            'Incorrect Parent PIN. Schedule was not deleted.'
        );
        if (!pinOk) {
            return;
        }
    }
    const schedules = (data.schedules || []).filter(s => s.id !== scheduleId);
    await chrome.storage.sync.set({ schedules });
    loadSchedules(schedules);
}

function loadSettings(settings) {
    // Security — never display stored Parent PIN in cleartext
    const overrideEl = /** @type {HTMLInputElement|null} */ (document.getElementById('overridePin'));
    if (overrideEl) {
        overrideEl.value = '';
        overrideEl.placeholder = 'unchanged';
    }
    updateSecurityToggleUi(settings);

    // Behavior
    document.getElementById('autoCloseTabs').checked = settings.autoCloseTabs !== false;
    document.getElementById('showNotifications').checked = settings.showNotifications !== false;
    document.getElementById('allowSearchEngineResults').checked = settings.allowSearchEngineResults !== false;
    const themeVal = settings.theme === 'dark' || settings.theme === 'blue' || settings.theme === 'light'
        ? settings.theme
        : 'light';
    document.getElementById('themeSelect').value = themeVal;

    // Focus sessions
    document.getElementById('pomodoroMinutes').value = settings.pomodoroMinutes || 25;
    document.getElementById('breakMinutes').value = settings.breakMinutes || 5;

    // Sync frequency
    const syncVal = settings.syncFrequency === 'hourly' || settings.syncFrequency === 'daily' || settings.syncFrequency === 'instant'
        ? settings.syncFrequency
        : 'instant';
    document.getElementById('syncFrequency').value = syncVal;

    updateBlockThemePreview();
    updateSyncFrequencyHint();
}

function updateFocusSessionUI(focusSessions) {
    const activeSession = focusSessions.find(s => s.active);
    if (activeSession) {
        document.getElementById('startFocus').textContent =
            `Focus Session Active (${activeSession.duration} min)`;
        document.getElementById('startFocus').disabled = true;
    }
}

function setupEventListeners() {
    // Blocklist management
    document.getElementById('addBlockUrl').addEventListener('click', addBlockUrl);
    document.getElementById('newBlockUrl').addEventListener('keypress', (e) => {
        if (e.key === 'Enter') addBlockUrl();
    });

    // Whitelist management
    document.getElementById('addWhiteUrl').addEventListener('click', addWhiteUrl);
    document.getElementById('newWhiteUrl').addEventListener('keypress', (e) => {
        if (e.key === 'Enter') addWhiteUrl();
    });

    // Time limits
    document.getElementById('addLimit').addEventListener('click', addTimeLimit);

    // Preset buttons
    document.querySelectorAll('.preset-btn').forEach(btn => {
        btn.addEventListener('click', (e) => {
            const preset = e.currentTarget?.dataset?.preset;
            if (preset) {
                applyPreset(preset);
            }
        });
    });

    // Add new schedule
    document.getElementById('addSchedule').addEventListener('click', addNewSchedule);

    // Settings
    document.getElementById('updatePin').addEventListener('click', updatePin);
    document.getElementById('resetAccessPin')?.addEventListener('click', () => {
        changeAccessPinFromSecurity().catch((err) => console.error(err));
    });
    document.getElementById('accessPinSetupSave')?.addEventListener('click', () => {
        saveAccessPinSetupFromModal().catch((err) => console.error(err));
    });
    document.getElementById('accessPinUnlockSubmit')?.addEventListener('click', () => {
        unlockAccessPinFromModal().catch((err) => console.error(err));
    });
    document.getElementById('accessPinUnlockInput')?.addEventListener('keydown', (e) => {
        if (e.key === 'Enter') {
            unlockAccessPinFromModal().catch((err) => console.error(err));
        }
    });
    document.getElementById('blocklistPinLock')?.addEventListener('change', (e) => {
        onBlocklistPinLockChange(e).catch((err) => console.error(err));
    });
    document.getElementById('strictMode')?.addEventListener('change', (e) => {
        onStrictModeChange(e).catch((err) => console.error(err));
    });
    document.getElementById('btnHeaderLock')?.addEventListener('click', () => {
        onHeaderLockButtonClick().catch((err) => console.error(err));
    });
    document.getElementById('exportData').addEventListener('click', () => exportData());
    document.getElementById('importData').addEventListener('click', () => importData());
    document.getElementById('resetData').addEventListener('click', () => resetData());
    document.getElementById('startFocus').addEventListener('click', startFocusSession);

    document.getElementById('unlockDataMgmt')?.addEventListener('click', async () => {
        const pin = await requireParentPin(
            `Enter your parent PIN (${PARENT_PIN_MIN_LEN}–${PARENT_PIN_MAX_LEN} digits, same as Security → Parent PIN).`,
            'Your parental Pin is required to unlock Data Management.'
        );
        if (pin != null) {
            sessionStorage.setItem(DM_SESSION_KEY, '1');
            await grantBlocklistMutationAuth(pin);
            updateDataMgmtLockUI();
            await updateHeaderLockButton();
        }
    });

    document.getElementById('initialPinSave')?.addEventListener('click', () => saveInitialParentPin());

    document.getElementById('themeSelect')?.addEventListener('change', () => {
        updateBlockThemePreview();
        saveAllSettings();
    });

    document.getElementById('syncFrequency')?.addEventListener('change', () => {
        updateSyncFrequencyHint();
        saveAllSettings();
    });

    // Statistics
    document.getElementById('exportStats').addEventListener('click', exportStatistics);
    document.getElementById('clearStats').addEventListener('click', clearStatistics);

    // Save all
    document.getElementById('saveAll').addEventListener('click', saveAllSettings);
}

async function addBlockUrl() {
    const input = document.getElementById('newBlockUrl');
    const url = input.value.trim();

    if (!url) {
        await openEllyAlertModal({ title: 'Blocklist', message: 'Please enter a URL' });
        return;
    }

    const data = await chrome.storage.sync.get(['blockedSites']);
    const blockedSites = data.blockedSites || [];

    if (!blockedSites.includes(url)) {
        blockedSites.push(url);
        await chrome.storage.sync.set({ blockedSites });
        loadBlocklist(blockedSites);
        input.value = '';
    } else {
        await openEllyAlertModal({ title: 'Blocklist', message: 'This URL is already in the blocklist' });
    }
}

async function addWhiteUrl() {
    const input = document.getElementById('newWhiteUrl');
    const url = input.value.trim();

    if (!url) {
        await openEllyAlertModal({ title: 'Whitelist', message: 'Please enter a URL' });
        return;
    }

    const ok = await ensureBlocklistAccess(
        'Enter your Parent PIN to add a whitelist bypass (it skips the Blocklist).',
        'Incorrect Parent PIN. Whitelist was not changed.'
    );
    if (!ok) {
        return;
    }

    const data = await chrome.storage.sync.get(['whitelist']);
    const whitelist = data.whitelist || [];

    if (!whitelist.includes(url)) {
        whitelist.push(url);
        await chrome.storage.sync.set({ whitelist });
        loadWhitelist(whitelist);
        input.value = '';
    } else {
        await openEllyAlertModal({ title: 'Whitelist', message: 'This URL is already in the whitelist' });
    }
}

async function addTimeLimit() {
    const urlInput = document.getElementById('limitUrl');
    const minutesInput = document.getElementById('limitMinutes');

    const domain = urlInput.value.trim();
    const minutes = parseInt(minutesInput.value);

    if (!domain || isNaN(minutes) || minutes < 1) {
        await openEllyAlertModal({
            title: 'Time limits',
            message: 'Please enter a valid domain and time limit'
        });
        return;
    }

    const data = await chrome.storage.sync.get(['limitedSites', 'settings']);
    if (isBlocklistPinLockActive(data.settings)) {
        const pinOk = await requireParentPinOk(
            'Enter your Parent PIN to change time limits.',
            'Incorrect Parent PIN. Time limit was not saved.'
        );
        if (!pinOk) {
            return;
        }
    }
    const limitedSites = data.limitedSites || {};

    limitedSites[domain] = {
        limitMinutes: minutes,
        added: new Date().toISOString()
    };

    await chrome.storage.sync.set({ limitedSites });
    loadTimeLimits(limitedSites);

    urlInput.value = '';
    minutesInput.value = '';
}

async function applyPreset(preset) {
    const presets = typeof window !== 'undefined' && window.ELLY_PRESETS
        ? window.ELLY_PRESETS
        : {
            social: [
                '*.facebook.com', '*.twitter.com', '*.x.com', '*.instagram.com', '*.tiktok.com', '*.reddit.com',
                '*.linkedin.com', '*.discord.com', 'discord.com', 'vrilchat.com', '*.vrilchat.com', 'vrilone.com', '*.vrilone.com',
            ],
            entertainment: ['*.youtube.com', '*.netflix.com', '*.twitch.tv', '*.hulu.com', '*.disneyplus.com'],
            shopping: ['*.amazon.com', '*.ebay.com', '*.aliexpress.com', '*.etsy.com', '*.bestbuy.com'],
            gaming: ['*.steampowered.com', '*.epicgames.com', '*.xbox.com', '*.playstation.com'],
            news: ['*.cnn.com', '*.foxnews.com', '*.nytimes.com', '*.bbc.com', '*.reuters.com'],
            prohibited: [],
        };

    const presetUrls = presets[preset];
    if (!presetUrls) return;

    const data = await chrome.storage.sync.get(['blockedSites']);
    const blockedSites = data.blockedSites || [];
    const newUrls = presetUrls.filter(url => !blockedSites.includes(url));

    if (newUrls.length > 0) {
        blockedSites.push(...newUrls);
        await chrome.storage.sync.set({ blockedSites });
        loadBlocklist(blockedSites);
        await openEllyAlertModal({
            title: 'Preset applied',
            message: `Added ${newUrls.length} ${preset} sites to blocklist`
        });
    } else {
        await openEllyAlertModal({
            title: 'Preset',
            message: `All ${preset} sites are already in your blocklist`
        });
    }
}

function addNewSchedule() {
    const schedulesList = document.getElementById('schedulesList');
    const newSchedule = {
        id: Date.now().toString(),
        name: 'New Schedule',
        enabled: true,
        startTime: '09:00',
        endTime: '17:00',
        type: 'block',
        days: [1, 2, 3, 4, 5], // Weekdays by default
        allowedSites: []
    };

    const scheduleCard = createScheduleCard(newSchedule);
    schedulesList.appendChild(scheduleCard);
}

async function updatePin() {
    const newPin = normalizePinDigits(document.getElementById('overridePin')?.value);

    if (!newPin) {
        showEllyToast('Enter a new Parent PIN, or leave blank to keep the current one.', 'warning');
        return;
    }

    if (!isAcceptableNewParentPin(newPin)) {
        showEllyToast(
            isForbiddenParentPin(newPin)
                ? `Choose a PIN other than the default ${LEGACY_DEFAULT_PARENT_PIN}.`
                : `PIN must be ${PARENT_PIN_MIN_LEN}-${PARENT_PIN_MAX_LEN} digits (numbers only).`,
            'warning'
        );
        return;
    }

    let hasUsable = false;
    try {
        const status = await chrome.runtime.sendMessage({ action: 'getAppStatus' });
        hasUsable = !!status?.parentPinConfigured;
    } catch {
        hasUsable = false;
    }

    let currentPin = '';
    if (hasUsable) {
        const entered = await requireParentPin(
            'Enter your current Parent PIN to set a new one.',
            'Incorrect Parent PIN. Parent PIN was not changed.'
        );
        if (entered == null) {
            return;
        }
        currentPin = entered;
    }

    try {
        const response = await chrome.runtime.sendMessage({
            action: 'setParentPin',
            currentPin,
            newPin
        });
        if (!response?.success) {
            showEllyToast(response?.error || 'Could not update Parent PIN.', 'warning');
            return;
        }
    } catch (err) {
        showEllyToast(err?.message || 'Could not update Parent PIN.', 'warning');
        return;
    }

    await chrome.storage.local.set({ [LOCAL_PARENT_PIN_KEY]: true });
    const pinField = /** @type {HTMLInputElement|null} */ (document.getElementById('overridePin'));
    if (pinField) {
        pinField.value = '';
        pinField.placeholder = 'unchanged';
    }
    showEllyToast('Parent PIN updated successfully.', 'success');
}

async function exportData() {
    if (!(await assertDataMgmtUnlocked())) {
        return;
    }
    chrome.storage.sync.get(null, (data) => {
        const safe = stripSecretsFromStorageSnapshot(data);
        const json = JSON.stringify(safe, null, 2);
        const blob = new Blob([json], { type: 'application/json' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = `nanny-settings-${new Date().toISOString().split('T')[0]}.json`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    });
}

async function importData() {
    if (!(await assertDataMgmtUnlocked())) {
        return;
    }
    const input = document.createElement('input');
    input.type = 'file';
    input.accept = '.json';

    input.onchange = async (e) => {
        const file = e.target.files[0];
        if (!file) return;

        try {
            const text = await file.text();
            const data = JSON.parse(text);

            if (importContainsPlaintextParentPin(data)) {
                await openEllyAlertModal({
                    title: 'Import refused',
                    message: 'This file contains a plaintext Parent PIN. Remove it and export again from a current Elly build, or set the Parent PIN manually after importing a clean file.'
                });
                return;
            }

            const proceed = await openEllyConfirmModal({
                title: 'Import settings',
                message: 'This will overwrite all current settings (PIN hashes and JWT are never imported). Continue?',
                confirmLabel: 'Import',
                danger: true
            });
            if (!proceed) {
                return;
            }

            const response = await chrome.runtime.sendMessage({
                action: 'importSettings',
                data: stripSecretsFromStorageSnapshot(data)
            });
            if (!response?.success) {
                await openEllyAlertModal({
                    title: 'Import failed',
                    message: response?.error || 'Error importing settings.'
                });
                return;
            }
            sessionStorage.removeItem(BLOCKLIST_SESSION_KEY);
            await loadAllData();
            await enforceBlocklistTabOnLoad();
            await openEllyAlertModal({
                title: 'Import complete',
                message: 'Settings imported successfully!'
            });
        } catch (error) {
            await openEllyAlertModal({
                title: 'Import failed',
                message: 'Error importing settings. Invalid file format.'
            });
        }
    };

    input.click();
}

async function resetData() {
    if (!(await assertDataMgmtUnlocked())) {
        return;
    }
    const ok = await openEllyConfirmModal({
        title: 'Reset all data',
        message: 'This will reset ALL settings and statistics. This cannot be undone!',
        confirmLabel: 'Reset everything',
        danger: true
    });
    if (ok) {
        const defaultSettings = {
            blockedSites: Array.isArray(window.ELLY_DEFAULT_BLOCKED_SITES)
                ? [...window.ELLY_DEFAULT_BLOCKED_SITES]
                : [],
            limitedSites: {},
            schedules: [],
            settings: {
                strictMode: true,
                blocklistPinLock: true,
                autoCloseTabs: true,
                showNotifications: true,
                theme: "light",
                allowSearchEngineResults: true,
                syncFrequency: "instant"
            },
            statistics: {},
            ellyBlockTallyByHost: {},
            whitelist: [],
            focusSessions: []
        };

        await grantBlocklistMutationAuth();
        await chrome.storage.sync.set(defaultSettings);
        await chrome.storage.local.remove([LOCAL_PARENT_PIN_KEY]);
        sessionStorage.removeItem(DM_SESSION_KEY);
        sessionStorage.removeItem(BLOCKLIST_SESSION_KEY);
        await loadAllData();
        await enforceBlocklistTabOnLoad();
        await openEllyAlertModal({
            title: 'Reset complete',
            message:
                'All settings have been reset to defaults (Strict Mode and PIN lock on). Set a new Parent PIN under Security. Your Access PIN is unchanged — change it under Security if needed.'
        });
    }
}

async function startFocusSession() {
    const minutes = parseInt(document.getElementById('pomodoroMinutes').value);
    const breakMinutes = parseInt(document.getElementById('breakMinutes').value);

    const data = await chrome.storage.sync.get(['focusSessions']);
    const sessions = data.focusSessions || [];

    sessions.push({
        start: new Date().toISOString(),
        duration: minutes,
        break: breakMinutes,
        active: true
    });

    await chrome.storage.sync.set({ focusSessions: sessions });
    updateFocusSessionUI(sessions);

    // Notify background script
    chrome.runtime.sendMessage({
        action: 'startFocusSession',
        duration: minutes
    });

    showEllyToast(`Focus session started for ${minutes} minutes!`, 'success');

    // Timer to end session
    setTimeout(async () => {
        const data = await chrome.storage.sync.get(['focusSessions']);
        const updatedSessions = data.focusSessions || [];
        const currentSession = updatedSessions.find(s => s.active);

        if (currentSession) {
            currentSession.active = false;
            currentSession.end = new Date().toISOString();
            await chrome.storage.sync.set({ focusSessions: updatedSessions });
            updateFocusSessionUI(updatedSessions);

            await openEllyAlertModal({
                title: 'Focus session ended',
                message: `Focus session ended! Take a ${breakMinutes} minute break.`
            });

            // Start break timer
            setTimeout(async () => {
                await openEllyAlertModal({
                    title: 'Break over',
                    message: 'Break time is over! Ready for another session?'
                });
            }, breakMinutes * 60000);
        }
    }, minutes * 60000);
}

/** Min interval between successful quiet-stats POSTs when opening the Stats tab (ms). */
const QUIET_STATS_TAB_COOLDOWN_MS = 6 * 60 * 60 * 1000;

/**
 * If signed in, occasionally POST stats so the server is not only updated on the daily alarm.
 * @returns {Promise<void>}
 */
async function maybePostQuietStatsAfterStatsTabLoad() {
    try {
        const jwt = await getJwt();
        if (!jwt) {
            return;
        }
        const { ellyLastQuietStatsPostAt } = await chrome.storage.local.get(['ellyLastQuietStatsPostAt']);
        const now = Date.now();
        if (ellyLastQuietStatsPostAt && now - ellyLastQuietStatsPostAt < QUIET_STATS_TAB_COOLDOWN_MS) {
            return;
        }
        const ok = await tryPostQuietStatsReport();
        if (ok) {
            await chrome.storage.local.set({ ellyLastQuietStatsPostAt: now });
        }
    } catch {
        /* ignore */
    }
}

/**
 * @param {string} s
 * @returns {string}
 */
function escapeHtmlForUi(s) {
    const d = document.createElement('div');
    d.textContent = s;
    return d.innerHTML;
}

async function loadStatistics() {
    const data = await chrome.storage.sync.get(['statistics', 'ellyBlockTallyByHost']);
    const stats = data.statistics || {};

    // Calculate totals
    const totalBlocks = Object.values(stats).reduce((sum, day) => sum + (day.blocks || 0), 0);
    const totalTimeSaved = Object.values(stats).reduce((sum, day) => sum + (day.timeSaved || 0), 0);
    const daysActive = Object.keys(stats).length;

    document.getElementById('totalBlocks').textContent = totalBlocks;
    document.getElementById('totalTimeSaved').textContent = `${Math.floor(totalTimeSaved / 3600)}h`;
    document.getElementById('daysActive').textContent = daysActive;

    // Productivity score (0-100%)
    const productivityScore = Math.min(100, totalBlocks * 0.5);
    document.getElementById('productivityScore').textContent = `${productivityScore}%`;

    // Create charts if Chart.js is loaded
    if (typeof Chart !== 'undefined') {
        createCharts(stats);
    }

    // Load top sites (live tallies from background; same source as POST /i/v1/e/s)
    loadTopSites(data.ellyBlockTallyByHost || {});

    void maybePostQuietStatsAfterStatsTabLoad();
}

function createCharts(stats) {
    const last7Days = getLast7Days();
    const dailyBlocksData = last7Days.map(day => stats[day]?.blocks || 0);
    const timeSavedData = last7Days.map(day => Math.floor((stats[day]?.timeSaved || 0) / 3600));

    // Daily Blocks Chart
    createOrUpdateChart('dailyBlocksChart', 'bar', {
        labels: last7Days.map(d => new Date(d).toLocaleDateString('en-US', { weekday: 'short' })),
        datasets: [{
            label: 'Blocks',
            data: dailyBlocksData,
            backgroundColor: 'rgba(102, 126, 234, 0.5)',
            borderColor: 'rgb(102, 126, 234)',
            borderWidth: 1
        }]
    }, {
        responsive: true,
        plugins: {
            legend: { display: false }
        }
    });

    // Time Saved Chart
    createOrUpdateChart('timeSavedChart', 'line', {
        labels: last7Days.map(d => new Date(d).toLocaleDateString('en-US', { weekday: 'short' })),
        datasets: [{
            label: 'Hours Saved',
            data: timeSavedData,
            borderColor: 'rgb(46, 213, 115)',
            backgroundColor: 'rgba(46, 213, 115, 0.1)',
            fill: true,
            tension: 0.4
        }]
    }, {
        responsive: true
    });
}

function getLast7Days() {
    const days = [];
    for (let i = 6; i >= 0; i--) {
        const date = new Date();
        date.setDate(date.getDate() - i);
        days.push(date.toDateString());
    }
    return days;
}

/**
 * @param {Record<string, number>} tally Per-host block counts (`ellyBlockTallyByHost` in sync storage).
 */
function loadTopSites(tally) {
    const topSitesList = document.getElementById('topSitesList');
    if (!topSitesList) {
        return;
    }
    const rows = getTopBlockedSitesForUi(tally, 10);
    if (rows.length === 0) {
        topSitesList.innerHTML =
            '<p class="text-secondary small mb-0">No per-site data yet. Counts appear as Elly blocks tabs by policy.</p>';
        return;
    }
    topSitesList.innerHTML = rows
        .map(
            (r, i) => `
    <div class="top-site">
      <span>${i + 1}. ${escapeHtmlForUi(r.host)}</span>
      <span>${r.count} blocks</span>
    </div>`
        )
        .join('');
}

function exportStatistics() {
    chrome.storage.sync.get(['statistics'], (data) => {
        const csv = convertToCSV(data.statistics);
        const blob = new Blob([csv], { type: 'text/csv' });
        const url = URL.createObjectURL(blob);

        const a = document.createElement('a');
        a.href = url;
        a.download = `nanny-stats-${new Date().toISOString().split('T')[0]}.csv`;
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        URL.revokeObjectURL(url);
    });
}

function convertToCSV(stats) {
    const rows = [['Date', 'Blocks', 'Time Saved (seconds)', 'Time Saved (hours)']];

    for (const [date, data] of Object.entries(stats)) {
        rows.push([
            date,
            data.blocks || 0,
            data.timeSaved || 0,
            Math.floor((data.timeSaved || 0) / 3600)
        ]);
    }

    return rows.map(row => row.join(',')).join('\n');
}

async function clearStatistics() {
    const ok = await requireParentPinOk(
        'Enter your parent override PIN to clear all statistics.',
        'Your parental Pin is required to remove stats.'
    );
    if (!ok) {
        return;
    }
    await chrome.storage.sync.set({ statistics: {}, ellyBlockTallyByHost: {} });
    loadStatistics();
    showEllyToast('Statistics cleared.', 'success');
}

async function saveAllSettings() {
    try {
        // Gather all settings from the UI
        const currentData = await chrome.storage.sync.get(['settings']);
        const prev = currentData.settings || {};

        // Security toggles are saved only via dedicated handlers (PIN-gated).
        // Preserve stored values here so auto-save cannot bypass Strict Mode / PIN lock.
        // Parent PIN: never write overridePIN from the field — use setParentPin / Update PIN only.
        const settings = {
            strictMode: !!prev.strictMode,
            blocklistPinLock: !!(prev.strictMode || prev.blocklistPinLock),
            autoCloseTabs: document.getElementById('autoCloseTabs').checked,
            showNotifications: document.getElementById('showNotifications').checked,
            allowSearchEngineResults: document.getElementById('allowSearchEngineResults').checked,
            theme: document.getElementById('themeSelect').value,
            pomodoroMinutes: parseInt(document.getElementById('pomodoroMinutes').value),
            breakMinutes: parseInt(document.getElementById('breakMinutes').value),
            syncFrequency: document.getElementById('syncFrequency').value
        };

        const updatedSettings = { ...prev, ...settings };
        // Never re-persist Parent PIN material from options auto-save.
        delete updatedSettings.overridePIN;
        delete updatedSettings.overridePinHash;
        if (typeof prev.overridePinHash === 'string' && prev.overridePinHash) {
            updatedSettings.overridePinHash = prev.overridePinHash;
        }
        await chrome.storage.sync.set({ settings: updatedSettings });
        updateSecurityToggleUi(updatedSettings);

        // Update last sync time
        document.getElementById('lastSync').textContent =
            `Last sync: ${new Date().toLocaleTimeString()}`;

        console.log('All settings saved');
    } catch (error) {
        console.error('Error saving settings:', error);
        await openEllyAlertModal({
            title: 'Save failed',
            message: 'Error saving settings. Please try again.'
        });
    }
}