CPH Multi-Site Submit Helper

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==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();
})();