CPH Multi-Site Submit Helper

Unified CPH auto-submit helper for Codeforces, AtCoder, and NowCoder.

Dovrai installare un'estensione come Tampermonkey, Greasemonkey o Violentmonkey per installare questo script.

You will need to install an extension such as Tampermonkey to install this script.

Dovrai installare un'estensione come Tampermonkey o Violentmonkey per installare questo script.

Dovrai installare un'estensione come Tampermonkey o Userscripts per installare questo script.

Dovrai installare un'estensione come ad esempio Tampermonkey per installare questo script.

Dovrai installare un gestore di script utente per installare questo script.

(Ho già un gestore di script utente, lasciamelo installare!)

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione come ad esempio Stylus per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

Dovrai installare un'estensione per la gestione degli stili utente per installare questo stile.

(Ho già un gestore di stile utente, lasciamelo installare!)

// ==UserScript==
// @name         CPH Multi-Site Submit Helper
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Unified CPH auto-submit helper for Codeforces, AtCoder, and NowCoder.
// @author       Modified by OpenAI
// @match        *://codeforces.com/*
// @match        *://codeforces.ml/*
// @match        https://atcoder.jp/*
// @match        https://ac.nowcoder.com/*
// @match        https://www.nowcoder.com/*
// @grant        GM_xmlhttpRequest
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_addStyle
// @grant        GM_openInTab
// @grant        unsafeWindow
// @connect      localhost
// @run-at       document-idle
// ==/UserScript==

(function() {
    'use strict';

    const CPH_SERVER_ENDPOINT = 'http://localhost:27121/getSubmit';
    const MAX_HISTORY_ENTRIES = 30;

    const SITE_KEYS = {
        codeforces: {
            submission: 'cph_multi_submission_codeforces',
            settings: 'cph_multi_settings_codeforces',
            history: 'cph_multi_history_codeforces'
        },
        atcoder: {
            submission: 'cph_multi_submission_atcoder',
            settings: 'cph_multi_settings_atcoder',
            history: 'cph_multi_history_atcoder'
        },
        nowcoder: {
            submission: 'cph_multi_submission_nowcoder',
            settings: 'cph_multi_settings_nowcoder',
            history: 'cph_multi_history_nowcoder'
        }
    };

    const DEFAULT_SETTINGS = {
        codeforces: {
            pollingEnabled: true,
            loopTimeout: 3000,
            debug: false,
            autoSubmit: true,
            submitDelay: 0
        },
        atcoder: {
            pollingEnabled: true,
            loopTimeout: 3000,
            debug: true,
            autoSubmit: true,
            submitDelay: 5,
            aceFillRetries: 3,
            languageMappings: JSON.stringify({
                cpp: { name: 'C++ (GCC)', cph_ids: [54, 50, 42, 61, 89, 91, 52], new_id: '6017', old_id: '5028' },
                python_pypy: { name: 'Python (PyPy)', cph_ids: [40, 41, 70, 31, 7], new_id: '6083', old_id: '5078' }
            }, null, 2)
        },
        nowcoder: {
            pollingEnabled: true,
            loopTimeout: 3000,
            debug: true,
            autoSubmit: true,
            submitDelay: 3,
            languageMappings: JSON.stringify({
                cpp: { name: 'C++', cph_ids: [54, 50, 42, 61, 89, 91, 52], nowcoder_text: 'C++(clang++18)', nowcoder_text_new: 'C++' },
                c: { name: 'C', cph_ids: [48, 49, 53], nowcoder_text: 'C(gcc 10)', nowcoder_text_new: 'C' },
                java: { name: 'Java', cph_ids: [62], nowcoder_text: 'Java', nowcoder_text_new: 'Java' },
                python3: { name: 'Python 3', cph_ids: [71, 70], nowcoder_text: 'pypy3', nowcoder_text_new: 'PyPy3' },
                python2: { name: 'Python 2', cph_ids: [69], nowcoder_text: 'Python2', nowcoder_text_new: 'Python2' },
                go: { name: 'Go', cph_ids: [60, 95], nowcoder_text: 'Go', nowcoder_text_new: 'Go' },
                csharp: { name: 'C#', cph_ids: [51], nowcoder_text: 'C#', nowcoder_text_new: 'C#' },
                js_node: { name: 'JavaScript', cph_ids: [63], nowcoder_text: 'JavaScript (Node)', nowcoder_text_new: 'JavaScript Node' },
                ts: { name: 'TypeScript', cph_ids: [74], nowcoder_text: 'TypeScript', nowcoder_text_new: 'TypeScript' },
                php: { name: 'PHP', cph_ids: [68], nowcoder_text: 'PHP', nowcoder_text_new: 'PHP' },
                rust: { name: 'Rust', cph_ids: [73], nowcoder_text: 'Rust', nowcoder_text_new: 'Rust' },
                kotlin: { name: 'Kotlin', cph_ids: [72], nowcoder_text: 'Kotlin', nowcoder_text_new: 'Kotlin' }
            }, null, 2)
        }
    };

    const currentSite = detectSite();
    if (!currentSite) {
        return;
    }

    const storageKeys = SITE_KEYS[currentSite];
    let settings = { ...DEFAULT_SETTINGS[currentSite] };
    let pollingIntervalId = null;

    function detectSite() {
        const host = window.location.hostname;
        if (host.includes('codeforces.com') || host.includes('codeforces.ml')) return 'codeforces';
        if (host.includes('atcoder.jp')) return 'atcoder';
        if (host.includes('nowcoder.com')) return 'nowcoder';
        return null;
    }

    function debugLog(...args) {
        if (!settings.debug) return;
        const tag = `[cph-${currentSite}]`;
        const message = args.map((arg) => typeof arg === 'object' ? JSON.stringify(arg) : String(arg)).join(' ');
        console.log(tag, message);
    }

    function escapeHtml(value) {
        return String(value)
            .replace(/&/g, '&')
            .replace(/</g, '&lt;')
            .replace(/>/g, '&gt;')
            .replace(/"/g, '&quot;')
            .replace(/'/g, '&#39;');
    }

    function wait(ms) {
        return new Promise((resolve) => setTimeout(resolve, ms));
    }

    function waitForElement(selector, timeout = 10000) {
        return new Promise((resolve, reject) => {
            const immediate = document.querySelector(selector);
            if (immediate) {
                resolve(immediate);
                return;
            }

            const observer = new MutationObserver(() => {
                const element = document.querySelector(selector);
                if (element) {
                    clearTimeout(timer);
                    observer.disconnect();
                    resolve(element);
                }
            });

            const timer = setTimeout(() => {
                observer.disconnect();
                reject(new Error(`Timeout waiting for ${selector}`));
            }, timeout);

            observer.observe(document.body, { childList: true, subtree: true });
        });
    }

    function normalizeUrl(url) {
        try {
            const parsed = new URL(url, window.location.origin);
            return `${parsed.origin}${parsed.pathname}`;
        } catch {
            return url;
        }
    }

    async function readJson(key, fallback) {
        try {
            const raw = await GM_getValue(key, null);
            if (!raw) return fallback;
            return JSON.parse(raw);
        } catch (error) {
            console.error(`[cph-${currentSite}] Failed to parse storage key ${key}:`, error);
            return fallback;
        }
    }

    async function writeJson(key, value) {
        await GM_setValue(key, JSON.stringify(value));
    }

    async function logSubmission(submissionData) {
        const history = await readJson(storageKeys.history, []);
        history.unshift({
            timestamp: new Date().toISOString(),
            problemName: submissionData.problemName || 'Unknown',
            url: submissionData.url,
            languageId: submissionData.languageId,
            sourceCode: submissionData.sourceCode,
            rawJson: JSON.stringify(submissionData, null, 2)
        });
        await writeJson(storageKeys.history, history.slice(0, MAX_HISTORY_ENTRIES));
    }

    async function loadSettings() {
        const saved = await readJson(storageKeys.settings, null);
        settings = saved ? { ...DEFAULT_SETTINGS[currentSite], ...saved } : { ...DEFAULT_SETTINGS[currentSite] };

        if (currentSite === 'nowcoder') {
            settings.languageMappings = DEFAULT_SETTINGS.nowcoder.languageMappings;
        }
    }

    async function saveSettings(newSettings) {
        settings = { ...settings, ...newSettings };
        await writeJson(storageKeys.settings, settings);
        startPolling();
    }

    function startPolling() {
        if (pollingIntervalId) {
            clearInterval(pollingIntervalId);
            pollingIntervalId = null;
        }

        if (!settings.pollingEnabled) {
            debugLog('Polling disabled.');
            return;
        }

        pollCphServer();
        pollingIntervalId = setInterval(pollCphServer, settings.loopTimeout);
        debugLog(`Polling every ${settings.loopTimeout}ms.`);
    }

    function taskBelongsToCurrentSite(data) {
        if (!data || !data.url) return false;
        if (currentSite === 'codeforces') return data.url.includes('codeforces.com') || data.url.includes('codeforces.ml');
        if (currentSite === 'atcoder') return data.url.includes('atcoder.jp');
        if (currentSite === 'nowcoder') return data.url.includes('nowcoder.com');
        return false;
    }

    function pollCphServer() {
        GM_xmlhttpRequest({
            method: 'GET',
            url: CPH_SERVER_ENDPOINT,
            headers: { 'cph-submit': 'true' },
            timeout: Math.max(1000, settings.loopTimeout - 500),
            onload: async (response) => {
                if (response.status !== 200) return;

                try {
                    const data = JSON.parse(response.responseText);
                    if (!data || data.empty || !taskBelongsToCurrentSite(data)) {
                        return;
                    }

                    debugLog('Received CPH task:', data.problemName || data.url);
                    await logSubmission(data);
                    await GM_setValue(storageKeys.submission, JSON.stringify(data));

                    const targetUrl = getSubmitUrlForSite(data.url);
                    const samePage = normalizeUrl(window.location.href) === normalizeUrl(targetUrl);
                    if (samePage) {
                        fillAndSubmitCurrentSite();
                    } else {
                        GM_openInTab(targetUrl, { active: true });
                    }
                } catch (error) {
                    debugLog('Failed to parse CPH response:', error);
                }
            },
            onerror: () => debugLog('Could not connect to CPH server.'),
            ontimeout: () => debugLog('CPH request timed out.')
        });
    }

    function getSubmitUrlForSite(problemUrl) {
        if (currentSite === 'codeforces') {
            const contestUrl = problemUrl.includes('/contest/');
            if (!contestUrl) return 'https://codeforces.com/problemset/submit';
            try {
                const parsed = new URL(problemUrl);
                const contestId = parsed.pathname.split('/')[2];
                return `https://codeforces.com/contest/${contestId}/submit`;
            } catch {
                return 'https://codeforces.com/problemset/submit';
            }
        }

        if (currentSite === 'atcoder') {
            try {
                const parsed = new URL(problemUrl);
                if (parsed.pathname.includes('/tasks/')) {
                    return `${parsed.origin}${parsed.pathname.split('/tasks/')[0]}/submit`;
                }
            } catch {
                return problemUrl;
            }
        }

        return problemUrl;
    }

    async function fillAndSubmitCurrentSite() {
        const raw = await GM_getValue(storageKeys.submission, null);
        if (!raw) return;
        await GM_setValue(storageKeys.submission, null);

        let data;
        try {
            data = JSON.parse(raw);
        } catch (error) {
            console.error(`[cph-${currentSite}] Invalid pending submission data:`, error);
            return;
        }

        if (currentSite === 'codeforces') {
            await fillCodeforces(data);
            return;
        }

        if (currentSite === 'atcoder') {
            await fillAtCoder(data);
            return;
        }

        await fillNowCoder(data);
    }

    async function fillCodeforces(data) {
        const langEl = document.querySelector('select[name="programTypeId"]');
        const sourceEl = document.querySelector('textarea[name="source"]');
        const submitBtn = document.querySelector('input.submit');

        if (!langEl || !sourceEl || !submitBtn) {
            console.error('[cph-codeforces] Missing submit form elements.');
            return;
        }

        sourceEl.value = data.sourceCode;
        sourceEl.dispatchEvent(new Event('input', { bubbles: true }));
        langEl.value = String(data.languageId);
        langEl.dispatchEvent(new Event('change', { bubbles: true }));

        if (data.url.includes('/contest/')) {
            const problemIndexEl = document.querySelector('select[name="submittedProblemIndex"]');
            if (problemIndexEl) {
                problemIndexEl.value = data.url.split('/problem/')[1];
                problemIndexEl.dispatchEvent(new Event('change', { bubbles: true }));
            }
        } else {
            const problemEl = document.querySelector('input[name="submittedProblemCode"]');
            if (problemEl) {
                problemEl.value = data.problemName;
                problemEl.dispatchEvent(new Event('input', { bubbles: true }));
            }
        }

        submitBtn.disabled = false;
        if (settings.autoSubmit) {
            setTimeout(() => submitBtn.click(), Math.max(0, settings.submitDelay) * 1000);
        }
    }

    function getAtCoderTargetLanguageId(cphLangId) {
        const visibleSelect = document.querySelector('#select-lang > div[style*="display: block"] select.form-control');
        if (!visibleSelect) return String(cphLangId);

        const firstOption = visibleSelect.querySelector('option[value]');
        if (!firstOption || !firstOption.value) return String(cphLangId);

        const isNewContest = parseInt(firstOption.value, 10) >= 6000;
        try {
            const mappings = JSON.parse(settings.languageMappings);
            for (const key of Object.keys(mappings)) {
                const mapping = mappings[key];
                if (mapping.cph_ids && mapping.cph_ids.includes(cphLangId)) {
                    return isNewContest ? mapping.new_id : mapping.old_id;
                }
            }
        } catch (error) {
            console.error('[cph-atcoder] Failed to parse language mappings:', error);
        }

        return String(cphLangId);
    }

    function fillAceEditorViaInjection(sourceCode) {
        const script = document.createElement('script');
        const scriptId = `cph-atcoder-inject-${Date.now()}`;
        script.id = scriptId;
        script.textContent = `
            (function() {
                const codeToFill = ${JSON.stringify(sourceCode)};
                let retries = ${Math.max(1, settings.aceFillRetries || 3)};
                const selfId = ${JSON.stringify(scriptId)};

                function cleanup() {
                    const self = document.getElementById(selfId);
                    if (self) self.remove();
                }

                function attempt() {
                    try {
                        if (window.ace) {
                            const editor = window.ace.edit('editor');
                            if (editor && editor.session) {
                                editor.session.setValue(codeToFill, 1);
                                cleanup();
                                return;
                            }
                        }
                    } catch (error) {
                        console.error('[cph-atcoder][inject]', error);
                    }

                    if (retries > 0) {
                        retries -= 1;
                        setTimeout(attempt, 500);
                        return;
                    }

                    const fallback = document.getElementById('plain-textarea');
                    if (fallback) {
                        fallback.value = codeToFill;
                        fallback.dispatchEvent(new Event('input', { bubbles: true }));
                    }
                    cleanup();
                }

                attempt();
            })();
        `;
        (document.head || document.documentElement).appendChild(script);
    }

    async function fillAtCoder(data) {
        const taskEl = document.querySelector('select[name="data.TaskScreenName"]');
        const submitBtn = document.getElementById('submit');
        if (!taskEl || !submitBtn) {
            console.error('[cph-atcoder] Missing submit form elements.');
            return;
        }

        const problemId = data.url.split('/tasks/')[1];
        if (!problemId) {
            console.error('[cph-atcoder] Could not parse problem ID from URL:', data.url);
            return;
        }

        taskEl.value = problemId;
        taskEl.dispatchEvent(new Event('change', { bubbles: true }));
        await wait(250);

        const langEl = document.querySelector('#select-lang > div[style*="display: block"] select[name="data.LanguageId"]');
        if (!langEl) {
            console.error('[cph-atcoder] Visible language selector not found.');
            return;
        }

        langEl.value = getAtCoderTargetLanguageId(data.languageId);
        langEl.dispatchEvent(new Event('change', { bubbles: true }));

        fillAceEditorViaInjection(data.sourceCode);

        submitBtn.disabled = false;
        if (settings.autoSubmit) {
            setTimeout(() => submitBtn.click(), Math.max(0, settings.submitDelay) * 1000);
        }
    }

    function isNowCoderNewEditor() {
        return Boolean(document.querySelector('.monaco-editor'));
    }

    function getNowCoderTargetLanguageText(cphLangId, isNewEditor) {
        try {
            const mappings = JSON.parse(settings.languageMappings);
            for (const key of Object.keys(mappings)) {
                const mapping = mappings[key];
                if (mapping.cph_ids && mapping.cph_ids.includes(cphLangId)) {
                    return isNewEditor ? mapping.nowcoder_text_new : mapping.nowcoder_text;
                }
            }
        } catch (error) {
            console.error('[cph-nowcoder] Failed to parse language mappings:', error);
        }
        return null;
    }

    function fillNowCoderMonaco(sourceCode) {
        const script = document.createElement('script');
        script.textContent = `
            (function() {
                const codeToFill = ${JSON.stringify(sourceCode)};
                try {
                    const editorApi = document.querySelector('.default-monaco-editor').__vue__.$refs.editor.getEditorApi();
                    if (editorApi && typeof editorApi.setValue === 'function') {
                        editorApi.setValue(codeToFill);
                        return;
                    }
                } catch (error) {
                    console.error('[cph-nowcoder][inject]', error);
                }

                if (window.monaco && typeof window.monaco.editor.getModels === 'function') {
                    const models = window.monaco.editor.getModels();
                    if (models.length > 0) {
                        models[0].setValue(codeToFill);
                    }
                }
            })();
        `;
        (document.head || document.documentElement).appendChild(script);
        script.remove();
    }

    function fillNowCoderCodeMirror(sourceCode) {
        const script = document.createElement('script');
        script.textContent = `
            (function() {
                const codeToFill = ${JSON.stringify(sourceCode)};
                const editor = document.querySelector('.CodeMirror');
                if (editor && editor.CodeMirror) {
                    editor.CodeMirror.setValue(codeToFill);
                    editor.CodeMirror.save();
                }
            })();
        `;
        (document.head || document.documentElement).appendChild(script);
        script.remove();
    }

    async function selectNowCoderLanguage(targetText) {
        if (!targetText) return;

        const newEditor = isNowCoderNewEditor();
        const trigger = newEditor
            ? document.querySelector('.monaco-toolbar-left .el-select')
            : document.querySelector('.btn-language');
        if (!trigger) {
            console.error('[cph-nowcoder] Language selector trigger not found.');
            return;
        }

        trigger.click();
        await wait(300);

        const options = Array.from(document.querySelectorAll('.el-select-dropdown__item span'));
        const exact = options.find((option) => option.textContent.trim() === targetText);
        const fuzzy = options.find((option) => option.textContent.trim().startsWith(targetText));
        const chosen = exact || fuzzy;

        if (!chosen) {
            console.error('[cph-nowcoder] Language not found:', targetText);
            return;
        }

        const item = chosen.closest('li.el-select-dropdown__item');
        if (item) {
            item.click();
            await wait(200);
        }
    }

    async function fillNowCoder(data) {
        const newEditor = isNowCoderNewEditor();
        const editorSelector = newEditor ? '.monaco-editor' : '.CodeMirror';
        const submitBtnSelector = newEditor ? '.submit-btn' : '.btn-submit';

        try {
            await waitForElement(editorSelector);
        } catch (error) {
            console.error('[cph-nowcoder] Editor did not become available:', error);
            return;
        }

        const submitBtn = document.querySelector(submitBtnSelector);
        if (!submitBtn) {
            console.error('[cph-nowcoder] Submit button not found.');
            return;
        }

        const targetLangText = getNowCoderTargetLanguageText(data.languageId, newEditor);
        await selectNowCoderLanguage(targetLangText);

        if (newEditor) {
            fillNowCoderMonaco(data.sourceCode);
        } else {
            fillNowCoderCodeMirror(data.sourceCode);
        }

        if (settings.autoSubmit) {
            setTimeout(() => submitBtn.click(), Math.max(0, settings.submitDelay) * 1000);
        }
    }

    function createBasePanel(title, includeMappings = false) {
        GM_addStyle(`
            #cph-multi-settings-panel {
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                width: 720px;
                max-height: 85vh;
                background: #fff;
                border: 1px solid #d9d9d9;
                border-radius: 10px;
                box-shadow: 0 10px 30px rgba(0, 0, 0, 0.25);
                z-index: 10000;
                display: none;
                flex-direction: column;
                color: #333;
                font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
            }
            #cph-multi-settings-panel .header {
                padding: 14px 18px;
                background: #f6f7f9;
                border-bottom: 1px solid #e5e7eb;
                font-weight: 700;
                display: flex;
                justify-content: space-between;
                align-items: center;
            }
            #cph-multi-settings-panel .close-btn {
                cursor: pointer;
                font-size: 20px;
                color: #666;
            }
            #cph-multi-settings-panel .content {
                padding: 18px;
                overflow-y: auto;
                flex: 1;
            }
            #cph-multi-settings-panel .row {
                display: flex;
                justify-content: space-between;
                align-items: center;
                gap: 12px;
                margin-bottom: 12px;
            }
            #cph-multi-settings-panel input[type="number"] {
                width: 100px;
                padding: 5px 8px;
            }
            #cph-multi-settings-panel textarea {
                width: 100%;
                min-height: 220px;
                font-family: Consolas, monospace;
                font-size: 12px;
                padding: 8px;
            }
            #cph-multi-history .entry {
                border: 1px solid #eee;
                border-radius: 6px;
                padding: 10px;
                margin-bottom: 10px;
                background: #fafafa;
            }
            #cph-multi-history details {
                margin-top: 8px;
            }
            #cph-multi-history pre {
                max-height: 200px;
                overflow-y: auto;
                white-space: pre-wrap;
                word-break: break-word;
                background: #fff;
                border: 1px solid #eee;
                border-radius: 4px;
                padding: 8px;
            }
            #cph-multi-settings-panel .actions {
                display: flex;
                gap: 8px;
                justify-content: flex-end;
                margin-top: 12px;
            }
            #cph-multi-settings-panel button {
                cursor: pointer;
                padding: 6px 10px;
            }
        `);

        const panel = document.createElement('div');
        panel.id = 'cph-multi-settings-panel';
        panel.innerHTML = `
            <div class="header">
                <span>${escapeHtml(title)}</span>
                <span class="close-btn">&times;</span>
            </div>
            <div class="content">
                <div class="row">
                    <label>Enable Polling</label>
                    <input type="checkbox" id="cph-setting-polling">
                </div>
                <div class="row">
                    <label>Enable Debug Logs</label>
                    <input type="checkbox" id="cph-setting-debug">
                </div>
                <div class="row">
                    <label>Polling Interval (ms)</label>
                    <input type="number" id="cph-setting-timeout" min="1000" step="500">
                </div>
                <div class="row">
                    <label>Auto Submit</label>
                    <input type="checkbox" id="cph-setting-auto-submit">
                </div>
                <div class="row">
                    <label>Submit Delay (s)</label>
                    <input type="number" id="cph-setting-submit-delay" min="0" step="1">
                </div>
                ${currentSite === 'atcoder' ? `
                    <div class="row">
                        <label>ACE Fill Retries</label>
                        <input type="number" id="cph-setting-ace-retries" min="1" step="1">
                    </div>
                ` : ''}
                ${includeMappings ? `
                    <hr>
                    <div>
                        <label>Language Mappings (JSON)</label>
                        <textarea id="cph-setting-mappings"></textarea>
                        <div class="actions">
                            <button id="cph-save-mappings">Save Mappings</button>
                        </div>
                    </div>
                ` : ''}
                <hr>
                <div style="display:flex; justify-content:space-between; align-items:center;">
                    <strong>Submission History</strong>
                    <div class="actions" style="margin-top:0;">
                        <button id="cph-refresh-history">Refresh</button>
                        <button id="cph-clear-history">Clear</button>
                    </div>
                </div>
                <div id="cph-multi-history" style="margin-top:12px;"></div>
            </div>
        `;

        document.body.appendChild(panel);
        panel.querySelector('.close-btn').addEventListener('click', () => {
            panel.style.display = 'none';
        });

        document.getElementById('cph-setting-polling').addEventListener('change', (event) => {
            saveSettings({ pollingEnabled: event.target.checked });
        });
        document.getElementById('cph-setting-debug').addEventListener('change', (event) => {
            saveSettings({ debug: event.target.checked });
        });
        document.getElementById('cph-setting-timeout').addEventListener('change', (event) => {
            saveSettings({ loopTimeout: parseInt(event.target.value, 10) || DEFAULT_SETTINGS[currentSite].loopTimeout });
        });
        document.getElementById('cph-setting-auto-submit').addEventListener('change', (event) => {
            saveSettings({ autoSubmit: event.target.checked });
        });
        document.getElementById('cph-setting-submit-delay').addEventListener('change', (event) => {
            saveSettings({ submitDelay: parseInt(event.target.value, 10) || 0 });
        });

        if (currentSite === 'atcoder') {
            document.getElementById('cph-setting-ace-retries').addEventListener('change', (event) => {
                saveSettings({ aceFillRetries: parseInt(event.target.value, 10) || DEFAULT_SETTINGS.atcoder.aceFillRetries });
            });
        }

        if (includeMappings) {
            document.getElementById('cph-save-mappings').addEventListener('click', async () => {
                const text = document.getElementById('cph-setting-mappings').value;
                try {
                    JSON.parse(text);
                    await saveSettings({ languageMappings: text });
                    alert('Language mappings saved.');
                } catch (error) {
                    alert(`Invalid JSON: ${error.message}`);
                }
            });
        }

        document.getElementById('cph-refresh-history').addEventListener('click', renderHistory);
        document.getElementById('cph-clear-history').addEventListener('click', async () => {
            await writeJson(storageKeys.history, []);
            renderHistory();
        });

        return panel;
    }

    async function renderHistory() {
        const container = document.getElementById('cph-multi-history');
        if (!container) return;

        const history = await readJson(storageKeys.history, []);
        if (!history.length) {
            container.innerHTML = '<p>No submissions recorded yet.</p>';
            return;
        }

        container.innerHTML = history.map((entry) => `
            <div class="entry">
                <div><strong>Time:</strong> ${escapeHtml(new Date(entry.timestamp).toLocaleString())}</div>
                <div><strong>Problem:</strong> <a href="${escapeHtml(entry.url)}" target="_blank">${escapeHtml(entry.problemName)}</a></div>
                <div><strong>Language ID:</strong> ${escapeHtml(entry.languageId)}</div>
                <details>
                    <summary>View Source</summary>
                    <pre>${escapeHtml(entry.sourceCode)}</pre>
                </details>
            </div>
        `).join('');
    }

    function syncPanelValues(includeMappings) {
        const polling = document.getElementById('cph-setting-polling');
        const debug = document.getElementById('cph-setting-debug');
        const timeout = document.getElementById('cph-setting-timeout');
        const autoSubmit = document.getElementById('cph-setting-auto-submit');
        const submitDelay = document.getElementById('cph-setting-submit-delay');

        if (polling) polling.checked = Boolean(settings.pollingEnabled);
        if (debug) debug.checked = Boolean(settings.debug);
        if (timeout) timeout.value = settings.loopTimeout;
        if (autoSubmit) autoSubmit.checked = Boolean(settings.autoSubmit);
        if (submitDelay) submitDelay.value = settings.submitDelay || 0;

        if (currentSite === 'atcoder') {
            const aceRetries = document.getElementById('cph-setting-ace-retries');
            if (aceRetries) aceRetries.value = settings.aceFillRetries || DEFAULT_SETTINGS.atcoder.aceFillRetries;
        }

        if (includeMappings) {
            const mappings = document.getElementById('cph-setting-mappings');
            if (mappings) {
                try {
                    mappings.value = JSON.stringify(JSON.parse(settings.languageMappings), null, 2);
                } catch {
                    mappings.value = settings.languageMappings || '';
                }
            }
        }
    }

    function togglePanel(includeMappings) {
        const panel = document.getElementById('cph-multi-settings-panel');
        if (!panel) return;

        const showing = panel.style.display === 'flex';
        if (showing) {
            panel.style.display = 'none';
            return;
        }

        syncPanelValues(includeMappings);
        renderHistory();
        panel.style.display = 'flex';
    }

    function injectEntryPoint() {
        const includeMappings = currentSite === 'atcoder' || currentSite === 'nowcoder';
        createBasePanel(`CPH Settings (${currentSite})`, includeMappings);

        if (currentSite === 'codeforces') {
            const ratingLink = document.querySelector('.menu-list a[href="/ratings"]');
            if (ratingLink && ratingLink.parentElement) {
                const item = document.createElement('li');
                item.innerHTML = '<a href="#" id="cph-multi-open-settings" style="color:#00aaff;">CPH Settings</a>';
                ratingLink.parentElement.insertAdjacentElement('afterend', item);
            }
        } else if (currentSite === 'atcoder') {
            const navbar = document.querySelector('.nav.navbar-nav');
            if (navbar) {
                const item = document.createElement('li');
                item.innerHTML = '<a href="#" id="cph-multi-open-settings" style="color:#00aaff; font-weight:bold;">CPH Settings</a>';
                navbar.appendChild(item);
            }
        } else if (currentSite === 'nowcoder') {
            const button = document.createElement('div');
            button.id = 'cph-multi-open-settings';
            button.textContent = 'CPH';
            button.style.cssText = 'position:fixed; bottom:20px; right:20px; z-index:9999; width:42px; height:42px; border-radius:50%; background:#28a745; color:#fff; text-align:center; line-height:42px; cursor:pointer; box-shadow:0 2px 8px rgba(0,0,0,0.25); font-weight:bold; user-select:none;';
            document.body.appendChild(button);
        }

        const openButton = document.getElementById('cph-multi-open-settings');
        if (openButton) {
            openButton.addEventListener('click', (event) => {
                event.preventDefault();
                togglePanel(includeMappings);
            });
        }
    }

    async function main() {
        await loadSettings();
        injectEntryPoint();
        startPolling();

        if (currentSite === 'codeforces' && window.location.href.includes('/submit')) {
            setTimeout(fillAndSubmitCurrentSite, 500);
        }

        if (currentSite === 'atcoder' && window.location.pathname.endsWith('/submit')) {
            setTimeout(fillAndSubmitCurrentSite, 750);
        }

        if (currentSite === 'nowcoder') {
            setTimeout(fillAndSubmitCurrentSite, 1500);
        }

        debugLog('Unified helper initialized.');
    }

    main();
})();