(function () {
    'use strict';

    function getRoot() {
        return document.querySelector('.aiap-wrap');
    }

    function config(name, fallback) {
        var root = getRoot();
        return root ? (root.getAttribute(name) || fallback) : fallback;
    }

    function parseMap(node) {
        if (!node) {
            return {};
        }
        try {
            return JSON.parse(node.getAttribute('data-aiap-map') || '{}');
        } catch (e) {
            return {};
        }
    }

    function renderRows(table, rows) {
        if (!table || !rows) {
            return;
        }
        var fragment = document.createDocumentFragment();
        Object.keys(rows).forEach(function (provider) {
            var tr = document.createElement('tr');
            var tdProvider = document.createElement('td');
            var tdValue = document.createElement('td');
            tdProvider.textContent = provider;
            tdValue.textContent = rows[provider];
            tr.appendChild(tdProvider);
            tr.appendChild(tdValue);
            fragment.appendChild(tr);
        });
        table.replaceChildren(fragment);
    }

    function setPreviewRatio(value) {
        var preview = document.querySelector('.aiap-format-preview');
        if (!preview) {
            return;
        }
        var dims = {
            '16:9': [180, 101],
            '1:1': [128, 128],
            '9:16': [90, 160],
            '4:3': [160, 120]
        }[value] || [180, 101];
        preview.style.width = dims[0] + 'px';
        preview.style.height = dims[1] + 'px';
        preview.textContent = value;
    }

    function setResult(result, ok, message) {
        result.classList.remove('aiap-test-result-ok', 'aiap-test-result-error', 'aiap-test-result-loading');
        result.classList.add(ok ? 'aiap-test-result-ok' : 'aiap-test-result-error');
        result.textContent = message;
        result.title = message;
    }

    function initRadioPills() {
        document.querySelectorAll('.aiap-radio-pill input[type=radio]').forEach(function (radio) {
            var pill = radio.closest('.aiap-radio-pill');
            if (radio.checked && pill) {
                pill.classList.add('checked');
            }
            radio.addEventListener('change', function () {
                var group = radio.closest('.aiap-radio-group');
                if (!group) {
                    return;
                }
                group.querySelectorAll('.aiap-radio-pill').forEach(function (item) {
                    item.classList.remove('checked');
                });
                if (radio.checked && pill) {
                    pill.classList.add('checked');
                }
            });
        });
    }

    function initCardSelects() {
        document.querySelectorAll('.js-aiap-card-select').forEach(function (select) {
            select.addEventListener('change', function () {
                document.querySelectorAll(select.getAttribute('data-card-selector')).forEach(function (card) {
                    card.hidden = true;
                });
                var card = document.getElementById(select.getAttribute('data-card-prefix') + select.value);
                if (card) {
                    card.hidden = false;
                    card.scrollIntoView({ behavior: 'smooth', block: 'start' });
                }
                var tab = document.getElementById(select.getAttribute('data-tab-target'));
                if (tab) {
                    tab.value = select.value;
                }
            });
        });
    }

    function initImageSettings() {
        var formatSelect = document.querySelector('select[name="image_format"]');
        var formatTable = document.querySelector('[data-aiap-format-table]');
        var formatMap = parseMap(formatTable);
        if (formatSelect) {
            formatSelect.addEventListener('change', function () {
                setPreviewRatio(formatSelect.value);
                renderRows(formatTable, formatMap[formatSelect.value]);
            });
            setPreviewRatio(formatSelect.value);
            renderRows(formatTable, formatMap[formatSelect.value]);
        }

        var qualitySelect = document.querySelector('select[name="image_quality"]');
        var qualityTable = document.querySelector('[data-aiap-quality-table]');
        var qualityKnob = document.querySelector('[data-aiap-quality-knob]');
        var qualityMap = parseMap(qualityTable);
        function setQuality(value) {
            renderRows(qualityTable, qualityMap[value]);
            if (qualityKnob) {
                qualityKnob.style.left = ({ low: '20%', medium: '50%', high: '80%' }[value] || '20%');
            }
        }
        if (qualitySelect) {
            qualitySelect.addEventListener('change', function () {
                setQuality(qualitySelect.value);
            });
            setQuality(qualitySelect.value);
        }
    }

    function initToggles() {
        document.addEventListener('click', function (event) {
            var toggle = event.target.closest('.js-aiap-toggle');
            if (!toggle) {
                return;
            }
            event.preventDefault();
            var target = document.getElementById(toggle.getAttribute('data-target'));
            if (target) {
                target.hidden = !target.hidden;
            }
        });
    }

    function initAddTaskToggle() {
        document.addEventListener('click', function (event) {
            var openButton = event.target.closest('.js-aiap-toggle-add-task');
            if (openButton) {
                event.preventDefault();
                // Phase 9 fix: ボタンが wrapper の外側 (セクションヘッダーなど) にあっても動くよう、
                // closest が見つからなければ document 全体から最初の wrapper を取得する
                var wrapper = openButton.closest('.aiap-add-task-wrapper') || document.querySelector('.aiap-add-task-wrapper');
                if (!wrapper) { return; }
                var form = wrapper.querySelector('.aiap-add-task-form');
                if (!form) { return; }
                form.hidden = false;
                wrapper.setAttribute('data-collapsed', '0');
                // form がページ下部にある場合スクロールしてユーザーが見えるように
                try { form.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
                var firstField = form.querySelector('textarea, input:not([type=hidden])');
                if (firstField) { firstField.focus(); }
                return;
            }

            var cancelButton = event.target.closest('.js-aiap-cancel-add-task');
            if (cancelButton) {
                event.preventDefault();
                var confirmMsg = cancelButton.getAttribute('data-confirm-message') || (window.AIAP_I18N && window.AIAP_I18N.discard_changes ? window.AIAP_I18N.discard_changes : '');
                if (!window.confirm(confirmMsg)) {
                    return;
                }
                var wrapper2 = cancelButton.closest('.aiap-add-task-wrapper') || document.querySelector('.aiap-add-task-wrapper');
                if (!wrapper2) { return; }
                var form2 = wrapper2.querySelector('.aiap-add-task-form');
                if (form2) {
                    form2.reset();
                    form2.hidden = true;
                }
                wrapper2.setAttribute('data-collapsed', '1');
            }
        });
    }

    // RSS フィード追加フォームの開閉トグル
    // initAddTaskToggle と同じパターン (規約: 既存パターンと整合)。
    // 共通化のためタスク側の汎用化リファクタも可能だが、指示範囲外のため別関数として並列追加。
    function initAddRssToggle() {
        document.addEventListener('click', function (event) {
            var openButton = event.target.closest('.js-aiap-toggle-add-rss');
            if (openButton) {
                event.preventDefault();
                var wrapper = openButton.closest('.aiap-add-rss-wrapper') || document.querySelector('.aiap-add-rss-wrapper');
                if (!wrapper) { return; }
                var form = wrapper.querySelector('.aiap-add-rss-form');
                if (!form) { return; }
                form.hidden = false;
                wrapper.setAttribute('data-collapsed', '0');
                try { form.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
                var firstField = form.querySelector('textarea, input:not([type=hidden]):not([type=checkbox])');
                if (firstField) { firstField.focus(); }
                return;
            }

            var cancelButton = event.target.closest('.js-aiap-cancel-add-rss');
            if (cancelButton) {
                event.preventDefault();
                var confirmMsg = cancelButton.getAttribute('data-confirm-message') || (window.AIAP_I18N && window.AIAP_I18N.discard_changes ? window.AIAP_I18N.discard_changes : '');
                if (!window.confirm(confirmMsg)) {
                    return;
                }
                var wrapper2 = cancelButton.closest('.aiap-add-rss-wrapper') || document.querySelector('.aiap-add-rss-wrapper');
                if (!wrapper2) { return; }
                var form2 = wrapper2.querySelector('.aiap-add-rss-form');
                if (form2) {
                    // フォーム内の <form> 要素を reset (aiap-add-rss-form は div、中の form を取る)
                    var innerForm = form2.querySelector('form');
                    if (innerForm && typeof innerForm.reset === 'function') {
                        innerForm.reset();
                    }
                    form2.hidden = true;
                }
                wrapper2.setAttribute('data-collapsed', '1');
            }
        });
    }

    // タスク編集フォームのキャンセル: form.reset() で入力を元に戻し、<details>を閉じる
    function initCancelEditTask() {
        document.addEventListener('click', function (event) {
            var btn = event.target.closest('.js-aiap-cancel-edit-task');
            if (!btn) { return; }
            event.preventDefault();
            var form = btn.closest('form');
            if (form && typeof form.reset === 'function') {
                form.reset();
            }
            var details = btn.closest('details');
            if (details) {
                details.open = false;
            }
        });
    }

    function initRefreshModelsButtons() {
        document.addEventListener('click', function (event) {
            var button = event.target.closest('.aiap-btn-refresh-models');
            if (!button) { return; }
            event.preventDefault();
            var provider = button.getAttribute('data-provider') || '';
            if (!provider) { return; }

            var card = button.closest('.aiap-provider-card');
            var select = card ? card.querySelector('.aiap-model-select[data-provider="' + provider + '"]') : null;
            var feedback = card ? card.querySelector('.aiap-model-feedback[data-provider="' + provider + '"]') : null;
            if (!select) { return; }

            var originalHtml = button.innerHTML;
            button.disabled = true;
            button.innerHTML = '<span class="dashicons dashicons-update aiap-spin"></span> ...';
            if (feedback) { feedback.hidden = true; feedback.textContent = ''; feedback.className = 'aiap-model-feedback'; feedback.setAttribute('data-provider', provider); }

            var body = new FormData();
            body.append('action', 'aiap_refresh_models');
            body.append('nonce', config('data-aiap-nonce', ''));
            body.append('provider', provider);

            fetch(config('data-aiap-ajax-url', window.ajaxurl || ''), {
                method: 'POST',
                credentials: 'same-origin',
                body: body
            }).then(function (r) {
                return r.text().then(function (txt) {
                    var json = null;
                    try { json = JSON.parse(txt.replace(/^\uFEFF/, '').trim()); } catch (e) {}
                    return { status: r.status, json: json };
                });
            }).then(function (payload) {
                var json = payload.json;
                if (!json || !json.success) {
                    var msg = (json && json.data && json.data.message) ? json.data.message : ('HTTP ' + payload.status);
                    if (feedback) {
                        feedback.hidden = false;
                        feedback.className = 'aiap-model-feedback is-error';
                        feedback.setAttribute('data-provider', provider);
                        feedback.textContent = msg;
                    }
                    return;
                }
                var data = json.data || {};
                var models = data.models || {};
                var current = data.current || '';

                // selectのoptionを差し替え
                while (select.firstChild) { select.removeChild(select.firstChild); }
                Object.keys(models).forEach(function (id) {
                    var opt = document.createElement('option');
                    opt.value = id;
                    opt.textContent = models[id];
                    if (id === current) { opt.selected = true; }
                    select.appendChild(opt);
                });

                // 画像プロバイダの場合、スタイル欄も同期更新する
                // (サーバー側でモデル自動切替に伴うスタイル再検証が行われている)
                if (data.styles && card) {
                    applyStyleUpdate(card, provider, data.styles.options || [], data.styles.current || '');
                }

                if (feedback) {
                    feedback.hidden = false;
                    if (data.auto_switched && data.notice) {
                        feedback.className = 'aiap-model-feedback is-warning';
                        feedback.setAttribute('data-provider', provider);
                        feedback.textContent = data.notice;
                    } else {
                        feedback.className = 'aiap-model-feedback is-ok';
                        feedback.setAttribute('data-provider', provider);
                        feedback.textContent = ((window.AIAP_I18N && AIAP_I18N.ok) ? AIAP_I18N.ok : 'OK') + ' (' + Object.keys(models).length + ')';
                    }
                }
            }).catch(function (err) {
                if (feedback) {
                    feedback.hidden = false;
                    feedback.className = 'aiap-model-feedback is-error';
                    feedback.setAttribute('data-provider', provider);
                    feedback.textContent = (err && err.message) ? err.message : (window.AIAP_I18N && window.AIAP_I18N.network_error ? window.AIAP_I18N.network_error : 'Network error');
                }
            }).then(function () {
                button.disabled = false;
                button.innerHTML = originalHtml;
            });
        });
    }

    function initEditTaskButtons() {
        document.addEventListener('click', function (event) {
            var button = event.target.closest('.js-aiap-edit-task');
            if (!button) {
                return;
            }
            event.preventDefault();
            var target = document.getElementById(button.getAttribute('data-target'));
            if (!target) {
                return;
            }
            target.open = !target.open;
            if (target.open) {
                target.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
        });
    }

    function initConfirmForms() {
        document.querySelectorAll('.js-aiap-confirm').forEach(function (form) {
            form.addEventListener('submit', function (event) {
                if (!window.confirm(form.getAttribute('data-confirm') || (window.AIAP_I18N && window.AIAP_I18N.are_you_sure ? window.AIAP_I18N.are_you_sure : 'Are you sure?'))) {
                    event.preventDefault();
                }
            });
        });
    }

    function initApiControls() {
        document.addEventListener('click', function (event) {
            var testButton = event.target.closest('.aiap-btn-test');
            if (testButton) {
                event.preventDefault();
                runApiTest(testButton);
                return;
            }
        });
    }

    function runApiTest(button) {
        var card = button.closest('.aiap-provider-card');
        var row = button.parentElement;
        // 同じ aiap-field-row 内に配置された API キー入力欄を取得
        var input = row ? row.querySelector('input[type=password],input[type=text]') : null;
        var key = input ? input.value.trim() : '';
        // 既存の別要素表示があればクリアしておく (互換)
        var legacyResult = button.parentNode ? button.parentNode.querySelector('.aiap-test-result') : null;
        if (legacyResult) {
            legacyResult.textContent = '';
            legacyResult.className = 'aiap-test-result';
        }

        // 入力欄が空でもサーバーへ送信 → DB保存済みキーがあればそれを使う
        var provider = button.getAttribute('data-provider') || '';

        // ボタンを「テスト中」状態に変化
        setButtonLoading(button);

        var data = new FormData();
        data.append('action', 'aiap_test_provider');
        data.append('nonce', config('data-aiap-nonce', ''));
        data.append('kind', button.getAttribute('data-test-kind') || 'text');
        data.append('provider', button.getAttribute('data-provider') || '');
        data.append('api_key', key);
        var customModel = card && card.querySelector('input[data-aiap-custom-model]')
            ? card.querySelector('input[data-aiap-custom-model]').value.trim()
            : '';
        data.append('model', customModel || (card && card.querySelector('.aiap-model-select') ? card.querySelector('.aiap-model-select').value : (card && card.querySelector('select') ? card.querySelector('select').value : '')));
        var styleSelect = card ? card.querySelector('select[name$="_style"]') : null;
        data.append('style', styleSelect ? styleSelect.value : '');
        // Bug fix (audit r4): the provider tiles are <button role="radio"> rendered
        // OUTSIDE the provider card panel, so card.querySelector('input[type=radio]:checked')
        // would always be null and use_provider would always be '0'. The active provider is
        // tracked via the hidden <input name="provider"> (text AI) or <input name="image_source">
        // (image AI). We send use_provider=1 only when this card's provider matches the one the
        // user has currently selected as active — preserving the original UX intent.
        var providerKey = button.getAttribute('data-provider') || '';
        var kindAttr = button.getAttribute('data-test-kind') || 'text';
        var activeInput = document.querySelector(
            kindAttr === 'image' ? 'input[name="image_source"]' : 'input[name="provider"]'
        );
        var isActiveTab = !!(activeInput && activeInput.value && activeInput.value === providerKey);
        data.append('use_provider', isActiveTab ? '1' : '0');

        fetch(config('data-aiap-ajax-url', window.ajaxurl || ''), {
            method: 'POST',
            credentials: 'same-origin',
            body: data
        }).then(function (response) {
            return response.text().then(function (text) {
                var cleanText = text.replace(/^\uFEFF/, '').trim();
                var json = null;
                try {
                    json = JSON.parse(cleanText);
                } catch (e) {}
                return { status: response.status, json: json, text: cleanText };
            });
        }).then(function (payload) {
            var json = payload.json;
            var ok = json && json.success;
            var data = json && json.data ? json.data : null;
            var isWarning = !!(ok && data && data.warning);
            var message;
            if (ok) {
                if (isWarning && data && data.message) {
                    // 課金/クォータ系: キーは有効だが残高/上限の問題。警告メッセージを表示。
                    message = data.message;
                } else if (data && data.used_saved_key) {
                    // 入力欄が空 → DB保存済みキーを使ってテストした旨を明示
                    message = (window.AIAP_I18N && window.AIAP_I18N.ok_saved_key) ? window.AIAP_I18N.ok_saved_key : 'OK (saved key)';
                } else {
                    message = (window.AIAP_I18N && window.AIAP_I18N.ok) ? window.AIAP_I18N.ok : 'OK';
                }
            } else {
                var noRespFallback = (window.AIAP_I18N && window.AIAP_I18N.no_response) ? window.AIAP_I18N.no_response : 'No response';
                message = (data && data.message)
                    ? data.message
                    : ('HTTP ' + payload.status + ': ' + (payload.text ? payload.text.replace(/<[^>]*>/g, '').trim().slice(0, 120) : noRespFallback));
                // DB保存済みキーで失敗した場合、入力欄が空であった旨を明示する
                if (data && data.used_saved_key) {
                    var prefix = (window.AIAP_I18N && window.AIAP_I18N.saved_key_prefix) ? window.AIAP_I18N.saved_key_prefix : '[saved key]';
                    message = prefix + ' ' + message;
                }
            }
            showButtonResult(button, ok && !isWarning, message);

            // テスト成功 (および warning=quota_exhausted) 時: テスト済みリストを更新
            // (warning でもキー自体は有効なので、トグル切替を許可する)
            if (ok && data && data.provider) {
                if (verifiedProviders.indexOf(data.provider) === -1) {
                    verifiedProviders.push(data.provider);
                }
            }
        }).catch(function (error) {
            var fallback = (window.AIAP_I18N && window.AIAP_I18N.network_error) ? window.AIAP_I18N.network_error : 'Network error';
            showButtonResult(button, false, error && error.message ? error.message : fallback);
        });
    }

    // テストボタン: ローディング状態に切り替え(元のHTMLを保存)
    function setButtonLoading(button) {
        if (!button.hasAttribute('data-original-html')) {
            button.setAttribute('data-original-html', button.innerHTML);
        }
        // タイマーで元に戻すスケジュールがあればキャンセル
        if (button._aiapRestoreTimer) {
            clearTimeout(button._aiapRestoreTimer);
            button._aiapRestoreTimer = null;
        }
        button.disabled = true;
        button.classList.remove('aiap-btn-test-ok', 'aiap-btn-test-error');
        button.classList.add('aiap-btn-test-loading');
        button.innerHTML = '<span class="dashicons dashicons-update aiap-spin"></span> ...';
    }

    // テストボタン: 結果をボタン内に表示 → 数秒後に元に戻す
    function showButtonResult(button, ok, message) {
        button.classList.remove('aiap-btn-test-loading');
        button.classList.add(ok ? 'aiap-btn-test-ok' : 'aiap-btn-test-error');
        button.disabled = false;
        // 長いエラーは短く
        var shortMsg = message.length > 40 ? message.slice(0, 40) + '…' : message;
        var icon = ok ? 'dashicons-yes' : 'dashicons-no-alt';
        button.innerHTML = '<span class="dashicons ' + icon + '"></span> ' + escapeHtml(shortMsg);
        button.title = message; // 完全なメッセージはtooltipで
        // OKは3秒、エラーは6秒後に元の表示に戻す
        if (button._aiapRestoreTimer) { clearTimeout(button._aiapRestoreTimer); }
        button._aiapRestoreTimer = setTimeout(function () {
            var original = button.getAttribute('data-original-html');
            if (original) {
                button.innerHTML = original;
            }
            button.classList.remove('aiap-btn-test-ok', 'aiap-btn-test-error');
            button.title = '';
            button._aiapRestoreTimer = null;
        }, ok ? 3000 : 6000);
    }

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

    /**
     * RFC4122 v4 風の UUID を生成する。crypto.randomUUID が使えればそれを優先。
     * フォールバックは「衝突しない程度」の安全性で十分 (idempotency_key 用途)。
     */
    function generateRunIdempotencyKey() {
        if (window.crypto && typeof window.crypto.randomUUID === 'function') {
            return window.crypto.randomUUID();
        }
        return 'aiap-' + Date.now().toString(36) + '-' +
            Math.random().toString(36).slice(2, 10) + '-' +
            Math.random().toString(36).slice(2, 10);
    }


    function initWeatherSettingsValidation() {
        document.querySelectorAll('form.aiap-settings-form').forEach(function (form) {
            var action = form.querySelector('input[name="action"][value="aiap_save_weather"]');
            if (!action) {
                return;
            }

            var enabled = form.querySelector('input[name="weather_enabled"]');
            var fields = [
                form.querySelector('input[name="weather_location_name"]'),
                form.querySelector('input[name="weather_latitude"]'),
                form.querySelector('input[name="weather_longitude"]')
            ].filter(Boolean);

            var locationInput = form.querySelector('input[name="weather_location_name"]');
            var latitudeInput = form.querySelector('input[name="weather_latitude"]');
            var longitudeInput = form.querySelector('input[name="weather_longitude"]');
            var geocodeButton = form.querySelector('.aiap-weather-geocode-button');
            var geocodeFeedback = form.querySelector('.aiap-weather-geocode-feedback');

            function t(key, fallback) {
                return (window.AIAP_I18N && window.AIAP_I18N[key]) ? window.AIAP_I18N[key] : fallback;
            }

            function showGeocodeFeedback(message, state) {
                if (!geocodeFeedback) { return; }
                geocodeFeedback.hidden = false;
                geocodeFeedback.textContent = message;
                geocodeFeedback.className = 'aiap-weather-geocode-feedback';
                if (state) {
                    geocodeFeedback.classList.add('is-' + state);
                }
            }

            function syncRequiredState() {
                var required = !!(enabled && enabled.checked);
                fields.forEach(function (field) {
                    field.required = required;
                    field.setAttribute('aria-required', required ? 'true' : 'false');
                });
            }

            if (enabled) {
                enabled.addEventListener('change', syncRequiredState);
            }
            syncRequiredState();

            if (geocodeButton && locationInput && latitudeInput && longitudeInput) {
                var lastResolvedLocation = locationInput.value.trim();
                locationInput.addEventListener('input', function () {
                    if (locationInput.value.trim() === lastResolvedLocation) {
                        return;
                    }
                    latitudeInput.value = '';
                    longitudeInput.value = '';
                    if (geocodeFeedback) {
                        geocodeFeedback.hidden = true;
                        geocodeFeedback.textContent = '';
                    }
                });

                geocodeButton.addEventListener('click', function () {
                    var location = locationInput.value.trim();
                    if (!location) {
                        locationInput.focus();
                        showGeocodeFeedback(t('weather_enter_location', '地域名を入力してください。'), 'error');
                        return;
                    }

                    var originalHtml = geocodeButton.innerHTML;
                    geocodeButton.disabled = true;
                    geocodeButton.innerHTML = '<span class="dashicons dashicons-update aiap-spin" aria-hidden="true"></span> ' + t('weather_lookup_running', '地域を検索しています...');
                    showGeocodeFeedback(t('weather_lookup_running', '地域を検索しています...'), 'loading');

                    var body = new FormData();
                    body.append('action', 'aiap_weather_geocode');
                    body.append('nonce', config('data-aiap-nonce', ''));
                    body.append('location', location);

                    fetch(config('data-aiap-ajax-url', window.ajaxurl || ''), {
                        method: 'POST',
                        credentials: 'same-origin',
                        body: body
                    }).then(function (response) {
                        return response.text().then(function (text) {
                            var json = null;
                            try { json = JSON.parse(text.replace(/^\uFEFF/, '').trim()); } catch (e) {}
                            return { status: response.status, json: json };
                        });
                    }).then(function (payload) {
                        var json = payload.json;
                        if (!json || !json.success || !json.data) {
                            var message = (json && json.data && json.data.message) ? json.data.message : t('weather_lookup_not_found', '地域を見つけられませんでした。地域名を再入力してください。');
                            showGeocodeFeedback(message, 'error');
                            return;
                        }
                        latitudeInput.value = json.data.latitude || '';
                        longitudeInput.value = json.data.longitude || '';
                        lastResolvedLocation = locationInput.value.trim();
                        syncRequiredState();
                        showGeocodeFeedback(t('weather_lookup_success', '緯度・経度を設定しました。保存できます。'), 'ok');
                    }).catch(function () {
                        showGeocodeFeedback(t('network_error', '通信エラー'), 'error');
                    }).then(function () {
                        geocodeButton.disabled = false;
                        geocodeButton.innerHTML = originalHtml;
                    });
                });
            }

            form.addEventListener('submit', function (event) {
                if (!enabled || !enabled.checked) {
                    return;
                }
                syncRequiredState();
                for (var i = 0; i < fields.length; i += 1) {
                    if (!fields[i].value.trim()) {
                        event.preventDefault();
                        fields[i].focus();
                        if (typeof form.reportValidity === 'function') {
                            form.reportValidity();
                        }
                        return;
                    }
                }
            });
        });
    }

    function initRunNow() {
        var form = document.querySelector('.aiap-run-now-form');
        if (!form) {
            return;
        }
        var button = form.querySelector('.aiap-run-now-btn');
        var textarea = form.querySelector('textarea[name="manual_prompt"]');
        var progress = form.querySelector('.aiap-progress');
        var resultBox = form.querySelector('.aiap-progress-result');
        var video = form.querySelector('.aiap-progress-video');
        var stepNodes = {
            ai: form.querySelector('.aiap-progress-step[data-step="ai"]'),
            post: form.querySelector('.aiap-progress-step[data-step="post"]'),
            image: form.querySelector('.aiap-progress-step[data-step="image"]'),
            seo: form.querySelector('.aiap-progress-step[data-step="seo"]')
        };
        var labels = {
            run: form.getAttribute('data-label-run') || 'Run',
            running: form.getAttribute('data-label-running') || 'Running…',
            ai: form.getAttribute('data-label-ai') || 'Generating…',
            post: form.getAttribute('data-label-post') || 'Creating post…',
            image: form.getAttribute('data-label-image') || 'Generating image…',
            imageWarning: form.getAttribute('data-label-image-warning') || 'Featured image generation failed, but the post was created (%s)',
            seo: form.getAttribute('data-label-seo') || 'Applying SEO…',
            done: form.getAttribute('data-label-done') || 'Completed',
            error: form.getAttribute('data-label-error') || 'Error',
            promptRequired: form.getAttribute('data-label-prompt-required') || 'Prompt is required.',
            retryAi: form.getAttribute('data-label-retry-ai') || 'AI retries: %d',
            retryDedup: form.getAttribute('data-label-retry-dedup') || 'Dedup retries: %d',
            serverRetry: form.getAttribute('data-label-server-retry') || 'Recovering from server error %1$d/%2$d',
            dedupRetry: form.getAttribute('data-label-dedup-retry') || 'Resolving similar content %1$d/%2$d'
        };
        var maxRetryAi = parseInt(form.getAttribute('data-max-retry-ai') || '3', 10);
        var maxRetryDedup = parseInt(form.getAttribute('data-max-retry-dedup') || '3', 10);
        if (!(maxRetryAi >= 1)) { maxRetryAi = 3; }
        if (!(maxRetryDedup >= 1)) { maxRetryDedup = 3; }
        var ajaxUrl = config('data-aiap-ajax-url', window.ajaxurl || '');
        var nonce = config('data-aiap-nonce', '');

        function resetProgress() {
            Object.keys(stepNodes).forEach(function (k) {
                var node = stepNodes[k];
                if (!node) { return; }
                node.className = 'aiap-progress-step';
                node.setAttribute('data-step', k);
                node.textContent = '';
            });
            if (resultBox) {
                resultBox.className = 'aiap-progress-result';
                resultBox.textContent = '';
            }
            progress.hidden = false;
            progress.classList.remove('is-idle', 'is-done', 'is-error');
        }

        function playVideo() {
            if (!video) { return; }
            try {
                video.currentTime = 0;
            } catch (e) { /* ブラウザ次第で投げることがあるが致命的ではない */ }
            var p = video.play();
            // play() は Promise を返すブラウザがある。autoplay ポリシー違反等で reject されても無視
            if (p && typeof p.catch === 'function') { p.catch(function () {}); }
        }

        function stopVideo() {
            if (!video) { return; }
            try { video.pause(); } catch (e) {}
        }

        function markStep(step, state, text) {
            var node = stepNodes[step];
            if (!node) { return; }
            node.className = 'aiap-progress-step is-' + state;
            node.setAttribute('data-step', step);
            // text 省略時(undefined) は現状の表示テキストを維持する。
            // エラー時に「AIで記事を作成しています…」等の進捗メッセージを残したまま、
            // 状態クラス(is-error)だけを切り替える用途で使う。
            if (typeof text !== 'undefined') {
                node.textContent = text;
            }
        }

        function showResult(state, message, extra) {
            if (!resultBox) { return; }
            resultBox.className = 'aiap-progress-result is-' + state;
            resultBox.textContent = '';
            // 完了/エラー時は動画を止めて、CSS で薄く表示する
            stopVideo();
            progress.classList.remove('is-idle');
            if (state === 'error') {
                progress.classList.add('is-error');
            } else {
                progress.classList.add('is-done');
            }
            var msgSpan = document.createElement('span');
            msgSpan.textContent = message;
            resultBox.appendChild(msgSpan);
            if (extra && extra.editUrl && extra.postId) {
                resultBox.appendChild(document.createTextNode(' '));
                var link = document.createElement('a');
                link.href = extra.editUrl;
                link.textContent = '#' + extra.postId;
                resultBox.appendChild(link);
            }
            // Pro アップグレード CTA (上限到達時など)
            if (extra && extra.upgradeUrl) {
                var ctaWrap = document.createElement('div');
                ctaWrap.className = 'aiap-progress-upgrade';
                var ctaLink = document.createElement('a');
                ctaLink.className = 'aiap-pro-cta';
                ctaLink.href = extra.upgradeUrl;
                var icon = document.createElement('span');
                icon.className = 'dashicons dashicons-star-filled';
                ctaLink.appendChild(icon);
                ctaLink.appendChild(document.createTextNode(
                    ' ' + (form.getAttribute('data-label-upgrade') || 'Upgrade to Pro')
                ));
                ctaWrap.appendChild(ctaLink);
                resultBox.appendChild(ctaWrap);
            }
            if (extra && extra.warnings && extra.warnings.length) {
                extra.warnings.forEach(function (w) {
                    var warnDiv = document.createElement('div');
                    warnDiv.className = 'aiap-progress-warning';
                    warnDiv.textContent = w;
                    resultBox.appendChild(warnDiv);
                });
            }
        }

        function setBusy(busy) {
            button.disabled = busy;
            while (button.firstChild) { button.removeChild(button.firstChild); }
            var icon = document.createElement('span');
            icon.className = busy ? 'dashicons dashicons-update aiap-spin' : 'dashicons dashicons-controls-play';
            button.appendChild(icon);
            button.appendChild(document.createTextNode(' ' + (busy ? labels.running : labels.run)));
            if (busy) {
                playVideo();
            } else {
                stopVideo();
            }
        }

        function callAjax(params) {
            var body = new FormData();
            Object.keys(params).forEach(function (k) {
                // 配列値の場合は同じキーで複数 append（PHP 側で $_POST に配列として届くようにする）
                if (Array.isArray(params[k])) {
                    params[k].forEach(function (v) { body.append(k, v); });
                } else {
                    body.append(k, params[k]);
                }
            });
            body.append('nonce', nonce);
            return fetch(ajaxUrl, {
                method: 'POST',
                credentials: 'same-origin',
                body: body
            }).then(function (response) {
                return response.text().then(function (text) {
                    var cleaned = text.replace(/^\uFEFF/, '').trim();
                    var json = null;
                    try { json = JSON.parse(cleaned); } catch (e) {}
                    return { status: response.status, json: json, text: cleaned };
                });
            }).catch(function (err) {
                var transportErr = new Error((err && err.message) ? err.message : ((window.AIAP_I18N && AIAP_I18N.load_failed) ? AIAP_I18N.load_failed : 'Load failed'));
                transportErr.aiapTransportError = true;
                throw transportErr;
            });
        }

        function runStep(step, warnings, transportRetryCount) {
            transportRetryCount = transportRetryCount || 0;
            var stepLabel = labels[step] || step;
            markStep(step, 'running', stepLabel);
            return callAjax({ action: 'aiap_run_now_step', step: step }).then(function (payload) {
                var json = payload.json;
                if (!json || !json.success) {
                    var msg = (json && json.data && json.data.message) ? json.data.message : ('HTTP ' + payload.status);
                    // 進捗ステップ表示・結果バナーの描画は catch ハンドラに集約する。
                    // ここで markStep / showResult を呼ぶと catch 側でも同じメッセージが描かれ、
                    // 同じエラー文が画面に二重に出る(支出上限超過時のステップ行+バナー重複の原因)。
                    var stepErr = new Error(msg);
                    stepErr.aiapErrorStep = step;
                    stepErr.aiapKind      = (json && json.data && json.data.kind) ? json.data.kind : '';
                    stepErr.aiapServerFinal = true;
                    throw stepErr;
                }
                markStep(step, 'done', stepLabel);
                if (json.data && json.data.warnings) {
                    json.data.warnings.forEach(function (w) {
                        if (warnings.indexOf(w) === -1) { warnings.push(w); }
                    });
                }
                return { data: json.data || {}, warnings: warnings };
            }).then(function (result) {
                var data = result.data || {};
                var next = data.next_step;
                if (next && next !== 'done') {
                    return runStep(next, warnings);
                }
                return { data: data, warnings: warnings };
            }).catch(function (err) {
                // 画像生成は外部API待ち・画像保存・メタデータ生成で時間がかかり、
                // ブラウザ側の fetch だけが Load failed になることがある。
                // サーバ側は ignore_user_abort で完走している可能性があるため、
                // 画像ステップだけ短く待ってから同じステップを再確認する。
                // PHP 側は image_step_done / アイキャッチ有無で二重生成を防ぐ。
                if (step === 'image' && err && err.aiapTransportError && transportRetryCount < 2) {
                    return new Promise(function (resolve) {
                        setTimeout(resolve, 3000);
                    }).then(function () {
                        return runStep(step, warnings, transportRetryCount + 1);
                    });
                }
                if (err && err.aiapTransportError) {
                    err.aiapErrorStep = step;
                }
                throw err;
            });
        }

        // ページ離脱対応は 2 段に分離する。
        //
        // 旧実装は beforeunload の中で sendBeacon('aiap_run_now_cancel') を呼んでいたが、
        // ユーザーが「ページに残る」を選んだ場合でもサーバー側の run_state がクリアされ、
        // 後続の step AJAX が state 不見つかりで失敗していた。
        //
        // 修正:
        //   - beforeunload: 離脱確認ダイアログを出すだけ (副作用なし)。
        //     ユーザーが「残る」を選べばここで終わり、run_state は維持される。
        //   - pagehide   : 実際にページが破棄されるタイミングで sendBeacon を送る。
        //     確認ダイアログでキャンセルした場合は発火しないので、無駄なキャンセルが起きない。
        function beforeUnloadHandler(e) {
            e.preventDefault();
            e.returnValue = '';
            return '';
        }
        function pageHideHandler() {
            try {
                if (navigator && typeof navigator.sendBeacon === 'function') {
                    var ajaxUrl = config('data-aiap-ajax-url', window.ajaxurl || '/wp-admin/admin-ajax.php');
                    var nonce = config('data-aiap-nonce', '');
                    var body = new FormData();
                    body.append('action', 'aiap_run_now_cancel');
                    body.append('nonce', nonce);
                    navigator.sendBeacon(ajaxUrl, body);
                }
            } catch (err) {
                // sendBeacon に失敗してもブラウザの離脱処理を妨げない
            }
        }
        var runActive = false;
        function setRunActive(flag) {
            if (flag === runActive) {
                return;
            }
            runActive = flag;
            if (flag) {
                window.addEventListener('beforeunload', beforeUnloadHandler);
                window.addEventListener('pagehide', pageHideHandler);
            } else {
                window.removeEventListener('beforeunload', beforeUnloadHandler);
                window.removeEventListener('pagehide', pageHideHandler);
            }
        }

        form.addEventListener('submit', function (event) {
            event.preventDefault();
            var prompt = textarea ? textarea.value.trim() : '';
            // プロンプト必須化 (旧: 空欄→次予定タスク自動選択。仕様変更で廃止)
            if (prompt === '') {
                window.alert(labels.promptRequired);
                if (textarea) { textarea.focus(); }
                return;
            }
            setBusy(true);
            resetProgress();
            setRunActive(true);

            // 二重投稿防止: 同一 click セッションで使い回す idempotency_key を発行。
            // start / retry / step すべてに付与し、サーバ側で _aiap_idempotency_key を
            // postmeta に保存。再来訪時に post step が再走しても既存投稿を再利用する。
            var idempotencyKey = generateRunIdempotencyKey();

            var langInput = form.querySelector('select[name="manual_content_language"]');
            var lenInput = form.querySelector('input[name="manual_content_length"]');
            var imageModeInput = form.querySelector('input[name="manual_featured_image_mode"]:checked');
            var taskSelectInput = form.querySelector('select[name="manual_selected_task_id"]');
            var startParams = {
                action: 'aiap_run_now_start',
                manual_prompt: prompt,
                idempotency_key: idempotencyKey
            };
            if (langInput) { startParams.manual_content_language = langInput.value; }
            if (lenInput) { startParams.manual_content_length = lenInput.value; }
            if (imageModeInput) { startParams.manual_featured_image_mode = imageModeInput.value; }
            if (taskSelectInput && taskSelectInput.value !== '') { startParams.manual_selected_task_id = taskSelectInput.value; }

            // RSS 関連フィールド（Quick Post の RSS Settings セクションから収集）
            // ここで明示的にコピーしないと callAjax の body に含まれず、サーバー側で RSS が使われない。
            var rssFormFlag = form.querySelector('input[name="manual_rss_form_submitted"]');
            if (rssFormFlag) { startParams.manual_rss_form_submitted = rssFormFlag.value; }
            var rssEnabledInput = form.querySelector('input[name="manual_rss_enabled"]');
            if (rssEnabledInput && rssEnabledInput.checked) {
                startParams.manual_rss_enabled = '1';
            }
            var rssCountInput = form.querySelector('input[name="manual_rss_count"]');
            if (rssCountInput && rssCountInput.value !== '') {
                startParams.manual_rss_count = rssCountInput.value;
            }
            var rssModeInput = form.querySelector('input[name="manual_rss_mode"]:checked');
            if (rssModeInput) { startParams.manual_rss_mode = rssModeInput.value; }
            var rssFallbackInput = form.querySelector('input[name="manual_rss_fallback"]:checked');
            if (rssFallbackInput) { startParams.manual_rss_fallback = rssFallbackInput.value; }
            var rssFeedInputs = form.querySelectorAll('input[name="manual_rss_feed_ids[]"]:checked');
            if (rssFeedInputs.length > 0) {
                var feedIds = [];
                rssFeedInputs.forEach(function (cb) { feedIds.push(cb.value); });
                startParams['manual_rss_feed_ids[]'] = feedIds;
            }

            var warnings = [];
            markStep('ai', 'running', labels.ai);

            // サーバエラー(レート制限・サーバ一時障害)で連続リトライした回数。
            // サーバ側でリトライしないので JS が制御。各サイクルでサーバ側の dedup_attempts と独立してカウント。
            var serverRetryCount = 0;

            // 「これから入るリトライが何回目か」を表示する。
            // サーバー側の dedup_attempts は、リトライが必要だと確定した時点で ++ してから JS に返ってくる。
            // serverRetryCount は JS 側で「サーバエラーが返るたびに ++」する。両方とも値=これから入るリトライ回数。
            // 0 のとき(まだ一度も失敗していない初回試行中)は何も表示しない。
            //
            // Bug fix (audit r9 self-audit, #37): server retry の分母を「試行最大回数」
            // (= GENERATE_MAX_RETRY + 1 = 4) にする。旧実装は分母 = MAX_RETRY (=3) で
            // 「3/3」表示時にユーザーが「リトライ終了」と読むが、実際はその後 4 回目試行
            // (=最後のリトライ) が残っている、という UI 混乱があった。PHP のログ側
            // (log_server_error_retry の引数) も同じく MAX_RETRY+1 に変更済み。
            // dedup_retry はリトライ概念が異なる (3 回類似回避したら諦める) ので
            // 分母は MAX_RETRY のまま (修正対象外)。
            function fmtAttempt(tmpl, n, max) {
                return tmpl.replace('%1$d', n).replace('%2$d', max);
            }
            function formatAiLabelWithRetry(d) {
                var parts = [];
                var server = serverRetryCount > 0 ? serverRetryCount : 0;
                var dedup = (d && d.dedup_attempts > 0) ? d.dedup_attempts : 0;
                if (server > 0) {
                    var maxAttemptsAi = maxRetryAi + 1; // 試行最大回数 (初回 + リトライ N 回)
                    parts.push(fmtAttempt(labels.serverRetry, Math.min(server, maxAttemptsAi), maxAttemptsAi));
                }
                if (dedup > 0) {
                    parts.push(fmtAttempt(labels.dedupRetry, Math.min(dedup, maxRetryDedup), maxRetryDedup));
                }
                return parts.length ? labels.ai + ' (' + parts.join(', ') + ')' : labels.ai;
            }

            // retry_needed のループ: 各リトライを別Ajaxで投げる(Cloudflareタイムアウト回避)
            function runAiLoop(params) {
                // server_retry_count: PHP 側でログラベル(N/M)に使うため、毎回現在値を送る
                var sendParams = {};
                if (params) {
                    for (var k in params) {
                        if (Object.prototype.hasOwnProperty.call(params, k)) {
                            sendParams[k] = params[k];
                        }
                    }
                }
                sendParams.server_retry_count = serverRetryCount;
                return callAjax(sendParams).then(function (payload) {
                    var json = payload.json;
                    if (!json || !json.success) {
                        var errData = (json && json.data) ? json.data : {};

                        // サーバエラーで retryable=true の場合は、JS 側でリトライ判定する
                        // (画面に「サーバ障害対応中 N/M」進捗を表示するため)。
                        // 上限判定は PHP 側で行い、上限超過時は retryable=false に書き換えて返す。
                        if (errData.retryable === true) {
                            serverRetryCount++;
                            // 表示更新: 「サーバ障害対応中 N/M」(dedup の値は維持して両方併記)
                            markStep('ai', 'running', formatAiLabelWithRetry({
                                dedup_attempts: (params && params.dedup_attempts) || 0
                            }));
                            // Bug fix (audit r9 self-audit, #35): リトライ前の待機。
                            // 待機秒数の計算は PHP 側 (AIAP_AI_Client::compute_retry_sleep_seconds)
                            // が単一の真実の供給源。レスポンスの next_sleep_seconds をそのまま使う。
                            // 万一 PHP が値を含めずに返してきた場合のフォールバックは 5 秒。
                            var nextSleepSec = (errData.next_sleep_seconds && errData.next_sleep_seconds > 0)
                                ? errData.next_sleep_seconds : 5;
                            // Bug fix: サーバエラー再試行は必ず aiap_run_now_retry 経由で行う。
                            // 旧実装は runAiLoop(params) と元の params をそのまま再利用していたため、
                            // 初回サイクル (params.action === 'aiap_run_now_start') で retryable エラーが
                            // 返ると、再試行が再び aiap_run_now_start を叩いてしまう。start には
                            // 「進行中の run_state があれば busy で弾く」多重起動ガードがあるため、
                            // 直前の試行が残した run_state に自分自身が衝突し
                            // 「他のタブで Quick Post が進行中」エラーになっていた。
                            // retry 用 action は run_state を継続利用し busy ガードを持たないので、
                            // dedup リトライと同じく action を retry に切り替えて再投入する。
                            // attempt / dedup_attempts は現サイクルの値を維持 (dedup ループは進めない)。
                            var retryParams = {
                                action: 'aiap_run_now_retry',
                                attempt: (params && params.attempt) || 1,
                                dedup_attempts: (params && params.dedup_attempts) || 0
                            };
                            if (errData.retry_prompt) {
                                retryParams.retry_prompt = errData.retry_prompt;
                            } else if (params && params.retry_prompt) {
                                retryParams.retry_prompt = params.retry_prompt;
                            }
                            return new Promise(function (resolve) {
                                setTimeout(resolve, nextSleepSec * 1000);
                            }).then(function () {
                                return runAiLoop(retryParams);
                            });
                        }

                        // ここに来るのは: 永続エラー(retryable=false) / リトライ上限到達 / その他のエラー
                        var msg;
                        if (errData.message) {
                            msg = errData.message;
                        } else if (payload.status >= 500) {
                            // 500系: PHP Fatal Error等でJSONが返っていない。レスポンス本文の先頭を見せる
                            var body = (payload.text || '').replace(/<[^>]*>/g, '').replace(/\s+/g, ' ').trim().slice(0, 200);
                            var serverErrLabel = form.getAttribute('data-label-server-error') || (window.AIAP_I18N && window.AIAP_I18N.server_error ? window.AIAP_I18N.server_error : 'HTTP %d');
                            msg = serverErrLabel.replace('%d', payload.status) + (body ? ': ' + body : '');
                        } else {
                            msg = 'HTTP ' + payload.status;
                        }
                        var err = new Error(msg);
                        // Free 上限到達などの特殊ケース用にメタデータを保持
                        err.aiapStep      = errData.step    || '';
                        err.aiapErrorStep = 'ai';                  // 進捗UI上のエラー対象ステップ
                        err.aiapKind      = errData.kind    || ''; // PHP classify_api_error 由来 (quota_exhausted / auth_error 等)
                        err.aiapUpgrade   = errData.upgrade || '';
                        err.aiapLimit     = errData.limit   || 0;
                        err.aiapCount     = errData.count   || 0;
                        err.aiapServerFinal = true;
                        throw err;
                    }
                    var data = json.data || {};
                    // リトライが必要ならUIを更新してもう一度投げる(dedup リトライ)
                    if (data.retry_needed) {
                        // dedup リトライに入る瞬間は serverRetryCount をリセット
                        // (dedup ループ単位でサーバエラー試行回数は仕切り直す)
                        serverRetryCount = 0;
                        markStep('ai', 'running', formatAiLabelWithRetry(data));
                        return runAiLoop({
                            action: 'aiap_run_now_retry',
                            attempt: data.next_attempt,
                            retry_prompt: data.retry_prompt,
                            dedup_attempts: data.dedup_attempts
                        });
                    }
                    // 完了時は試行回数を残さずベースラベルのみ表示する。
                    // (試行中の途中経過はリトライ開始時の running 表示で見せる。)
                    markStep('ai', 'done', labels.ai);
                    var next = data.next_step;
                    if (next && next !== 'done') {
                        return runStep(next, warnings);
                    }
                    return { data: data, warnings: warnings };
                });
            }

            runAiLoop(startParams).then(function (final) {
                showResult('done', labels.done + ': ' + (final.data.message || ''), {
                    postId: final.data.post_id,
                    editUrl: final.data.edit_url,
                    warnings: final.warnings
                });
            }).catch(function (err) {
                var msg = err && err.message ? err.message : ((window.AIAP_I18N && AIAP_I18N.error_generic) ? AIAP_I18N.error_generic : 'Error');
                var clearPromise = err && (err.aiapServerFinal || err.aiapTransportError)
                    ? callAjax({ action: 'aiap_run_now_clear' }).catch(function () {})
                    : Promise.resolve();
                return clearPromise.then(function () {
                // ステップ行は一切触らない: 進行中メッセージ(「AIで記事を生成しています…」等)を
                // テキストも状態クラスもそのまま維持する。
                // 失敗の通知は「進行中ステップ行の真下にエラー行を追加表示する」形で行う。
                // (.aiap-progress-result.is-error は CSS でステップ行のエラー形式と揃えてある: ❌ + 赤 + 太字)
                if (err && err.aiapStep === 'limit') {
                    // Free プランの月次上限到達は専用 UI(アップグレード CTA 付きバナー)
                    showResult('limit', msg, { upgradeUrl: err.aiapUpgrade });
                    return;
                }
                showResult('error', labels.error + ': ' + msg);
                });
            }).then(function () {
                setBusy(false);
                // beforeunload を解除 (生成サイクルが終わったので離脱しても警告しない)
                setRunActive(false);
            });
        });

        // ページロード時に「進行中の手動実行があるか」サーバへ確認する。
        // ユーザーが生成中に別メニューへ移動 → 戻ってきた時、サーバ側は ignore_user_abort で
        // 完走している/または進行中の可能性がある。JS はその状態を知らないので、
        // 軽量 endpoint で確認し、「進行中」なら離脱警告 + 注意メッセージを表示する。
        // 完了済み (stale_state) なら何もしない (ユーザーは投稿一覧で確認できる)。
        (function checkExistingRun() {
            var statusParams = { action: 'aiap_run_now_status' };
            callAjax(statusParams).then(function (payload) {
                var json = payload && payload.json;
                if (!json || !json.success || !json.data) {
                    return;
                }
                if (json.data.in_progress === true) {
                    // 進行中: 離脱警告を有効化 + ユーザーに通知。
                    // 自動的に AJAX サイクルを再開すると state や lock と競合するため、
                    // ここでは「進行中である事実だけ」を伝え、ユーザー操作には介入しない。
                    setRunActive(true);
                    var ageSec = parseInt(json.data.age_seconds || 0, 10);
                    var msgTpl = (window.AIAP_I18N && window.AIAP_I18N.run_in_progress_resume) ||
                        'A previous Run Now is still in progress (started %d sec ago). Please wait for it to finish before retrying.';
                    showResult('error', msgTpl.replace('%d', ageSec));
                }
            }).catch(function () {
                // ネットワーク不通等: 何もしない (運用継続)
            });
        })();
    }

    /**
     * スタイルselectを新しい選択肢で差し替える共通ヘルパー
     * @param {Element} card     対象プロバイダカード要素
     * @param {string}  provider プロバイダキー
     * @param {Array}   options  [ { value, label }, ... ]
     * @param {string}  current  選択しておくべき値
     */
    function applyStyleUpdate(card, provider, options, current) {
        var wrapper = card.querySelector('[data-style-wrapper][data-provider="' + provider + '"]');
        if (!wrapper) { return; }
        var styleSelect = wrapper.querySelector('.aiap-style-select[data-provider="' + provider + '"]');
        var notice = wrapper.querySelector('[data-style-notice]');
        var noticeText = notice ? notice.querySelector('[data-style-notice-text]') : null;

        if (!options || options.length === 0) {
            // このモデルはスタイル非対応 -> 欄ごと非表示にする
            wrapper.hidden = true;
            return;
        }
        wrapper.hidden = false;

        if (!styleSelect) { return; }

        // 現在選択されている値を覚えておき、新リストに無い場合は警告を出しつつ先頭値にフォールバック
        var previousValue = styleSelect.value;
        var valueSet = options.map(function (o) { return o.value; });
        var unsupportedValue = '';
        if (previousValue && valueSet.indexOf(previousValue) === -1) {
            unsupportedValue = previousValue;
        }

        while (styleSelect.firstChild) { styleSelect.removeChild(styleSelect.firstChild); }
        options.forEach(function (o) {
            var opt = document.createElement('option');
            opt.value = o.value;
            opt.textContent = o.label;
            styleSelect.appendChild(opt);
        });

        // 選択値を決定: サーバー指定のcurrent優先、なければ以前の値(サポート内なら)、なければ先頭
        var chosen = '';
        if (current && valueSet.indexOf(current) !== -1) {
            chosen = current;
        } else if (previousValue && valueSet.indexOf(previousValue) !== -1) {
            chosen = previousValue;
        } else {
            chosen = options[0].value;
        }
        styleSelect.value = chosen;

        // 「前に選ばれていた値がサポート外になった」場合のみ警告表示
        if (notice) {
            if (unsupportedValue) {
                var tpl = (window.AIAP_STYLE_MATRIX && window.AIAP_STYLE_MATRIX.unsupported_text) || '%s is not supported by the current model.';
                var msg = tpl.replace('%s', unsupportedValue);
                if (noticeText) {
                    noticeText.textContent = msg;
                } else {
                    // テキスト専用spanが無い旧DOMの場合、notice自体のテキストを差し替え
                    notice.textContent = '';
                    var icon = document.createElement('span');
                    icon.className = 'dashicons dashicons-warning';
                    notice.appendChild(icon);
                    notice.appendChild(document.createTextNode(' ' + msg));
                }
                notice.hidden = false;
            } else {
                notice.hidden = true;
                if (noticeText) { noticeText.textContent = ''; }
            }
        }
    }

    /**
     * 画像プロバイダのモデルselectが変更された瞬間、サーバーに問い合わせて
     * そのモデルに対応するスタイル欄を動的に更新する
     */
    function initModelChangeStyleSync() {
        var IMAGE_PROVIDERS = ['dalle3', 'imagen', 'stability', 'flux'];

        document.addEventListener('change', function (event) {
            var select = event.target;
            if (!select || !select.classList || !select.classList.contains('aiap-model-select')) { return; }
            var provider = select.getAttribute('data-provider') || '';
            if (IMAGE_PROVIDERS.indexOf(provider) === -1) { return; }

            var card = select.closest('.aiap-image-provider-card');
            if (!card) { return; }

            var model = select.value;
            var body = new FormData();
            body.append('action', 'aiap_styles_for_model');
            body.append('nonce', config('data-aiap-nonce', ''));
            body.append('provider', provider);
            body.append('model', model);

            fetch(config('data-aiap-ajax-url', window.ajaxurl || ''), {
                method: 'POST',
                credentials: 'same-origin',
                body: body
            }).then(function (r) {
                return r.text().then(function (txt) {
                    var json = null;
                    try { json = JSON.parse(txt.replace(/^\uFEFF/, '').trim()); } catch (e) {}
                    return json;
                });
            }).then(function (json) {
                if (!json || !json.success) { return; }
                var data = json.data || {};
                applyStyleUpdate(card, provider, data.styles || [], '');
            }).catch(function () {
                // ネットワークエラーは無視(次回保存時にサーバー側検証が効くため致命的ではない)
            });
        });
    }

    // ========== 「使用するプロバイダ」トグルガード ==========
    // テスト未済のプロバイダをONにしたら、トグルをOFFに戻して通知を出す

    var verifiedProviders = [];
    try {
        var rootEl = document.querySelector('.aiap-wrap');
        if (rootEl) {
            verifiedProviders = JSON.parse(rootEl.getAttribute('data-aiap-verified') || '[]');
        }
    } catch (e) {
        verifiedProviders = [];
    }

    function isProviderVerified(provider) {
        return verifiedProviders.indexOf(provider) !== -1;
    }

    function initProviderToggleGuard() {
        document.addEventListener('change', function (e) {
            var radio = e.target;
            if (!radio || radio.type !== 'radio') return;
            if (radio.name !== 'provider' && radio.name !== 'image_source') return;
            if (!radio.checked) return;
            if (isProviderVerified(radio.value)) return;
            // 元々ONだったラジオに戻す
            var radios = document.querySelectorAll('input[type="radio"][name="' + radio.name + '"]');
            radios.forEach(function (r) { r.checked = r.defaultChecked; });
            alert(config('data-aiap-not-tested-msg', 'Please test the API key first.'));
        });
    }

    // パスワード入力欄の目アイコントグル + autofill 対策
    // data-aiap-password-target にひもづく input の type を password <-> text で切り替える。
    // aria-label / aria-pressed と dashicons クラスも同期する。
    //
    // autofill 対策: 同一ページ内に複数の type="password" があると Chrome 等の
    // パスワードマネージャーが保存済み値を heuristic で全 input に注入する
    // (例: OpenAI のキーが Claude の input に書き込まれる)。input は PHP 側で
    // readonly でレンダリングされており、ここで以下の挙動を追加する:
    //   - ユーザーが input に focus した時点で readonly を解除し、編集可能にする
    //   - 目アイコン押下でも readonly を解除する (表示中に編集する導線のため)
    function initPasswordToggle() {
        document.querySelectorAll('.aiap-password-wrapper .aiap-input-text').forEach(function (input) {
            input.addEventListener('focus', function () {
                input.removeAttribute('readonly');
            });
        });

        document.querySelectorAll('.aiap-password-toggle').forEach(function (btn) {
            var targetId = btn.getAttribute('data-aiap-password-target');
            if (!targetId) return;
            var input = document.getElementById(targetId);
            if (!input) return;
            var icon = btn.querySelector('.dashicons');
            var labelShow = btn.getAttribute('aria-label') || '';
            var labelHide = config('data-aiap-password-hide-label', labelShow);
            var labelShowInitial = config('data-aiap-password-show-label', labelShow);

            btn.addEventListener('click', function () {
                input.removeAttribute('readonly');
                var isPassword = input.getAttribute('type') === 'password';
                if (isPassword) {
                    input.setAttribute('type', 'text');
                    btn.setAttribute('aria-pressed', 'true');
                    btn.setAttribute('aria-label', labelHide);
                    if (icon) {
                        icon.classList.remove('dashicons-visibility');
                        icon.classList.add('dashicons-hidden');
                    }
                } else {
                    input.setAttribute('type', 'password');
                    btn.setAttribute('aria-pressed', 'false');
                    btn.setAttribute('aria-label', labelShowInitial);
                    if (icon) {
                        icon.classList.remove('dashicons-hidden');
                        icon.classList.add('dashicons-visibility');
                    }
                }
            });
        });
    }

    document.addEventListener('DOMContentLoaded', function () {
        initRadioPills();
        initCardSelects();
        initImageSettings();
        initToggles();
        initConfirmForms();
        initApiControls();
        initEditTaskButtons();
        initRefreshModelsButtons();
        initModelChangeStyleSync();
        initAddTaskToggle();
        initAddRssToggle();
        initCancelEditTask();
        initRunNow();
        initWeatherSettingsValidation();
        initDedupSensitivity();
        initEnabledToggle();
        initProviderToggleGuard();
        initRandomMode();
        initSidebarToggle();
        initWizard();
        initQuickPostCharCount();
        initQuickPostTaskSelect();
        initPasswordToggle();
    });

    // Phase 6: Quick Post の文字数カウンター
    function initQuickPostCharCount() {
        var ta = document.getElementById('js-aiap-manual-prompt');
        var cnt = document.querySelector('[data-aiap-charcount]');
        if (!ta || !cnt) return;
        function update() {
            var n = ta.value.length;
            cnt.textContent = n;
            cnt.classList.toggle('is-active', n > 0);
        }
        ta.addEventListener('input', update);
        update();
    }

    // Quick Post: タスク選択 → フォーム全フィールドへ流し込み
    function initQuickPostTaskSelect() {
        var sel = document.getElementById('js-aiap-manual-task');
        if (!sel) return;
        var ta   = document.getElementById('js-aiap-manual-prompt');
        var lang = document.getElementById('aiap-manual-lang');
        var len  = document.getElementById('aiap-manual-length');
        var imageRadios       = document.querySelectorAll('input[name="manual_featured_image_mode"]');
        var rssEnable         = document.querySelector('input[name="manual_rss_enabled"]');
        var rssFeedBoxes      = document.querySelectorAll('input[name="manual_rss_feed_ids[]"]');
        var rssCount          = document.querySelector('input[name="manual_rss_count"]');
        var rssModeRadios     = document.querySelectorAll('input[name="manual_rss_mode"]');
        var rssFallbackRadios = document.querySelectorAll('input[name="manual_rss_fallback"]');
        var rssDetails        = document.querySelector('details.aiap-quickpost-rss');
        var last = '';

        sel.addEventListener('change', function () {
            var v = sel.value;
            if (v === '') { last = ''; return; }

            if (ta && ta.value.trim() !== '') {
                var msg = sel.getAttribute('data-confirm') || '';
                if (msg && !window.confirm(msg)) {
                    sel.value = last;
                    return;
                }
            }

            var opt = sel.options[sel.selectedIndex];
            if (!opt) { last = v; return; }
            var d = opt.dataset;

            if (ta) {
                ta.value = d.prompt || '';
                try { ta.dispatchEvent(new Event('input', { bubbles: true })); } catch (e) {}
            }
            if (lang && d.lang)   { lang.value = d.lang; }
            if (len && d.length)  { len.value = d.length; }
            if (d.imageMode && imageRadios.length) {
                imageRadios.forEach(function (r) { r.checked = (r.value === d.imageMode); });
            }

            // RSS 上書き
            var enabled = d.rssEnabled === '1';
            if (rssEnable) {
                rssEnable.checked = enabled;
                try { rssEnable.dispatchEvent(new Event('change', { bubbles: true })); } catch (e) {}
            }
            var feedIds = (d.rssFeedIds || '').split(',').filter(function (s) { return s !== ''; });
            rssFeedBoxes.forEach(function (cb) {
                cb.checked = feedIds.indexOf(cb.value) !== -1;
            });
            if (rssCount && d.rssCount) { rssCount.value = d.rssCount; }
            if (d.rssMode && rssModeRadios.length) {
                rssModeRadios.forEach(function (r) { r.checked = (r.value === d.rssMode); });
            }
            if (d.rssFallback && rssFallbackRadios.length) {
                rssFallbackRadios.forEach(function (r) { r.checked = (r.value === d.rssFallback); });
            }
            if (enabled && rssDetails) {
                rssDetails.setAttribute('open', '');
            }

            last = v;
        });
    }

    // Phase 3: Quick Start ウィザード (4 ステップ + 進捗バー + プリセット + レビュー反映)
    function initWizard() {
        var form = document.querySelector('[data-aiap-wizard-form]');
        if (!form) return;

        var TOTAL = 4;
        var current = 1;

        function show(step) {
            current = Math.max(1, Math.min(TOTAL, step));
            var steps = form.querySelectorAll('[data-aiap-wizard-step]');
            steps.forEach(function (s) {
                s.classList.toggle('is-active', parseInt(s.getAttribute('data-aiap-wizard-step'), 10) === current);
            });
            var fill = document.querySelector('[data-aiap-wizard-progress-fill]');
            if (fill) fill.style.width = (current / TOTAL * 100) + '%';
            var nums = document.querySelectorAll('[data-stepnum]');
            nums.forEach(function (n) {
                var v = parseInt(n.getAttribute('data-stepnum'), 10);
                n.classList.remove('is-active', 'is-done');
                if (v < current) n.classList.add('is-done');
                else if (v === current) n.classList.add('is-active');
            });
            if (current === TOTAL) updateReview();
            try { form.scrollIntoView({ behavior: 'smooth', block: 'start' }); } catch (e) {}
        }

        function validateCurrent() {
            if (current === 1) {
                var p = form.querySelector('#aiap-wiz-prompt');
                if (!p || !p.value.trim()) {
                    if (p) p.focus();
                    return false;
                }
            }
            return true;
        }

        function updateReview() {
            var review = document.querySelector('[data-aiap-wizard-review]');
            if (!review) return;
            var p = (form.querySelector('#aiap-wiz-prompt') || {}).value || '';
            var freqInput = form.querySelector('input[name="wiz_freq"]:checked');
            var freqLabel = freqInput ? ((freqInput.parentNode.querySelector('.aiap-wizard-option-name') || {}).textContent || '') : '';
            var prov = (form.querySelector('#aiap-wiz-provider') || {}).value || '';
            var provText = prov === 'anthropic' ? 'Anthropic (Claude)' : (prov === 'openai' ? 'OpenAI (GPT)' : (prov === 'gemini' ? 'Google (Gemini)' : prov));
            var apik = (form.querySelector('#aiap-wiz-apikey') || {}).value || '';
            var apikText = apik ? (apik.substring(0, 4) + '••••••••') : '—';

            var setReview = function (key, text) {
                var el = review.querySelector('[data-review="' + key + '"]');
                if (el) el.textContent = text;
            };
            setReview('prompt', p.length > 80 ? p.substring(0, 80) + '…' : p);
            setReview('freq', (freqLabel || '').trim());
            setReview('provider', provText);
            setReview('apikey', apikText);
        }

        form.addEventListener('click', function (e) {
            if (e.target.matches('[data-aiap-wizard-next]')) {
                e.preventDefault();
                if (validateCurrent()) show(current + 1);
            } else if (e.target.matches('[data-aiap-wizard-prev]')) {
                e.preventDefault();
                show(current - 1);
            }
        });

        form.addEventListener('click', function (e) {
            var btn = e.target.closest('[data-aiap-wizard-preset]');
            if (!btn) return;
            e.preventDefault();
            var v = btn.getAttribute('data-aiap-wizard-preset') || '';
            var p = form.querySelector('#aiap-wiz-prompt');
            if (p) {
                p.value = v;
                p.focus();
            }
            form.querySelectorAll('[data-aiap-wizard-preset]').forEach(function (b) {
                b.classList.remove('is-selected');
            });
            btn.classList.add('is-selected');
        });

        show(1);
    }

    // モバイル用サイドバーのハンバーガー トグル (#nav-redesign)
    function initSidebarToggle() {
        document.addEventListener('click', function (e) {
            var t = e.target.closest('[data-aiap-sidebar-toggle]');
            if (!t) return;
            e.preventDefault();
            var sidebar = document.querySelector('[data-aiap-sidebar]');
            if (!sidebar) return;
            var isOpen = sidebar.classList.toggle('is-open');
            t.setAttribute('aria-expanded', isOpen ? 'true' : 'false');
        });

        // サイドバー外クリックで閉じる (モバイルのみ)
        document.addEventListener('click', function (e) {
            if (window.innerWidth > 768) return;
            var sidebar = document.querySelector('[data-aiap-sidebar]');
            if (!sidebar || !sidebar.classList.contains('is-open')) return;
            if (sidebar.contains(e.target)) return;
            if (e.target.closest('[data-aiap-sidebar-toggle]')) return;
            sidebar.classList.remove('is-open');
            var t = document.querySelector('[data-aiap-sidebar-toggle]');
            if (t) t.setAttribute('aria-expanded', 'false');
        });
    }

    // ランダム投稿モード UI (#47): ON/OFF 切替で表示を切り替え + 各行内の追加/削除
    function initRandomMode() {
        // 表示切替: data-aiap-random-toggle のチェック状態で random-fields/schedule-fields を切替
        document.addEventListener('change', function (e) {
            var t = e.target;
            if (!t || t.getAttribute('data-aiap-random-toggle') !== '1') return;
            var form = t.closest('form');
            if (!form) return;
            var randomFields   = form.querySelector('[data-aiap-random-fields]');
            var scheduleFields = form.querySelector('[data-aiap-schedule-fields]');
            if (t.checked) {
                if (randomFields)   { randomFields.classList.remove('aiap-hidden'); }
                if (scheduleFields) { scheduleFields.classList.add('aiap-hidden'); }
            } else {
                if (randomFields)   { randomFields.classList.add('aiap-hidden'); }
                if (scheduleFields) { scheduleFields.classList.remove('aiap-hidden'); }
            }
        });

        // Dedup: 旧実装は 2 箇所 ([data-aiap-allowed-add] / [data-aiap-allowed-add-empty]) で
        // ほぼ同一の DOM テンプレート (input × 2 + 削除/追加ボタン) を独立にコピペしていた。
        // 単一ファクトリに集約 (CLAUDE.md 重複排除)。
        function buildAllowedRowElement(idx) {
            var newRow = document.createElement('div');
            newRow.className = 'aiap-allowed-row';
            newRow.setAttribute('data-aiap-allowed-row', '');
            var labelRemove = (window.AIAP_I18N && AIAP_I18N.remove) ? AIAP_I18N.remove : 'Remove';
            var labelAdd    = (window.AIAP_I18N && AIAP_I18N.add)    ? AIAP_I18N.add    : '+ Add';
            newRow.innerHTML =
                '<input type="time" name="task_allowed_periods[' + idx + '][start]" step="60" class="aiap-input-text">'
                + '<span class="aiap-counts-sep">–</span>'
                + '<input type="time" name="task_allowed_periods[' + idx + '][end]" step="60" class="aiap-input-text">'
                + '<div class="aiap-allowed-actions">'
                + '<button type="button" class="button" data-aiap-allowed-remove>' + labelRemove + '</button>'
                + '<button type="button" class="button" data-aiap-allowed-add>' + labelAdd + '</button>'
                + '</div>';
            return newRow;
        }

        // 許可時間帯 (Phase 13): 各行内の「+ 追加」 → 自分の行の直下に新しい行を挿入
        document.addEventListener('click', function (e) {
            var t = e.target;
            if (!t || !t.matches('[data-aiap-allowed-add]')) return;
            e.preventDefault();
            var row = t.closest('[data-aiap-allowed-row]');
            if (!row) return;
            var list = row.parentNode;
            // index は現存数を使う (重複しても sanitize で問題なし、見た目のみ)
            list.insertBefore(buildAllowedRowElement(list.children.length), row.nextSibling);
        });

        // 許可時間帯: 0 行のとき表示される「+ 時間帯を追加」 → 1 行追加 + 自分を隠す
        document.addEventListener('click', function (e) {
            var t = e.target;
            if (!t || !t.matches('[data-aiap-allowed-add-empty]')) return;
            e.preventDefault();
            var field = t.closest('.aiap-field');
            if (!field) return;
            var list = field.querySelector('[data-aiap-allowed-list]');
            if (!list) return;
            list.appendChild(buildAllowedRowElement(list.children.length));
            t.classList.add('aiap-hidden');
        });

        // 許可時間帯: 「削除」 → その行を削除 (0 行になったら「+ 時間帯を追加」を表示)
        document.addEventListener('click', function (e) {
            var t = e.target;
            if (!t || !t.matches('[data-aiap-allowed-remove]')) return;
            e.preventDefault();
            var row = t.closest('[data-aiap-allowed-row]');
            if (!row) return;
            var list = row.parentNode;
            list.removeChild(row);
            if (list.children.length === 0) {
                var field = list.closest('.aiap-field');
                var emptyBtn = field ? field.querySelector('[data-aiap-allowed-add-empty]') : null;
                if (emptyBtn) emptyBtn.classList.remove('aiap-hidden');
            }
        });
    }

    // 判定基準の3ボタン型ラジオ: クリックでアクティブ表示を切り替え
    function initDedupSensitivity() {
        document.querySelectorAll('.aiap-dedup-sens-group').forEach(function (group) {
            group.addEventListener('click', function (e) {
                var label = e.target.closest('.aiap-dedup-sens-option');
                if (!label) { return; }
                var radio = label.querySelector('input[type="radio"]');
                if (!radio) { return; }
                radio.checked = true;
                group.querySelectorAll('.aiap-dedup-sens-option').forEach(function (el) {
                    el.classList.remove('is-active');
                });
                label.classList.add('is-active');
            });
        });
    }

    // ============================================================
    // ダッシュボードの「自動投稿」ON/OFF トグル
    // ============================================================
    function initEnabledToggle() {
        var enabledToggle = document.getElementById('js-aiap-toggle-enabled');
        if (!enabledToggle) { return; }
        enabledToggle.addEventListener('change', function () {
            var on = enabledToggle.checked ? 1 : 0;
            var nonce = enabledToggle.getAttribute('data-nonce') || '';
            var labelEl = document.getElementById('js-aiap-toggle-label');
            var statusEl = document.getElementById('js-aiap-toggle-status');

            // 保存中は二重操作防止
            enabledToggle.disabled = true;
            if (statusEl) { statusEl.textContent = ''; }

            var body = new FormData();
            body.append('action', 'aiap_toggle_enabled');
            body.append('nonce', nonce);
            body.append('on', String(on));

            fetch(window.ajaxurl || '/wp-admin/admin-ajax.php', {
                method: 'POST',
                credentials: 'same-origin',
                body: body
            }).then(function (r) {
                return r.text().then(function (txt) {
                    var json = null;
                    try { json = JSON.parse(txt.replace(/^\uFEFF/, '').trim()); } catch (e) {}
                    return { status: r.status, json: json };
                });
            }).then(function (payload) {
                var json = payload.json;
                if (!json || !json.success) {
                    enabledToggle.checked = !on;
                    if (statusEl) { statusEl.textContent = (json && json.data && json.data.message) ? json.data.message : ('HTTP ' + payload.status); }
                    return;
                }
                var data = json.data || {};
                if (labelEl && data.label) {
                    // textContent + DOM API 経由で確実に XSS を回避する
                    while (labelEl.firstChild) { labelEl.removeChild(labelEl.firstChild); }
                    var strong = document.createElement('strong');
                    strong.textContent = data.label;
                    labelEl.appendChild(strong);
                }
                if (statusEl) {
                    statusEl.textContent = data.msg || '';
                    setTimeout(function () { statusEl.textContent = ''; }, 2000);
                }
            }).catch(function () {
                enabledToggle.checked = !on;
                if (statusEl) { statusEl.textContent = (window.AIAP_I18N && window.AIAP_I18N.network_error) ? window.AIAP_I18N.network_error : 'Network error'; }
            }).finally(function () {
                enabledToggle.disabled = false;
            });
        });
    }
}());

/* ============================================
   画像設定 — プリセット選択 / 詳細パネル
   ============================================ */
(function () {
    'use strict';
    var presetEls = document.querySelectorAll('[data-aiap-preset]');
    if (!presetEls.length) return;

    var hidden    = document.getElementById('aiap-image-preset');
    var labelEl   = document.getElementById('aiap-current-preset-label');
    var fmtEl     = document.getElementById('aiap-image-format');
    var qualEl    = document.getElementById('aiap-image-quality');
    var widthEl   = document.getElementById('aiap-image-max-width');
    var compEl    = document.getElementById('aiap-image-compress');
    var advanced  = document.getElementById('aiap-imgset-advanced');
    var advToggle = document.getElementById('aiap-imgset-advanced-toggle');

    function setPreset(btn) {
        presetEls.forEach(function (b) {
            b.classList.remove('is-selected');
            b.setAttribute('aria-checked', 'false');
        });
        btn.classList.add('is-selected');
        btn.setAttribute('aria-checked', 'true');
        if (hidden)  { hidden.value = btn.getAttribute('data-aiap-preset'); }
        if (labelEl) {
            var nameEl = btn.querySelector('.aiap-imgset-card__name');
            if (nameEl) { labelEl.textContent = nameEl.textContent; }
        }
        if (fmtEl)   { fmtEl.value   = btn.getAttribute('data-aiap-format');    }
        if (qualEl)  { qualEl.value  = btn.getAttribute('data-aiap-quality');   }
        if (widthEl) { widthEl.value = btn.getAttribute('data-aiap-max-width'); }
        if (compEl)  { compEl.value  = btn.getAttribute('data-aiap-compress');  }
    }

    presetEls.forEach(function (btn) {
        btn.addEventListener('click', function () { setPreset(btn); });
    });

    function markCustom() {
        presetEls.forEach(function (b) {
            b.classList.remove('is-selected');
            b.setAttribute('aria-checked', 'false');
        });
        if (hidden)  { hidden.value = 'custom'; }
        if (labelEl) { labelEl.textContent = labelEl.getAttribute('data-custom-label') || 'Custom'; }
    }
    [fmtEl, qualEl, widthEl, compEl].forEach(function (el) {
        if (!el) return;
        el.addEventListener('change', markCustom);
        el.addEventListener('input', markCustom);
    });

    if (advToggle && advanced) {
        advToggle.addEventListener('click', function () {
            var open = advanced.classList.toggle('is-open');
            advToggle.setAttribute('aria-expanded', open ? 'true' : 'false');
        });
    }
}());

/* ============================================
   Provider Selection - タイル選択 → パネル切替 + active 設定
   ============================================ */
(function () {
    'use strict';
    var popular = document.getElementById('aiap-providers-popular');
    if (!popular) return;

    var allTiles = document.querySelectorAll('[data-aiap-provider]');
    var allPanels = document.querySelectorAll('[data-aiap-panel-for]');
    var activeInput = document.getElementById('aiap-active-provider-input');
    var tabInput = document.getElementById('aiap-provider-tab');

    function selectProvider(tile) {
        var key = tile.getAttribute('data-aiap-provider');
        if (!key) return;

        // タイルの選択状態を切り替え
        allTiles.forEach(function (t) {
            t.classList.remove('is-selected');
            t.setAttribute('aria-checked', 'false');
        });
        tile.classList.add('is-selected');
        tile.setAttribute('aria-checked', 'true');

        // 「使用するプロバイダ」hidden input
        if (activeInput) activeInput.value = key;
        // タブ位置(リダイレクト先)も同期
        if (tabInput) tabInput.value = key;

        // パネル表示切替
        allPanels.forEach(function (p) {
            if (p.getAttribute('data-aiap-panel-for') === key) {
                p.removeAttribute('hidden');
            } else {
                p.setAttribute('hidden', '');
            }
        });
    }

    allTiles.forEach(function (tile) {
        tile.addEventListener('click', function () {
            selectProvider(tile);
        });
    });
})();

/* ============================================
   Image Provider tile selection
   ============================================ */
(function () {
    'use strict';
    var popular = document.getElementById('aiap-image-providers-popular');
    if (!popular) return;

    var allTiles = document.querySelectorAll('[data-aiap-image-source]');
    var allPanels = document.querySelectorAll('[data-aiap-image-panel-for]');
    var activeInput = document.getElementById('aiap-active-image-source-input');
    var tabInput = document.getElementById('aiap-image-tab');

    function selectImageSource(tile) {
        var key = tile.getAttribute('data-aiap-image-source');
        if (!key) return;
        allTiles.forEach(function (t) {
            t.classList.remove('is-selected');
            t.setAttribute('aria-checked', 'false');
        });
        tile.classList.add('is-selected');
        tile.setAttribute('aria-checked', 'true');
        if (activeInput) activeInput.value = key;
        if (tabInput) tabInput.value = key;
        allPanels.forEach(function (p) {
            if (p.getAttribute('data-aiap-image-panel-for') === key) {
                p.removeAttribute('hidden');
            } else {
                p.setAttribute('hidden', '');
            }
        });
    }

    allTiles.forEach(function (tile) {
        tile.addEventListener('click', function () {
            selectImageSource(tile);
        });
    });
})();

/* ============================================
   Segment control (sensitivity radio)
   ============================================ */
(function () {
    'use strict';
    document.querySelectorAll('.aiap-segment').forEach(function (seg) {
        var opts = seg.querySelectorAll('.aiap-segment-option');
        opts.forEach(function (opt) {
            var input = opt.querySelector('input[type=radio]');
            if (!input) return;
            input.addEventListener('change', function () {
                opts.forEach(function (o) { o.classList.remove('is-active'); });
                if (input.checked) opt.classList.add('is-active');
            });
        });
    });
})();

/* ============================================
   RSS feed handlers
   ============================================ */
(function () {
    'use strict';

    // 共通: AJAX POST
    // サーバー側の check_ajax_referer( NONCE, 'nonce' ) と整合するフィールド名 'nonce' を使う。
    // 旧実装は '_ajax_nonce' を使っており、サーバー側のフィールド名指定省略 (= デフォルトの
    // _ajax_nonce / _wpnonce のフォールバック) に依存していた。プラグイン全体で
    // フィールド名を 'nonce' に統一 (C5)。
    function aiapRssPost(action, payload, onSuccess, onError) {
        var body = new URLSearchParams();
        body.set('action', action);
        // Bug fix: 旧実装は document 全体から最初の _wpnonce を拾っていたため、他プラグインが
        // WP 管理画面に挿入したフォームの異なるアクション nonce を誤って拾うリスクがあった。
        // AIAP のラッパ (.aiap-wrap) 内に限定して検索する。
        var nonce = document.querySelector('.aiap-wrap input[name="_wpnonce"]')
                 || document.querySelector('input[name="_wpnonce"]');
        if (nonce) { body.set('nonce', nonce.value); }
        Object.keys(payload || {}).forEach(function (k) {
            body.set(k, payload[k]);
        });
        fetch(window.ajaxurl, {
            method: 'POST',
            credentials: 'same-origin',
            body: body
        }).then(function (r) { return r.json(); })
          .then(function (json) {
              if (json && json.success) {
                  if (onSuccess) { onSuccess(json.data || {}); }
              } else {
                  if (onError) { onError((json && json.data && json.data.message) || ((window.AIAP_I18N && AIAP_I18N.unknown_error) ? AIAP_I18N.unknown_error : 'Unknown error')); }
              }
          })
          .catch(function (e) {
              if (onError) { onError(String(e)); }
          });
    }

    // 接続テスト
    document.querySelectorAll('.js-aiap-rss-test').forEach(function (btn) {
        btn.addEventListener('click', function (ev) {
            ev.preventDefault();
            var url = btn.getAttribute('data-url');
            if (!url) {
                var form = btn.closest('form');
                var urlInput = form ? form.querySelector('input[name="url"]') : null;
                url = urlInput ? urlInput.value : '';
            }
            var scope = btn.closest('.aiap-rss-feed-card') || btn.closest('form') || document;
            var resultEl = scope.querySelector('.aiap-rss-test-result');
            if (resultEl) {
                resultEl.textContent = '...';
                resultEl.classList.remove('is-success', 'is-error');
            }
            aiapRssPost('aiap_test_rss_feed', { url: url },
                function (data) {
                    if (resultEl) {
                        resultEl.textContent = data.message || 'OK';
                        resultEl.classList.add('is-success');
                    }
                },
                function (msg) {
                    if (resultEl) {
                        resultEl.textContent = msg;
                        resultEl.classList.add('is-error');
                    }
                }
            );
        });
    });

    // 有効/無効トグル
    document.querySelectorAll('.js-aiap-rss-toggle-enabled').forEach(function (cb) {
        cb.addEventListener('change', function () {
            var card = cb.closest('.aiap-rss-feed-card');
            var feedId = card ? card.getAttribute('data-feed-id') : '';
            if (!feedId) { return; }
            aiapRssPost('aiap_rss_toggle_enabled', {
                feed_id: feedId,
                enabled: cb.checked ? 1 : 0
            }, function () {
                // AJAX 成功: タスクと同じく card / status pill の「無効化グレーアウト」表示を即時反映
                // (タスクは form submit + リロードで PHP 側がクラス付与するが、
                //  RSS は AJAX 設計のため DOM 同期を JS 側で行う必要がある)
                if (!card) { return; }
                var enabled = cb.checked;
                card.classList.toggle('is-enabled', enabled);
                card.classList.toggle('is-disabled', !enabled);
                var pill = card.querySelector('.aiap-provider-tile-status');
                if (pill) {
                    pill.classList.toggle('is-configured', enabled);
                    pill.classList.toggle('is-empty', !enabled);
                    var labelConfigured = (window.AIAP_I18N && AIAP_I18N.rss_status_configured) ? AIAP_I18N.rss_status_configured : 'Configured';
                    var labelNotSet     = (window.AIAP_I18N && AIAP_I18N.rss_status_not_set)     ? AIAP_I18N.rss_status_not_set     : 'Not set';
                    var newLabel = enabled ? labelConfigured : labelNotSet;
                    pill.textContent = newLabel;
                    pill.setAttribute('aria-label', newLabel);
                }
            }, function (msg) {
                cb.checked = !cb.checked; // 失敗時は戻す (DOM クラスは成功時のみ変更するので rollback 不要)
                window.alert(msg);
            });
        });
    });

    // タスクカードの有効/無効トグル: change で内包フォームを submit する
    // (既存の hidden input + nonce 経由で admin-post.php の aiap_toggle_task ハンドラを呼ぶ)
    document.querySelectorAll('.js-aiap-task-toggle-cb').forEach(function (cb) {
        cb.addEventListener('change', function () {
            var form = cb.closest('form');
            if (form) { form.submit(); }
        });
    });

    // 履歴クリア
    document.querySelectorAll('.js-aiap-rss-purge-history').forEach(function (btn) {
        btn.addEventListener('click', function (ev) {
            ev.preventDefault();
            if (!window.confirm((window.AIAP_I18N && AIAP_I18N.rss_purge_history_confirm) ? AIAP_I18N.rss_purge_history_confirm : 'Purge history for this feed?')) { return; }
            var feedId = btn.getAttribute('data-feed-id');
            aiapRssPost('aiap_rss_purge_history', { feed_id: feedId },
                function (data) {
                    var aiapPurgedTpl = (window.AIAP_I18N && AIAP_I18N.rss_purged_result) ? AIAP_I18N.rss_purged_result : 'Purged %d guids.';
                    window.alert(aiapPurgedTpl.replace('%d', (data.purged || 0)));
                    window.location.reload();
                },
                function (msg) {
                    window.alert(msg);
                }
            );
        });
    });

    // 削除確認（admin-post.php への submit は通常フォーム）
    document.querySelectorAll('.js-aiap-rss-delete-confirm').forEach(function (btn) {
        btn.addEventListener('click', function (ev) {
            if (!window.confirm((window.AIAP_I18N && AIAP_I18N.rss_delete_feed_confirm) ? AIAP_I18N.rss_delete_feed_confirm : 'Delete this feed and its history?')) {
                ev.preventDefault();
            }
        });
    });

    // 編集モード切替: 「編集」クリックでカードに .is-editing を付与
    document.querySelectorAll('.js-aiap-rss-edit').forEach(function (btn) {
        btn.addEventListener('click', function (ev) {
            ev.preventDefault();
            var card = btn.closest('.aiap-rss-feed-card');
            if (!card) { return; }
            card.classList.add('is-editing');
            // 表示名入力にフォーカスを当てて操作しやすく
            var nameInput = card.querySelector('.aiap-rss-feed-edit-form input[name="name"]');
            if (nameInput) { nameInput.focus(); }
        });
    });

    // 編集キャンセル: フォームを初期値に戻して閲覧モードへ
    document.querySelectorAll('.js-aiap-rss-edit-cancel').forEach(function (btn) {
        btn.addEventListener('click', function (ev) {
            ev.preventDefault();
            var card = btn.closest('.aiap-rss-feed-card');
            if (!card) { return; }
            var form = card.querySelector('.aiap-rss-feed-edit-form');
            if (form) { form.reset(); }
            card.classList.remove('is-editing');
        });
    });

    // RSS Use チェックの変化をアコーディオンヘッダの「Configured」バッジに反映する。
    // フォーム未保存でも、チェック ON にした瞬間にヘッダがバッジ付きになる UX 改善 v2.1。
    // 詳細項目自体は常時表示するため、ここでは show/hide は行わない。
    function aiapToggleRssConfiguredBadge(cb) {
        var details = cb.closest('details.aiap-task-section');
        if (!details) { return; }
        var summary = details.querySelector('summary.aiap-task-section__summary');
        if (!summary) { return; }
        var existing = summary.querySelector('.aiap-task-section__configured-badge');
        if (cb.checked) {
            if (!existing) {
                var label = details.getAttribute('data-aiap-configured-label') || 'Configured';
                var badge = document.createElement('span');
                badge.className = 'aiap-task-section__configured-badge';
                badge.textContent = label;
                var title = summary.querySelector('.aiap-task-section__title');
                if (title && title.parentNode) {
                    title.parentNode.insertBefore(badge, title.nextSibling);
                }
            }
        } else if (existing) {
            existing.parentNode.removeChild(existing);
        }
    }
    document.querySelectorAll('.js-aiap-task-rss-enable').forEach(function (cb) {
        cb.addEventListener('change', function () {
            aiapToggleRssConfiguredBadge(cb);
        });
    });

    // ログクリア確認: HTML 側の onclick="return confirm(...)" を撤去し、
    // data-confirm-message 属性に格納したメッセージで window.confirm を発火する。
    document.querySelectorAll('.js-confirm-clear-logs').forEach(function (el) {
        el.addEventListener('click', function (event) {
            var message = el.getAttribute('data-confirm-message') || '';
            if (message && !window.confirm(message)) {
                event.preventDefault();
            }
        });
    });
    function initMobileActionMenus() {
        document.addEventListener('toggle', function (event) {
            var menu = event.target.closest && event.target.closest('.js-aiap-mobile-action-menu');
            if (!menu) {
                return;
            }
            var row = menu.closest('tr');
            if (menu.open) {
                document.querySelectorAll('.js-aiap-mobile-action-menu[open]').forEach(function (other) {
                    if (other !== menu) {
                        other.open = false;
                        var otherRow = other.closest('tr');
                        if (otherRow) {
                            otherRow.classList.remove('aiap-mobile-action-row-open');
                        }
                    }
                });
                if (row) {
                    row.classList.add('aiap-mobile-action-row-open');
                }
            } else if (row) {
                row.classList.remove('aiap-mobile-action-row-open');
            }
        }, true);

        document.addEventListener('click', function (event) {
            if (event.target.closest('.js-aiap-mobile-action-menu')) {
                return;
            }
            document.querySelectorAll('.js-aiap-mobile-action-menu[open]').forEach(function (menu) {
                menu.open = false;
                var row = menu.closest('tr');
                if (row) {
                    row.classList.remove('aiap-mobile-action-row-open');
                }
            });
        });
    }

    initMobileActionMenus();

    // ================================================================
    // 投稿完了通知の送信テストボタン (= 設定画面の「送信テスト」)
    //
    // 保存済みの設定値を使って AIAP_Notifier::send_test_email を呼び出す。
    // 結果を inline でフィードバック表示する (成功=緑、失敗=赤)。
    // ================================================================
    function initNotifyTestButton() {
        document.addEventListener('click', function (event) {
            var btn = event.target.closest('#aiap-notify-test-btn');
            if (!btn) return;
            event.preventDefault();
            if (btn.disabled) return;

            var nonce        = btn.getAttribute('data-nonce') || '';
            var labelIdle    = btn.getAttribute('data-label-idle') || '';
            var labelSending = btn.getAttribute('data-label-sending') || '';
            var feedback     = document.getElementById('aiap-notify-test-feedback');
            // dashicons + ラベル のテンプレート (= ボタン HTML の再構築用)
            var iconHtml     = '<span class="dashicons dashicons-email-alt" aria-hidden="true"></span> ';

            btn.disabled  = true;
            btn.innerHTML = iconHtml + labelSending;
            if (feedback) {
                feedback.textContent = '';
                feedback.style.color = '';
            }

            // フォーム全体の値を AJAX に乗せる (= ユーザーが入力中の内容でテスト送信できる)。
            // ボタンが属する <form> から FormData を作成する。これにより smtp_host / smtp_port / smtp_auth /
            // smtp_username / smtp_password / smtp_from_email / smtp_from_name / enabled / email すべてが
            // 一度に送られる。サーバ側は AIAP_Notifier::normalize_input → runtime override で扱う。
            var form = btn.closest('form');
            var data = form ? new FormData(form) : new FormData();
            data.set('action', 'aiap_notify_test');
            data.set('nonce',  nonce);

            fetch(window.ajaxurl || '/wp-admin/admin-ajax.php', {
                method: 'POST',
                credentials: 'same-origin',
                body: data
            }).then(function (r) {
                return r.text().then(function (txt) {
                    var json = null;
                    try { json = JSON.parse(txt.replace(/^\uFEFF/, '').trim()); } catch (e) {}
                    return { status: r.status, json: json };
                });
            }).then(function (payload) {
                var json = payload.json;
                if (!feedback) return;
                if (json && json.success) {
                    feedback.textContent = '✓ ' + (json.data && json.data.message ? json.data.message : '');
                    feedback.style.color = '#00a32a';
                } else {
                    var msg = (json && json.data && json.data.message) ? json.data.message : ('HTTP ' + payload.status);
                    feedback.textContent = '✗ ' + msg;
                    feedback.style.color = '#d63638';
                }
            }).catch(function (err) {
                if (!feedback) return;
                feedback.textContent = '✗ ' + ((err && err.message) ? err.message : ((window.AIAP_I18N && AIAP_I18N.network_error) ? AIAP_I18N.network_error : 'Network error'));
                feedback.style.color = '#d63638';
            }).then(function () {
                // finally の代わりに then(同じ関数) で fulfill/reject 両方を拾う
                // (= IE / 古い Safari の Promise.finally 非サポート対策、既存コードと整合)
                btn.disabled  = false;
                btn.innerHTML = iconHtml + labelIdle;
            });
        });
    }

    initNotifyTestButton();

})();
