<?php
if ( ! defined( 'ABSPATH' ) ) exit;

/**
 * 管理画面コントローラ
 *
 * Nonce 検証パターンの注記:
 *   - すべての form ハンドラ (handle_*) は冒頭で self::verify() を呼び、
 *     check_admin_referer( self::NONCE ) で nonce を検証する。
 *   - すべての AJAX ハンドラ (ajax_*) は冒頭で self::ajax_verify() か
 *     check_ajax_referer( self::NONCE, 'nonce' ) を呼ぶ。
 *   - render_*() 等の表示関数で $_GET を読む箇所は管理画面 URL パラメータの
 *     表示用途のみ (e.g. ?tab=settings)。current_user_can( 'manage_options' )
 *     のチェックを併用しており、書き込みは行わないため nonce は不要。
 *
 * 入力サニタイズパターンの注記:
 *   - 文字列: sanitize_text_field( wp_unslash( $_POST['x'] ?? '' ) )
 *   - 整数:   absint( wp_unslash( $_POST['x'] ?? 0 ) )
 *   - 配列:   parse_schedule_from_form() などで型・キー・値を逐一検証
 *   - 列挙:   in_array( $value, $allowed_values, true ) で許可リスト検証
 *
 * Plugin Check はこれらの呼び出し連鎖を静的解析できないため、誤検出を回避する
 * phpcs ディレクティブをファイル全体に適用する。実際のサニタイズは各箇所で
 * 上記パターンに従って行われている。
 */
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce verified via verify()/ajax_verify()
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only $_GET access for admin page rendering
// phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization performed inline (absint/sanitize_text_field) and via internal validators

class AIAP_Admin {
    const MENU_SLUG = 'ai-auto-poster';
    const NONCE     = 'aiap_admin_nonce';

    private static $prov_icons = array(
        'claude'     => array( 'ico' => 'ico-claude',     'letter' => 'C',  'label' => 'Claude (Anthropic)' ),
        'openai'     => array( 'ico' => 'ico-openai',     'letter' => 'O',  'label' => 'OpenAI (ChatGPT)' ),
        'gemini'     => array( 'ico' => 'ico-gemini',     'letter' => 'G',  'label' => 'Google Gemini' ),
        'mistral'    => array( 'ico' => 'ico-mistral',    'letter' => 'M',  'label' => 'Mistral AI' ),
        'groq'       => array( 'ico' => 'ico-groq',       'letter' => 'G',  'label' => 'Groq' ),
        'deepseek'   => array( 'ico' => 'ico-deepseek',   'letter' => 'D',  'label' => 'DeepSeek' ),
        'openrouter' => array( 'ico' => 'ico-openrouter', 'letter' => 'R',  'label' => 'OpenRouter' ),
    );

    private static $prov_urls = array(
        'claude'     => 'https://console.anthropic.com/',
        'openai'     => 'https://platform.openai.com/api-keys',
        'gemini'     => 'https://aistudio.google.com/apikey',
        'mistral'    => 'https://console.mistral.ai/api-keys',
        'groq'       => 'https://console.groq.com/',
        'deepseek'   => 'https://platform.deepseek.com/',
        'openrouter' => 'https://openrouter.ai/keys',
    );

    // 画像プロバイダのアイコンメタ。label は resolve_dynamic_key() で取得
    private static $img_icons = array(
        'dalle3'    => array( 'ico' => 'ico-dalle',     'letter' => 'D',  'label_key' => 'imgprov_dalle3' ),
        'imagen'    => array( 'ico' => 'ico-imagen',    'letter' => 'I',  'label_key' => 'imgprov_imagen' ),
        'stability' => array( 'ico' => 'ico-stability', 'letter' => 'S',  'label_key' => 'imgprov_stability' ),
        'flux'      => array( 'ico' => 'ico-flux',      'letter' => 'F',  'label_key' => 'imgprov_flux' ),
        'unsplash'  => array( 'ico' => 'ico-unsplash',  'letter' => 'U',  'label_key' => 'imgprov_unsplash' ),
        'pixabay'   => array( 'ico' => 'ico-pixabay',   'letter' => 'P',  'label_key' => 'imgprov_pixabay' ),
        'pexels'    => array( 'ico' => 'ico-pexels',    'letter' => 'Px', 'label_key' => 'imgprov_pexels' ),
        'openverse' => array( 'ico' => 'ico-openverse', 'letter' => 'Ov', 'label_key' => 'imgprov_openverse' ),
    );

    // 画像フォーマット(アスペクト比)のキー一覧。表示は resolve_dynamic_key() で取得
    private static $image_format_keys = array( '16:9', '1:1', '9:16', '4:3' );

    // 画像品質のキー一覧。表示は resolve_dynamic_key() で取得
    private static $image_quality_keys = array( 'low', 'medium', 'high' );

    // テキスト AI プロバイダ → APIキー保存先 field 名
    private static $text_provider_key_field = array(
        'claude'     => 'claude_api_key',
        'openai'     => 'openai_api_key',
        'gemini'     => 'gemini_api_key',
        'mistral'    => 'mistral_api_key',
        'groq'       => 'groq_api_key',
        'deepseek'   => 'deepseek_api_key',
        'openrouter' => 'openrouter_api_key',
    );

    // テキスト AI プロバイダ → モデル名保存先 field 名
    private static $text_provider_model_field = array(
        'claude'     => 'claude_model',
        'openai'     => 'openai_model',
        'gemini'     => 'gemini_model',
        'mistral'    => 'mistral_model',
        'groq'       => 'groq_model',
        'deepseek'   => 'deepseek_model',
        'openrouter' => 'openrouter_model',
    );

    // 画像 AI プロバイダ → APIキー保存先 field 名
    private static $image_provider_key_field = array(
        'dalle3'    => 'image_openai_api_key',
        'imagen'    => 'image_gemini_api_key',
        'stability' => 'stability_api_key',
        'flux'      => 'flux_api_key',
        'unsplash'  => 'unsplash_access_key',
        'pixabay'   => 'pixabay_api_key',
        'pexels'    => 'pexels_api_key',
    );

    public static function init() {
        add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) );
        $actions = array( 'save_settings','save_provider','save_image_provider','save_image_settings','save_system_prompt','add_task','edit_task','toggle_task','delete_task','clear_logs','delete_generated_post','wizard_complete' );
        foreach ( $actions as $a ) add_action( 'admin_post_aiap_' . $a, array( __CLASS__, 'handle_' . $a ) );
        // Phase 3: 「ウィザードをスキップ」 → 完了フラグを立てる
        add_action( 'admin_init', array( __CLASS__, 'handle_wizard_skip' ) );
        add_action( 'wp_ajax_aiap_test_provider', array( __CLASS__, 'ajax_test_provider' ) );
        add_action( 'wp_ajax_aiap_run_now_start', array( __CLASS__, 'ajax_run_now_start' ) );
        add_action( 'wp_ajax_aiap_run_now_retry', array( __CLASS__, 'ajax_run_now_retry' ) );
        add_action( 'wp_ajax_aiap_run_now_step',  array( __CLASS__, 'ajax_run_now_step' ) );
        add_action( 'wp_ajax_aiap_refresh_models', array( __CLASS__, 'ajax_refresh_models' ) );
        add_action( 'wp_ajax_aiap_styles_for_model', array( __CLASS__, 'ajax_styles_for_model' ) );
        add_action( 'wp_ajax_aiap_toggle_enabled', array( __CLASS__, 'ajax_toggle_enabled' ) );
        add_action( 'admin_post_aiap_save_rss_feed',     array( __CLASS__, 'handle_save_rss_feed' ) );
        add_action( 'admin_post_aiap_delete_rss_feed',   array( __CLASS__, 'handle_delete_rss_feed' ) );
        add_action( 'wp_ajax_aiap_rss_toggle_enabled',   array( __CLASS__, 'ajax_rss_toggle_enabled' ) );
        add_action( 'wp_ajax_aiap_test_rss_feed',        array( __CLASS__, 'ajax_test_rss_feed' ) );
        add_action( 'wp_ajax_aiap_rss_purge_history',    array( __CLASS__, 'ajax_rss_purge_history' ) );
        add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
        // Bug fix (audit r9): AUTH_KEY が未設定/空の環境では Crypto が暗号化できず、設定保存時に
        // API キーが空文字に置換される (= 保存→再読み込みでキーが消える) 挙動になる。これに気付か
        // ないユーザーが多いため、AIAP の管理画面でだけ警告 notice を出して再設定の必要性を伝える。
        add_action( 'admin_notices', array( __CLASS__, 'maybe_render_auth_key_warning' ) );
    }

    /**
     * AUTH_KEY 未定義環境向けの管理画面 notice。
     *
     * AIAP 管理画面のみで表示する (他のページに出すと全管理者を混乱させる)。
     * dismissible にして頻繁に再表示されないようにする。
     */
    public static function maybe_render_auth_key_warning() {
        // current_user_can で権限をチェック
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        // AIAP 配下のページのみで出す (screen ID は menu slug を含む)
        $screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
        if ( ! $screen || strpos( (string) $screen->id, self::MENU_SLUG ) === false ) {
            return;
        }
        // AUTH_KEY と SECURE_AUTH_KEY のいずれかが空 / 未定義なら警告
        $auth_missing = ! defined( 'AUTH_KEY' ) || AUTH_KEY === '';
        if ( ! $auth_missing ) {
            return;
        }
        ?>
        <div class="notice notice-error is-dismissible">
            <p>
                <strong><?php echo esc_html( __( 'AI Auto Poster: AUTH_KEY is not configured', 'ai-auto-poster' ) ); ?></strong>
            </p>
            <p>
                <?php echo esc_html( __( 'WordPress security keys (AUTH_KEY) are missing in wp-config.php. Without them, AI Auto Poster cannot encrypt your API keys at rest, so saving settings will silently clear them. Generate fresh keys at https://api.wordpress.org/secret-key/1.1/salt/ and paste them into wp-config.php, then re-enter your API keys.', 'ai-auto-poster' ) ); ?>
            </p>
        </div>
        <?php
    }

    public static function register_menu() {
        add_menu_page( 'AI Auto Poster', 'AI Auto Poster', 'manage_options', self::MENU_SLUG, array( __CLASS__, 'render_page' ), 'dashicons-edit-large', 30 );
        add_submenu_page(
            self::MENU_SLUG,
            __( 'FAQ', 'ai-auto-poster' ),
            __( 'FAQ', 'ai-auto-poster' ),
            'manage_options',
            self::MENU_SLUG . '-faq',
            array( __CLASS__, 'render_faq_page' )
        );
        add_submenu_page(
            self::MENU_SLUG,
            __( 'Terms of Use', 'ai-auto-poster' ),
            __( 'Terms of Use', 'ai-auto-poster' ),
            'manage_options',
            self::MENU_SLUG . '-terms',
            array( __CLASS__, 'render_terms_page' )
        );
    }

    /**
     * モダンテーマ overlay 有効化のため、admin <body> に aiap-theme-modern クラスを追加。
     * enqueue_assets 内で add_filter() されたときだけ呼ばれる (= プラグイン管理画面のみ)。
     */
    public static function add_modern_body_class( $classes ) {
        return $classes . ' aiap-theme-modern';
    }

    public static function enqueue_assets( $hook ) {
        // プラグイン一覧画面では Upgrade リンク用のスタイルだけ読み込んで終了
        if ( $hook === 'plugins.php' ) {
            $css_path = AIAP_PLUGIN_DIR . 'assets/admin.css';
            $css_ver  = file_exists( $css_path ) ? AIAP_VERSION . '.' . filemtime( $css_path ) : AIAP_VERSION;
            wp_enqueue_style( 'aiap-admin', AIAP_PLUGIN_URL . 'assets/admin.css', array(), $css_ver );
            return;
        }
        if ( strpos( $hook, self::MENU_SLUG ) === false ) return;
        // ファイル更新時刻をバージョン文字列に含めることで、AIAP_VERSION を変えなくてもアセットが更新される
        $css_path        = AIAP_PLUGIN_DIR . 'assets/admin.css';
        $css_modern_path = AIAP_PLUGIN_DIR . 'assets/admin-modern.css';
        $js_path         = AIAP_PLUGIN_DIR . 'assets/admin.js';
        $css_ver         = file_exists( $css_path ) ? AIAP_VERSION . '.' . filemtime( $css_path ) : AIAP_VERSION;
        $css_modern_ver  = file_exists( $css_modern_path ) ? AIAP_VERSION . '.' . filemtime( $css_modern_path ) : AIAP_VERSION;
        $js_ver          = file_exists( $js_path )  ? AIAP_VERSION . '.' . filemtime( $js_path )  : AIAP_VERSION;
        wp_enqueue_style( 'aiap-admin', AIAP_PLUGIN_URL . 'assets/admin.css', array(), $css_ver );
        // モダンテーマ overlay (Linear / Stripe 風) - admin.css の後に読み込んで主要トークンを上書き
        wp_enqueue_style( 'aiap-admin-modern', AIAP_PLUGIN_URL . 'assets/admin-modern.css', array( 'aiap-admin' ), $css_modern_ver );
        wp_enqueue_script( 'aiap-admin', AIAP_PLUGIN_URL . 'assets/admin.js', array(), $js_ver, true );

        // モダンテーマ用の body class を付与 (admin-modern.css の :root.aiap-theme-modern スコープが効くようにする)
        add_filter( 'admin_body_class', array( __CLASS__, 'add_modern_body_class' ) );

        // モデル変更時のスタイル問い合わせ用の情報を渡す
        wp_localize_script( 'aiap-admin', 'AIAP_STYLE_MATRIX', array(
            /* translators: %s = style name (e.g. "Watercolor") that the current model does not support */
            'unsupported_text' => __( 'Selected style "%s" is not supported by the current model. It will be reset to default on save.', 'ai-auto-poster' ),
        ) );

        // Bug fix (audit r9 self-audit): JS 側でハードコードされていたユーザー向け
        // メッセージ ('Network error', 'No response', 'Are you sure?') を i18n 経由で渡す。
        // wp_localize_script で AIAP_I18N オブジェクトを定義し、JS は AIAP_I18N.* を参照する。
        // これにより日本語等のロケールに切り替えたときも、フォールバックメッセージが
        // 適切に翻訳される。
        wp_localize_script( 'aiap-admin', 'AIAP_I18N', array(
            'network_error'    => __( 'Network error', 'ai-auto-poster' ),
            'no_response'      => __( 'No response', 'ai-auto-poster' ),
            'are_you_sure'     => __( 'Are you sure?', 'ai-auto-poster' ),
            'discard_changes'  => __( 'Discard changes?', 'ai-auto-poster' ),
            'server_error'     => __( 'Server error (HTTP %d)', 'ai-auto-poster' ),
            'ok'               => __( 'OK', 'ai-auto-poster' ),
            'ok_saved_key'     => __( 'OK (saved key)', 'ai-auto-poster' ),
            'saved_key_prefix' => __( '[saved key]', 'ai-auto-poster' ),
            // #49: 除外時間帯の動的追加行 (initRandomMode で利用)
            'remove'           => __( 'Remove', 'ai-auto-poster' ),
            'add'              => __( '+ Add', 'ai-auto-poster' ),
        ) );
    }

    /**
     * FAQサブメニューを表示する
     *
     * FAQ本文は WordPress 標準の gettext 翻訳(.po/.mo)で管理する。
     * PHP内には英語の msgid のみを置き、日本語は languages/*.po/*.mo に分離する。
     */
    public static function render_faq_page() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        $sections = self::get_faq_items();
        ?>
        <div class="wrap aiap-wrap aiap-faq-wrap">
            <div class="aiap-page-head">
                <h1 class="aiap-page-title"><?php echo esc_html( __( 'FAQ', 'ai-auto-poster' ) ); ?></h1>
            </div>
            <div class="aiap-card aiap-card-accent">
                <p class="description"><?php echo esc_html( __( 'Find answers about plans, API keys, article generation, scheduling, retry behavior, images, SEO, logs, and data handling.', 'ai-auto-poster' ) ); ?></p>
            </div>
            <?php foreach ( $sections as $section ) : ?>
                <div class="aiap-card aiap-faq-section">
                    <h2 class="aiap-faq-section-title"><?php echo esc_html( $section['title'] ); ?></h2>
                    <div class="aiap-faq-list">
                        <?php foreach ( $section['items'] as $item ) : ?>
                            <details class="aiap-faq-item">
                                <summary><?php echo esc_html( $item['question'] ); ?></summary>
                                <p><?php echo esc_html( $item['answer'] ); ?></p>
                            </details>
                        <?php endforeach; ?>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
        <?php
    }

    /**
     * FAQ項目を返す
     *
     * @return array<int,array{title:string,items:array<int,array{question:string,answer:string}>}>
     */
    private static function get_faq_items() {
        return array(

            // ========================================
            // 1. Getting started
            // ========================================
            array(
                'title' => __( 'Getting started', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'What is the minimum setup to publish my first article?', 'ai-auto-poster' ),
                        'answer'   => __( 'Three steps: (1) Open the Providers tab and save an API key for one text AI provider. (2) Open the Tasks tab and create a task with a prompt. (3) Click Quick Post to generate immediately, or wait for the scheduled time. We recommend keeping the post status as Draft for the first few articles so you can review them before publishing.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Which AI provider should I choose first?', 'ai-auto-poster' ),
                        'answer'   => __( 'If you are not sure, Gemini Flash and Groq are good starting points because they offer free tiers and very low per-article cost. For higher article quality, Claude (Anthropic) and GPT-4 class models from OpenAI are popular choices. You can change the provider any time, so it is fine to start cheap and switch later.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Where do I get an API key?', 'ai-auto-poster' ),
                        'answer'   => __( 'Each provider issues its own API key. The Providers tab in this plugin links directly to the API key page of every supported provider. You will need to create an account with that provider, add a payment method (most providers require it even for free tiers), and copy the key into this plugin.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Will my article be published immediately, or can I review it first?', 'ai-auto-poster' ),
                        'answer'   => __( 'You decide. The post status setting has three options: Draft, Pending Review, and Publish. With Draft (the default and our recommendation for new users), every generated article waits in your draft list until you publish it manually. Switch to Publish only after you trust the output.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 2. AI providers and API keys
            // ========================================
            array(
                'title' => __( 'AI providers and API keys', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'Are API keys stored securely?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. API keys are encrypted with AES-256-CBC using your WordPress AUTH_KEY before being saved in the database. Keys are never written to execution logs and are never displayed in the admin UI after saving.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'The Test button fails. What should I check?', 'ai-auto-poster' ),
                        'answer'   => __( 'Check four things in order: (1) The API key is correct and still active in the provider dashboard. (2) Your provider account has remaining credit or quota - running out of credit is the most common cause. (3) The selected model is currently available on your account. (4) Your server can reach the provider (some hosting environments block outbound HTTPS). If a key has been saved before, the test uses the saved key even when the field is blank.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I use multiple AI providers at the same time?', 'ai-auto-poster' ),
                        'answer'   => __( 'You cannot switch providers per task. The plugin uses one active text AI provider and one active image provider site-wide. You can change the active provider in the Providers tab at any time, and saved API keys for inactive providers are kept so switching back is instant.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'I want to use a newer model that is not in the dropdown.', 'ai-auto-poster' ),
                        'answer'   => __( 'Enter the model ID directly in the custom model field. The custom model value takes priority over whatever is selected in the dropdown, so you can always use the latest model the moment a provider releases it.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'The settings page says my selected model is unavailable.', 'ai-auto-poster' ),
                        'answer'   => __( 'A model that has been deprecated or removed by the provider is still selected. When you save the settings, the plugin will automatically switch to a working replacement model. You can also pick a different model manually before saving.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'I have credits but generation still fails.', 'ai-auto-poster' ),
                        'answer'   => __( 'Common causes: a per-minute rate limit on the provider side (wait a few minutes and retry), a model that requires a specific account tier, a regional restriction, or a network timeout. The execution log records the provider response, which usually points to the exact reason.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 3. Article generation
            // ========================================
            array(
                'title' => __( 'Article generation', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'How should I write prompts to get better articles?', 'ai-auto-poster' ),
                        'answer'   => __( 'The more specifically you describe the target reader, the writing style, the tone, and the structure you expect, the more consistent the result. A short generic prompt like "Write about coffee" produces generic articles. A specific prompt like "Write a 1500-word article for beginner home baristas, friendly tone, with three practical tips and one common mistake" produces much better output. Adjust prompts gradually while reviewing what you get.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Are generated articles published automatically?', 'ai-auto-poster' ),
                        'answer'   => __( 'Only if you set the post status to Publish. The default is Draft, which is the safest option while you are tuning your prompts. With Pending Review, articles wait in the editorial queue. With Publish, articles go live immediately at the scheduled time.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I specify the category and author for generated articles?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. Both can be set in the General Settings section. All articles created by the plugin will use that category and that author. If no category is specified, articles go into the default WordPress category.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I generate articles in English or other non-Japanese languages?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. About 50 languages are supported. Set the Content Language option per task. Even if your prompt is written in a different language, the article is generated in the language you select. A single site can publish in multiple languages by using different tasks.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I control the article length?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. Each task has a Content Length setting (in characters). Set 0 to let the AI decide based on the prompt. Set a specific number to instruct the AI to target that length. Note that AI providers do not always hit the exact target - treat it as a guideline rather than a hard limit.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Will the same prompt always produce the same article?', 'ai-auto-poster' ),
                        'answer'   => __( 'No. AI models are non-deterministic, so the same prompt produces different articles each time. This is normal and is what makes a daily schedule work - you do not need to vary the prompt to avoid repetition. The duplicate prevention feature exists for cases where the variations are still too similar to each other.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I control the writing tone or style?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes, through the prompt itself. Add instructions like "professional and concise tone," "friendly and conversational," or "academic style with citations" to your prompt. Different AI models respond to style instructions with different fidelity - Claude and GPT-4 class models tend to follow style cues most closely.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 4. Scheduling
            // ========================================
            array(
                'title' => __( 'Scheduling', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'WP-Cron does not seem to be running.', 'ai-auto-poster' ),
                        'answer'   => __( 'WordPress built-in WP-Cron only runs when someone visits the site, so on low-traffic sites it can be delayed or skipped. For reliable execution at the scheduled time, we recommend a real system cron that calls wp-cron.php every few minutes. Most managed WordPress hosts already do this - check with your host if you are unsure.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Will scheduled posting work when my computer is turned off?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. WordPress runs on the server, not on your computer. As long as the server is online, scheduled posting works regardless of whether your computer is on or off.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Which time zone is used for scheduled times?', 'ai-auto-poster' ),
                        'answer'   => __( 'The time zone configured in WordPress Settings > General > Timezone. This is independent of your server time zone. If your scheduled times look wrong, check that setting first.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'A post was not published at the scheduled time.', 'ai-auto-poster' ),
                        'answer'   => __( 'If missed schedule recovery is enabled, missed schedules are retried the next time WP-Cron runs. The plugin restores the original scheduled date and time on the recovered post, so your archive looks the same as if the schedule had not been missed. Recovery can be turned off if you prefer to skip missed slots entirely.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'How precisely can I schedule? Can I post on weekdays only, or every hour?', 'ai-auto-poster' ),
                        'answer'   => __( 'Each task has two scheduling modes. Fixed mode: a day-of-week selector (any combination of Monday through Sunday) and time slots in HH:MM format. You can configure weekdays-only schedules, weekend-only schedules, or any mix. Random mode: the plugin picks N to M random times within the day, with optional allowed hours and a minimum gap between posts. Both modes can be combined across different tasks. The plugin checks for scheduled tasks every 5 minutes, which is also the minimum granularity for reliable execution.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I post multiple articles per day?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. In fixed mode, a single task can have multiple time slots per day, and you can have multiple tasks running in parallel. In random mode, a single task can be configured to post N to M times per day automatically at random times. Be mindful of API costs and the Free plan monthly limit (10 posts) when scheduling high-frequency posting.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What happens if multiple tasks are scheduled at the same time?', 'ai-auto-poster' ),
                        'answer'   => __( 'They are executed sequentially within the same WP-Cron tick. Because each task waits for the AI response, total execution time can be several minutes when many tasks fire together. Spreading task times by a few minutes makes execution smoother.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What is random scheduling and how does it work?', 'ai-auto-poster' ),
                        'answer'   => __( 'Random mode lets a task post a randomized number of articles (between a minimum and a maximum you set) at randomized times throughout the day. Each day at 0:00 the plugin draws a fresh count and time list for that day. Times are picked on a 5-minute grid. This is useful when you want output to feel less mechanical than a fixed daily schedule. Time slots that have already passed for today are kept as-is when you edit task settings, so already-posted articles are never duplicated.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I restrict random posts to certain hours?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. The allowed hours option in random mode lets you define one or more time windows (for example 09:00 to 18:00) within which random times must fall. If you leave it empty, the entire 24-hour day is eligible. You can also set a minimum gap (in minutes) so two random posts will not fall too close together.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Why does the planned post count change after I edit a random task?', 'ai-auto-poster' ),
                        'answer'   => __( 'When you change random-mode settings (min, max, allowed hours, or minimum gap), the plugin redraws the count and refreshes future time slots for today using the new settings. Past time slots for today are kept unchanged so already-posted articles are preserved. Because the count is randomized within your min and max, two consecutive edits can produce different planned totals even if you did not change the min/max values.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Random mode for tomorrow shows no upcoming posts. Is that a bug?', 'ai-auto-poster' ),
                        'answer'   => __( 'No. Random times for a given day are generated only on that day (at 0:00 or on first access). The dashboard cannot show specific upcoming times for future days because they have not been drawn yet. Fixed-mode tasks, by contrast, do display upcoming times for future days.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 5. Featured images
            // ========================================
            array(
                'title' => __( 'Featured images', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'Featured images are not being added to my posts.', 'ai-auto-poster' ),
                        'answer'   => __( 'Open the Image Provider tab, choose a provider, save its API key, and make sure it is set to in use. Without an active image provider, posts are created without a featured image. Stock providers (Unsplash, Pexels, Pixabay, Openverse) search existing photos; AI providers (DALL-E, Imagen, Stability, Flux/fal.ai) generate new images.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'AI image generation vs stock photos - which should I choose?', 'ai-auto-poster' ),
                        'answer'   => __( 'Stock photos are essentially free and have predictable quality, but you are limited to what already exists in the library. AI generation gives you exactly what you asked for and a unique image every time, but costs money per image and quality varies. A practical approach: use stock for general topics and AI generation for niche or branded content.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'How much do AI images cost compared to text generation?', 'ai-auto-poster' ),
                        'answer'   => __( 'AI image generation is usually significantly more expensive per item than text generation - sometimes 10x or more. Check the provider pricing page for current rates. If cost is a concern, stock image providers are essentially free aside from API account requirements.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Who owns the copyright of generated or searched images?', 'ai-auto-poster' ),
                        'answer'   => __( 'Rules differ by provider and you should review each provider terms before commercial use. As a general guide: most AI image providers grant commercial use rights to the user who generated the image. Stock providers each have their own license - Unsplash, Pexels, and Pixabay are typically free for commercial use with attribution requirements that vary; Openverse aggregates Creative Commons content with per-image license differences.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Can I customize image size, aspect ratio, or style?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. The Image Provider tab has size, aspect ratio (where supported by the provider), and style options. Available styles depend on the model - for example, DALL-E 3 supports vivid and natural; Flux and Imagen have their own preset lists. The plugin shows only the options the selected model actually supports.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'The featured image often does not match the article content.', 'ai-auto-poster' ),
                        'answer'   => __( 'Image relevance depends on the prompt the plugin sends to the image API, which is derived from the article title and a short excerpt. If results feel off, try a more descriptive article title, switch to a different image model, or use a stock provider where you can search by keyword. AI image models also vary widely in how literal their interpretation is.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 6. SEO features
            // ========================================
            array(
                'title' => __( 'SEO features', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'Does this plugin integrate with Yoast SEO, Rank Math, or All in One SEO?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. With the SEO plugin link option enabled, this plugin writes generated meta descriptions, focus keywords, and SEO titles into the fields used by Yoast SEO, Rank Math, and All in One SEO. Your existing SEO plugin remains the source of truth for site-wide SEO settings.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What SEO features does this plugin add to my posts?', 'ai-auto-poster' ),
                        'answer'   => __( 'When the SEO section is enabled, the plugin can add: meta description, focus keyword, JSON-LD article schema, OGP tags, canonical URL, reading time, automatic internal links to related posts, related-posts block, table of contents, FAQ sections, and image alt text. Each item can be turned on or off individually.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Are SEO features available on the Free plan?', 'ai-auto-poster' ),
                        'answer'   => __( 'No. All SEO features are part of the Pro plan. On the Free plan, articles are created without schema, OGP, or other SEO enhancements. The plan card on the dashboard shows your current plan.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What is llms.txt and should I enable it?', 'ai-auto-poster' ),
                        'answer'   => __( 'llms.txt is a public text file that tells AI crawlers which content on your site is available for them. Some site owners enable it to improve discoverability in AI search engines; others prefer not to expose this information. It is opt-in and off by default. The file is generated at the WordPress root only when this option is on.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What does the Sitemap ping option do?', 'ai-auto-poster' ),
                        'answer'   => __( 'When enabled, the plugin notifies the IndexNow protocol (used by Bing, Yandex, and others) that a new article has been published, so search engines can index it faster. IndexNow requires a key file placed in your wp-content directory; without it, ping is silently skipped. Google and most other engines no longer support sitemap ping and are not contacted.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 7. Duplicate prevention (Pro)
            // ========================================
            array(
                'title' => __( 'Duplicate prevention (Pro)', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'Is duplicate prevention available on the Free plan?', 'ai-auto-poster' ),
                        'answer'   => __( 'No. Duplicate prevention is a Pro-only feature. On the Free plan, every generated article is published as-is without similarity checks. If you publish frequently and want to avoid near-duplicate content, this is one of the main reasons to consider upgrading.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What do "Similarity avoidance succeeded" and "Retry requested" mean in the log?', 'ai-auto-poster' ),
                        'answer'   => __( 'They are messages from the duplicate prevention feature. The plugin compares each newly generated article against your recent posts using two methods: keyword overlap and optional AI scoring. When an article is judged too similar, regeneration is requested ("Retry requested"). When the regenerated article passes, it is logged as "Similarity avoidance succeeded."', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What is the difference between Loose, Balanced, and Strict?', 'ai-auto-poster' ),
                        'answer'   => __( 'They use different similarity thresholds. Strict rejects even articles that are only somewhat similar - safer for archives where uniqueness matters. Loose rejects only clearly duplicate articles - minimizes API usage. Balanced is the default and works well for most sites. Strict consumes more API calls because it requests more regenerations.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What happens if the article still does not pass after the maximum retries?', 'ai-auto-poster' ),
                        'answer'   => __( 'That run is skipped and recorded in the execution log. The next scheduled run is unaffected. The retry maximum is configurable from 1 to 10 (default 3); higher values increase the chance of producing a unique article but also increase API cost.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 7b. RSS feeds (Pro)
            // ========================================
            array(
                'title' => __( 'RSS feeds (Pro)', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'What does the RSS feature do?', 'ai-auto-poster' ),
                        'answer'   => __( 'Per-task RSS integration lets the plugin fetch the latest articles from RSS feeds you register, and pass them to the AI as reference material when generating a new post. Useful for news commentary, curation sites, or topical roundups where you want articles grounded in current external sources rather than the AI writing from general knowledge alone.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Is RSS available on the Free plan?', 'ai-auto-poster' ),
                        'answer'   => __( 'No. RSS feed integration is a Pro-only feature. On the Free plan the RSS settings are visible but cannot be activated.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What is the difference between pick mode and digest mode?', 'ai-auto-poster' ),
                        'answer'   => __( 'Pick mode: the AI is shown several candidate articles and chooses the single most interesting one to write about. Digest mode: the AI is shown several articles and writes one cross-topic summary that covers all of them. Pick produces focused single-topic articles; digest produces broader overview articles.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'How many RSS feeds can I register?', 'ai-auto-poster' ),
                        'answer'   => __( 'Up to 30 feeds total. Per task you can select which feeds it should read from and how many candidate articles to pass to the AI (1 to 10).', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'How does the plugin avoid using the same article twice?', 'ai-auto-poster' ),
                        'answer'   => __( 'The plugin remembers the most recent 500 article identifiers (GUID/link) per feed. Articles already used are skipped automatically. If you delete the article history for a feed (or delete the feed itself), all of its identifiers are cleared and previously-used articles can be picked again.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What happens when there are no new (unused) articles in a feed?', 'ai-auto-poster' ),
                        'answer'   => __( 'Each task with RSS enabled has a fallback option. "Skip post" cancels that run entirely until new articles are available. "Generate without RSS" lets the task fall back to a normal AI-only article. Choose based on whether your editorial concept requires external sources.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Why do I sometimes see "AI returned invalid selected_rss_index" in the log?', 'ai-auto-poster' ),
                        'answer'   => __( 'In pick mode the AI is asked to return the number of the article it chose (between 1 and N). If the AI returns 0 or a number out of range, the plugin uses the first candidate as a safe fallback and logs a warning. The post itself is still created normally.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 8. Troubleshooting
            // ========================================
            array(
                'title' => __( 'Troubleshooting', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'I set up everything but no articles are being created.', 'ai-auto-poster' ),
                        'answer'   => __( 'Run through this checklist: (1) Is the task enabled (toggle in the Tasks tab)? (2) Is an API key saved and tested for an active provider? (3) Does your provider account have remaining credit? (4) Is WP-Cron running on your site? (5) Have you reached the Free plan monthly limit (10 posts)? The execution log usually shows which step failed.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'I see "Free plan monthly post limit reached" in the log.', 'ai-auto-poster' ),
                        'answer'   => __( 'The Free plan allows 10 posts per calendar month. The counter resets at 00:00 on the first day of the next month. If you need more, you can upgrade to Pro from the plan card on the dashboard. Quick Posts and scheduled posts share the same monthly counter.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'The article was generated in the wrong language.', 'ai-auto-poster' ),
                        'answer'   => __( 'Check that the Content Language is set on the task (not just the global setting). Some AI models follow language instructions less strictly than others - if a provider keeps producing English despite a Japanese setting, try a different model or add an explicit language instruction to the end of your prompt (for example, "Write the entire article in Japanese.").', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Image generation fails but text generation works.', 'ai-auto-poster' ),
                        'answer'   => __( 'Image and text providers usually require separate accounts and separate credit. Confirm that the image provider has its own valid API key and remaining credit. Some image models also have stricter content filters that reject prompts the text model accepts - try a more neutral prompt or switch to a stock photo provider as a workaround.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Where can I see what went wrong?', 'ai-auto-poster' ),
                        'answer'   => __( 'The Logs tab shows execution history for the past 7 days. Filter by status to see only errors. Each error log entry includes the task name, the AI provider response, and the step that failed (text generation, image generation, similarity check, post creation, etc.).', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'I generated too many posts by mistake. How do I find and delete them all?', 'ai-auto-poster' ),
                        'answer'   => __( 'Every post created by this plugin is tagged internally with the meta key "_aiap_generated". You can find them by going to Posts and filtering, or with a database query that selects posts with that meta key. This makes mass deletion straightforward without affecting articles you wrote yourself.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 9. Plans, trial, and data
            // ========================================
            array(
                'title' => __( 'Plans, trial, and data', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'question' => __( 'What is the difference between the Free plan and the Pro plan?', 'ai-auto-poster' ),
                        'answer'   => __( 'The Free plan is limited to 10 posts per month and does not include duplicate prevention, SEO features, or configurable missed-schedule recovery. The Pro plan removes the post limit and unlocks all of those features. All AI providers and all image providers are available on both plans.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'When does the monthly limit of 10 posts reset?', 'ai-auto-poster' ),
                        'answer'   => __( 'The count resets automatically at 00:00 on the first day of each calendar month, in the WordPress site time zone. Quick Posts and scheduled automatic posts share the same counter.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'How much do AI provider API fees cost?', 'ai-auto-poster' ),
                        'answer'   => __( 'Costs vary widely by provider, model, and article length, and prices change frequently - please check each provider current pricing page. As a rough guide, budget models such as Gemini Flash, Groq, and DeepSeek can produce a short article for a fraction of a cent, while premium models can cost several cents per article. The most reliable estimate is to generate a few articles and check usage in the provider dashboard.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Is there a free trial of the Pro plan?', 'ai-auto-poster' ),
                        'answer'   => __( 'Yes. A 7-day Pro trial is available from the plan card and does not require a payment method to start. During the trial, the post limit is removed and all Pro features (duplicate prevention, SEO, recovery settings) are unlocked.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'Where are logs stored?', 'ai-auto-poster' ),
                        'answer'   => __( 'In the WordPress wp_options table on your own site. Logs are automatically deleted after 7 days. They are never sent to any external server.', 'ai-auto-poster' ),
                    ),
                    array(
                        'question' => __( 'What happens to my data when I deactivate or uninstall the plugin?', 'ai-auto-poster' ),
                        'answer'   => __( 'Deactivation keeps all data so you can re-enable later without losing settings. Uninstall (deleting the plugin from the Plugins screen) removes plugin settings, encrypted API keys, tasks, schedules, execution logs, transient cache, and the plugin internal post metadata. The articles themselves and their featured images in the Media Library are kept - your published content is never deleted by uninstall.', 'ai-auto-poster' ),
                    ),
                ),
            ),

        );
    }

    /**
     * 使用許諾(Terms of Use)サブメニューを表示する
     *
     * 本文は WordPress 標準の gettext 翻訳(.po/.mo)で管理する。
     * PHP内には英語の msgid のみを置き、日本語は languages/*.po/*.mo に分離する。
     */
    public static function render_terms_page() {
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }

        $sections   = self::get_terms_items();
        $intro      = __( 'By installing, activating, or using this plugin, you agree to the terms below. Please read them carefully before use.', 'ai-auto-poster' );
        $updated_at = __( 'Last updated: October 30, 2026', 'ai-auto-poster' );
        $consent_h  = __( 'Important: Use this plugin ONLY if you agree to these Terms of Use', 'ai-auto-poster' );
        $consent_p1 = __( 'These terms are NOT optional. Installing, activating, or otherwise using this plugin in any form constitutes your full and unconditional agreement to every article below, including the disclaimer of any and all damages.', 'ai-auto-poster' );
        $consent_p2 = __( 'If you do not agree to any part of these terms, you must stop using the plugin immediately, deactivate it, and uninstall it from your site. Continued use after disagreement is strictly prohibited.', 'ai-auto-poster' );
        ?>
        <div class="wrap aiap-wrap aiap-faq-wrap">
            <div class="aiap-page-head">
                <h1 class="aiap-page-title"><?php echo esc_html( __( 'Terms of Use', 'ai-auto-poster' ) ); ?></h1>
            </div>
            <div class="aiap-terms-consent" role="alert">
                <h2 class="aiap-terms-consent-title"><?php echo esc_html( $consent_h ); ?></h2>
                <p><strong><?php echo esc_html( $consent_p1 ); ?></strong></p>
                <p><?php echo esc_html( $consent_p2 ); ?></p>
            </div>
            <div class="aiap-card aiap-card-accent">
                <p class="description"><?php echo esc_html( $intro ); ?></p>
                <p class="description" style="margin-top:6px;opacity:.7;"><?php echo esc_html( $updated_at ); ?></p>
            </div>
            <?php foreach ( $sections as $section ) : ?>
                <div class="aiap-card aiap-faq-section">
                    <h2 class="aiap-faq-section-title"><?php echo esc_html( $section['title'] ); ?></h2>
                    <div class="aiap-faq-list">
                        <?php foreach ( $section['items'] as $item ) : ?>
                            <details class="aiap-faq-item" open>
                                <summary><?php echo esc_html( $item['heading'] ); ?></summary>
                                <p><?php echo esc_html( $item['body'] ); ?></p>
                            </details>
                        <?php endforeach; ?>
                    </div>
                </div>
            <?php endforeach; ?>
        </div>
        <?php
    }

    /**
     * 使用許諾の項目を返す
     *
     * @return array<int,array{title:string,items:array<int,array{heading:string,body:string}>}>
     */
    private static function get_terms_items() {
        return array(

            // ========================================
            // 1. Application
            // ========================================
            array(
                'title' => __( 'Article 1. Application', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Acceptance of these terms', 'ai-auto-poster' ),
                        'body'    => __( 'These Terms of Use govern the relationship between the user (the site administrator who installs and operates this plugin) and the developer of this plugin. The user must use this plugin ONLY IF they fully agree to every provision of these terms. By installing, activating, or using this plugin in any way, the user is deemed to have given their full and unconditional agreement to all terms below, including the complete disclaimer of damages. If the user does not agree to any part of these terms, the user MUST NOT use this plugin: they must deactivate it and uninstall it from their site immediately. Continued use without agreement is strictly prohibited and is itself a violation of these terms.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 2. Disclaimer (no liability)
            // ========================================
            array(
                'title' => __( 'Article 2. Disclaimer', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'No liability for any damages', 'ai-auto-poster' ),
                        'body'    => __( 'The developer assumes no liability whatsoever for any damages of any kind arising out of or in connection with the use of this plugin, including but not limited to direct, indirect, incidental, special, consequential, or punitive damages, loss of profits, loss of business opportunity, loss of data, site downtime, search ranking impact, third-party claims, or any other damages, regardless of whether such damages were foreseeable. The plugin is provided on an "as is" and "as available" basis, with no warranty of any kind, express or implied, including merchantability, fitness for a particular purpose, accuracy, or non-infringement.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 3. Bugs, defects, and unexpected behavior
            // ========================================
            array(
                'title' => __( 'Article 3. Bugs, defects, and unexpected behavior', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'The plugin may contain bugs and the developer is not obligated to fix them', 'ai-auto-poster' ),
                        'body'    => __( 'Like any software, this plugin may contain bugs, defects, regressions, security vulnerabilities, performance issues, or unexpected behavior, whether known or unknown to the developer. The developer does not guarantee that all such issues are eliminated, that the plugin operates without error, or that any specific issue will be fixed within any specific timeframe. The developer is under no obligation to provide bug fixes, patches, hotfixes, workarounds, individual support, or even a response to inquiries. Any decision to fix, postpone, or not address an issue is at the developer\'s sole discretion.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 4. Updates, continued availability, and support
            // ========================================
            array(
                'title' => __( 'Article 4. Updates, continued availability, and support', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'No guarantee of updates, continued availability, or support', 'ai-auto-poster' ),
                        'body'    => __( 'The developer makes no guarantee regarding the continued availability of this plugin, the frequency or timing of updates, or the provision of any support. The developer may, at any time and at the developer\'s sole discretion, discontinue development, stop releasing updates, terminate distribution, add new features, remove existing features, change specifications, change pricing, change the license model, or transfer the plugin to another party, all without prior notice and without any obligation of compensation, refund, migration assistance, or replacement. Existing users have no right to demand continued operation, feature parity, or backward compatibility.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 5. AI service changes / interruptions
            // ========================================
            array(
                'title' => __( 'Article 5. Changes and interruptions of AI services', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Third-party AI services may change or stop without notice', 'ai-auto-poster' ),
                        'body'    => __( 'This plugin works by calling external AI services (OpenAI, Anthropic, Google, Groq, Unsplash, Pixabay, Pexels, and others). Any of these services may, at any time and without notice, change their API specifications, change pricing, deprecate or remove models, restrict regions, suspend service, or terminate service entirely. As a result, all or part of this plugin may stop functioning, return errors, or produce different results. The developer cannot prevent these changes and provides no compensation, refund, or guarantee of continued operation in such cases.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 6. WordPress / PHP / server environment changes
            // ========================================
            array(
                'title' => __( 'Article 6. Changes to WordPress, PHP, and server environment', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Environment changes may break the plugin without warning', 'ai-auto-poster' ),
                        'body'    => __( 'This plugin runs on top of WordPress, PHP, MySQL/MariaDB, web servers (Apache, Nginx, etc.), and the user\'s hosting provider. Updates, specification changes, security tightening, deprecations, or policy changes by any of these layers - including WordPress core releases, PHP version upgrades, database engine changes, server configuration changes, hosting provider restrictions, or third-party CDN/firewall behavior - may cause this plugin to malfunction, return errors, lose data, or stop working entirely. The developer is under no obligation to track, anticipate, or follow up on such environmental changes, and assumes no liability for any damages arising from them.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 7. Conflicts with other plugins and themes
            // ========================================
            array(
                'title' => __( 'Article 7. Conflicts with other plugins and themes', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'No guarantee of compatibility with other plugins or themes', 'ai-auto-poster' ),
                        'body'    => __( 'WordPress is an open ecosystem with thousands of plugins and themes that hook into the same core. The developer does not test or guarantee compatibility with every combination, and conflicts may occur. Plugins in the following categories are particularly prone to causing issues: caching plugins, SEO plugins, security plugins, REST API restriction plugins, multilingual plugins, page builders, custom post type plugins, and Cron-control plugins. Themes that override WordPress core hooks or admin styling may also cause display or behavioral issues. Investigating, isolating, and resolving such conflicts is the user\'s responsibility, and the developer is not liable for any malfunction, data loss, or damage arising from them.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 8. WP-Cron and scheduling
            // ========================================
            array(
                'title' => __( 'Article 8. WP-Cron and scheduling', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Scheduled jobs may be delayed, missed, duplicated, or executed at unexpected times', 'ai-auto-poster' ),
                        'body'    => __( 'This plugin schedules article generation and posting through WP-Cron, the WordPress task scheduler. WP-Cron is not a real-time scheduler: it depends on visitor traffic to the site to trigger execution. As a result, scheduled jobs may be delayed (especially on low-traffic sites), missed entirely, executed earlier or later than intended, executed multiple times in unexpected ways, or fail silently. Server-level Cron, hosting-side limitations, WP-Cron-disabling plugins, object caches, persistent transients, page caches, and other factors may further affect timing. Configuring server-side Cron and verifying execution timing are the user\'s responsibility, and the developer is not liable for any consequences arising from missed posts, delayed posts, duplicated posts, or posts published at unexpected times.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 9. Data loss, corruption, and backup
            // ========================================
            array(
                'title' => __( 'Article 9. Data loss, corruption, and backup', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Backup is the user\'s responsibility', 'ai-auto-poster' ),
                        'body'    => __( 'Use of this plugin may, in connection with bugs, environmental changes, conflicts, server failures, manual operations, or other causes, result in loss, corruption, partial truncation, or unintended modification of posts, media files, taxonomy terms, plugin settings, options, transients, or any other data stored in the WordPress database or on the server. Taking regular full backups (database and files) before installation, before any update, and on an ongoing basis, and verifying that those backups can actually be restored, is the sole responsibility of the user. The developer is not liable for any data loss or for the cost, effort, or impossibility of recovery.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 10. Security incidents and unauthorized access
            // ========================================
            array(
                'title' => __( 'Article 10. Security incidents and unauthorized access', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'No liability for security incidents', 'ai-auto-poster' ),
                        'body'    => __( 'Despite reasonable care, this plugin may contain security vulnerabilities, whether known or unknown. Through such vulnerabilities, through compromised credentials elsewhere on the site, or through attacks on the underlying WordPress, server, or hosting environment, third parties may gain unauthorized access, leak or steal API keys, leak or steal database content, modify or delete content, install malware, or otherwise harm the site. The developer is not liable for any damage, loss, regulatory penalty, customer notification cost, recovery cost, reputational harm, or any other consequence arising from security incidents, and is under no obligation to provide breach notifications, forensic support, or remediation assistance. Site security, monitoring, hardening, and incident response are the user\'s responsibility.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 11. Responsibility for generated content
            // ========================================
            array(
                'title' => __( 'Article 11. Responsibility for generated content', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'The user is solely responsible for what is published', 'ai-auto-poster' ),
                        'body'    => __( 'AI-generated content may contain factual errors, misleading information, copyright infringement, defamation, privacy violations, discriminatory expressions, or violations of laws or platform policies. The user is solely responsible for reviewing every generated article before publication and bears full responsibility for the content that is actually published on their site. Using the Draft post status and reviewing manually before publishing is strongly recommended. The developer is not responsible for any consequences of published content.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 12. API keys and usage fees
            // ========================================
            array(
                'title' => __( 'Article 12. API keys and usage fees', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Fees and key management are the user\'s responsibility', 'ai-auto-poster' ),
                        'body'    => __( 'All usage fees charged by AI providers (text generation, image generation, search, etc.) are the sole responsibility of the user. The developer does not pay any portion of these fees. The user is responsible for safely managing their API keys, monitoring usage, setting spending limits in each provider dashboard, and preventing unauthorized use. The developer is not liable for unexpectedly high charges, leaked keys, or misuse, even if these result from a bug, misconfiguration, or unexpected behavior of this plugin.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 13. Compliance with third-party terms
            // ========================================
            array(
                'title' => __( 'Article 13. Compliance with third-party service terms', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'The user must follow each provider\'s terms', 'ai-auto-poster' ),
                        'body'    => __( 'The user must comply with the terms of service, usage policies, content policies, and applicable laws of every external service called by this plugin (OpenAI, Anthropic, Google, Groq, Unsplash, Pixabay, Pexels, and others). This includes restrictions on automated content generation, prohibited content categories, attribution requirements, and commercial use rules. Any violation is the sole responsibility of the user, and the developer is not liable for account suspensions, bans, or legal action taken by these providers.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 14. Search engines, SEO, platform evaluations
            // ========================================
            array(
                'title' => __( 'Article 14. Search engines, SEO, and platform evaluations', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'No guarantee about search rankings or platform evaluations', 'ai-auto-poster' ),
                        'body'    => __( 'Publishing AI-generated content, especially in large volume or at high frequency, may negatively affect the site\'s search engine rankings, indexation status, advertising eligibility (Google AdSense and others), social media reach, and other forms of platform evaluation. Search engines and platforms periodically change their content policies and evaluation criteria, sometimes specifically targeting AI-generated content, automated publishing, or low-quality content at scale. The developer makes no representation or warranty regarding any SEO outcome, ranking, traffic level, click-through rate, or platform standing, and is not liable for any drop in rankings, deindexing, manual action, advertising account suspension, or related damages.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 15. Data handling
            // ========================================
            array(
                'title' => __( 'Article 15. Data handling', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Data is sent to AI providers during generation', 'ai-auto-poster' ),
                        'body'    => __( 'During article generation, the prompts, task settings, and other inputs configured by the user are transmitted to the selected AI provider over HTTPS. The data handling, retention, and learning policies of the provider apply to this transmitted data. API keys are encrypted with AES-256-CBC using the WordPress AUTH_KEY and stored in the local database, but the developer cannot guarantee absolute security. The user is responsible for not entering personal information, confidential information, or other sensitive data into prompts.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 16. Prohibited acts
            // ========================================
            array(
                'title' => __( 'Article 16. Prohibited acts', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Acts the user must not perform', 'ai-auto-poster' ),
                        'body'    => __( 'The user must not use this plugin for any of the following: generating illegal content, infringing the rights of third parties (copyright, trademark, privacy, portrait rights, etc.), generating content that defames or discriminates against specific individuals or groups, mass-producing low-quality content for the purpose of search engine spam, generating misleading information that could cause real-world harm, reverse engineering or modifying the plugin in violation of its license, or any other act that violates applicable law or these terms.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 17. Changes to these terms
            // ========================================
            array(
                'title' => __( 'Article 17. Changes to these terms', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Terms may be updated without prior notice', 'ai-auto-poster' ),
                        'body'    => __( 'These Terms of Use may be updated from time to time without prior individual notice. The latest version will always be available on this page. Continued use of the plugin after an update constitutes acceptance of the updated terms. The user is encouraged to review this page periodically.', 'ai-auto-poster' ),
                    ),
                ),
            ),

            // ========================================
            // 18. Governing law and jurisdiction
            // ========================================
            array(
                'title' => __( 'Article 18. Governing law and jurisdiction', 'ai-auto-poster' ),
                'items' => array(
                    array(
                        'heading' => __( 'Japanese law applies', 'ai-auto-poster' ),
                        'body'    => __( 'These Terms of Use are governed by and construed in accordance with the laws of Japan. Any dispute arising out of or in connection with these terms or the use of this plugin shall be subject to the exclusive jurisdiction of the court having jurisdiction over the location of the developer, as the court of first instance.', 'ai-auto-poster' ),
                    ),
                ),
            ),

        );
    }

    // ========== Icon helper ==========
    /**
     * プロバイダ用アイコン HTML を生成
     *
     * 内部で esc_attr / esc_html を適用済みの安全な HTML を返すため、
     * 呼び出し側で再度エスケープする必要はない。
     *
     * @param string $class  CSS クラス名
     * @param string $letter 表示する 1 文字
     * @param string $size   '' or 'lg'
     * @return string Escape-safe HTML.
     */
    private static function ico( $class, $letter, $size = '' ) {
        $cls = 'ico ' . esc_attr( $class ) . ( $size ? ' ico-lg' : '' );
        return '<span class="' . $cls . '">' . esc_html( $letter ) . '</span>';
    }

    private static function tab_label( $key ) {
        $label = self::resolve_dynamic_key( $key );
        return preg_replace( '/^[^\p{L}\p{N}]+/u', '', $label );
    }

    private static function meta_label( $meta ) {
        // プロバイダ名(Claude, OpenAI等)は両言語共通の英字表記
        // 'label' があればそれを使い、'label_key' があれば i18n で展開する
        if ( ! empty( $meta['label'] ) ) {
            return $meta['label'];
        }
        if ( ! empty( $meta['label_key'] ) ) {
            return self::resolve_dynamic_key( $meta['label_key'] );
        }
        return '';
    }

    private static function log_status_label( $status ) {
        switch ( $status ) {
            case 'error':   return __( 'Error',   'ai-auto-poster' );
            case 'info':    return __( 'Info',    'ai-auto-poster' );
            case 'success': return __( 'Success', 'ai-auto-poster' );
        }
        return $status;
    }

    /**
     * 動的キーを WordPress 標準翻訳に解決する
     *
     * 動的に組み立てられたキー(例: 'imgfmt_16_9', 'dedup_sens_high')を
     * switch で個別の __( '...', 'ai-auto-poster' ) に振り分けて翻訳する。
     * これにより WordPress.org の翻訳ツール (makepot) がソースから抽出可能になる。
     *
     * @param string $key
     * @return string 翻訳後の文字列(キーが未定義ならキー自体を返す)
     */
    private static function resolve_dynamic_key( $key ) {
        switch ( $key ) {
            // 曜日
            case 'mon': return __( 'Mon', 'ai-auto-poster' );
            case 'tue': return __( 'Tue', 'ai-auto-poster' );
            case 'wed': return __( 'Wed', 'ai-auto-poster' );
            case 'thu': return __( 'Thu', 'ai-auto-poster' );
            case 'fri': return __( 'Fri', 'ai-auto-poster' );
            case 'sat': return __( 'Sat', 'ai-auto-poster' );
            case 'sun': return __( 'Sun', 'ai-auto-poster' );
            case 'dow_mon': return __( 'Mon', 'ai-auto-poster' );
            case 'dow_tue': return __( 'Tue', 'ai-auto-poster' );
            case 'dow_wed': return __( 'Wed', 'ai-auto-poster' );
            case 'dow_thu': return __( 'Thu', 'ai-auto-poster' );
            case 'dow_fri': return __( 'Fri', 'ai-auto-poster' );
            case 'dow_sat': return __( 'Sat', 'ai-auto-poster' );
            case 'dow_sun': return __( 'Sun', 'ai-auto-poster' );

            // 投稿ステータス
            case 'draft':   return __( 'Draft',          'ai-auto-poster' );
            case 'pending': return __( 'Pending Review', 'ai-auto-poster' );
            case 'publish': return __( 'Publish',        'ai-auto-poster' );

            // 有効/無効
            case 'enabled':  return __( 'Enabled',  'ai-auto-poster' );
            case 'disabled': return __( 'Disabled', 'ai-auto-poster' );
            case 'activate':   return __( 'Activate',   'ai-auto-poster' );
            case 'deactivate': return __( 'Disable', 'ai-auto-poster' );
            case 'auto_posting_turned_on':  return __( 'Auto posting turned ON',  'ai-auto-poster' );
            case 'auto_posting_turned_off': return __( 'Auto posting turned OFF', 'ai-auto-poster' );

            // dedup_sens_*
            case 'dedup_sens_high':   return __( 'Strict (catch only barely similar)', 'ai-auto-poster' );
            case 'dedup_sens_medium': return __( 'Balanced',                            'ai-auto-poster' );
            case 'dedup_sens_low':    return __( 'Loose (only flag clearly similar)',  'ai-auto-poster' );

            // imgfmt_*
            case 'imgfmt_16_9': return __( 'Landscape (16:9) - Blog headers', 'ai-auto-poster' );
            case 'imgfmt_1_1':  return __( 'Square (1:1) - Social posts',     'ai-auto-poster' );
            case 'imgfmt_9_16': return __( 'Portrait (9:16) - Stories',       'ai-auto-poster' );
            case 'imgfmt_4_3':  return __( 'Wide (4:3) - Thumbnails',         'ai-auto-poster' );

            // imgqual_*
            case 'imgqual_low':    return __( 'Low - Faster & Cheaper',  'ai-auto-poster' );
            case 'imgqual_medium': return __( 'Medium - Balanced',       'ai-auto-poster' );
            case 'imgqual_high':   return __( 'High - Best Quality',     'ai-auto-poster' );

            // imgprov_*(画像プロバイダ名)
            case 'imgprov_dalle3':    return __( 'DALL-E 3',     'ai-auto-poster' );
            case 'imgprov_imagen':    return __( 'Imagen',       'ai-auto-poster' );
            case 'imgprov_stability': return __( 'Stability AI', 'ai-auto-poster' );
            case 'imgprov_flux':      return __( 'Flux',         'ai-auto-poster' );
            case 'imgprov_unsplash':  return __( 'Unsplash',     'ai-auto-poster' );
            case 'imgprov_pixabay':   return __( 'Pixabay',      'ai-auto-poster' );
            case 'imgprov_pexels':    return __( 'Pexels',       'ai-auto-poster' );
            case 'imgprov_openverse': return __( 'Openverse',    'ai-auto-poster' );

            // imgstyle_*(画像スタイル)
            case 'imgstyle_none':              return __( 'None (Default)',     'ai-auto-poster' );
            case 'imgstyle_natural':           return __( 'Natural',            'ai-auto-poster' );
            case 'imgstyle_vivid':             return __( 'Vivid',              'ai-auto-poster' );
            case 'imgstyle_realistic':         return __( 'Realistic',          'ai-auto-poster' );
            case 'imgstyle_cartoon':           return __( 'Cartoon',            'ai-auto-poster' );
            case 'imgstyle_anime':             return __( 'Anime',              'ai-auto-poster' );
            case 'imgstyle_3d-render':         return __( '3D Render',          'ai-auto-poster' );
            case 'imgstyle_3d-model':          return __( '3D Model',           'ai-auto-poster' );
            case 'imgstyle_watercolor':        return __( 'Watercolor',         'ai-auto-poster' );
            case 'imgstyle_oil-painting':      return __( 'Oil Painting',       'ai-auto-poster' );
            case 'imgstyle_flat-design':       return __( 'Flat Design',        'ai-auto-poster' );
            case 'imgstyle_pixel-art':         return __( 'Pixel Art',          'ai-auto-poster' );
            case 'imgstyle_analog-film':       return __( 'Analog Film',        'ai-auto-poster' );
            case 'imgstyle_cinematic':         return __( 'Cinematic',          'ai-auto-poster' );
            case 'imgstyle_comic-book':        return __( 'Comic Book',         'ai-auto-poster' );
            case 'imgstyle_digital-art':       return __( 'Digital Art',        'ai-auto-poster' );
            case 'imgstyle_enhance':           return __( 'Enhance',            'ai-auto-poster' );
            case 'imgstyle_fantasy-art':       return __( 'Fantasy Art',        'ai-auto-poster' );
            case 'imgstyle_isometric':         return __( 'Isometric',          'ai-auto-poster' );
            case 'imgstyle_line-art':          return __( 'Line Art',           'ai-auto-poster' );
            case 'imgstyle_low-poly':          return __( 'Low Poly',           'ai-auto-poster' );
            case 'imgstyle_modeling-compound': return __( 'Modeling Compound',  'ai-auto-poster' );
            case 'imgstyle_neon-punk':         return __( 'Neon Punk',          'ai-auto-poster' );
            case 'imgstyle_origami':           return __( 'Origami',            'ai-auto-poster' );
            case 'imgstyle_photographic':      return __( 'Photographic',       'ai-auto-poster' );
            case 'imgstyle_tile-texture':      return __( 'Tile Texture',       'ai-auto-poster' );

            // SEO グループ
            case 'seo_content_group':   return __( 'Content Optimization',     'ai-auto-poster' );
            case 'seo_image_group':     return __( 'Image Optimization',       'ai-auto-poster' );
            case 'seo_structure_group': return __( 'Site Structure',           'ai-auto-poster' );
            case 'seo_meta_group':      return __( 'Meta and Structured Data', 'ai-auto-poster' );
            case 'seo_other_group':     return __( 'Other Settings',           'ai-auto-poster' );

            // SEO 個別フィールド
            case 'seo_meta_desc':     return __( 'Auto-generate meta description',                'ai-auto-poster' );
            case 'seo_heading':       return __( 'Optimize heading structure',                    'ai-auto-poster' );
            case 'seo_focus_kw':      return __( 'Generate and apply focus keyword',              'ai-auto-poster' );
            case 'seo_faq':           return __( 'Auto-generate FAQ section',                     'ai-auto-poster' );
            case 'seo_toc':           return __( 'Auto-insert table of contents',                 'ai-auto-poster' );
            case 'seo_template':      return __( 'Apply article template',                        'ai-auto-poster' );
            case 'seo_img_alt':       return __( 'Auto-set image alt text',                       'ai-auto-poster' );
            case 'seo_img_webp':      return __( 'WebP conversion and compression',               'ai-auto-poster' );
            case 'seo_internal_link': return __( 'Auto-insert internal links',                    'ai-auto-poster' );
            case 'seo_auto_tag':      return __( 'Auto-generate and set tags',                    'ai-auto-poster' );
            case 'seo_related':       return __( 'Add related posts section',                     'ai-auto-poster' );
            case 'seo_schema':        return __( 'Auto-output Article JSON-LD schema',            'ai-auto-poster' );
            case 'seo_ogp':           return __( 'Auto-set OGP meta tags',                        'ai-auto-poster' );
            case 'seo_canonical':     return __( 'Set canonical URL',                             'ai-auto-poster' );
            case 'seo_plugin_link':   return __( 'Integrate with SEO plugins',                    'ai-auto-poster' );
            case 'seo_reading_time':  return __( 'Calculate and save reading time',               'ai-auto-poster' );
            case 'seo_sitemap_ping':  return __( 'Update XML sitemap and notify search engines',  'ai-auto-poster' );
            case 'seo_llms_txt':      return __( 'Generate llms.txt',                             'ai-auto-poster' );

            // タブ系
            case 'tab_settings':        return __( 'General Settings',     'ai-auto-poster' );
            case 'tab_provider':        return __( 'AI Providers',         'ai-auto-poster' );
            case 'tab_image_provider':  return __( 'Image AI Providers',   'ai-auto-poster' );
            case 'tab_image_settings':  return __( 'Image Settings',       'ai-auto-poster' );
            case 'tab_system_prompt':   return __( 'System Prompt',        'ai-auto-poster' );
            case 'tab_rss_feeds':       return __( 'RSS Feeds',            'ai-auto-poster' );
            case 'tab_prompts':         return __( 'Tasks',                'ai-auto-poster' );
            case 'tab_logs':            return __( 'Post history',         'ai-auto-poster' );
            case 'tab_run_logs':        return __( 'Execution log',        'ai-auto-poster' );
            case 'tab_quick_post':      return __( 'Quick Post',           'ai-auto-poster' );

            // notes (画像プロバイダの説明)
            case 'shared_openai':   return __( 'Uses the OpenAI API key already configured in AI Providers.', 'ai-auto-poster' );
            case 'shared_gemini':   return __( 'Uses the Gemini API key already configured in AI Providers.', 'ai-auto-poster' );
            case 'openverse_desc':  return __( 'Open-licensed image search by WordPress.org. Ready to use immediately.', 'ai-auto-poster' );
        }
        // 未定義: キーをそのまま返す(開発時の発見性重視)
        return $key;
    }

    private static function log_message( $log ) {
        // 新形式(キー+パラメータ)は Logger::format_message で展開、旧形式はメッセージそのまま
        $message = AIAP_Logger::format_message( $log );

        // 過去ログ互換: 英語UIで日本語の曜日が混入していたら英語表記に置換
        $locale = function_exists( 'determine_locale' ) ? determine_locale() : get_locale();
        $is_ja = ( strpos( $locale, 'ja' ) === 0 );
        if ( ! $is_ja ) {
            $dow_replace = array(
                '月曜日' => 'Mon', '火曜日' => 'Tue', '水曜日' => 'Wed', '木曜日' => 'Thu',
                '金曜日' => 'Fri', '土曜日' => 'Sat', '日曜日' => 'Sun',
            );
            $message = strtr( $message, $dow_replace );
        }

        return $message;
    }

    // ========== Main render ==========
    /**
     * Phase 3: ウィザード完了フラグ
     */
    const OPTION_WIZARD_DONE = 'aiap_wizard_completed';

    /**
     * ウィザードを表示すべきかを判定
     */
    public static function should_show_wizard() {
        // 明示的に wizard モードなら見せる
        if ( ! empty( $_GET['wizard'] ) ) return true;
        // 完了フラグが立っていれば見せない
        if ( get_option( self::OPTION_WIZARD_DONE, false ) ) return false;
        // 既に何らかのタスクや API キーがあれば見せない (既存ユーザーへの配慮)
        $tasks = AIAP_Prompts::get_all();
        if ( ! empty( $tasks ) ) return false;
        $s = AIAP_AI_Client::get_settings();
        foreach ( array( 'api_key_anthropic', 'api_key_openai', 'api_key_gemini' ) as $k ) {
            if ( ! empty( $s[ $k ] ) ) return false;
        }
        return true;
    }

    public static function render_page() {
        if ( ! current_user_can( 'manage_options' ) ) return;

        // Phase 3: ウィザードを表示すべきならそちらに分岐
        if ( self::should_show_wizard() ) {
            self::render_wizard();
            return;
        }

        $tab  = sanitize_key( wp_unslash( $_GET['tab'] ?? 'settings' ) );
        $s    = AIAP_AI_Client::get_settings();
        $im   = AIAP_Image_Client::get_settings();
        ?>
        <?php
        $verified_providers = get_option( 'aiap_verified_providers', array() );
        $verified_keys = is_array( $verified_providers ) ? array_keys( $verified_providers ) : array();
        ?>
        <div class="wrap aiap-wrap" data-aiap-ajax-url="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" data-aiap-nonce="<?php echo esc_attr( wp_create_nonce( self::NONCE ) ); ?>" data-aiap-test-no-key="<?php echo esc_attr( __( 'Enter a key first', 'ai-auto-poster' ) ); ?>" data-aiap-btn-show="<?php echo esc_attr( __( 'Show', 'ai-auto-poster' ) ); ?>" data-aiap-btn-hide="<?php echo esc_attr( __( 'Hide', 'ai-auto-poster' ) ); ?>" data-aiap-verified="<?php echo esc_attr( wp_json_encode( $verified_keys ) ); ?>" data-aiap-not-tested-msg="<?php echo esc_attr( __( 'Please test the API key first.', 'ai-auto-poster' ) ); ?>">
            <div class="aiap-page-head">
                <h1 class="aiap-page-title">AI Auto Poster</h1>
            </div>
            <?php self::render_notices(); ?>
            <?php
            $tabs = array(
                'settings' => array(
                    'key'  => 'tab_settings',
                    'icon' => 'dashicons-admin-generic',
                ),
                'quick_post' => array(
                    'key'  => 'tab_quick_post',
                    'icon' => 'dashicons-welcome-write-blog',
                ),
                'provider' => array(
                    'key'  => 'tab_provider',
                    'icon' => 'dashicons-edit-page',
                ),
                'image_provider' => array(
                    'key'  => 'tab_image_provider',
                    'icon' => 'dashicons-format-image',
                ),
                'image_settings' => array(
                    'key'  => 'tab_image_settings',
                    'icon' => 'dashicons-admin-tools',
                ),
                'system_prompt' => array(
                    'key'  => 'tab_system_prompt',
                    'icon' => 'dashicons-editor-code',
                ),
                'rss_feeds' => array(
                    'key'  => 'tab_rss_feeds',
                    'icon' => 'dashicons-rss',
                ),
                'prompts' => array(
                    'key'  => 'tab_prompts',
                    'icon' => 'dashicons-list-view',
                ),
                'logs' => array(
                    'key'  => 'tab_logs',
                    'icon' => 'dashicons-media-text',
                ),
                'run_logs' => array(
                    'key'  => 'tab_run_logs',
                    'icon' => 'dashicons-media-document',
                ),
            );
            // サイドバーのセクションごとのグループ化 (#nav-redesign)
            $groups = array(
                'setup' => array(
                    'label' => __( 'Setup', 'ai-auto-poster' ),
                    'tabs'  => array( 'settings', 'provider', 'image_provider', 'rss_feeds', 'image_settings', 'system_prompt' ),
                ),
                'operation' => array(
                    'label' => __( 'Operation', 'ai-auto-poster' ),
                    'tabs'  => array( 'prompts', 'quick_post', 'logs', 'run_logs' ),
                ),
            );
            ?>
            <button type="button" class="aiap-sidebar-toggle" data-aiap-sidebar-toggle aria-label="<?php echo esc_attr( __( 'Toggle menu', 'ai-auto-poster' ) ); ?>" aria-expanded="false">
                <span class="dashicons dashicons-menu" aria-hidden="true"></span>
                <span class="aiap-sidebar-toggle-label"><?php echo esc_html( self::tab_label( $tabs[ $tab ]['key'] ?? 'tab_settings' ) ); ?></span>
            </button>

            <div class="aiap-shell">
                <aside class="aiap-sidebar" data-aiap-sidebar>
                    <nav class="aiap-tabs" role="tablist">
                        <?php foreach ( $groups as $group_key => $group ) : ?>
                            <div class="aiap-sidebar-section">
                                <div class="aiap-sidebar-section-title"><?php echo esc_html( $group['label'] ); ?></div>
                                <?php foreach ( $group['tabs'] as $slug ) : if ( ! isset( $tabs[ $slug ] ) ) continue; $meta = $tabs[ $slug ]; ?>
                                    <a href="?page=<?php echo esc_attr( self::MENU_SLUG ); ?>&tab=<?php echo esc_attr( $slug ); ?>"
                                       class="aiap-tab<?php echo $tab === $slug ? ' is-active' : ''; ?>"
                                       role="tab"
                                       aria-selected="<?php echo $tab === $slug ? 'true' : 'false'; ?>">
                                        <span class="dashicons <?php echo esc_attr( $meta['icon'] ); ?> aiap-tab-icon" aria-hidden="true"></span>
                                        <span class="aiap-tab-label"><?php echo esc_html( self::tab_label( $meta['key'] ) ); ?></span>
                                    </a>
                                <?php endforeach; ?>
                            </div>
                        <?php endforeach; ?>
                    </nav>
                </aside>

                <main class="aiap-main">
                <div class="aiap-tab-panel">
            <?php
            switch ( $tab ) {
                case 'quick_post':     self::render_quick_post(); break;
                case 'provider':       self::render_provider( $s ); break;
                case 'image_provider': self::render_image_provider( $s, $im ); break;
                case 'image_settings': self::render_image_settings( $im ); break;
                case 'prompts':        self::render_tasks( $s ); break;
                case 'system_prompt':  self::render_system_prompt( $s ); break;
                case 'rss_feeds':      self::render_rss_feeds(); break;
                case 'logs':           self::render_logs(); break;
                case 'run_logs':       self::render_run_logs(); break;
                default:               self::render_settings( $s ); break;
            }
            ?>
                </div>
                </main>
            </div><!-- /.aiap-shell -->
        </div>
        <?php
    }

    // ================================================================
    // 基本設定 (unchanged from original)
    // ================================================================
    // ================================================================
    // Phase 3: Quick Start Wizard
    // ================================================================
    private static function render_wizard() {
        ?>
        <div class="wrap aiap-wrap aiap-theme-modern" data-aiap-ajax-url="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>" data-aiap-nonce="<?php echo esc_attr( wp_create_nonce( self::NONCE ) ); ?>">
            <div class="aiap-wizard">
                <header class="aiap-wizard-header">
                    <div class="aiap-wizard-eyebrow"><?php echo esc_html( __( 'Quick Start', 'ai-auto-poster' ) ); ?></div>
                    <h1 class="aiap-wizard-title"><?php echo esc_html( __( 'Welcome to AI Auto Poster', 'ai-auto-poster' ) ); ?></h1>
                    <p class="aiap-wizard-sub"><?php echo esc_html( __( 'Set up automatic posting in 4 steps.', 'ai-auto-poster' ) ); ?></p>
                    <div class="aiap-wizard-progress" data-aiap-wizard-progress>
                        <div class="aiap-wizard-progress-fill" data-aiap-wizard-progress-fill style="width:25%"></div>
                    </div>
                    <div class="aiap-wizard-stepnums">
                        <span class="aiap-wizard-stepnum is-active" data-stepnum="1">1</span>
                        <span class="aiap-wizard-stepnum" data-stepnum="2">2</span>
                        <span class="aiap-wizard-stepnum" data-stepnum="3">3</span>
                        <span class="aiap-wizard-stepnum" data-stepnum="4">4</span>
                    </div>
                </header>

                <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-wizard-form" data-aiap-wizard-form>
                    <input type="hidden" name="action" value="aiap_wizard_complete">
                    <?php wp_nonce_field( self::NONCE ); ?>

                    <!-- Step 1: theme / prompt -->
                    <section class="aiap-wizard-step is-active" data-aiap-wizard-step="1">
                        <h2 class="aiap-wizard-step-title"><?php echo esc_html( __( 'What do you want to post about?', 'ai-auto-poster' ) ); ?></h2>
                        <p class="aiap-wizard-step-sub"><?php echo esc_html( __( 'Pick a preset, or describe your topic in your own words.', 'ai-auto-poster' ) ); ?></p>

                        <div class="aiap-wizard-presets">
                            <button type="button" class="aiap-wizard-preset" data-aiap-wizard-preset="<?php echo esc_attr( __( 'Daily small observations and discoveries about nature, written in a warm, gentle tone.', 'ai-auto-poster' ) ); ?>">
                                <span class="aiap-wizard-preset-icon">🌿</span>
                                <span class="aiap-wizard-preset-name"><?php echo esc_html( __( 'Nature observations', 'ai-auto-poster' ) ); ?></span>
                            </button>
                            <button type="button" class="aiap-wizard-preset" data-aiap-wizard-preset="<?php echo esc_attr( __( 'Practical business tips for small companies and freelancers, focused on productivity and growth.', 'ai-auto-poster' ) ); ?>">
                                <span class="aiap-wizard-preset-icon">💼</span>
                                <span class="aiap-wizard-preset-name"><?php echo esc_html( __( 'Business tips', 'ai-auto-poster' ) ); ?></span>
                            </button>
                            <button type="button" class="aiap-wizard-preset" data-aiap-wizard-preset="<?php echo esc_attr( __( 'Hobby and lifestyle topics with a casual, friendly voice.', 'ai-auto-poster' ) ); ?>">
                                <span class="aiap-wizard-preset-icon">🎨</span>
                                <span class="aiap-wizard-preset-name"><?php echo esc_html( __( 'Hobbies & lifestyle', 'ai-auto-poster' ) ); ?></span>
                            </button>
                            <button type="button" class="aiap-wizard-preset" data-aiap-wizard-preset="">
                                <span class="aiap-wizard-preset-icon">✏</span>
                                <span class="aiap-wizard-preset-name"><?php echo esc_html( __( 'Custom', 'ai-auto-poster' ) ); ?></span>
                            </button>
                        </div>

                        <div class="aiap-field" style="margin-top:18px">
                            <label class="aiap-field-label" for="aiap-wiz-prompt"><?php echo esc_html( __( 'Prompt (what the AI should write about)', 'ai-auto-poster' ) ); ?> <span class="aiap-required">*</span></label>
                            <textarea id="aiap-wiz-prompt" name="wiz_prompt" rows="3" class="aiap-input-text aiap-editor-textarea" placeholder="<?php echo esc_attr( __( 'e.g. Daily small observations about nature.', 'ai-auto-poster' ) ); ?>" required></textarea>
                        </div>

                        <div class="aiap-wizard-actions">
                            <span></span>
                            <button type="button" class="button button-primary aiap-btn-primary" data-aiap-wizard-next><?php echo esc_html( __( 'Next', 'ai-auto-poster' ) ); ?> →</button>
                        </div>
                    </section>

                    <!-- Step 2: frequency -->
                    <section class="aiap-wizard-step" data-aiap-wizard-step="2">
                        <h2 class="aiap-wizard-step-title"><?php echo esc_html( __( 'How often should it post?', 'ai-auto-poster' ) ); ?></h2>
                        <p class="aiap-wizard-step-sub"><?php echo esc_html( __( 'You can adjust this later in the task settings.', 'ai-auto-poster' ) ); ?></p>

                        <div class="aiap-wizard-options">
                            <label class="aiap-wizard-option">
                                <input type="radio" name="wiz_freq" value="daily" checked>
                                <span class="aiap-wizard-option-body">
                                    <span class="aiap-wizard-option-icon">📅</span>
                                    <span class="aiap-wizard-option-name"><?php echo esc_html( __( 'Once a day', 'ai-auto-poster' ) ); ?></span>
                                    <span class="aiap-wizard-option-desc"><?php echo esc_html( __( '1 post every day at 09:00', 'ai-auto-poster' ) ); ?></span>
                                </span>
                            </label>
                            <label class="aiap-wizard-option">
                                <input type="radio" name="wiz_freq" value="random">
                                <span class="aiap-wizard-option-body">
                                    <span class="aiap-wizard-option-icon">🔁</span>
                                    <span class="aiap-wizard-option-name"><?php echo esc_html( __( '5–10 times a day, random', 'ai-auto-poster' ) ); ?></span>
                                    <span class="aiap-wizard-option-desc"><?php echo esc_html( __( 'Random count and times each day (excluding 01:00–06:00)', 'ai-auto-poster' ) ); ?></span>
                                </span>
                            </label>
                            <label class="aiap-wizard-option">
                                <input type="radio" name="wiz_freq" value="weekly">
                                <span class="aiap-wizard-option-body">
                                    <span class="aiap-wizard-option-icon">📌</span>
                                    <span class="aiap-wizard-option-name"><?php echo esc_html( __( 'A few times a week', 'ai-auto-poster' ) ); ?></span>
                                    <span class="aiap-wizard-option-desc"><?php echo esc_html( __( 'Mon / Wed / Fri at 09:00', 'ai-auto-poster' ) ); ?></span>
                                </span>
                            </label>
                        </div>

                        <div class="aiap-wizard-actions">
                            <button type="button" class="button" data-aiap-wizard-prev>← <?php echo esc_html( __( 'Back', 'ai-auto-poster' ) ); ?></button>
                            <button type="button" class="button button-primary aiap-btn-primary" data-aiap-wizard-next><?php echo esc_html( __( 'Next', 'ai-auto-poster' ) ); ?> →</button>
                        </div>
                    </section>

                    <!-- Step 3: provider + API key -->
                    <section class="aiap-wizard-step" data-aiap-wizard-step="3">
                        <h2 class="aiap-wizard-step-title"><?php echo esc_html( __( 'Pick an AI provider', 'ai-auto-poster' ) ); ?></h2>
                        <p class="aiap-wizard-step-sub"><?php echo esc_html( __( 'You can change provider or add more later.', 'ai-auto-poster' ) ); ?></p>

                        <div class="aiap-field">
                            <label class="aiap-field-label" for="aiap-wiz-provider"><?php echo esc_html( __( 'Provider', 'ai-auto-poster' ) ); ?></label>
                            <select id="aiap-wiz-provider" name="wiz_provider" class="aiap-select">
                                <option value="anthropic" selected>Anthropic (Claude)</option>
                                <option value="openai">OpenAI (GPT)</option>
                                <option value="gemini">Google (Gemini)</option>
                            </select>
                        </div>
                        <div class="aiap-field">
                            <label class="aiap-field-label" for="aiap-wiz-apikey"><?php echo esc_html( __( 'API Key', 'ai-auto-poster' ) ); ?></label>
                            <input type="password" id="aiap-wiz-apikey" name="wiz_apikey" class="aiap-input-text" autocomplete="new-password" placeholder="sk-...">
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'You can skip this step and set it up later from the Provider tab.', 'ai-auto-poster' ) ); ?></span>
                        </div>

                        <div class="aiap-wizard-actions">
                            <button type="button" class="button" data-aiap-wizard-prev>← <?php echo esc_html( __( 'Back', 'ai-auto-poster' ) ); ?></button>
                            <button type="button" class="button button-primary aiap-btn-primary" data-aiap-wizard-next><?php echo esc_html( __( 'Next', 'ai-auto-poster' ) ); ?> →</button>
                        </div>
                    </section>

                    <!-- Step 4: review + complete -->
                    <section class="aiap-wizard-step" data-aiap-wizard-step="4">
                        <h2 class="aiap-wizard-step-title"><?php echo esc_html( __( 'Review and start', 'ai-auto-poster' ) ); ?></h2>
                        <p class="aiap-wizard-step-sub"><?php echo esc_html( __( 'Confirm your setup. Everything is editable later.', 'ai-auto-poster' ) ); ?></p>

                        <ul class="aiap-wizard-review" data-aiap-wizard-review>
                            <li><strong><?php echo esc_html( __( 'Prompt', 'ai-auto-poster' ) ); ?>:</strong> <span data-review="prompt">—</span></li>
                            <li><strong><?php echo esc_html( __( 'Frequency', 'ai-auto-poster' ) ); ?>:</strong> <span data-review="freq">—</span></li>
                            <li><strong><?php echo esc_html( __( 'Provider', 'ai-auto-poster' ) ); ?>:</strong> <span data-review="provider">—</span></li>
                            <li><strong><?php echo esc_html( __( 'API Key', 'ai-auto-poster' ) ); ?>:</strong> <span data-review="apikey">—</span></li>
                        </ul>

                        <div class="aiap-wizard-actions">
                            <button type="button" class="button" data-aiap-wizard-prev>← <?php echo esc_html( __( 'Back', 'ai-auto-poster' ) ); ?></button>
                            <button type="submit" class="button button-primary aiap-btn-primary"><?php echo esc_html( __( 'Complete setup', 'ai-auto-poster' ) ); ?></button>
                        </div>
                    </section>

                </form>

                <p class="aiap-wizard-skip">
                    <a href="<?php echo esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&wizard_skip=1' ) ); ?>"><?php echo esc_html( __( 'Skip setup and configure manually', 'ai-auto-poster' ) ); ?></a>
                </p>
            </div>
        </div>
        <?php
    }

    /**
     * ウィザード POST: タスク作成 + API キー保存 + プラグイン有効化
     */
    public static function handle_wizard_complete() {
        if ( ! current_user_can( 'manage_options' ) ) wp_die( 'forbidden' );
        if ( ! isset( $_POST['_wpnonce'] ) || ! wp_verify_nonce( $_POST['_wpnonce'], self::NONCE ) ) wp_die( 'nonce' );

        $prompt   = trim( wp_unslash( $_POST['wiz_prompt'] ?? '' ) );
        $freq     = sanitize_key( $_POST['wiz_freq'] ?? 'daily' );
        $provider = sanitize_key( $_POST['wiz_provider'] ?? 'anthropic' );
        $apikey   = trim( wp_unslash( $_POST['wiz_apikey'] ?? '' ) );

        if ( $prompt === '' ) {
            wp_safe_redirect( add_query_arg( array( 'page' => self::MENU_SLUG, 'wizard' => 1, 'err' => 'prompt' ), admin_url( 'admin.php' ) ) );
            exit;
        }

        // 頻度ごとのスケジュール組み立て
        $schedule    = array();
        $random_mode = 0;
        $random_min  = 0;
        $random_max  = 0;
        $exclude     = array();
        switch ( $freq ) {
            case 'random':
                $random_mode = 1;
                $random_min  = 5;
                $random_max  = 10;
                $exclude     = array( array( 'start' => '01:00', 'end' => '06:00' ) );
                break;
            case 'weekly':
                $schedule = array(
                    'mon' => array( '09:00' ),
                    'wed' => array( '09:00' ),
                    'fri' => array( '09:00' ),
                );
                break;
            case 'daily':
            default:
                $days = array( 'mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun' );
                foreach ( $days as $d ) $schedule[ $d ] = array( '09:00' );
                break;
        }

        // タスク作成
        AIAP_Prompts::add(
            $prompt,
            $schedule,
            1, // enabled
            '',
            0,
            'on',
            $random_mode,
            $random_min,
            $random_max,
            $exclude
        );

        // API キー保存
        if ( $apikey !== '' ) {
            $s = AIAP_AI_Client::get_settings();
            $key_field = isset( self::$text_provider_key_field[ $provider ] ) ? self::$text_provider_key_field[ $provider ] : null;
            if ( $key_field ) {
                $s[ $key_field ] = $apikey;
                $s['provider']   = $provider;
                update_option( AIAP_AI_Client::OPTION_KEY, $s, false );
            }
        }

        // プラグイン有効化 + ウィザード完了フラグ
        $s = AIAP_AI_Client::get_settings();
        $s['enabled'] = 1;
        update_option( AIAP_AI_Client::OPTION_KEY, $s, false );
        update_option( self::OPTION_WIZARD_DONE, 1, false );

        // ダッシュボードへリダイレクト
        wp_safe_redirect( add_query_arg( array( 'page' => self::MENU_SLUG, 'wizard_done' => 1 ), admin_url( 'admin.php' ) ) );
        exit;
    }

    /**
     * 「ウィザードをスキップ」 → 完了フラグだけ立てて通常画面へ
     */
    public static function handle_wizard_skip() {
        if ( empty( $_GET['wizard_skip'] ) ) return;
        if ( ! current_user_can( 'manage_options' ) ) return;
        update_option( self::OPTION_WIZARD_DONE, 1, false );
    }

    private static function render_settings( $s ) {
        $tasks  = AIAP_Prompts::get_all();
        $active = array_filter( $tasks, function( $t ) { return ! empty( $t['enabled'] ); } );
        $tkey   = AIAP_Prompts::get_today_key();
        $today  = array_filter( $tasks, function( $t ) use ( $tkey ) { return ! empty( $t['enabled'] ) && ! empty( $t['schedule'][ $tkey ] ); } );
        $users  = get_users( array( 'capability' => 'edit_posts' ) );
        $cats   = get_categories( array( 'hide_empty' => false ) );

        $plan_is_pro    = AIAP_License::is_pro();
        $plan_is_trial  = AIAP_License::is_trial();
        $plan_count     = AIAP_License::monthly_post_count();
        $plan_limit     = AIAP_License::FREE_MONTHLY_LIMIT;
        $plan_remaining = AIAP_License::remaining_posts();
        $plan_pct       = $plan_is_pro ? 0 : min( 100, max( 0, ( $plan_count / max( 1, $plan_limit ) ) * 100 ) );
        $usage_class    = '';
        if ( ! $plan_is_pro ) {
            if ( $plan_remaining === 0 ) $usage_class = 'aiap-usage-fill-full';
            elseif ( $plan_pct >= 70 )   $usage_class = 'aiap-usage-fill-warn';
        }
        $is_pro = $plan_is_pro;
        ?>
        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-settings-form">
        <input type="hidden" name="action" value="aiap_save_settings"><?php wp_nonce_field( self::NONCE ); ?>

        <?php
        // ============================================================
        // Phase 1 Live Dashboard - 「今、何が動いているか」を最初に表示
        // ============================================================
        $active_count    = count( $active );
        $today_count     = count( $today );
        // Phase 12: 本日の投稿予定数 (固定スケジュール + ランダムタスクの今日の乱数決定値の合算)。
        // 旧 $today_count は「タスク数」だったため、ランダム 6〜10 のタスクが「今日 1 件」のように
        // 表示されて誤解を招いていた。実際の投稿予定件数を出す。
        $today_planned   = AIAP_Prompts::count_today_planned_posts();
        $next_run        = AIAP_Scheduler::get_next_scheduled_run();
        $queue_count     = AIAP_Scheduler::get_queue_count();
        $daily_counts    = AIAP_License::daily_post_counts( 7 );
        $week_max        = max( 1, max( $daily_counts ) );
        $is_enabled      = ! empty( $s['enabled'] );

        // 次の予定の表示文字列を組み立て
        $now             = new DateTimeImmutable( 'now', wp_timezone() );
        $next_label      = '';
        $next_relative   = '';
        if ( $next_run ) {
            if ( $next_run['is_today'] ) {
                $next_label = $next_run['time'];
                // 残り時間を分単位で計算
                $target = new DateTimeImmutable( $next_run['date'] . ' ' . $next_run['time'], wp_timezone() );
                $diff   = $target->getTimestamp() - $now->getTimestamp();
                if ( $diff > 0 ) {
                    $hours   = floor( $diff / 3600 );
                    $minutes = floor( ( $diff % 3600 ) / 60 );
                    if ( $hours > 0 ) {
                        /* translators: 1: hours remaining, 2: minutes remaining */
                        $next_relative = sprintf( __( 'in %1$dh %2$dm', 'ai-auto-poster' ), $hours, $minutes );
                    } else {
                        /* translators: %d = minutes remaining */
                        $next_relative = sprintf( __( 'in %d min', 'ai-auto-poster' ), max( 1, $minutes ) );
                    }
                }
            } else {
                $next_label    = mysql2date( 'M j', $next_run['date'] ) . ' ' . $next_run['time'];
                $next_relative = '';
            }
        }
        ?>
        <div class="aiap-dash">
            <div class="aiap-dash-header">
                <h2 class="aiap-dash-title"><?php echo esc_html( __( 'Live status', 'ai-auto-poster' ) ); ?></h2>
                <?php if ( $is_enabled ) : ?>
                    <span class="aiap-dash-pill aiap-dash-pill-on"><?php echo esc_html( __( 'Active', 'ai-auto-poster' ) ); ?></span>
                <?php else : ?>
                    <span class="aiap-dash-pill aiap-dash-pill-off"><?php echo esc_html( __( 'Paused', 'ai-auto-poster' ) ); ?></span>
                <?php endif; ?>
            </div>

            <div class="aiap-dash-grid">
                <div class="aiap-dash-card">
                    <div class="aiap-dash-card-label"><?php echo esc_html( __( 'Posts this month', 'ai-auto-poster' ) ); ?></div>
                    <div class="aiap-dash-card-value">
                        <?php echo esc_html( $plan_count ); ?>
                        <?php if ( ! $plan_is_pro ) : ?>
                            <span class="aiap-dash-card-sub">/ <?php echo esc_html( $plan_limit ); ?></span>
                        <?php endif; ?>
                    </div>
                    <?php if ( ! $plan_is_pro ) : ?>
                        <div class="aiap-dash-bar"><div class="aiap-dash-bar-fill <?php echo esc_attr( $usage_class ); ?>" style="width: <?php echo (int) $plan_pct; ?>%"></div></div>
                    <?php endif; ?>
                </div>

                <div class="aiap-dash-card">
                    <div class="aiap-dash-card-label"><?php echo esc_html( __( 'Active tasks', 'ai-auto-poster' ) ); ?></div>
                    <div class="aiap-dash-card-value"><?php echo esc_html( $active_count ); ?></div>
                </div>

                <?php
                // Phase 12: 本日の投稿カード。
                // 自動 (cron) と 手動 (Quick Post) を分けて表示。
                //   自動: 完了 (今日の自動投稿数) / 予定 (固定スケジュール + ランダム今日分)
                //   手動: 完了 (今日の手動投稿数のみ。手動には「予定」概念がないため X のみ)
                $tp_total       = (int) $today_planned['total'];
                $today_breakdown = AIAP_License::today_post_breakdown();
                $today_auto     = (int) $today_breakdown['auto'];
                $today_manual   = (int) $today_breakdown['manual'];
                ?>
                <div class="aiap-dash-card">
                    <div class="aiap-dash-card-label"><?php echo esc_html( __( 'Today', 'ai-auto-poster' ) ); ?></div>
                    <div class="aiap-dash-card-rows">
                        <div class="aiap-dash-card-row">
                            <span class="aiap-dash-card-row-label"><?php echo esc_html( _x( 'Auto', 'dashboard short label', 'ai-auto-poster' ) ); ?></span>
                            <span class="aiap-dash-card-row-value"><?php echo esc_html( $today_auto ); ?> / <?php echo esc_html( $tp_total ); ?></span>
                        </div>
                        <div class="aiap-dash-card-row">
                            <span class="aiap-dash-card-row-label"><?php echo esc_html( _x( 'Manual', 'dashboard short label', 'ai-auto-poster' ) ); ?></span>
                            <span class="aiap-dash-card-row-value"><?php echo esc_html( $today_manual ); ?></span>
                        </div>
                    </div>
                </div>

                <div class="aiap-dash-card">
                    <div class="aiap-dash-card-label"><?php echo esc_html( __( 'Next post', 'ai-auto-poster' ) ); ?></div>
                    <?php if ( $next_label ) : ?>
                        <div class="aiap-dash-card-value"><?php echo esc_html( $next_label ); ?></div>
                        <?php if ( $next_relative ) : ?>
                            <div class="aiap-dash-card-foot"><?php echo esc_html( $next_relative ); ?></div>
                        <?php endif; ?>
                    <?php else : ?>
                        <div class="aiap-dash-card-value aiap-dash-card-value-muted">—</div>
                        <div class="aiap-dash-card-foot"><?php echo esc_html( __( 'No upcoming runs', 'ai-auto-poster' ) ); ?></div>
                    <?php endif; ?>
                </div>

                <div class="aiap-dash-card">
                    <div class="aiap-dash-card-label"><?php echo esc_html( __( 'Recovery queue', 'ai-auto-poster' ) ); ?></div>
                    <div class="aiap-dash-card-value <?php echo $queue_count > 0 ? 'aiap-dash-card-value-warn' : ''; ?>"><?php echo esc_html( $queue_count ); ?></div>
                    <div class="aiap-dash-card-foot">
                        <?php
                        if ( $queue_count === 0 ) {
                            echo esc_html( __( 'All caught up', 'ai-auto-poster' ) );
                        } else {
                            /* translators: %d = number of pending recovery items */
                            echo esc_html( sprintf( _n( '%d pending', '%d pending', $queue_count, 'ai-auto-poster' ), $queue_count ) );
                        }
                        ?>
                    </div>
                </div>
            </div>

            <?php
            // 直近 7 日のミニグラフ
            $has_data = array_sum( $daily_counts ) > 0;
            ?>
            <div class="aiap-dash-chart">
                <div class="aiap-dash-chart-title"><?php echo esc_html( __( 'Last 7 days', 'ai-auto-poster' ) ); ?></div>
                <div class="aiap-dash-chart-bars" role="img" aria-label="<?php echo esc_attr( __( 'Posts per day for the last 7 days', 'ai-auto-poster' ) ); ?>">
                    <?php foreach ( $daily_counts as $date => $count ) :
                        $h_pct = (int) round( ( $count / $week_max ) * 100 );
                        $is_today_bar = ( $date === $now->format( 'Y-m-d' ) );
                    ?>
                        <div class="aiap-dash-chart-bar <?php echo $is_today_bar ? 'is-today' : ''; ?>" title="<?php echo esc_attr( $date . ': ' . $count ); ?>">
                            <div class="aiap-dash-chart-bar-count"><?php echo esc_html( $count ); ?></div>
                            <div class="aiap-dash-chart-bar-track">
                                <div class="aiap-dash-chart-bar-fill" style="height: <?php echo (int) max( 4, $h_pct ); ?>%"></div>
                            </div>
                            <div class="aiap-dash-chart-bar-label"><?php echo esc_html( mysql2date( 'D', $date ) ); ?></div>
                        </div>
                    <?php endforeach; ?>
                </div>
                <?php if ( ! $has_data ) : ?>
                    <div class="aiap-dash-chart-empty"><?php echo esc_html( __( 'No posts generated in the last 7 days yet.', 'ai-auto-poster' ) ); ?></div>
                <?php endif; ?>
            </div>
        </div>

        <?php /* Phase 2: ヒーロー (How should auto-posting behave?) は削除。ダッシュボード見出しで代替。 */ ?>

        <?php // ===== Plan card ===== ?>
        <div class="aiap-plan-card-v2 aiap-plan-card-v2-<?php echo esc_attr( $plan_is_pro ? 'pro' : ( $plan_is_trial ? 'trial' : 'free' ) ); ?>">
            <div class="aiap-plan-card-v2-row">
                <div class="aiap-plan-card-v2-name">
                    <?php if ( $plan_is_trial ) : ?>
                        <span class="aiap-plan-card-v2-label"><?php echo esc_html( __( 'Pro Trial', 'ai-auto-poster' ) ); ?></span>
                        <span class="aiap-plan-card-v2-badge aiap-plan-card-v2-badge-trial"><?php echo esc_html( __( 'TRIAL', 'ai-auto-poster' ) ); ?></span>
                    <?php elseif ( $plan_is_pro ) : ?>
                        <span class="aiap-plan-card-v2-label"><?php echo esc_html( __( 'Pro Plan', 'ai-auto-poster' ) ); ?></span>
                        <span class="aiap-plan-card-v2-badge aiap-plan-card-v2-badge-pro"><?php echo esc_html( __( 'PRO', 'ai-auto-poster' ) ); ?></span>
                    <?php else : ?>
                        <span class="aiap-plan-card-v2-label"><?php echo esc_html( __( 'Free Plan', 'ai-auto-poster' ) ); ?></span>
                    <?php endif; ?>
                </div>
                <div class="aiap-plan-card-v2-info">
                    <?php if ( $plan_is_trial ) :
                        $remain_days = AIAP_License::trial_days_remaining();
                        if ( $remain_days > 0 ) : ?>
                            <span class="aiap-plan-card-v2-stat"><?php
                                /* translators: %d = number of trial days remaining */
                                echo esc_html( sprintf( _n( '%d day remaining', '%d days remaining', $remain_days, 'ai-auto-poster' ), $remain_days ) );
                            ?></span>
                        <?php endif;
                    elseif ( $plan_is_pro ) : ?>
                        <span class="aiap-plan-card-v2-stat"><?php echo esc_html( __( 'Unlimited posting', 'ai-auto-poster' ) ); ?></span>
                    <?php else : ?>
                        <span class="aiap-plan-card-v2-stat">
                            <strong><?php echo esc_html( $plan_count ); ?></strong>
                            <span class="aiap-plan-card-v2-stat-sep">/</span>
                            <span class="aiap-plan-card-v2-stat-limit"><?php echo esc_html( $plan_limit ); ?></span>
                            <span class="aiap-plan-card-v2-stat-unit"><?php echo esc_html( __( 'posts this month', 'ai-auto-poster' ) ); ?></span>
                        </span>
                        <a href="<?php echo esc_url( AIAP_License::upgrade_url() ); ?>" class="aiap-plan-card-v2-cta">
                            <span class="dashicons dashicons-star-filled" aria-hidden="true"></span>
                            <?php echo esc_html( __( 'Upgrade to Pro', 'ai-auto-poster' ) ); ?>
                        </a>
                    <?php endif; ?>
                </div>
            </div>
            <?php if ( ! $plan_is_pro && ! $plan_is_trial ) : ?>
                <div class="aiap-plan-card-v2-bar">
                    <div class="aiap-plan-card-v2-bar-fill <?php echo esc_attr( $usage_class ); ?>" style="--aiap-bar-pct: <?php echo (int) $plan_pct; ?>%;"></div>
                </div>
                <?php if ( $plan_remaining === 0 ) : ?>
                    <p class="aiap-plan-card-v2-warning"><?php echo esc_html( __( 'Monthly limit reached. New posts will resume on the 1st of next month.', 'ai-auto-poster' ) ); ?></p>
                <?php elseif ( $plan_remaining <= 3 ) : ?>
                    <p class="aiap-plan-card-v2-warning"><?php
                        /* translators: %d = posts remaining */
                        echo esc_html( sprintf( _n( '%d post remaining this month.', '%d posts remaining this month.', $plan_remaining, 'ai-auto-poster' ), $plan_remaining ) );
                    ?></p>
                <?php endif; ?>
            <?php endif; ?>
        </div>

        <?php // ===== Status card (Phase 2: アコーディオン化, デフォルト展開) ===== ?>
        <details class="aiap-section aiap-section-status" open>
            <summary class="aiap-section-header">
                <h3 class="aiap-section-title"><?php echo esc_html( __( 'Status', 'ai-auto-poster' ) ); ?></h3>
            </summary>
            <div class="aiap-section-body">
                <div class="aiap-row-flex">
                    <div class="aiap-row-flex-text">
                        <strong class="aiap-row-flex-title"><?php echo esc_html( __( 'Auto Posting', 'ai-auto-poster' ) ); ?></strong>
                        <span class="aiap-row-flex-sub"><?php
                            /* translators: %d = number of tasks scheduled today */
                            echo esc_html( sprintf( __( 'Tasks scheduled today: %d', 'ai-auto-poster' ), count( $today ) ) );
                        ?></span>
                    </div>
                    <label class="aiap-toggle-switch aiap-toggle-switch-large">
                        <input type="checkbox" id="js-aiap-toggle-enabled" <?php checked( ! empty( $s['enabled'] ) ); ?> data-nonce="<?php echo esc_attr( wp_create_nonce( self::NONCE ) ); ?>">
                        <span class="aiap-toggle-slider"></span>
                        <span class="aiap-toggle-label" id="js-aiap-toggle-label"><strong><?php echo esc_html( self::resolve_dynamic_key( ! empty( $s['enabled'] ) ? 'enabled' : 'disabled' ) ); ?></strong></span>
                    </label>
                </div>
                <span id="js-aiap-toggle-status" class="aiap-toggle-status" aria-live="polite"></span>
            </div>
        </details>

        <?php // ===== Missed posts recovery (Phase 2: アコーディオン化) ===== ?>
        <details class="aiap-section">
            <summary class="aiap-section-header">
                <h3 class="aiap-section-title"><?php echo esc_html( __( 'When a post gets missed', 'ai-auto-poster' ) ); ?></h3>
                <p class="aiap-section-sub"><?php echo esc_html( __( 'If a scheduled run is skipped (server, WP issue), what to do.', 'ai-auto-poster' ) ); ?></p>
            </summary>
            <div class="aiap-section-body">
                <div class="aiap-field">
                    <label class="aiap-checkbox-row">
                        <input type="checkbox" name="cron_recover_missed" value="1" <?php checked( ! empty( $s['cron_recover_missed'] ) ); ?>>
                        <span>
                            <strong><?php echo esc_html( __( 'Run missed posts later', 'ai-auto-poster' ) ); ?></strong>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Skipped posts are queued and posted one-by-one on the next cron run.', 'ai-auto-poster' ) ); ?></span>
                        </span>
                    </label>
                </div>
                <div class="aiap-field">
                    <label class="aiap-field-label" for="aiap-cron-recover-max">
                        <?php echo esc_html( __( 'Max items to process', 'ai-auto-poster' ) ); ?>
                        <?php if ( ! $is_pro ) : ?>
                            <span class="aiap-pro-pill"><?php echo esc_html( __( 'PRO', 'ai-auto-poster' ) ); ?></span>
                        <?php endif; ?>
                    </label>
                    <?php $cur_max = $is_pro ? max( 1, min( 5, (int) ( $s['cron_recover_max'] ?? 3 ) ) ) : 1; ?>
                    <div class="aiap-field-row">
                        <input type="number" id="aiap-cron-recover-max" class="aiap-input-text aiap-input-narrow" name="cron_recover_max" value="<?php echo esc_attr( $cur_max ); ?>" min="1" max="5" <?php echo $is_pro ? '' : 'disabled'; ?>>
                        <span class="aiap-field-unit"><?php echo esc_html( __( 'items', 'ai-auto-poster' ) ); ?></span>
                    </div>
                    <?php if ( $is_pro ) : ?>
                        <span class="aiap-field-hint"><?php echo esc_html( __( 'Range 1–5. Older missed schedules beyond this are skipped.', 'ai-auto-poster' ) ); ?></span>
                    <?php else : ?>
                        <span class="aiap-field-hint">
                            <?php echo esc_html( __( 'Free plan: 1 item only. Upgrade to process up to 5.', 'ai-auto-poster' ) ); ?>
                            <a href="<?php echo esc_url( AIAP_License::upgrade_url() ); ?>" class="aiap-field-link aiap-field-link-pro">
                                <?php echo esc_html( __( 'Upgrade to Pro', 'ai-auto-poster' ) ); ?> →
                            </a>
                        </span>
                    <?php endif; ?>
                </div>
                <p class="aiap-field-note"><?php echo esc_html( __( 'Recovered posts use the original scheduled date/time, not the actual run time.', 'ai-auto-poster' ) ); ?></p>
            </div>
        </details>

        <?php // ===== Post Output Settings (Phase 2: アコーディオン化) ===== ?>
        <details class="aiap-section">
            <summary class="aiap-section-header">
                <h3 class="aiap-section-title"><?php echo esc_html( __( 'Post output', 'ai-auto-poster' ) ); ?></h3>
                <p class="aiap-section-sub"><?php echo esc_html( __( 'How generated posts appear on your site.', 'ai-auto-poster' ) ); ?></p>
            </summary>
            <div class="aiap-section-body">
                <div class="aiap-grid-2">
                    <div class="aiap-field">
                        <label class="aiap-field-label" for="aiap-post-status"><?php echo esc_html( __( 'Post Status', 'ai-auto-poster' ) ); ?></label>
                        <select id="aiap-post-status" class="aiap-select" name="post_status">
                            <?php foreach ( array('draft','pending','publish') as $v ) : ?>
                                <option value="<?php echo esc_attr( $v ); ?>" <?php selected( $s['post_status'], $v ); ?>><?php echo esc_html( self::resolve_dynamic_key( $v ) ); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                    <div class="aiap-field">
                        <label class="aiap-field-label" for="aiap-post-category"><?php echo esc_html( __( 'Category', 'ai-auto-poster' ) ); ?></label>
                        <select id="aiap-post-category" class="aiap-select" name="post_category">
                            <option value="0"><?php echo esc_html( __( 'None', 'ai-auto-poster' ) ); ?></option>
                            <?php foreach ( $cats as $c ) : ?>
                                <option value="<?php echo esc_attr( $c->term_id ); ?>" <?php selected( $s['post_category'], $c->term_id ); ?>><?php echo esc_html( $c->name ); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                    <div class="aiap-field aiap-field-fullspan">
                        <label class="aiap-field-label" for="aiap-post-author"><?php echo esc_html( __( 'Author', 'ai-auto-poster' ) ); ?></label>
                        <select id="aiap-post-author" class="aiap-select" name="post_author">
                            <?php foreach ( $users as $u ) : ?>
                                <option value="<?php echo esc_attr( $u->ID ); ?>" <?php selected( $s['post_author'], $u->ID ); ?>><?php echo esc_html( $u->display_name ); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                </div>
            </div>
        </details>

        <?php // ===== Dedup ===== ?>
        <?php
        $dis = $is_pro ? '' : 'disabled';
        $dd_enabled = $is_pro && ! empty( $s['dedup_enabled'] );
        $dd_step1   = $is_pro ? ! empty( $s['dedup_step1'] ) : true;
        $dd_step2   = $is_pro ? ! empty( $s['dedup_step2'] ) : true;
        $dd_sens    = $is_pro && isset( $s['dedup_sensitivity'] ) ? $s['dedup_sensitivity'] : 'medium';
        $dd_retry   = $is_pro ? (int) ( $s['dedup_max_retry'] ?? 3 ) : 3;
        ?>
        <details class="aiap-section aiap-section-pro">
            <summary class="aiap-section-header">
                <div class="aiap-section-title-row">
                    <h3 class="aiap-section-title"><?php echo esc_html( __( 'Prevent duplicate articles', 'ai-auto-poster' ) ); ?></h3>
                    <span class="aiap-pro-pill"><?php echo esc_html( __( 'PRO', 'ai-auto-poster' ) ); ?></span>
                    <?php if ( ! $is_pro ) : ?>
                        <a href="<?php echo esc_url( AIAP_License::upgrade_url() ); ?>" class="aiap-section-cta">
                            <?php echo esc_html( __( 'Upgrade to unlock', 'ai-auto-poster' ) ); ?> →
                        </a>
                    <?php endif; ?>
                </div>
            </summary>
            <div class="aiap-section-body <?php echo $is_pro ? '' : 'aiap-section-locked'; ?>">
                <div class="aiap-field">
                    <label class="aiap-checkbox-row">
                        <input type="checkbox" name="dedup_enabled" value="1" <?php checked( $dd_enabled ); ?> <?php echo esc_attr( $dis ); ?>>
                        <span>
                            <strong><?php echo esc_html( __( 'Avoid overlap with past posts', 'ai-auto-poster' ) ); ?></strong>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Compares new articles with posts from the past week and rewrites if similar.', 'ai-auto-poster' ) ); ?></span>
                        </span>
                    </label>
                </div>

                <div class="aiap-field">
                    <span class="aiap-field-label"><?php echo esc_html( __( 'Detection method', 'ai-auto-poster' ) ); ?></span>
                    <label class="aiap-checkbox-row">
                        <input type="checkbox" name="dedup_step1" value="1" <?php checked( $dd_step1 ); ?> <?php echo esc_attr( $dis ); ?>>
                        <span>
                            <strong><?php echo esc_html( __( 'Method 1: Compare keywords', 'ai-auto-poster' ) ); ?></strong>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Compares keywords in titles and content.', 'ai-auto-poster' ) ); ?></span>
                        </span>
                    </label>
                    <label class="aiap-checkbox-row aiap-dedup-step2-row">
                        <input type="checkbox" name="dedup_step2" value="1" <?php checked( $dd_step2 ); ?> <?php echo esc_attr( $dis ); ?>>
                        <span>
                            <strong><?php echo esc_html( __( 'Method 2: Let AI judge', 'ai-auto-poster' ) ); ?></strong>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'AI scores similarity. More accurate but uses more API credits.', 'ai-auto-poster' ) ); ?></span>
                        </span>
                    </label>
                </div>

                <div class="aiap-field">
                    <span class="aiap-field-label"><?php echo esc_html( __( 'Sensitivity', 'ai-auto-poster' ) ); ?></span>
                    <div class="aiap-segment" role="radiogroup">
                        <?php foreach ( array( 'low', 'medium', 'high' ) as $sv ) : ?>
                            <label class="aiap-segment-option<?php echo $dd_sens === $sv ? ' is-active' : ''; ?>">
                                <input type="radio" name="dedup_sensitivity" value="<?php echo esc_attr( $sv ); ?>" <?php checked( $dd_sens, $sv ); ?> <?php echo esc_attr( $dis ); ?>>
                                <span><?php echo esc_html( self::resolve_dynamic_key( 'dedup_sens_' . $sv ) ); ?></span>
                            </label>
                        <?php endforeach; ?>
                    </div>
                    <span class="aiap-field-hint"><?php echo esc_html( __( 'Applies to both methods. When both are ON, an article is regenerated only if both flag it.', 'ai-auto-poster' ) ); ?></span>
                </div>

                <div class="aiap-field">
                    <label class="aiap-field-label" for="aiap-dedup-retry"><?php echo esc_html( __( 'Max retries', 'ai-auto-poster' ) ); ?></label>
                    <div class="aiap-field-row">
                        <input type="number" id="aiap-dedup-retry" class="aiap-input-text aiap-input-narrow" name="dedup_max_retry" value="<?php echo esc_attr( $dd_retry ); ?>" min="1" max="10" <?php echo esc_attr( $dis ); ?>>
                        <span class="aiap-field-unit"><?php echo esc_html( __( 'retries', 'ai-auto-poster' ) ); ?></span>
                    </div>
                </div>
            </div>
        </details>

        <?php // ===== SEO (Phase 2: アコーディオン化) ===== ?>
        <?php
        // Phase 12: 画像設定タブの「SEO セクションへ」リンクから来た場合は ?open=seo が付くので、
        // <details> を開いた状態で描画する。直接 #aiap-seo-section へスクロールしても、
        // アコーディオンが閉じたままだとユーザーは何も見えないので、open 属性を強制する。
        $aiap_open_seo = isset( $_GET['open'] ) && sanitize_key( wp_unslash( $_GET['open'] ) ) === 'seo';
        ?>
        <details class="aiap-section aiap-section-pro" id="aiap-seo-section" data-aiap-anchor="seo"<?php echo $aiap_open_seo ? ' open' : ''; ?>>
            <summary class="aiap-section-header">
                <div class="aiap-section-title-row">
                    <h3 class="aiap-section-title"><?php echo esc_html( __( 'SEO optimization', 'ai-auto-poster' ) ); ?></h3>
                    <span class="aiap-pro-pill"><?php echo esc_html( __( 'PRO', 'ai-auto-poster' ) ); ?></span>
                    <?php if ( ! $is_pro ) : ?>
                        <a href="<?php echo esc_url( AIAP_License::upgrade_url() ); ?>" class="aiap-section-cta">
                            <?php echo esc_html( __( 'Upgrade to unlock', 'ai-auto-poster' ) ); ?> →
                        </a>
                    <?php endif; ?>
                </div>
            </summary>
            <div class="aiap-section-body <?php echo $is_pro ? '' : 'aiap-section-locked'; ?>">
                <div class="aiap-field">
                    <label class="aiap-checkbox-row">
                        <input type="checkbox" name="seo_enabled" value="1" <?php checked( $is_pro && ! empty( $s['seo_enabled'] ) ); ?> id="aiap-seo-switch" <?php echo $is_pro ? '' : 'disabled'; ?>>
                        <span>
                            <strong><?php echo esc_html( __( 'Enable SEO optimization', 'ai-auto-poster' ) ); ?></strong>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Automatically applies search engine optimization to generated articles.', 'ai-auto-poster' ) ); ?></span>
                        </span>
                    </label>
                </div>
                <p class="aiap-toggle-row">
                    <button type="button" class="aiap-btn js-aiap-toggle" data-target="aiap-seo-detail" <?php echo $is_pro ? '' : 'disabled'; ?>>
                        <span class="dashicons dashicons-admin-generic" aria-hidden="true"></span>
                        <?php echo esc_html( __( 'SEO options', 'ai-auto-poster' ) ); ?>
                    </button>
                </p>
                <div id="aiap-seo-detail" class="aiap-seo-detail" hidden>
                    <?php
                    $seo_groups = array(
                        'seo_content_group'   => array( 'seo_meta_desc','seo_heading','seo_focus_kw','seo_faq','seo_toc','seo_template' ),
                        'seo_image_group'     => array( 'seo_img_alt','seo_img_webp' ),
                        'seo_structure_group' => array( 'seo_internal_link','seo_auto_tag','seo_related' ),
                        'seo_meta_group'      => array( 'seo_schema','seo_ogp','seo_canonical','seo_plugin_link' ),
                        'seo_other_group'     => array( 'seo_reading_time','seo_sitemap_ping','seo_llms_txt' ),
                    );
                    $seo_standard = array(
                        'seo_meta_desc'     => 1,
                        'seo_heading'       => 1,
                        'seo_focus_kw'      => 1,
                        'seo_img_alt'       => 1,
                        'seo_internal_link' => 1,
                        'seo_auto_tag'      => 1,
                        'seo_schema'        => 1,
                    );
                    $disabled_attr = $is_pro ? '' : ' disabled';
                    foreach ( $seo_groups as $group_key => $fields ) :
                        echo '<span class="aiap-seo-group-label">' . esc_html( self::resolve_dynamic_key( $group_key ) ) . '</span>';
                        foreach ( $fields as $f ) :
                            $checked_state = $is_pro ? ! empty( $s[ $f ] ) : ! empty( $seo_standard[ $f ] );
                            echo '<label class="aiap-seo-check"><input type="checkbox" name="' . esc_attr( $f ) . '" value="1" ' . checked( $checked_state, true, false ) . esc_attr( $disabled_attr ) . '> <span>' . esc_html( self::resolve_dynamic_key( $f ) ) . '</span></label>';
                        endforeach;
                    endforeach;
                    ?>
                </div>
            </div>
        </details>

        <p class="submit"><button type="submit" class="button button-primary aiap-btn-primary"><span class="dashicons dashicons-saved" aria-hidden="true"></span> <?php echo esc_html( __( 'Save Settings', 'ai-auto-poster' ) ); ?></button></p>
        </form>
        <?php
    }

    private static function render_quick_post() {
        $uid         = get_current_user_id();
        $last_prompt = get_user_meta( $uid, self::USER_META_LAST_MANUAL_PROMPT, true );
        $s           = AIAP_AI_Client::get_settings();
        $langs       = AIAP_AI_Client::get_language_options();
        $last_lang   = get_user_meta( $uid, self::USER_META_LAST_MANUAL_LANGUAGE, true );
        $last_length = get_user_meta( $uid, self::USER_META_LAST_MANUAL_LENGTH, true );
        $last_img_mode = get_user_meta( $uid, self::USER_META_LAST_MANUAL_IMAGE_MODE, true );
        $init_lang   = ( $last_lang && isset( $langs[ $last_lang ] ) ) ? $last_lang : $s['content_language'];
        $init_length = $last_length ? (int) $last_length : (int) ( $s['content_length'] ?? 0 );
        $init_img_mode = AIAP_Prompts::sanitize_image_mode( $last_img_mode );

        $qp_is_pro    = AIAP_License::is_pro();
        $qp_at_limit  = ( ! $qp_is_pro ) && ! AIAP_License::can_post_more();
        $qp_count     = AIAP_License::monthly_post_count();
        $qp_limit     = AIAP_License::FREE_MONTHLY_LIMIT;
        $qp_remaining = AIAP_License::remaining_posts();
        ?>
        <div class="aiap-quickpost">
            <header class="aiap-provider-hero">
                <span class="aiap-provider-hero-eyebrow"><?php echo esc_html( __( 'Quick Post', 'ai-auto-poster' ) ); ?></span>
                <h2 class="aiap-provider-hero-title"><?php echo esc_html( __( 'Write a post right now', 'ai-auto-poster' ) ); ?></h2>
                <p class="aiap-provider-hero-sub"><?php echo esc_html( __( 'Type a prompt and one post is created immediately. Leave blank to use the next scheduled task.', 'ai-auto-poster' ) ); ?></p>
            </header>

            <?php if ( $qp_at_limit ) : ?>
                <div class="aiap-banner aiap-banner-error">
                    <span class="dashicons dashicons-warning" aria-hidden="true"></span>
                    <span class="aiap-banner-text">
                        <strong><?php echo esc_html( __( 'Free plan monthly limit reached (10 posts).', 'ai-auto-poster' ) ); ?></strong>
                        <span><?php echo esc_html( __( 'Upgrade to Pro for unlimited posting.', 'ai-auto-poster' ) ); ?></span>
                    </span>
                    <a href="<?php echo esc_url( AIAP_License::upgrade_url() ); ?>" class="aiap-banner-cta">
                        <span class="dashicons dashicons-star-filled" aria-hidden="true"></span>
                        <?php echo esc_html( __( 'Upgrade', 'ai-auto-poster' ) ); ?>
                    </a>
                </div>
            <?php elseif ( ! $qp_is_pro && $qp_remaining <= 3 ) : ?>
                <div class="aiap-banner aiap-banner-warning">
                    <span class="aiap-banner-text">
                        <?php
                        /* translators: 1: posts used, 2: monthly limit */
                        echo esc_html( sprintf( __( 'This month: %1$d / %2$d posts', 'ai-auto-poster' ), $qp_count, $qp_limit ) );
                        ?>
                    </span>
                </div>
            <?php endif; ?>

            <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>"
                  class="aiap-run-now-form aiap-quickpost-form"
                  data-label-run="<?php echo esc_attr( __( 'Run Now', 'ai-auto-poster' ) ); ?>"
                  data-label-running="<?php echo esc_attr( __( 'Running…', 'ai-auto-poster' ) ); ?>"
                  data-label-ai="<?php echo esc_attr( __( 'Generating article with AI…', 'ai-auto-poster' ) ); ?>"
                  data-label-post="<?php echo esc_attr( __( 'Creating post…', 'ai-auto-poster' ) ); ?>"
                  data-label-image="<?php echo esc_attr( __( 'Generating featured image…', 'ai-auto-poster' ) ); ?>"
                  data-label-seo="<?php echo esc_attr( __( 'Applying SEO settings…', 'ai-auto-poster' ) ); ?>"
                  data-label-done="<?php echo esc_attr( __( 'Completed', 'ai-auto-poster' ) ); ?>"
                  data-label-error="<?php echo esc_attr( __( 'Error', 'ai-auto-poster' ) ); ?>"
                  data-label-prompt-required="<?php echo esc_attr( __( 'Prompt is required.', 'ai-auto-poster' ) ); ?>"
                  data-label-retry-ai="<?php
                    /* translators: %d = number of AI retries (legacy) */
                    echo esc_attr( __( 'AI retries: %d', 'ai-auto-poster' ) );
                  ?>"
                  data-label-retry-dedup="<?php
                    /* translators: %d = number of dedup retries (legacy) */
                    echo esc_attr( __( 'Dedup retries: %d', 'ai-auto-poster' ) );
                  ?>"
                  data-label-server-retry="<?php
                    /* translators: 1: current retry, 2: max retries (e.g. "Recovering from server error 2/3") */
                    echo esc_attr( __( 'Recovering from server error %1$d/%2$d', 'ai-auto-poster' ) );
                  ?>"
                  data-label-dedup-retry="<?php
                    /* translators: 1: current retry, 2: max retries (e.g. "Resolving similar content 2/3") */
                    echo esc_attr( __( 'Resolving similar content %1$d/%2$d', 'ai-auto-poster' ) );
                  ?>"
                  data-max-retry-ai="<?php echo esc_attr( (string) AIAP_AI_Client::GENERATE_MAX_RETRY ); ?>"
                  data-max-retry-dedup="<?php echo esc_attr( (string) max( 1, (int) ( $s['dedup_max_retry'] ?? 3 ) ) ); ?>"
                  data-label-server-error="<?php
                    /* translators: %d = HTTP status code */
                    echo esc_attr( __( 'Server error (HTTP %d)', 'ai-auto-poster' ) );
                  ?>"
                  data-label-upgrade="<?php echo esc_attr( __( 'Upgrade to Pro', 'ai-auto-poster' ) ); ?>">
                <?php wp_nonce_field( self::NONCE ); ?>
                <input type="hidden" name="manual_rss_form_submitted" value="1">

                <div class="aiap-editor aiap-quickpost-editor">
                    <div class="aiap-quickpost-editor-head">
                        <label for="js-aiap-manual-prompt" class="aiap-editor-label"><?php echo esc_html( __( 'What do you want to write about?', 'ai-auto-poster' ) ); ?></label>
                        <span class="aiap-quickpost-charcount" data-aiap-charcount>0</span>
                    </div>
                    <textarea name="manual_prompt" id="js-aiap-manual-prompt" rows="6" class="aiap-editor-textarea aiap-quickpost-textarea" placeholder="<?php echo esc_attr( __( 'e.g. Write about three quick tips for staying focused while working from home, in a friendly tone.', 'ai-auto-poster' ) ); ?>" <?php echo $qp_at_limit ? 'disabled' : ''; ?>><?php echo esc_textarea( $last_prompt ); ?></textarea>
                    <p class="aiap-editor-hint aiap-manual-empty-hint"><?php echo esc_html( __( 'Leave empty to use the next scheduled task.', 'ai-auto-poster' ) ); ?></p>
                </div>

                <div class="aiap-quickpost-options aiap-quickpost-options-grid" id="js-aiap-manual-options">
                    <div class="aiap-quickpost-option-card">
                        <div class="aiap-quickpost-option-label">
                            <span class="dashicons dashicons-translation" aria-hidden="true"></span>
                            <?php echo esc_html( __( 'Language', 'ai-auto-poster' ) ); ?>
                        </div>
                        <select id="aiap-manual-lang" class="aiap-select" name="manual_content_language">
                            <?php foreach ( $langs as $v => $l ) : ?>
                                <option value="<?php echo esc_attr( $v ); ?>" <?php selected( $init_lang, $v ); ?>><?php echo esc_html( $l ); ?></option>
                            <?php endforeach; ?>
                        </select>
                    </div>
                    <div class="aiap-quickpost-option-card">
                        <div class="aiap-quickpost-option-label">
                            <span class="dashicons dashicons-editor-alignleft" aria-hidden="true"></span>
                            <?php echo esc_html( __( 'Length', 'ai-auto-poster' ) ); ?>
                        </div>
                        <div class="aiap-field-row">
                            <input type="number" id="aiap-manual-length" class="aiap-input-text aiap-input-narrow" name="manual_content_length" value="<?php echo esc_attr( $init_length > 0 ? $init_length : '' ); ?>" min="0" max="10000" placeholder="<?php echo esc_attr( __( 'Default', 'ai-auto-poster' ) ); ?>">
                            <span class="aiap-field-unit"><?php echo esc_html( __( 'chars', 'ai-auto-poster' ) ); ?></span>
                        </div>
                    </div>
                    <div class="aiap-quickpost-option-card aiap-quickpost-option-image">
                        <div class="aiap-quickpost-option-label">
                            <span class="dashicons dashicons-format-image" aria-hidden="true"></span>
                            <?php echo esc_html( __( 'Featured image', 'ai-auto-poster' ) ); ?>
                        </div>
                        <div class="aiap-quickpost-pill-group">
                            <label class="aiap-quickpost-pill"><input type="radio" name="manual_featured_image_mode" value="on" <?php checked( $init_img_mode !== 'off' ); ?> <?php disabled( $qp_at_limit ); ?>><span><?php echo esc_html( __( 'Yes', 'ai-auto-poster' ) ); ?></span></label>
                            <label class="aiap-quickpost-pill"><input type="radio" name="manual_featured_image_mode" value="off" <?php checked( $init_img_mode === 'off' ); ?> <?php disabled( $qp_at_limit ); ?>><span><?php echo esc_html( __( 'No', 'ai-auto-poster' ) ); ?></span></label>
                        </div>
                    </div>
                </div>

                <?php
                $qp_is_pro_for_rss = class_exists( 'AIAP_License' ) && AIAP_License::is_pro();
                $qp_rss_all_feeds  = class_exists( 'AIAP_RSS_Feeds' ) ? AIAP_RSS_Feeds::get_all() : array();
                // user_meta から最後の RSS 設定を復元
                $qp_uid = get_current_user_id();
                $qp_rss_defaults = array(
                    'rss_enabled'  => (int) get_user_meta( $qp_uid, self::USER_META_LAST_MANUAL_RSS_ENABLED, true ),
                    'rss_feed_ids' => array_filter( array_map( 'sanitize_key', (array) get_user_meta( $qp_uid, self::USER_META_LAST_MANUAL_RSS_FEED_IDS, true ) ) ),
                    'rss_count'    => max( 1, (int) ( get_user_meta( $qp_uid, self::USER_META_LAST_MANUAL_RSS_COUNT, true ) ?: 5 ) ),
                    'rss_mode'     => get_user_meta( $qp_uid, self::USER_META_LAST_MANUAL_RSS_MODE, true ) ?: 'pick',
                    'rss_fallback' => get_user_meta( $qp_uid, self::USER_META_LAST_MANUAL_RSS_FALLBACK, true ) ?: 'generate',
                );
                $qp_rss = AIAP_Prompts::get_rss_settings( $qp_rss_defaults );
                ?>
                <details class="aiap-task-section aiap-task-section--rss aiap-quickpost-rss" data-aiap-section="rss" data-aiap-configured-label="<?php echo esc_attr__( 'Configured', 'ai-auto-poster' ); ?>">
                    <summary class="aiap-task-section__summary">
                        <span class="aiap-task-section__toggle" aria-hidden="true"><span class="dashicons dashicons-arrow-right"></span></span>
                        <span class="aiap-task-section__icon dashicons dashicons-rss" aria-hidden="true"></span>
                        <span class="aiap-task-section__title"><?php echo esc_html__( 'RSS Settings', 'ai-auto-poster' ); ?></span>
                        <span class="aiap-pro-pill"><?php echo esc_html( __( 'PRO', 'ai-auto-poster' ) ); ?></span>
                        <?php if ( self::is_section_configured( 'rss', $qp_rss ) ) : ?>
                            <span class="aiap-task-section__configured-badge"><?php echo esc_html__( 'Configured', 'ai-auto-poster' ); ?></span>
                        <?php endif; ?>
                        <?php $qp_rss_summary = self::build_rss_section_summary( array( 'rss_enabled' => $qp_rss['rss_enabled'], 'rss_feed_ids' => $qp_rss['rss_feed_ids'], 'rss_count' => $qp_rss['rss_count'], 'rss_mode' => $qp_rss['rss_mode'], 'rss_fallback' => $qp_rss['rss_fallback'] ) ); if ( $qp_rss_summary !== '' ) : ?>
                            <span class="aiap-task-section__summary-text"><?php echo esc_html( $qp_rss_summary ); ?></span>
                        <?php else : ?>
                            <span class="aiap-task-section__summary-text"><?php echo esc_html__( '(optional)', 'ai-auto-poster' ); ?></span>
                        <?php endif; ?>
                    </summary>
                    <div class="aiap-task-section__body">

                    <?php if ( ! $qp_is_pro_for_rss ) : ?>
                        <p class="aiap-pro-badge"><?php echo esc_html__( 'This is a Pro feature.', 'ai-auto-poster' ); ?></p>
                    <?php endif; ?>

                    <fieldset <?php echo $qp_is_pro_for_rss ? '' : 'disabled'; ?>>
                        <p>
                            <label>
                                <input type="checkbox" name="manual_rss_enabled" value="1" class="js-aiap-task-rss-enable" <?php checked( 1, (int) $qp_rss['rss_enabled'] ); ?> />
                                <?php echo esc_html__( 'Use RSS as source material', 'ai-auto-poster' ); ?>
                            </label>
                        </p>

                        <div class="aiap-task-rss-detail" data-aiap-rss-detail>
                            <?php if ( empty( $qp_rss_all_feeds ) ) : ?>
                                <p class="description aiap-task-rss-empty">
                                    <?php echo esc_html__( 'Register an RSS feed first.', 'ai-auto-poster' ); ?>
                                    <a href="<?php echo esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=rss_feeds' ) ); ?>">
                                        <?php echo esc_html__( 'Open RSS Feeds tab', 'ai-auto-poster' ); ?>
                                    </a>
                                </p>
                            <?php else : ?>
                                <p>
                                    <strong><?php echo esc_html__( 'Source feeds', 'ai-auto-poster' ); ?></strong><br/>
                                    <?php foreach ( $qp_rss_all_feeds as $qp_feed ) :
                                        $qp_fid = isset( $qp_feed['id'] )   ? $qp_feed['id']   : '';
                                        $qp_fnm = isset( $qp_feed['name'] ) ? $qp_feed['name'] : '';
                                        $qp_chk = in_array( $qp_fid, $qp_rss['rss_feed_ids'], true );
                                    ?>
                                        <label class="aiap-task-rss-feed">
                                            <input type="checkbox" name="manual_rss_feed_ids[]" value="<?php echo esc_attr( $qp_fid ); ?>" <?php checked( $qp_chk ); ?> />
                                            <?php echo esc_html( $qp_fnm ); ?>
                                        </label><br/>
                                    <?php endforeach; ?>
                                </p>

                                <p>
                                    <label>
                                        <?php echo esc_html__( 'Item count', 'ai-auto-poster' ); ?>
                                        <input type="number" name="manual_rss_count" min="1" max="<?php echo esc_attr( AIAP_RSS_Feeds::COUNT_MAX ); ?>" step="1" value="<?php echo esc_attr( (int) $qp_rss['rss_count'] ); ?>" />
                                    </label>
                                </p>

                                <p>
                                    <strong><?php echo esc_html__( 'Mode', 'ai-auto-poster' ); ?></strong><br/>
                                    <label><input type="radio" name="manual_rss_mode" value="pick"   <?php checked( 'pick',   $qp_rss['rss_mode'] ); ?> /> <?php echo esc_html__( 'Pickup (AI picks one)', 'ai-auto-poster' ); ?></label><br/>
                                    <label><input type="radio" name="manual_rss_mode" value="digest" <?php checked( 'digest', $qp_rss['rss_mode'] ); ?> /> <?php echo esc_html__( 'Digest (summarize all)', 'ai-auto-poster' ); ?></label>
                                </p>

                                <p>
                                    <strong><?php echo esc_html__( 'When no fresh articles:', 'ai-auto-poster' ); ?></strong><br/>
                                    <label><input type="radio" name="manual_rss_fallback" value="skip"     <?php checked( 'skip',     $qp_rss['rss_fallback'] ); ?> /> <?php echo esc_html__( 'Skip post', 'ai-auto-poster' ); ?></label><br/>
                                    <label><input type="radio" name="manual_rss_fallback" value="generate" <?php checked( 'generate', $qp_rss['rss_fallback'] ); ?> /> <?php echo esc_html__( 'Generate without RSS', 'ai-auto-poster' ); ?></label>
                                </p>
                            <?php endif; ?>
                        </div>
                    </fieldset>
                    </div>
                </details>

                <div class="aiap-quickpost-action">
                    <button type="submit" class="button button-primary aiap-run-now-btn aiap-btn-primary aiap-btn-large aiap-quickpost-cta" <?php disabled( $qp_at_limit ); ?>>
                        <span class="dashicons dashicons-controls-play" aria-hidden="true"></span>
                        <?php echo esc_html( __( 'Generate now', 'ai-auto-poster' ) ); ?>
                    </button>
                </div>

                <div class="aiap-progress" aria-live="polite" hidden>
                    <div class="aiap-progress-main">
                        <div class="aiap-progress-step" data-step="ai"></div>
                        <div class="aiap-progress-step" data-step="post"></div>
                        <div class="aiap-progress-step" data-step="image"></div>
                        <div class="aiap-progress-step" data-step="seo"></div>
                        <div class="aiap-progress-result"></div>
                    </div>
                    <div class="aiap-progress-media" aria-hidden="true">
                        <video class="aiap-progress-video"
                               src="<?php echo esc_url( AIAP_PLUGIN_URL . 'assets/video/Robot.mp4' ); ?>"
                               muted
                               loop
                               playsinline
                               preload="metadata"></video>
                    </div>
                </div>
            </form>
        </div>
        <?php
    }

    // ================================================================
    // AIプロバイダ — サブタブ方式 (BotWriter style)
    // ================================================================
    private static function render_provider( $s ) {
        $sub = sanitize_key( wp_unslash( $_GET['sub'] ?? ( $s['provider'] ?? 'claude' ) ) );
        if ( ! isset( self::$prov_icons[ $sub ] ) ) $sub = 'claude';
        // 各プロバイダのモデル一覧（キャッシュがあればそれを、なければハードコードのデフォルト）
        $models = array();
        $discontinued = array();
        foreach ( array_keys( self::$prov_icons ) as $pkey ) {
            $list = AIAP_AI_Client::get_models_for( $pkey );
            $models[ $pkey ] = $list;
            $mfield = $pkey . '_model';
            $cur = $s[ $mfield ] ?? '';
            if ( $cur !== '' && ! empty( $list ) && ! isset( $list[ $cur ] ) ) {
                $discontinued[ $pkey ] = $cur;
            }
        }
        $key_map   = self::$text_provider_key_field;
        $model_map = self::$text_provider_model_field;
        // よく使われる3プロバイダを上に、残りを下にコンパクト表示
        $popular = array( 'openai', 'claude', 'gemini' );
        $others  = array_values( array_diff( array_keys( self::$prov_icons ), $popular ) );
        $active_provider = isset( $s['provider'] ) && isset( self::$prov_icons[ $s['provider'] ] ) ? $s['provider'] : 'claude';
        ?>
        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-provider-form">
        <input type="hidden" name="action" value="aiap_save_provider">
        <input type="hidden" id="aiap-provider-tab" name="provider_tab" value="<?php echo esc_attr( $active_provider ); ?>">
        <input type="hidden" name="provider" id="aiap-active-provider-input" value="<?php echo esc_attr( $active_provider ); ?>">
        <?php wp_nonce_field( self::NONCE ); ?>

        <header class="aiap-provider-hero">
            <span class="aiap-provider-hero-eyebrow"><?php echo esc_html( __( 'AI Provider', 'ai-auto-poster' ) ); ?></span>
            <h2 class="aiap-provider-hero-title"><?php echo esc_html( __( 'Which AI will write your articles?', 'ai-auto-poster' ) ); ?></h2>
            <p class="aiap-provider-hero-sub"><?php echo esc_html( __( 'Pick one, and it will be used for auto-posting. You can change it later.', 'ai-auto-poster' ) ); ?></p>
        </header>

        <div class="aiap-provider-section-label"><?php echo esc_html( __( 'Most common', 'ai-auto-poster' ) ); ?></div>
        <div class="aiap-provider-popular" id="aiap-providers-popular" role="radiogroup" aria-label="<?php echo esc_attr( __( 'Popular AI providers', 'ai-auto-poster' ) ); ?>">
            <?php
            $not_set_label   = __( 'Not set', 'ai-auto-poster' );
            $configured_label = __( 'Configured', 'ai-auto-poster' );
            foreach ( $popular as $pkey ) :
                $pi          = self::$prov_icons[ $pkey ];
                $name        = self::meta_label( $pi );
                $sub_text    = self::provider_subtitle( $pkey );
                $is_active   = ( $active_provider === $pkey );
                $is_configured = ! empty( $s[ $key_map[ $pkey ] ] );
                ?>
                <button type="button"
                    class="aiap-provider-tile<?php echo $is_active ? ' is-selected' : ''; ?>"
                    data-aiap-provider="<?php echo esc_attr( $pkey ); ?>"
                    data-aiap-name="<?php echo esc_attr( $name ); ?>"
                    data-aiap-sub="<?php echo esc_attr( $sub_text ); ?>"
                    data-aiap-icon-class="<?php echo esc_attr( $pi['ico'] ); ?>"
                    data-aiap-letter="<?php echo esc_attr( $pi['letter'] ); ?>"
                    role="radio"
                    aria-checked="<?php echo $is_active ? 'true' : 'false'; ?>">
                    <span class="aiap-provider-tile-icon ico ico-lg <?php echo esc_attr( $pi['ico'] ); ?>" aria-hidden="true"><?php echo esc_html( $pi['letter'] ); ?></span>
                    <span class="aiap-provider-tile-meta">
                        <span class="aiap-provider-tile-name"><?php echo esc_html( $name ); ?></span>
                        <?php if ( $sub_text !== '' ) : ?>
                            <span class="aiap-provider-tile-sub"><?php echo esc_html( $sub_text ); ?></span>
                        <?php endif; ?>
                    </span>
                    <span class="aiap-provider-tile-status<?php echo $is_configured ? ' is-configured' : ' is-empty'; ?>">
                        <?php echo esc_html( $is_configured ? $configured_label : $not_set_label ); ?>
                    </span>
                    <span class="aiap-provider-tile-check" aria-hidden="true">
                        <svg viewBox="0 0 24 24" width="13" height="13" focusable="false"><path d="M5 12l5 5L20 7" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
                    </span>
                </button>
            <?php endforeach; ?>
        </div>

        <div class="aiap-provider-section-label"><?php echo esc_html( __( 'Other providers', 'ai-auto-poster' ) ); ?></div>
        <div class="aiap-provider-other" role="radiogroup" aria-label="<?php echo esc_attr( __( 'Other AI providers', 'ai-auto-poster' ) ); ?>">
            <?php foreach ( $others as $pkey ) :
                $pi          = self::$prov_icons[ $pkey ];
                $name        = self::meta_label( $pi );
                $sub_text    = self::provider_subtitle( $pkey );
                $is_active   = ( $active_provider === $pkey );
                $is_configured = ! empty( $s[ $key_map[ $pkey ] ] );
                ?>
                <button type="button"
                    class="aiap-provider-tile-compact<?php echo $is_active ? ' is-selected' : ''; ?>"
                    data-aiap-provider="<?php echo esc_attr( $pkey ); ?>"
                    data-aiap-name="<?php echo esc_attr( $name ); ?>"
                    data-aiap-sub="<?php echo esc_attr( $sub_text ); ?>"
                    data-aiap-icon-class="<?php echo esc_attr( $pi['ico'] ); ?>"
                    data-aiap-letter="<?php echo esc_attr( $pi['letter'] ); ?>"
                    role="radio"
                    aria-checked="<?php echo $is_active ? 'true' : 'false'; ?>">
                    <span class="aiap-provider-tile-icon ico <?php echo esc_attr( $pi['ico'] ); ?>" aria-hidden="true"><?php echo esc_html( $pi['letter'] ); ?></span>
                    <span class="aiap-provider-tile-name"><?php echo esc_html( $name ); ?></span>
                    <span class="aiap-provider-tile-status<?php echo $is_configured ? ' is-configured' : ' is-empty'; ?>" aria-label="<?php echo esc_attr( $is_configured ? $configured_label : $not_set_label ); ?>"></span>
                    <span class="aiap-provider-tile-check" aria-hidden="true">
                        <svg viewBox="0 0 24 24" width="11" height="11" focusable="false"><path d="M5 12l5 5L20 7" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
                    </span>
                </button>
            <?php endforeach; ?>
        </div>

        <?php // 各プロバイダの設定パネル (active のみ表示)。フォーム項目はすべて DOM に存在するので保存ハンドラは無変更で動く。 ?>
        <?php foreach ( self::$prov_icons as $card_key => $pi ) :
            $is_active = ( $active_provider === $card_key );
            $key_field = $key_map[ $card_key ];
            $has_key   = ! empty( $s[ $key_field ] );
            $masked    = $has_key ? str_repeat( '*', 20 ) : '';
            $name      = self::meta_label( $pi );
            $sub_text  = self::provider_subtitle( $card_key );
            ?>
            <div class="aiap-provider-panel aiap-card aiap-provider-card aiap-text-provider-card" id="aiap-provider-card-<?php echo esc_attr( $card_key ); ?>" data-aiap-panel-for="<?php echo esc_attr( $card_key ); ?>" <?php echo $is_active ? '' : 'hidden'; ?>>
                <header class="aiap-provider-panel-header">
                    <span class="aiap-provider-panel-icon ico ico-lg <?php echo esc_attr( $pi['ico'] ); ?>" aria-hidden="true"><?php echo esc_html( $pi['letter'] ); ?></span>
                    <span class="aiap-provider-panel-text">
                        <span class="aiap-provider-panel-title"><?php echo esc_html( $name ); ?></span>
                        <?php if ( $sub_text !== '' ) : ?>
                            <span class="aiap-provider-panel-sub"><?php echo esc_html( $sub_text ); ?></span>
                        <?php endif; ?>
                    </span>
                    <span class="aiap-provider-panel-active"><?php echo esc_html( __( 'In use', 'ai-auto-poster' ) ); ?></span>
                </header>
                <div class="aiap-provider-panel-body">
                    <div class="aiap-field">
                        <label class="aiap-field-label" for="aiap-apikey-<?php echo esc_attr( $card_key ); ?>"><?php echo esc_html( __( 'API Key', 'ai-auto-poster' ) ); ?></label>
                        <div class="aiap-field-row">
                            <input type="password" id="aiap-apikey-<?php echo esc_attr( $card_key ); ?>" class="aiap-input-text" name="<?php echo esc_attr( $key_field ); ?>" value="<?php echo esc_attr( $masked ); ?>" autocomplete="new-password">
                            <button type="button" class="aiap-btn aiap-btn-primary aiap-btn-test" data-test-kind="text" data-provider="<?php echo esc_attr( $card_key ); ?>">
                                <span class="dashicons dashicons-search"></span> <?php echo esc_html( __( 'Test', 'ai-auto-poster' ) ); ?>
                            </button>
                        </div>
                        <div class="aiap-field-actions">
                            <a href="<?php echo esc_url( self::$prov_urls[ $card_key ] ); ?>" target="_blank" rel="noopener" class="aiap-field-link">
                                <?php echo esc_html( __( 'Get an API key', 'ai-auto-poster' ) ); ?>
                                <svg viewBox="0 0 24 24" width="11" height="11" fill="none" aria-hidden="true"><path d="M7 17L17 7M17 7H9M17 7v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
                            </a>
                        </div>
                    </div>

                    <div class="aiap-field">
                        <label class="aiap-field-label" for="aiap-model-<?php echo esc_attr( $card_key ); ?>"><?php echo esc_html( __( 'Model', 'ai-auto-poster' ) ); ?></label>
                        <div class="aiap-field-row">
                            <select id="aiap-model-<?php echo esc_attr( $card_key ); ?>" name="<?php echo esc_attr( $model_map[ $card_key ] ); ?>" class="aiap-select aiap-model-select" data-provider="<?php echo esc_attr( $card_key ); ?>">
                                <?php
                                $model_options = $models[ $card_key ] ?? array();
                                $custom_label  = __( '(custom)', 'ai-auto-poster' );
                                if ( ! empty( $s[ $model_map[ $card_key ] ] ) && ! isset( $model_options[ $s[ $model_map[ $card_key ] ] ] ) ) {
                                    $model_options = array( $s[ $model_map[ $card_key ] ] => $s[ $model_map[ $card_key ] ] . ' ' . $custom_label ) + $model_options;
                                }
                                foreach ( $model_options as $v => $l ) : ?>
                                    <option value="<?php echo esc_attr( $v ); ?>" <?php selected( $s[ $model_map[ $card_key ] ], $v ); ?>><?php echo esc_html( $l ); ?></option>
                                <?php endforeach; ?>
                            </select>
                            <button type="button" class="aiap-btn aiap-btn-icon aiap-btn-refresh-models" data-provider="<?php echo esc_attr( $card_key ); ?>" title="<?php echo esc_attr( __( 'Refresh model list', 'ai-auto-poster' ) ); ?>" aria-label="<?php echo esc_attr( __( 'Refresh model list', 'ai-auto-poster' ) ); ?>">
                                <span class="dashicons dashicons-update" aria-hidden="true"></span>
                            </button>
                        </div>
                        <?php if ( isset( $discontinued[ $card_key ] ) ) : ?>
                            <p class="aiap-model-notice"><span class="dashicons dashicons-warning" aria-hidden="true"></span> <?php
                                /* translators: %s = model identifier (e.g. "gpt-4-turbo") that has been discontinued */
                                echo esc_html( sprintf( __( 'Selected model "%s" is no longer available. It will be switched on save.', 'ai-auto-poster' ), $discontinued[ $card_key ] ) );
                            ?></p>
                        <?php endif; ?>
                        <p class="aiap-model-feedback" data-provider="<?php echo esc_attr( $card_key ); ?>" hidden></p>
                    </div>

                    <div class="aiap-field">
                        <label class="aiap-field-label" for="aiap-custom-model-<?php echo esc_attr( $card_key ); ?>">
                            <?php echo esc_html( __( 'Custom model', 'ai-auto-poster' ) ); ?>
                            <span class="aiap-field-label-hint"><?php echo esc_html( __( 'optional', 'ai-auto-poster' ) ); ?></span>
                        </label>
                        <input type="text" id="aiap-custom-model-<?php echo esc_attr( $card_key ); ?>" class="aiap-input-text" name="<?php echo esc_attr( $card_key ); ?>_custom_model" data-aiap-custom-model placeholder="<?php echo esc_attr( __( 'Enter a model ID not in the list', 'ai-auto-poster' ) ); ?>">
                    </div>
                </div>
            </div>
        <?php endforeach; ?>

        <p class="submit aiap-provider-submit"><button type="submit" class="button button-primary aiap-btn-primary"><span class="dashicons dashicons-saved"></span> <?php echo esc_html( __( 'Save', 'ai-auto-poster' ) ); ?></button></p>
        </form>
        <?php
    }

    /**
     * プロバイダのサブタイトル (タイル/パネルに表示)。
     */
    private static function provider_subtitle( $key ) {
        switch ( $key ) {
            case 'openai':     return __( 'ChatGPT API', 'ai-auto-poster' );
            case 'claude':     return __( 'Anthropic', 'ai-auto-poster' );
            case 'gemini':     return __( 'Google AI Studio', 'ai-auto-poster' );
            case 'mistral':    return __( 'Mistral AI', 'ai-auto-poster' );
            case 'groq':       return __( 'Groq Cloud', 'ai-auto-poster' );
            case 'deepseek':   return __( 'DeepSeek', 'ai-auto-poster' );
            case 'openrouter': return __( 'OpenRouter', 'ai-auto-poster' );
        }
        return '';
    }

    // ================================================================
    // 画像AIプロバイダ — サブタブ方式 (BotWriter style)
    // ================================================================
    private static function render_image_provider( $s, $im ) {
        $sub = sanitize_key( wp_unslash( $_GET['sub'] ?? ( $im['image_source'] ?? 'dalle3' ) ) );
        if ( ! isset( self::$img_icons[ $sub ] ) ) $sub = 'dalle3';
        $img_keys = array(
            'dalle3'    => array( 'key'=>'image_openai_api_key', 'model'=>null, 'url'=>'https://platform.openai.com/api-keys' ),
            'imagen'    => array( 'key'=>'image_gemini_api_key', 'model'=>null, 'url'=>'https://aistudio.google.com/app/apikey' ),
            'stability' => array( 'key'=>'stability_api_key', 'model'=>'stability_model', 'url'=>'https://platform.stability.ai/', 'models'=>array('sd3.5-medium','sd3.5-large','sd3.5-large-turbo') ),
            'flux'      => array( 'key'=>'flux_api_key',      'model'=>'flux_model',      'url'=>'https://fal.ai/dashboard/keys', 'models'=>array('flux-2-pro','flux-2-dev','flux-1.1-pro') ),
            'unsplash'  => array( 'key'=>'unsplash_access_key','model'=>null, 'url'=>'https://unsplash.com/developers' ),
            'pixabay'   => array( 'key'=>'pixabay_api_key',    'model'=>null, 'url'=>'https://pixabay.com/api/docs/' ),
            'pexels'    => array( 'key'=>'pexels_api_key',     'model'=>null, 'url'=>'https://www.pexels.com/api/' ),
        );
        $notes = array( 'openverse'=>'openverse_desc' );

        $dynamic_providers = array( 'dalle3', 'imagen' );
        $img_models = array();
        $img_discontinued = array();
        foreach ( $dynamic_providers as $pkey ) {
            $list = AIAP_AI_Client::get_models_for( $pkey );
            $img_models[ $pkey ] = $list;
            $mfield = $pkey . '_model';
            $cur = $im[ $mfield ] ?? '';
            if ( $cur !== '' && ! empty( $list ) && ! isset( $list[ $cur ] ) ) {
                $img_discontinued[ $pkey ] = $cur;
            }
        }

        // よく使われる4 (AI生成2 + 写真素材2)、その他
        $popular = array( 'dalle3', 'imagen', 'unsplash', 'pexels' );
        $others  = array_values( array_diff( array_keys( self::$img_icons ), $popular ) );
        $active = isset( $im['image_source'] ) && isset( self::$img_icons[ $im['image_source'] ] ) ? $im['image_source'] : 'dalle3';
        $not_set_label    = __( 'Not set', 'ai-auto-poster' );
        $configured_label = __( 'Configured', 'ai-auto-poster' );
        ?>
        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-provider-form">
        <input type="hidden" name="action" value="aiap_save_image_provider">
        <input type="hidden" id="aiap-image-tab" name="image_tab" value="<?php echo esc_attr( $active ); ?>">
        <input type="hidden" name="image_source" id="aiap-active-image-source-input" value="<?php echo esc_attr( $active ); ?>">
        <?php wp_nonce_field( self::NONCE ); ?>

        <header class="aiap-provider-hero">
            <span class="aiap-provider-hero-eyebrow"><?php echo esc_html( __( 'Featured Image', 'ai-auto-poster' ) ); ?></span>
            <h2 class="aiap-provider-hero-title"><?php echo esc_html( __( 'Where do your images come from?', 'ai-auto-poster' ) ); ?></h2>
            <p class="aiap-provider-hero-sub"><?php echo esc_html( __( 'Choose an AI image generator or a stock photo source. You can change it anytime.', 'ai-auto-poster' ) ); ?></p>
        </header>

        <div class="aiap-provider-section-label"><?php echo esc_html( __( 'Most common', 'ai-auto-poster' ) ); ?></div>
        <div class="aiap-provider-popular" id="aiap-image-providers-popular" role="radiogroup" aria-label="<?php echo esc_attr( __( 'Popular image sources', 'ai-auto-poster' ) ); ?>">
            <?php foreach ( $popular as $pkey ) :
                if ( ! isset( self::$img_icons[ $pkey ] ) ) continue;
                $ii = self::$img_icons[ $pkey ];
                $name = self::meta_label( $ii );
                $sub_text = self::image_provider_subtitle( $pkey );
                $is_active = ( $active === $pkey );
                $is_configured = isset( $img_keys[ $pkey ] ) && ! empty( $im[ $img_keys[ $pkey ]['key'] ] );
                ?>
                <button type="button"
                    class="aiap-provider-tile<?php echo $is_active ? ' is-selected' : ''; ?>"
                    data-aiap-image-source="<?php echo esc_attr( $pkey ); ?>"
                    role="radio"
                    aria-checked="<?php echo $is_active ? 'true' : 'false'; ?>">
                    <span class="aiap-provider-tile-icon ico ico-lg <?php echo esc_attr( $ii['ico'] ); ?>" aria-hidden="true"><?php echo esc_html( $ii['letter'] ); ?></span>
                    <span class="aiap-provider-tile-meta">
                        <span class="aiap-provider-tile-name"><?php echo esc_html( $name ); ?></span>
                        <?php if ( $sub_text !== '' ) : ?>
                            <span class="aiap-provider-tile-sub"><?php echo esc_html( $sub_text ); ?></span>
                        <?php endif; ?>
                    </span>
                    <span class="aiap-provider-tile-status<?php echo $is_configured ? ' is-configured' : ' is-empty'; ?>">
                        <?php echo esc_html( $is_configured ? $configured_label : $not_set_label ); ?>
                    </span>
                    <span class="aiap-provider-tile-check" aria-hidden="true">
                        <svg viewBox="0 0 24 24" width="13" height="13" focusable="false"><path d="M5 12l5 5L20 7" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
                    </span>
                </button>
            <?php endforeach; ?>
        </div>

        <div class="aiap-provider-section-label"><?php echo esc_html( __( 'Other sources', 'ai-auto-poster' ) ); ?></div>
        <div class="aiap-provider-other" role="radiogroup" aria-label="<?php echo esc_attr( __( 'Other image sources', 'ai-auto-poster' ) ); ?>">
            <?php foreach ( $others as $pkey ) :
                $ii = self::$img_icons[ $pkey ];
                $name = self::meta_label( $ii );
                $is_active = ( $active === $pkey );
                $is_configured = isset( $img_keys[ $pkey ] ) && ! empty( $im[ $img_keys[ $pkey ]['key'] ] );
                ?>
                <button type="button"
                    class="aiap-provider-tile-compact<?php echo $is_active ? ' is-selected' : ''; ?>"
                    data-aiap-image-source="<?php echo esc_attr( $pkey ); ?>"
                    role="radio"
                    aria-checked="<?php echo $is_active ? 'true' : 'false'; ?>">
                    <span class="aiap-provider-tile-icon ico <?php echo esc_attr( $ii['ico'] ); ?>" aria-hidden="true"><?php echo esc_html( $ii['letter'] ); ?></span>
                    <span class="aiap-provider-tile-name"><?php echo esc_html( $name ); ?></span>
                    <span class="aiap-provider-tile-status<?php echo $is_configured ? ' is-configured' : ' is-empty'; ?>" aria-label="<?php echo esc_attr( $is_configured ? $configured_label : $not_set_label ); ?>"></span>
                    <span class="aiap-provider-tile-check" aria-hidden="true">
                        <svg viewBox="0 0 24 24" width="11" height="11" focusable="false"><path d="M5 12l5 5L20 7" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
                    </span>
                </button>
            <?php endforeach; ?>
        </div>

        <?php foreach ( self::$img_icons as $card_key => $ii ) :
            $is_active = ( $active === $card_key );
            $name = self::meta_label( $ii );
            $sub_text = self::image_provider_subtitle( $card_key );
            $ik = $img_keys[ $card_key ] ?? null;
            $img_has_key = $ik && ! empty( $im[ $ik['key'] ] );
            $img_masked = $img_has_key ? str_repeat( '*', 20 ) : '';

            // スタイル選択
            $style_key = $card_key . '_style';
            $saved_style = $im[ $style_key ] ?? '';
            $current_model = '';
            if ( in_array( $card_key, $dynamic_providers, true ) ) {
                $current_model = $im[ $card_key . '_model' ] ?? '';
            }
            $style_opts = AIAP_Image_Client::get_provider_styles( $card_key, $current_model );
            $style_has_opts = ! empty( $style_opts );
            $style_unsupported = ( $saved_style !== '' && $style_has_opts && ! isset( $style_opts[ $saved_style ] ) ) ? $saved_style : '';
            $cur_style = ( $style_has_opts && isset( $style_opts[ $saved_style ] ) ) ? $saved_style : ( $style_has_opts ? array_key_first( $style_opts ) : '' );
            ?>
            <div class="aiap-provider-panel aiap-card aiap-provider-card aiap-image-provider-card" id="aiap-image-card-<?php echo esc_attr( $card_key ); ?>" data-aiap-image-panel-for="<?php echo esc_attr( $card_key ); ?>" <?php echo $is_active ? '' : 'hidden'; ?>>
                <header class="aiap-provider-panel-header">
                    <span class="aiap-provider-panel-icon ico ico-lg <?php echo esc_attr( $ii['ico'] ); ?>" aria-hidden="true"><?php echo esc_html( $ii['letter'] ); ?></span>
                    <span class="aiap-provider-panel-text">
                        <span class="aiap-provider-panel-title"><?php echo esc_html( $name ); ?></span>
                        <?php if ( $sub_text !== '' ) : ?>
                            <span class="aiap-provider-panel-sub"><?php echo esc_html( $sub_text ); ?></span>
                        <?php endif; ?>
                    </span>
                    <span class="aiap-provider-panel-active"><?php echo esc_html( __( 'In use', 'ai-auto-poster' ) ); ?></span>
                </header>
                <div class="aiap-provider-panel-body">
                    <?php if ( isset( $notes[ $card_key ] ) ) : ?>
                        <p class="aiap-field-note"><?php echo esc_html( self::resolve_dynamic_key( $notes[ $card_key ] ) ); ?></p>
                    <?php endif; ?>

                    <?php if ( $ik ) : ?>
                        <div class="aiap-field">
                            <label class="aiap-field-label" for="aiap-img-apikey-<?php echo esc_attr( $card_key ); ?>"><?php echo esc_html( __( 'API Key', 'ai-auto-poster' ) ); ?></label>
                            <div class="aiap-field-row">
                                <input type="password" id="aiap-img-apikey-<?php echo esc_attr( $card_key ); ?>" class="aiap-input-text" name="<?php echo esc_attr( $ik['key'] ); ?>" value="<?php echo esc_attr( $img_masked ); ?>" autocomplete="new-password">
                                <button type="button" class="aiap-btn aiap-btn-primary aiap-btn-test" data-test-kind="image" data-provider="<?php echo esc_attr( $card_key ); ?>">
                                    <span class="dashicons dashicons-search"></span> <?php echo esc_html( __( 'Test', 'ai-auto-poster' ) ); ?>
                                </button>
                            </div>
                            <div class="aiap-field-actions">
                                <a href="<?php echo esc_url( $ik['url'] ); ?>" target="_blank" rel="noopener" class="aiap-field-link">
                                    <?php echo esc_html( __( 'Get an API key', 'ai-auto-poster' ) ); ?>
                                    <svg viewBox="0 0 24 24" width="11" height="11" fill="none" aria-hidden="true"><path d="M7 17L17 7M17 7H9M17 7v8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>
                                </a>
                            </div>
                        </div>
                        <?php if ( ! empty( $ik['models'] ) && ! empty( $ik['model'] ) ) : ?>
                            <div class="aiap-field">
                                <label class="aiap-field-label" for="aiap-img-model-<?php echo esc_attr( $card_key ); ?>"><?php echo esc_html( __( 'Model', 'ai-auto-poster' ) ); ?></label>
                                <select id="aiap-img-model-<?php echo esc_attr( $card_key ); ?>" name="<?php echo esc_attr( $ik['model'] ); ?>" class="aiap-select">
                                    <?php foreach ( $ik['models'] as $m ) : ?>
                                        <option value="<?php echo esc_attr( $m ); ?>" <?php selected( $im[ $ik['model'] ] ?? '', $m ); ?>><?php echo esc_html( $m ); ?></option>
                                    <?php endforeach; ?>
                                </select>
                            </div>
                        <?php endif; ?>
                    <?php endif; ?>

                    <?php if ( in_array( $card_key, $dynamic_providers, true ) ) :
                        $mfield = $card_key . '_model';
                        $model_options = $img_models[ $card_key ] ?? array();
                        $cur_model = $im[ $mfield ] ?? '';
                        $custom_label = __( '(custom)', 'ai-auto-poster' );
                        if ( $cur_model !== '' && ! isset( $model_options[ $cur_model ] ) ) {
                            $model_options = array( $cur_model => $cur_model . ' ' . $custom_label ) + $model_options;
                        }
                        ?>
                        <div class="aiap-field">
                            <label class="aiap-field-label" for="aiap-img-dyn-model-<?php echo esc_attr( $card_key ); ?>"><?php echo esc_html( __( 'Model', 'ai-auto-poster' ) ); ?></label>
                            <div class="aiap-field-row">
                                <select id="aiap-img-dyn-model-<?php echo esc_attr( $card_key ); ?>" name="<?php echo esc_attr( $mfield ); ?>" class="aiap-select aiap-model-select" data-provider="<?php echo esc_attr( $card_key ); ?>">
                                    <?php foreach ( $model_options as $v => $l ) : ?>
                                        <option value="<?php echo esc_attr( $v ); ?>" <?php selected( $cur_model, $v ); ?>><?php echo esc_html( $l ); ?></option>
                                    <?php endforeach; ?>
                                </select>
                                <button type="button" class="aiap-btn aiap-btn-icon aiap-btn-refresh-models" data-provider="<?php echo esc_attr( $card_key ); ?>" title="<?php echo esc_attr( __( 'Refresh model list', 'ai-auto-poster' ) ); ?>" aria-label="<?php echo esc_attr( __( 'Refresh model list', 'ai-auto-poster' ) ); ?>">
                                    <span class="dashicons dashicons-update" aria-hidden="true"></span>
                                </button>
                            </div>
                            <?php if ( isset( $img_discontinued[ $card_key ] ) ) : ?>
                                <p class="aiap-model-notice"><span class="dashicons dashicons-warning" aria-hidden="true"></span> <?php
                                    /* translators: %s = image model identifier that has been discontinued */
                                    echo esc_html( sprintf( __( 'Selected model "%s" is no longer available. It will be switched on save.', 'ai-auto-poster' ), $img_discontinued[ $card_key ] ) );
                                ?></p>
                            <?php endif; ?>
                            <p class="aiap-model-feedback" data-provider="<?php echo esc_attr( $card_key ); ?>" hidden></p>
                        </div>
                    <?php endif; ?>

                    <div class="aiap-style-wrapper" data-style-wrapper data-provider="<?php echo esc_attr( $card_key ); ?>" <?php echo $style_has_opts ? '' : 'hidden'; ?>>
                        <div class="aiap-field">
                            <label class="aiap-field-label" for="aiap-img-style-<?php echo esc_attr( $card_key ); ?>"><?php echo esc_html( __( 'Style', 'ai-auto-poster' ) ); ?></label>
                            <select id="aiap-img-style-<?php echo esc_attr( $card_key ); ?>" name="<?php echo esc_attr( $style_key ); ?>" class="aiap-select aiap-style-select" data-provider="<?php echo esc_attr( $card_key ); ?>">
                                <?php if ( $style_has_opts ) : foreach ( $style_opts as $sk => $sv ) :
                                    $label = ! empty( $sv['label_key'] ) ? self::resolve_dynamic_key( $sv['label_key'] ) : ( $sv['label_ja'] ?? $sk );
                                    ?>
                                    <option value="<?php echo esc_attr( $sk ); ?>" <?php selected( $cur_style, $sk ); ?>><?php echo esc_html( ( ! empty( $sv['icon'] ) ? $sv['icon'] . ' ' : '' ) . $label ); ?></option>
                                <?php endforeach; endif; ?>
                            </select>
                            <?php if ( $style_unsupported ) : ?>
                                <p class="aiap-model-notice" data-style-notice><span class="dashicons dashicons-warning" aria-hidden="true"></span>
                                    <?php
                                    /* translators: %s = style name not supported by the current model */
                                    echo esc_html( sprintf( __( 'Selected style "%s" is not supported by the current model. It will be reset to default on save.', 'ai-auto-poster' ), $style_unsupported ) );
                                    ?>
                                </p>
                            <?php else : ?>
                                <p class="aiap-model-notice" data-style-notice hidden><span class="dashicons dashicons-warning" aria-hidden="true"></span><span data-style-notice-text></span></p>
                            <?php endif; ?>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Set the style for AI image generation.', 'ai-auto-poster' ) ); ?></span>
                        </div>
                    </div>
                </div>
            </div>
        <?php endforeach; ?>

        <p class="submit aiap-provider-submit"><button type="submit" class="button button-primary aiap-btn-primary"><span class="dashicons dashicons-saved"></span> <?php echo esc_html( __( 'Save', 'ai-auto-poster' ) ); ?></button></p>
        </form>
        <?php
    }

    /**
     * 画像プロバイダのサブタイトル
     */
    private static function image_provider_subtitle( $key ) {
        switch ( $key ) {
            case 'dalle3':    return __( 'OpenAI DALL-E', 'ai-auto-poster' );
            case 'imagen':    return __( 'Google Imagen', 'ai-auto-poster' );
            case 'stability': return __( 'Stable Diffusion', 'ai-auto-poster' );
            case 'flux':      return __( 'Black Forest Labs', 'ai-auto-poster' );
            case 'unsplash':  return __( 'Free stock photos', 'ai-auto-poster' );
            case 'pixabay':   return __( 'Free stock photos & videos', 'ai-auto-poster' );
            case 'pexels':    return __( 'Free stock photos', 'ai-auto-poster' );
            case 'openverse': return __( 'CC-licensed media', 'ai-auto-poster' );
        }
        return '';
    }

    // ================================================================
    // 画像設定 (BotWriter style — 2 column layout)
    // ================================================================
    private static function render_image_settings( $im ) {
        $cur_format  = $im['image_format']    ?? '16:9';
        $cur_quality = $im['image_quality']   ?? 'low';
        $cur_max     = (int) ( $im['image_max_width'] ?? 1200 );
        $cur_comp    = (int) ( $im['image_compress']  ?? 85 );

        $presets = self::get_image_presets();
        $current_preset = 'custom';
        foreach ( $presets as $key => $p ) {
            if ( $p['format'] === $cur_format
                && $p['quality'] === $cur_quality
                && $p['max_width'] === $cur_max
                && $p['compress'] === $cur_comp ) {
                $current_preset = $key;
                break;
            }
        }
        $current_label = ( 'custom' === $current_preset )
            ? __( 'Custom', 'ai-auto-poster' )
            : $presets[ $current_preset ]['title'];
        ?>
        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-imgset-form">
        <input type="hidden" name="action" value="aiap_save_image_settings">
        <?php wp_nonce_field( self::NONCE ); ?>
        <input type="hidden" name="image_preset" id="aiap-image-preset" value="<?php echo esc_attr( $current_preset ); ?>">

        <div class="aiap-imgset">

            <header class="aiap-imgset-hero">
                <span class="aiap-imgset-eyebrow"><?php echo esc_html__( 'Image Settings', 'ai-auto-poster' ); ?></span>
                <h2 class="aiap-imgset-title"><?php echo esc_html__( 'What is this image for?', 'ai-auto-poster' ); ?></h2>
                <p class="aiap-imgset-lede"><?php echo esc_html__( 'Pick a use case and we will set the size, quality, and file weight for you.', 'ai-auto-poster' ); ?></p>
            </header>

            <div class="aiap-imgset-presets" role="radiogroup" aria-label="<?php echo esc_attr__( 'Image preset', 'ai-auto-poster' ); ?>">
                <?php foreach ( $presets as $key => $p ) : ?>
                    <button type="button"
                        class="aiap-imgset-card aiap-imgset-card--<?php echo esc_attr( $key ); ?> <?php echo $current_preset === $key ? 'is-selected' : ''; ?>"
                        data-aiap-preset="<?php echo esc_attr( $key ); ?>"
                        data-aiap-format="<?php echo esc_attr( $p['format'] ); ?>"
                        data-aiap-quality="<?php echo esc_attr( $p['quality'] ); ?>"
                        data-aiap-max-width="<?php echo esc_attr( $p['max_width'] ); ?>"
                        data-aiap-compress="<?php echo esc_attr( $p['compress'] ); ?>"
                        role="radio"
                        aria-checked="<?php echo $current_preset === $key ? 'true' : 'false'; ?>">
                        <span class="aiap-imgset-card__visual" aria-hidden="true">
                            <?php echo self::get_preset_icon_svg( $key ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- hardcoded SVG ?>
                        </span>
                        <span class="aiap-imgset-card__body">
                            <span class="aiap-imgset-card__title">
                                <span class="aiap-imgset-card__name"><?php echo esc_html( $p['title'] ); ?></span>
                                <span class="aiap-imgset-card__check" aria-hidden="true">
                                    <svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M5 12l5 5L20 7" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/></svg>
                                    <?php echo esc_html__( 'Selected', 'ai-auto-poster' ); ?>
                                </span>
                            </span>
                            <span class="aiap-imgset-card__sub"><?php echo esc_html( $p['description'] ); ?></span>
                            <span class="aiap-imgset-card__chips">
                                <?php foreach ( $p['chips'] as $chip ) : ?>
                                    <span class="aiap-imgset-card__chip"><?php echo esc_html( $chip ); ?></span>
                                <?php endforeach; ?>
                            </span>
                        </span>
                    </button>
                <?php endforeach; ?>
            </div>

            <div class="aiap-imgset-current">
                <span class="aiap-imgset-current__dot" aria-hidden="true"></span>
                <span class="aiap-imgset-current__text">
                    <strong id="aiap-current-preset-label" data-custom-label="<?php echo esc_attr__( 'Custom', 'ai-auto-poster' ); ?>"><?php echo esc_html( $current_label ); ?></strong>
                    <?php echo esc_html__( 'will be used to generate images', 'ai-auto-poster' ); ?>
                </span>
            </div>

            <div class="aiap-imgset-advanced <?php echo $current_preset === 'custom' ? 'is-open' : ''; ?>" id="aiap-imgset-advanced">
                <button type="button" class="aiap-imgset-advanced__toggle" id="aiap-imgset-advanced-toggle" aria-expanded="<?php echo $current_preset === 'custom' ? 'true' : 'false'; ?>" aria-controls="aiap-imgset-advanced-panel">
                    <span class="aiap-imgset-advanced__label">
                        <span class="aiap-imgset-advanced__label-main"><?php echo esc_html__( 'Advanced settings', 'ai-auto-poster' ); ?></span>
                        <span class="aiap-imgset-advanced__caption"><?php echo esc_html__( 'For users who want to set size and quality manually', 'ai-auto-poster' ); ?></span>
                    </span>
                    <svg class="aiap-imgset-advanced__chev" width="18" height="18" viewBox="0 0 24 24" fill="none" aria-hidden="true">
                        <path d="M6 9l6 6 6-6" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
                    </svg>
                </button>

                <div class="aiap-imgset-advanced__panel" id="aiap-imgset-advanced-panel">
                    <div class="aiap-imgset-field">
                        <label for="aiap-image-format"><?php echo esc_html__( 'Image shape', 'ai-auto-poster' ); ?></label>
                        <select name="image_format" id="aiap-image-format" class="aiap-full-select">
                            <?php foreach ( self::$image_format_keys as $k ) : ?>
                                <option value="<?php echo esc_attr( $k ); ?>" <?php selected( $cur_format, $k ); ?>>
                                    <?php echo esc_html( self::resolve_dynamic_key( 'imgfmt_' . str_replace( ':', '_', $k ) ) ); ?>
                                </option>
                            <?php endforeach; ?>
                        </select>
                    </div>

                    <div class="aiap-imgset-field">
                        <label for="aiap-image-quality"><?php echo esc_html__( 'Image quality', 'ai-auto-poster' ); ?></label>
                        <select name="image_quality" id="aiap-image-quality" class="aiap-full-select">
                            <?php foreach ( self::$image_quality_keys as $k ) : ?>
                                <option value="<?php echo esc_attr( $k ); ?>" <?php selected( $cur_quality, $k ); ?>>
                                    <?php echo esc_html( self::resolve_dynamic_key( 'imgqual_' . $k ) ); ?>
                                </option>
                            <?php endforeach; ?>
                        </select>
                    </div>

                    <div class="aiap-imgset-field">
                        <label for="aiap-image-max-width"><?php echo esc_html__( 'Image size', 'ai-auto-poster' ); ?></label>
                        <div class="aiap-imgset-field__row">
                            <input type="number" name="image_max_width" id="aiap-image-max-width"
                                value="<?php echo esc_attr( $cur_max ); ?>" min="100" max="4096">
                            <span class="aiap-imgset-field__unit">px</span>
                        </div>
                        <span class="aiap-imgset-field__hint"><?php echo esc_html__( 'Recommended: Blog 1200px / SNS 1080px / Lite 800px', 'ai-auto-poster' ); ?></span>
                    </div>

                    <div class="aiap-imgset-field">
                        <label for="aiap-image-compress"><?php echo esc_html__( 'File weight (compression quality)', 'ai-auto-poster' ); ?></label>
                        <div class="aiap-imgset-field__row">
                            <input type="number" name="image_compress" id="aiap-image-compress"
                                value="<?php echo esc_attr( $cur_comp ); ?>" min="1" max="100">
                            <span class="aiap-imgset-field__unit">%</span>
                        </div>
                        <span class="aiap-imgset-field__hint"><?php echo esc_html__( 'Higher = better looking, lower = lighter file. Standard is 85%.', 'ai-auto-poster' ); ?></span>
                    </div>

                    <p class="aiap-imgset-field__note"><?php
                        /* Phase 12: 旧版は単に「in the SEO section」と書くだけで、ユーザーは
                           どのタブのどこにあるか分からなかった。基本設定タブの SEO アコーディオンへの
                           直接リンクに変更し、クリック 1 回で該当箇所まで飛べるようにする。
                           open=seo クエリパラメータと #aiap-seo-section ハッシュを併用し、
                           render_settings 側でアコーディオンを自動展開する。 */
                        $seo_url = add_query_arg(
                            array(
                                'page' => self::MENU_SLUG,
                                'tab'  => 'settings',
                                'open' => 'seo',
                            ),
                            admin_url( 'admin.php' )
                        ) . '#aiap-seo-section';
                        printf(
                            /* translators: %s = HTML link to "WebP conversion and compression" setting in SEO section */
                            esc_html__( 'Size and compression apply only when %s is enabled.', 'ai-auto-poster' ),
                            '<a href="' . esc_url( $seo_url ) . '">' . esc_html__( '"WebP conversion and compression" in the SEO section', 'ai-auto-poster' ) . '</a>'
                        );
                    ?></p>
                </div>
            </div>

            <p class="submit aiap-imgset-submit">
                <button type="submit" class="button button-primary aiap-btn-primary">
                    <span class="dashicons dashicons-saved"></span>
                    <?php echo esc_html__( 'Save Image Settings', 'ai-auto-poster' ); ?>
                </button>
            </p>

        </div>
        </form>
        <?php
    }

    /**
     * 画像設定プリセット定義。
     */
    private static function get_image_presets() {
        return array(
            'blog' => array(
                'title'       => __( 'Blog post', 'ai-auto-poster' ),
                'description' => __( 'For headers and inline article images. Balanced quality.', 'ai-auto-poster' ),
                'chips'       => array(
                    __( 'Landscape', 'ai-auto-poster' ),
                    __( 'Balanced', 'ai-auto-poster' ),
                    __( 'Standard size', 'ai-auto-poster' ),
                ),
                'format'      => '16:9',
                'quality'     => 'medium',
                'max_width'   => 1200,
                'compress'    => 85,
            ),
            'sns' => array(
                'title'       => __( 'Social media', 'ai-auto-poster' ),
                'description' => __( 'Square format that shares well on Instagram and X.', 'ai-auto-poster' ),
                'chips'       => array(
                    __( 'Square', 'ai-auto-poster' ),
                    __( 'Balanced', 'ai-auto-poster' ),
                    __( 'Compatible', 'ai-auto-poster' ),
                ),
                'format'      => '1:1',
                'quality'     => 'medium',
                'max_width'   => 1080,
                'compress'    => 80,
            ),
            'lite' => array(
                'title'       => __( 'Lightweight', 'ai-auto-poster' ),
                'description' => __( 'Site loads faster. Quality is slightly lower.', 'ai-auto-poster' ),
                'chips'       => array(
                    __( 'Smaller', 'ai-auto-poster' ),
                    __( 'Speed first', 'ai-auto-poster' ),
                    __( 'Saves data', 'ai-auto-poster' ),
                ),
                'format'      => '16:9',
                'quality'     => 'low',
                'max_width'   => 800,
                'compress'    => 70,
            ),
        );
    }

    /**
     * 装飾用 SVG (ハードコード、ユーザー入力を含まない)
     */
    private static function get_preset_icon_svg( $preset ) {
        switch ( $preset ) {
            case 'blog':
                return '<svg viewBox="0 0 64 64" width="56" height="56" fill="none" focusable="false"><rect x="6" y="14" width="52" height="36" rx="3" fill="#fff" stroke="#c2410c" stroke-width="2"/><line x1="12" y1="24" x2="40" y2="24" stroke="#c2410c" stroke-width="2.5" stroke-linecap="round"/><line x1="12" y1="32" x2="52" y2="32" stroke="#c2410c" stroke-width="2" stroke-linecap="round" opacity=".5"/><line x1="12" y1="38" x2="48" y2="38" stroke="#c2410c" stroke-width="2" stroke-linecap="round" opacity=".5"/><line x1="12" y1="44" x2="44" y2="44" stroke="#c2410c" stroke-width="2" stroke-linecap="round" opacity=".5"/></svg>';
            case 'sns':
                return '<svg viewBox="0 0 64 64" width="56" height="56" fill="none" focusable="false"><rect x="14" y="14" width="36" height="36" rx="3" fill="#fff" stroke="#047857" stroke-width="2"/><circle cx="42" cy="22" r="2.5" fill="#047857"/><path d="M14 42 L24 32 L34 40 L42 34 L50 42" stroke="#047857" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/><path d="M14 50 L50 50" stroke="#047857" stroke-width="2" stroke-linecap="round"/></svg>';
            case 'lite':
                return '<svg viewBox="0 0 64 64" width="56" height="56" fill="none" focusable="false"><path d="M28 8 L18 36 L30 36 L24 56 L46 28 L34 28 L40 8 Z" fill="#fff" stroke="#3730a3" stroke-width="2" stroke-linejoin="round"/></svg>';
        }
        return '';
    }

    // ================================================================
    // プロンプト管理 (unchanged)
    // ================================================================
    // システムプロンプトのみ (Phase 9: 別タブに分離)
    // ================================================================
    private static function render_system_prompt( $s ) {
        ?>
        <header class="aiap-provider-hero">
            <span class="aiap-provider-hero-eyebrow"><?php echo esc_html( __( 'System Prompt', 'ai-auto-poster' ) ); ?></span>
            <h2 class="aiap-provider-hero-title"><?php echo esc_html( __( 'Your overall tone, format, and rules', 'ai-auto-poster' ) ); ?></h2>
            <p class="aiap-provider-hero-sub"><?php echo esc_html( __( 'Instructions applied to every post (tone, format, audience, etc.). Each individual task adds its own prompt on top of this.', 'ai-auto-poster' ) ); ?></p>
        </header>

        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-prompts-form">
            <input type="hidden" name="action" value="aiap_save_system_prompt"><?php wp_nonce_field( self::NONCE ); ?>
            <div class="aiap-section">
                <?php /* Phase 12: section-header (title + sub) was redundant with the hero above
                       (both said "System prompt" + "Instructions applied to every post..."), so
                       removed entirely. The hero alone introduces the screen. */ ?>
                <div class="aiap-section-body">
                    <textarea name="system_prompt" rows="14" class="aiap-editor-textarea aiap-editor-textarea-mono"><?php echo esc_textarea( $s['system_prompt'] ); ?></textarea>
                    <input type="hidden" name="content_language" value="<?php echo esc_attr( $s['content_language'] ); ?>">
                    <input type="hidden" name="content_length" value="<?php echo esc_attr( $s['content_length'] ?? 0 ); ?>">
                </div>
            </div>
            <p class="submit"><button type="submit" class="button button-primary aiap-btn-primary"><span class="dashicons dashicons-saved" aria-hidden="true"></span> <?php echo esc_html( __( 'Save System Prompt', 'ai-auto-poster' ) ); ?></button></p>
        </form>
        <?php
    }

    // ================================================================
    // タスク一覧 (Phase 9: 別タブに分離 + 1 行レイアウト)
    // ================================================================
    private static function render_tasks( $s ) {
        $tasks = AIAP_Prompts::get_all();
        $days  = array( 'mon','tue','wed','thu','fri','sat','sun' );
        $langs = AIAP_AI_Client::get_language_options();
        $im    = AIAP_Image_Client::get_settings();
        ?>
        <header class="aiap-provider-hero">
            <span class="aiap-provider-hero-eyebrow"><?php echo esc_html( __( 'Tasks', 'ai-auto-poster' ) ); ?></span>
            <h2 class="aiap-provider-hero-title"><?php echo esc_html( __( 'Scheduled posting tasks', 'ai-auto-poster' ) ); ?></h2>
            <?php /* Phase 12: 旧 hero sub に「システムプロンプトは全タスク共通で使われます」が含まれていたが、
                   システムプロンプトはこの画面では編集できない別タブの機能で、ここで言及してもユーザーは
                   「で、それどこ?」となるだけ。タスクの説明だけに絞った。 */ ?>
            <p class="aiap-provider-hero-sub"><?php echo esc_html( __( 'Each task is a prompt + schedule.', 'ai-auto-poster' ) ); ?></p>
        </header>

        <?php
        if ( ! AIAP_License::is_pro() ) {
            $weekly_count = 0;
            foreach ( $tasks as $t ) {
                if ( empty( $t['enabled'] ) ) continue;
                $sched = isset( $t['schedule'] ) && is_array( $t['schedule'] ) ? $t['schedule'] : array();
                foreach ( $days as $d ) {
                    if ( ! empty( $sched[ $d ] ) && is_array( $sched[ $d ] ) ) {
                        $weekly_count += count( array_filter( $sched[ $d ], 'strlen' ) );
                    }
                }
            }
            $monthly_estimate = (int) round( $weekly_count * 4.345 );
            $free_limit = AIAP_License::FREE_MONTHLY_LIMIT;
            if ( $monthly_estimate > $free_limit ) :
                ?>
                <div class="aiap-banner aiap-banner-warning">
                    <span class="dashicons dashicons-warning" aria-hidden="true"></span>
                    <span class="aiap-banner-text">
                        <strong><?php
                            /* translators: 1: estimated monthly posts, 2: free plan limit */
                            echo esc_html( sprintf( __( 'Estimated %1$d posts/month from current schedule, but Free plan allows only %2$d.', 'ai-auto-poster' ), $monthly_estimate, $free_limit ) );
                        ?></strong>
                        <span><?php echo esc_html( __( 'Posts beyond the limit will be skipped automatically.', 'ai-auto-poster' ) ); ?></span>
                    </span>
                    <a href="<?php echo esc_url( AIAP_License::upgrade_url() ); ?>" class="aiap-banner-cta">
                        <span class="dashicons dashicons-star-filled" aria-hidden="true"></span>
                        <?php echo esc_html( __( 'Upgrade', 'ai-auto-poster' ) ); ?>
                    </a>
                </div>
                <?php
            endif;
        }
        ?>

        <div class="aiap-section">
            <div class="aiap-section-header aiap-section-header-with-action">
                <?php /* Phase 12: section title「Tasks」は hero eyebrow と完全に重複していたので削除。
                       カウントバッジと CTA ボタンだけ残し、ヘッダ機能は維持する。 */ ?>
                <div class="aiap-section-title-row">
                    <span class="aiap-section-count"><?php echo esc_html( count( $tasks ) ); ?></span>
                </div>
                <button type="button" class="button button-primary js-aiap-toggle-add-task aiap-btn-primary aiap-section-header-cta">
                    <span class="dashicons dashicons-plus-alt" aria-hidden="true"></span>
                    <?php echo esc_html( __( 'Add Task', 'ai-auto-poster' ) ); ?>
                </button>
            </div>
            <div class="aiap-section-body">
                <?php if ( empty( $tasks ) ) : ?>
                    <div class="aiap-empty-state">
                        <span class="dashicons dashicons-clipboard" aria-hidden="true"></span>
                        <p><?php echo esc_html( __( 'No tasks yet.', 'ai-auto-poster' ) ); ?></p>
                        <p class="aiap-empty-state-sub"><?php echo esc_html( __( 'Add your first task using the button above.', 'ai-auto-poster' ) ); ?></p>
                    </div>
                <?php else : ?>
                    <div class="aiap-task-list">
                        <?php $task_index = 0; foreach ( $tasks as $task ) :
                            $task_index++;
                            $is_on   = ! empty( $task['enabled'] );
                            $sched_summary = AIAP_Prompts::format_schedule_summary( $task );
                            $last_run_meta = isset( $task['last_run'] ) ? (string) $task['last_run'] : '';
                        ?>
                            <div class="aiap-task-card<?php echo $is_on ? ' is-enabled' : ' is-disabled'; ?>">
                                <div class="aiap-task-card-row aiap-task-card-row-compact">
                                    <span class="aiap-task-status-dot<?php echo $is_on ? ' is-on' : ''; ?>" aria-hidden="true"></span>
                                    <div class="aiap-task-card-main">
                                        <div class="aiap-task-name-line">
                                            <span class="aiap-task-name"><?php echo esc_html( AIAP_Prompts::get_display_name( $task, $task_index ) ); ?></span>
                                            <span class="aiap-task-id-mini"><?php
                                                /* translators: %d = sequential task number (1-based) */
                                                echo esc_html( sprintf( __( '#%d', 'ai-auto-poster' ), $task_index ) );
                                            ?></span>
                                        </div>
                                        <div class="aiap-task-card-meta-line">
                                            <span class="aiap-task-meta-item" title="<?php echo esc_attr( __( 'Schedule', 'ai-auto-poster' ) ); ?>">
                                                <span class="dashicons dashicons-clock" aria-hidden="true"></span>
                                                <?php echo esc_html( $sched_summary ); ?>
                                            </span>
                                            <span class="aiap-task-meta-item" title="<?php echo esc_attr( __( 'Run count', 'ai-auto-poster' ) ); ?>">
                                                <span class="dashicons dashicons-update" aria-hidden="true"></span>
                                                <?php
                                                /* translators: %d = times this task has been run */
                                                echo esc_html( sprintf( _n( 'Run %d time', 'Run %d times', (int) $task['used_count'], 'ai-auto-poster' ), (int) $task['used_count'] ) );
                                                ?>
                                            </span>
                                            <?php if ( $last_run_meta ) : ?>
                                                <span class="aiap-task-meta-item" title="<?php echo esc_attr( __( 'Last run', 'ai-auto-poster' ) ); ?>">
                                                    <span class="dashicons dashicons-yes-alt" aria-hidden="true"></span>
                                                    <?php echo esc_html( $last_run_meta ); ?>
                                                </span>
                                            <?php endif; ?>
                                        </div>
                                    </div>
                                    <div class="aiap-task-card-actions">
                                        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-inline-form">
                                            <input type="hidden" name="action" value="aiap_toggle_task">
                                            <input type="hidden" name="task_id" value="<?php echo esc_attr( $task['id'] ); ?>">
                                            <?php wp_nonce_field( self::NONCE ); ?>
                                            <label class="aiap-toggle-switch" title="<?php echo esc_attr( self::resolve_dynamic_key( $is_on ? 'deactivate' : 'activate' ) ); ?>" aria-label="<?php echo esc_attr( self::resolve_dynamic_key( $is_on ? 'deactivate' : 'activate' ) ); ?>">
                                                <input type="checkbox" <?php checked( $is_on ); ?> class="js-aiap-task-toggle-cb">
                                                <span class="aiap-toggle-slider"></span>
                                            </label>
                                        </form>
                                        <button type="button" class="aiap-btn aiap-btn-icon-only js-aiap-edit-task" data-target="aiap-task-edit-<?php echo esc_attr( $task['id'] ); ?>" title="<?php echo esc_attr( __( 'Edit', 'ai-auto-poster' ) ); ?>" aria-label="<?php echo esc_attr( __( 'Edit', 'ai-auto-poster' ) ); ?>">
                                            <span class="dashicons dashicons-edit" aria-hidden="true"></span>
                                        </button>
                                        <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-inline-form js-aiap-confirm" data-confirm="<?php echo esc_attr( __( 'Delete this task?', 'ai-auto-poster' ) ); ?>">
                                            <input type="hidden" name="action" value="aiap_delete_task">
                                            <input type="hidden" name="task_id" value="<?php echo esc_attr( $task['id'] ); ?>">
                                            <?php wp_nonce_field( self::NONCE ); ?>
                                            <button type="submit" class="aiap-btn aiap-btn-icon-only aiap-btn-danger-icon" title="<?php echo esc_attr( __( 'Delete', 'ai-auto-poster' ) ); ?>" aria-label="<?php echo esc_attr( __( 'Delete', 'ai-auto-poster' ) ); ?>">
                                                <span class="dashicons dashicons-trash" aria-hidden="true"></span>
                                            </button>
                                        </form>
                                    </div>
                                </div>
                                <details class="aiap-task-edit" id="aiap-task-edit-<?php echo esc_attr( $task['id'] ); ?>">
                                    <summary hidden></summary>
                                    <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-task-edit-form">
                                        <input type="hidden" name="action" value="aiap_edit_task">
                                        <input type="hidden" name="task_id" value="<?php echo esc_attr( $task['id'] ); ?>">
                                        <?php wp_nonce_field( self::NONCE ); ?>
                                        <div class="aiap-field">
                                            <label class="aiap-field-label"><?php echo esc_html( __( 'Task name', 'ai-auto-poster' ) ); ?> <span class="aiap-field-label-hint"><?php echo esc_html( __( 'optional', 'ai-auto-poster' ) ); ?></span></label>
                                            <input type="text" name="task_name" class="aiap-input-text" maxlength="60" value="<?php echo esc_attr( $task['name'] ?? '' ); ?>" placeholder="<?php echo esc_attr( __( 'e.g. Daily nature observations', 'ai-auto-poster' ) ); ?>">
                                            <span class="aiap-field-hint"><?php echo esc_html( __( 'A short label to identify this task. Leave blank to auto-generate from the prompt.', 'ai-auto-poster' ) ); ?></span>
                                        </div>
                                        <div class="aiap-field">
                                            <label class="aiap-field-label"><?php echo esc_html( __( 'Prompt', 'ai-auto-poster' ) ); ?> <span class="aiap-required" aria-hidden="true">*</span></label>
                                            <textarea name="prompt" rows="6" class="aiap-editor-textarea aiap-editor-textarea-mono" required><?php echo esc_textarea( $task['prompt'] ); ?></textarea>
                                        </div>
                                        <div class="aiap-grid-2">
                                            <div class="aiap-field">
                                                <label class="aiap-field-label"><?php echo esc_html( __( 'Content Language', 'ai-auto-poster' ) ); ?> <span class="aiap-required" aria-hidden="true">*</span></label>
                                                <?php $cur_lang = isset( $task['content_language'] ) ? $task['content_language'] : ''; ?>
                                                <select name="task_content_language" class="aiap-select" required>
                                                    <option value="" disabled <?php selected( '', $cur_lang ); ?> hidden><?php echo esc_html( __( '(Please select)', 'ai-auto-poster' ) ); ?></option>
                                                    <?php foreach ( $langs as $v => $l ) : ?>
                                                        <option value="<?php echo esc_attr( $v ); ?>" <?php selected( $cur_lang, $v ); ?>><?php echo esc_html( $l ); ?></option>
                                                    <?php endforeach; ?>
                                                </select>
                                            </div>
                                            <div class="aiap-field">
                                                <label class="aiap-field-label"><?php echo esc_html( __( 'Content Length', 'ai-auto-poster' ) ); ?></label>
                                                <div class="aiap-field-row">
                                                    <input type="number" name="task_content_length" value="<?php echo esc_attr( ! empty( $task['content_length'] ) ? (int) $task['content_length'] : '' ); ?>" min="0" max="10000" class="aiap-input-text aiap-input-narrow" placeholder="0">
                                                    <span class="aiap-field-unit"><?php echo esc_html( __( 'chars', 'ai-auto-poster' ) ); ?></span>
                                                </div>
                                                <span class="aiap-field-hint"><?php echo esc_html( __( '0 = unlimited', 'ai-auto-poster' ) ); ?></span>
                                            </div>
                                        </div>
                                        <details class="aiap-task-section aiap-task-section--image" data-aiap-section="image" <?php echo self::is_section_expanded( 'image', $task ) ? 'open' : ''; ?>>
                                            <summary class="aiap-task-section__summary">
                                                <span class="aiap-task-section__toggle" aria-hidden="true"><span class="dashicons dashicons-arrow-right"></span></span>
                                                <span class="aiap-task-section__icon dashicons dashicons-format-image" aria-hidden="true"></span>
                                                <span class="aiap-task-section__title"><?php echo esc_html( __( 'Featured image', 'ai-auto-poster' ) ); ?></span>
                                                <?php if ( self::is_section_configured( 'image', $task ) ) : ?>
                                                    <span class="aiap-task-section__configured-badge"><?php echo esc_html__( 'Configured', 'ai-auto-poster' ); ?></span>
                                                <?php endif; ?>
                                                <?php $image_summary = self::build_image_section_summary( $task ); if ( $image_summary !== '' ) : ?>
                                                    <span class="aiap-task-section__summary-text"><?php echo esc_html( $image_summary ); ?></span>
                                                <?php endif; ?>
                                            </summary>
                                            <div class="aiap-task-section__body">
                                                <?php $cur_img_mode = AIAP_Prompts::get_image_mode( $task ); ?>
                                                <label class="aiap-radio-row"><input type="radio" name="task_featured_image_mode" value="on" <?php checked( $cur_img_mode !== 'off' ); ?>> <span><?php echo esc_html( __( 'Always attach', 'ai-auto-poster' ) ); ?></span></label>
                                                <label class="aiap-radio-row"><input type="radio" name="task_featured_image_mode" value="off" <?php checked( $cur_img_mode === 'off' ); ?>> <span><?php echo esc_html( __( "Don't attach", 'ai-auto-poster' ) ); ?></span></label>
                                            </div>
                                        </details>
                                        <?php
                                        $is_pro_for_rss = class_exists( 'AIAP_License' ) && AIAP_License::is_pro();
                                        $rss_settings   = AIAP_Prompts::get_rss_settings( $task );
                                        $all_feeds      = class_exists( 'AIAP_RSS_Feeds' ) ? AIAP_RSS_Feeds::get_all() : array();
                                        ?>
                                        <details class="aiap-task-section aiap-task-section--rss" data-aiap-section="rss" data-aiap-rss-section data-aiap-configured-label="<?php echo esc_attr__( 'Configured', 'ai-auto-poster' ); ?>" <?php echo self::is_section_expanded( 'rss', $task ) ? 'open' : ''; ?>>
                                            <summary class="aiap-task-section__summary">
                                                <span class="aiap-task-section__toggle" aria-hidden="true"><span class="dashicons dashicons-arrow-right"></span></span>
                                                <span class="aiap-task-section__icon dashicons dashicons-rss" aria-hidden="true"></span>
                                                <span class="aiap-task-section__title"><?php echo esc_html__( 'RSS Settings', 'ai-auto-poster' ); ?></span>
                                                <span class="aiap-pro-pill"><?php echo esc_html( __( 'PRO', 'ai-auto-poster' ) ); ?></span>
                                                <?php if ( self::is_section_configured( 'rss', $task ) ) : ?>
                                                    <span class="aiap-task-section__configured-badge"><?php echo esc_html__( 'Configured', 'ai-auto-poster' ); ?></span>
                                                <?php endif; ?>
                                                <?php $rss_summary = self::build_rss_section_summary( $task ); if ( $rss_summary !== '' ) : ?>
                                                    <span class="aiap-task-section__summary-text"><?php echo esc_html( $rss_summary ); ?></span>
                                                <?php else : ?>
                                                    <span class="aiap-task-section__summary-text"><?php echo esc_html__( '(optional)', 'ai-auto-poster' ); ?></span>
                                                <?php endif; ?>
                                            </summary>
                                            <div class="aiap-task-section__body">

                                            <?php if ( ! $is_pro_for_rss ) : ?>
                                                <p class="aiap-pro-badge"><?php echo esc_html__( 'This is a Pro feature.', 'ai-auto-poster' ); ?></p>
                                            <?php endif; ?>

                                            <fieldset <?php echo $is_pro_for_rss ? '' : 'disabled'; ?>>
                                                <p>
                                                    <label>
                                                        <input type="checkbox" name="rss_enabled" value="1" class="js-aiap-task-rss-enable" <?php checked( 1, (int) $rss_settings['rss_enabled'] ); ?> />
                                                        <?php echo esc_html__( 'Use RSS as source material', 'ai-auto-poster' ); ?>
                                                    </label>
                                                </p>

                                                <div class="aiap-task-rss-detail" data-aiap-rss-detail>
                                                    <?php if ( empty( $all_feeds ) ) : ?>
                                                        <p class="description aiap-task-rss-empty">
                                                            <?php echo esc_html__( 'Register an RSS feed first.', 'ai-auto-poster' ); ?>
                                                            <a href="<?php echo esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=rss_feeds' ) ); ?>">
                                                                <?php echo esc_html__( 'Open RSS Feeds tab', 'ai-auto-poster' ); ?>
                                                            </a>
                                                        </p>
                                                    <?php else : ?>
                                                        <p>
                                                            <strong><?php echo esc_html__( 'Source feeds', 'ai-auto-poster' ); ?></strong><br/>
                                                            <?php foreach ( $all_feeds as $feed ) :
                                                                $fid = isset( $feed['id'] )   ? $feed['id']   : '';
                                                                $fnm = isset( $feed['name'] ) ? $feed['name'] : '';
                                                                $chk = in_array( $fid, $rss_settings['rss_feed_ids'], true );
                                                            ?>
                                                                <label class="aiap-task-rss-feed">
                                                                    <input type="checkbox" name="rss_feed_ids[]" value="<?php echo esc_attr( $fid ); ?>" <?php checked( $chk ); ?> />
                                                                    <?php echo esc_html( $fnm ); ?>
                                                                </label><br/>
                                                            <?php endforeach; ?>
                                                        </p>

                                                        <p>
                                                            <label>
                                                                <?php echo esc_html__( 'Item count', 'ai-auto-poster' ); ?>
                                                                <input type="number" name="rss_count" min="1" max="<?php echo esc_attr( AIAP_RSS_Feeds::COUNT_MAX ); ?>" step="1" value="<?php echo esc_attr( (int) $rss_settings['rss_count'] ); ?>" />
                                                            </label>
                                                        </p>

                                                        <p>
                                                            <strong><?php echo esc_html__( 'Mode', 'ai-auto-poster' ); ?></strong><br/>
                                                            <label><input type="radio" name="rss_mode" value="pick"   <?php checked( 'pick',   $rss_settings['rss_mode'] ); ?> /> <?php echo esc_html__( 'Pickup (AI picks one)', 'ai-auto-poster' ); ?></label><br/>
                                                            <label><input type="radio" name="rss_mode" value="digest" <?php checked( 'digest', $rss_settings['rss_mode'] ); ?> /> <?php echo esc_html__( 'Digest (summarize all)', 'ai-auto-poster' ); ?></label>
                                                        </p>

                                                        <p>
                                                            <strong><?php echo esc_html__( 'When no fresh articles:', 'ai-auto-poster' ); ?></strong><br/>
                                                            <label><input type="radio" name="rss_fallback" value="skip"     <?php checked( 'skip',     $rss_settings['rss_fallback'] ); ?> /> <?php echo esc_html__( 'Skip post', 'ai-auto-poster' ); ?></label><br/>
                                                            <label><input type="radio" name="rss_fallback" value="generate" <?php checked( 'generate', $rss_settings['rss_fallback'] ); ?> /> <?php echo esc_html__( 'Generate without RSS', 'ai-auto-poster' ); ?></label>
                                                        </p>
                                                    <?php endif; ?>
                                                </div>
                                            </fieldset>
                                            </div>
                                        </details>
                                        <details class="aiap-task-section aiap-task-section--schedule" data-aiap-section="schedule" <?php echo self::is_section_expanded( 'schedule', $task ) ? 'open' : ''; ?>>
                                            <summary class="aiap-task-section__summary">
                                                <span class="aiap-task-section__toggle" aria-hidden="true"><span class="dashicons dashicons-arrow-right"></span></span>
                                                <span class="aiap-task-section__icon dashicons dashicons-calendar-alt" aria-hidden="true"></span>
                                                <span class="aiap-task-section__title"><?php echo esc_html__( 'Schedule', 'ai-auto-poster' ); ?></span>
                                                <?php if ( self::is_section_configured( 'schedule', $task ) ) : ?>
                                                    <span class="aiap-task-section__configured-badge"><?php echo esc_html__( 'Configured', 'ai-auto-poster' ); ?></span>
                                                <?php endif; ?>
                                                <span class="aiap-task-section__summary-text"><?php echo esc_html( self::build_schedule_section_summary( $task ) ); ?></span>
                                            </summary>
                                            <div class="aiap-task-section__body">
                                        <div class="aiap-field">
                                            <label class="aiap-checkbox-row">
                                                <input type="checkbox" name="task_random_mode" value="1" data-aiap-random-toggle="1" <?php checked( ! empty( $task['random_mode'] ) ); ?>>
                                                <span><strong><?php echo esc_html( __( 'Random mode', 'ai-auto-poster' ) ); ?></strong></span>
                                            </label>
                                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Posts at random times and a random count per day.', 'ai-auto-poster' ) ); ?></span>
                                        </div>
                                        <?php
                                        $rm_on        = ! empty( $task['random_mode'] );
                                        $rm_min       = isset( $task['random_count_min'] ) ? (int) $task['random_count_min'] : 0;
                                        $rm_max       = isset( $task['random_count_max'] ) ? (int) $task['random_count_max'] : 0;
                                        $rm_allowed   = ( isset( $task['allowed_periods'] ) && is_array( $task['allowed_periods'] ) ) ? $task['allowed_periods'] : array();
                                        $rm_interval  = isset( $task['min_interval_minutes'] ) ? (int) $task['min_interval_minutes'] : 0;
                                        ?>
                                        <div class="aiap-random-fields" data-aiap-random-fields <?php echo $rm_on ? '' : 'style="display:none"'; ?>>
                                            <div class="aiap-field">
                                                <label class="aiap-field-label"><?php echo esc_html( __( 'Posts per day', 'ai-auto-poster' ) ); ?></label>
                                                <div class="aiap-counts-row">
                                                    <input type="number" name="task_random_count_min" value="<?php echo esc_attr( $rm_min ); ?>" min="0" max="50" class="aiap-input-text">
                                                    <span class="aiap-counts-sep"><?php echo esc_html( __( 'to', 'ai-auto-poster' ) ); ?></span>
                                                    <input type="number" name="task_random_count_max" value="<?php echo esc_attr( $rm_max ); ?>" min="0" max="50" class="aiap-input-text">
                                                    <span class="aiap-field-unit"><?php echo esc_html( __( '/ day (0–50)', 'ai-auto-poster' ) ); ?></span>
                                                </div>
                                            </div>
                                            <div class="aiap-field">
                                                <label class="aiap-field-label"><?php echo esc_html( __( 'Allowed time ranges (post only here)', 'ai-auto-poster' ) ); ?></label>
                                                <div class="aiap-allowed-list" data-aiap-allowed-list>
                                                    <?php foreach ( $rm_allowed as $api => $ap ) : ?>
                                                        <div class="aiap-allowed-row" data-aiap-allowed-row>
                                                            <input type="time" name="task_allowed_periods[<?php echo (int) $api; ?>][start]" value="<?php echo esc_attr( $ap['start'] ?? '' ); ?>" step="60" class="aiap-input-text">
                                                            <span class="aiap-counts-sep">–</span>
                                                            <input type="time" name="task_allowed_periods[<?php echo (int) $api; ?>][end]" value="<?php echo esc_attr( $ap['end'] ?? '' ); ?>" step="60" class="aiap-input-text">
                                                            <div class="aiap-allowed-actions">
                                                                <button type="button" class="button" data-aiap-allowed-remove><?php echo esc_html( __( 'Remove', 'ai-auto-poster' ) ); ?></button>
                                                                <button type="button" class="button" data-aiap-allowed-add><?php echo esc_html( __( '+ Add', 'ai-auto-poster' ) ); ?></button>
                                                            </div>
                                                        </div>
                                                    <?php endforeach; ?>
                                                </div>
                                                <?php // 0 行のとき表示する「再追加」ボタン (行があれば JS で隠す) ?>
                                                <button type="button" class="button aiap-allowed-add-empty" data-aiap-allowed-add-empty<?php echo ! empty( $rm_allowed ) ? ' style="display:none"' : ''; ?>><?php echo esc_html( __( '+ Add time range', 'ai-auto-poster' ) ); ?></button>
                                                <span class="aiap-field-hint"><?php echo esc_html( __( 'If empty, all 24 hours are allowed. Format: HH:MM – HH:MM. Crossing midnight (e.g. 22:00 – 07:00) is allowed.', 'ai-auto-poster' ) ); ?></span>
                                            </div>
                                            <div class="aiap-field">
                                                <label class="aiap-field-label"><?php echo esc_html( __( 'Minimum interval between posts', 'ai-auto-poster' ) ); ?></label>
                                                <div class="aiap-counts-row">
                                                    <input type="number" name="task_min_interval_minutes" value="<?php echo esc_attr( $rm_interval ); ?>" min="0" max="720" step="1" class="aiap-input-text">
                                                    <span class="aiap-field-unit"><?php echo esc_html( __( 'minutes (0 = no minimum)', 'ai-auto-poster' ) ); ?></span>
                                                </div>
                                                <span class="aiap-field-hint"><?php echo esc_html( __( 'If you cannot fit "posts per day" with this interval, fewer posts will be scheduled.', 'ai-auto-poster' ) ); ?></span>
                                            </div>
                                        </div>
                                        <div class="aiap-schedule-fields" data-aiap-schedule-fields <?php echo $rm_on ? 'style="display:none"' : ''; ?>>
                                        <div class="aiap-field">
                                            <label class="aiap-field-label"><?php echo esc_html( __( 'Schedule', 'ai-auto-poster' ) ); ?></label>
                                            <div class="aiap-sched-grid">
                                                <?php foreach ( $days as $dk ) : $task_times = isset( $task['schedule'][ $dk ] ) && is_array( $task['schedule'][ $dk ] ) ? implode( ', ', $task['schedule'][ $dk ] ) : ''; ?>
                                                    <div class="aiap-sched-row">
                                                        <span class="aiap-sched-day"><?php echo esc_html( self::resolve_dynamic_key( $dk ) ); ?></span>
                                                        <input type="text" name="schedule[<?php echo esc_attr( $dk ); ?>]" class="aiap-input-text" value="<?php echo esc_attr( $task_times ); ?>" placeholder="09:00, 14:00">
                                                    </div>
                                                <?php endforeach; ?>
                                            </div>
                                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Comma-separated times. Empty = skip that day.', 'ai-auto-poster' ) ); ?></span>
                                        </div>
                                        </div>
                                            </div>
                                        </details>
                                        <div class="aiap-field">
                                            <label class="aiap-checkbox-row">
                                                <input type="checkbox" name="task_enabled" value="1" <?php checked( ! empty( $task['enabled'] ) ); ?>>
                                                <span><strong><?php echo esc_html( __( 'Enable this task', 'ai-auto-poster' ) ); ?></strong></span>
                                            </label>
                                        </div>
                                        <div class="aiap-task-edit-action">
                                            <button type="button" class="aiap-btn js-aiap-cancel-edit-task"><span class="dashicons dashicons-no" aria-hidden="true"></span> <?php echo esc_html( __( 'Cancel', 'ai-auto-poster' ) ); ?></button>
                                            <button type="submit" class="aiap-btn aiap-btn-primary"><span class="dashicons dashicons-saved" aria-hidden="true"></span> <?php echo esc_html( __( 'Save', 'ai-auto-poster' ) ); ?></button>
                                        </div>
                                    </form>
                                </details>
                            </div>
                        <?php endforeach; ?>
                    </div>
                <?php endif; ?>
            </div>
        </div>

        <div class="aiap-add-task-wrapper" data-collapsed="1">
            <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-add-task-form" hidden>
                <input type="hidden" name="action" value="aiap_add_task"><?php wp_nonce_field( self::NONCE ); ?>
                <div class="aiap-section">
                    <div class="aiap-section-header">
                        <h3 class="aiap-section-title"><?php echo esc_html( __( 'New task', 'ai-auto-poster' ) ); ?></h3>
                    </div>
                    <div class="aiap-section-body">
                        <div class="aiap-field">
                            <label class="aiap-field-label"><?php echo esc_html( __( 'Task name', 'ai-auto-poster' ) ); ?> <span class="aiap-field-label-hint"><?php echo esc_html( __( 'optional', 'ai-auto-poster' ) ); ?></span></label>
                            <input type="text" name="task_name" class="aiap-input-text" maxlength="60" placeholder="<?php echo esc_attr( __( 'e.g. Daily nature observations', 'ai-auto-poster' ) ); ?>">
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'A short label to identify this task. Leave blank to auto-generate from the prompt.', 'ai-auto-poster' ) ); ?></span>
                        </div>
                        <div class="aiap-field">
                            <label class="aiap-field-label"><?php echo esc_html( __( 'Prompt', 'ai-auto-poster' ) ); ?> <span class="aiap-required" aria-hidden="true">*</span></label>
                            <textarea name="prompt" rows="6" class="aiap-editor-textarea aiap-editor-textarea-mono" placeholder="<?php echo esc_attr( __( 'What should this task write about?', 'ai-auto-poster' ) ); ?>" required></textarea>
                        </div>
                        <div class="aiap-grid-2">
                            <div class="aiap-field">
                                <label class="aiap-field-label"><?php echo esc_html( __( 'Content Language', 'ai-auto-poster' ) ); ?> <span class="aiap-required" aria-hidden="true">*</span></label>
                                <select name="task_content_language" class="aiap-select" required>
                                    <option value="" disabled selected hidden><?php echo esc_html( __( '(Please select)', 'ai-auto-poster' ) ); ?></option>
                                    <?php foreach ( $langs as $v => $l ) : ?>
                                        <option value="<?php echo esc_attr( $v ); ?>"><?php echo esc_html( $l ); ?></option>
                                    <?php endforeach; ?>
                                </select>
                            </div>
                            <div class="aiap-field">
                                <label class="aiap-field-label"><?php echo esc_html( __( 'Content Length', 'ai-auto-poster' ) ); ?></label>
                                <div class="aiap-field-row">
                                    <input type="number" name="task_content_length" value="" min="0" max="10000" class="aiap-input-text aiap-input-narrow" placeholder="0">
                                    <span class="aiap-field-unit"><?php echo esc_html( __( 'chars', 'ai-auto-poster' ) ); ?></span>
                                </div>
                            </div>
                        </div>
                        <details class="aiap-task-section aiap-task-section--image" data-aiap-section="image" <?php echo self::is_section_expanded( 'image', array(), true ) ? 'open' : ''; ?>>
                            <summary class="aiap-task-section__summary">
                                <span class="aiap-task-section__toggle" aria-hidden="true"><span class="dashicons dashicons-arrow-right"></span></span>
                                <span class="aiap-task-section__icon dashicons dashicons-format-image" aria-hidden="true"></span>
                                <span class="aiap-task-section__title"><?php echo esc_html( __( 'Featured image', 'ai-auto-poster' ) ); ?></span>
                                <span class="aiap-task-section__summary-text"><?php echo esc_html__( '(optional)', 'ai-auto-poster' ); ?></span>
                            </summary>
                            <div class="aiap-task-section__body">
                                <label class="aiap-radio-row"><input type="radio" name="task_featured_image_mode" value="on" checked> <span><?php echo esc_html( __( 'Always attach', 'ai-auto-poster' ) ); ?></span></label>
                                <label class="aiap-radio-row"><input type="radio" name="task_featured_image_mode" value="off"> <span><?php echo esc_html( __( "Don't attach", 'ai-auto-poster' ) ); ?></span></label>
                            </div>
                        </details>
                        <?php
                        $is_pro_for_rss  = class_exists( 'AIAP_License' ) && AIAP_License::is_pro();
                        $rss_settings    = AIAP_Prompts::get_rss_settings( array() );
                        $all_feeds       = class_exists( 'AIAP_RSS_Feeds' ) ? AIAP_RSS_Feeds::get_all() : array();
                        ?>
                        <details class="aiap-task-section aiap-task-section--rss" data-aiap-section="rss" data-aiap-rss-section data-aiap-configured-label="<?php echo esc_attr__( 'Configured', 'ai-auto-poster' ); ?>" <?php echo self::is_section_expanded( 'rss', array(), true ) ? 'open' : ''; ?>>
                            <summary class="aiap-task-section__summary">
                                <span class="aiap-task-section__toggle" aria-hidden="true"><span class="dashicons dashicons-arrow-right"></span></span>
                                <span class="aiap-task-section__icon dashicons dashicons-rss" aria-hidden="true"></span>
                                <span class="aiap-task-section__title"><?php echo esc_html__( 'RSS Settings', 'ai-auto-poster' ); ?></span>
                                <span class="aiap-pro-pill"><?php echo esc_html( __( 'PRO', 'ai-auto-poster' ) ); ?></span>
                                <span class="aiap-task-section__summary-text"><?php echo esc_html__( '(optional)', 'ai-auto-poster' ); ?></span>
                            </summary>
                            <div class="aiap-task-section__body">

                            <?php if ( ! $is_pro_for_rss ) : ?>
                                <p class="aiap-pro-badge"><?php echo esc_html__( 'This is a Pro feature.', 'ai-auto-poster' ); ?></p>
                            <?php endif; ?>

                            <fieldset <?php echo $is_pro_for_rss ? '' : 'disabled'; ?>>
                                <p>
                                    <label>
                                        <input type="checkbox" name="rss_enabled" value="1" class="js-aiap-task-rss-enable" <?php checked( 1, (int) $rss_settings['rss_enabled'] ); ?> />
                                        <?php echo esc_html__( 'Use RSS as source material', 'ai-auto-poster' ); ?>
                                    </label>
                                </p>

                                <div class="aiap-task-rss-detail" data-aiap-rss-detail>
                                    <?php if ( empty( $all_feeds ) ) : ?>
                                        <p class="description aiap-task-rss-empty">
                                            <?php echo esc_html__( 'Register an RSS feed first.', 'ai-auto-poster' ); ?>
                                            <a href="<?php echo esc_url( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=rss_feeds' ) ); ?>">
                                                <?php echo esc_html__( 'Open RSS Feeds tab', 'ai-auto-poster' ); ?>
                                            </a>
                                        </p>
                                    <?php else : ?>
                                        <p>
                                            <strong><?php echo esc_html__( 'Source feeds', 'ai-auto-poster' ); ?></strong><br/>
                                            <?php foreach ( $all_feeds as $feed ) :
                                                $fid = isset( $feed['id'] )   ? $feed['id']   : '';
                                                $fnm = isset( $feed['name'] ) ? $feed['name'] : '';
                                                $chk = in_array( $fid, $rss_settings['rss_feed_ids'], true );
                                            ?>
                                                <label class="aiap-task-rss-feed">
                                                    <input type="checkbox" name="rss_feed_ids[]" value="<?php echo esc_attr( $fid ); ?>" <?php checked( $chk ); ?> />
                                                    <?php echo esc_html( $fnm ); ?>
                                                </label><br/>
                                            <?php endforeach; ?>
                                        </p>

                                        <p>
                                            <label>
                                                <?php echo esc_html__( 'Item count', 'ai-auto-poster' ); ?>
                                                <input type="number" name="rss_count" min="1" max="<?php echo esc_attr( AIAP_RSS_Feeds::COUNT_MAX ); ?>" step="1" value="<?php echo esc_attr( (int) $rss_settings['rss_count'] ); ?>" />
                                            </label>
                                        </p>

                                        <p>
                                            <strong><?php echo esc_html__( 'Mode', 'ai-auto-poster' ); ?></strong><br/>
                                            <label><input type="radio" name="rss_mode" value="pick"   <?php checked( 'pick',   $rss_settings['rss_mode'] ); ?> /> <?php echo esc_html__( 'Pickup (AI picks one)', 'ai-auto-poster' ); ?></label><br/>
                                            <label><input type="radio" name="rss_mode" value="digest" <?php checked( 'digest', $rss_settings['rss_mode'] ); ?> /> <?php echo esc_html__( 'Digest (summarize all)', 'ai-auto-poster' ); ?></label>
                                        </p>

                                        <p>
                                            <strong><?php echo esc_html__( 'When no fresh articles:', 'ai-auto-poster' ); ?></strong><br/>
                                            <label><input type="radio" name="rss_fallback" value="skip"     <?php checked( 'skip',     $rss_settings['rss_fallback'] ); ?> /> <?php echo esc_html__( 'Skip post', 'ai-auto-poster' ); ?></label><br/>
                                            <label><input type="radio" name="rss_fallback" value="generate" <?php checked( 'generate', $rss_settings['rss_fallback'] ); ?> /> <?php echo esc_html__( 'Generate without RSS', 'ai-auto-poster' ); ?></label>
                                        </p>
                                    <?php endif; ?>
                                </div>
                            </fieldset>
                            </div>
                        </details>
                        <details class="aiap-task-section aiap-task-section--schedule" data-aiap-section="schedule" <?php echo self::is_section_expanded( 'schedule', array(), true ) ? 'open' : ''; ?>>
                            <summary class="aiap-task-section__summary">
                                <span class="aiap-task-section__toggle" aria-hidden="true"><span class="dashicons dashicons-arrow-right"></span></span>
                                <span class="aiap-task-section__icon dashicons dashicons-calendar-alt" aria-hidden="true"></span>
                                <span class="aiap-task-section__title"><?php echo esc_html__( 'Schedule', 'ai-auto-poster' ); ?></span>
                                <span class="aiap-task-section__summary-text"><?php echo esc_html( self::build_schedule_section_summary( array() ) ); ?></span>
                            </summary>
                            <div class="aiap-task-section__body">
                        <div class="aiap-field">
                            <label class="aiap-checkbox-row">
                                <input type="checkbox" name="task_random_mode" value="1" data-aiap-random-toggle="1">
                                <span><strong><?php echo esc_html( __( 'Random mode', 'ai-auto-poster' ) ); ?></strong></span>
                            </label>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Posts at random times and a random count per day.', 'ai-auto-poster' ) ); ?></span>
                        </div>
                        <div class="aiap-random-fields" data-aiap-random-fields style="display:none">
                            <div class="aiap-field">
                                <label class="aiap-field-label"><?php echo esc_html( __( 'Posts per day', 'ai-auto-poster' ) ); ?></label>
                                <div class="aiap-counts-row">
                                    <input type="number" name="task_random_count_min" value="" min="0" max="50" class="aiap-input-text" placeholder="0">
                                    <span class="aiap-counts-sep"><?php echo esc_html( __( 'to', 'ai-auto-poster' ) ); ?></span>
                                    <input type="number" name="task_random_count_max" value="" min="0" max="50" class="aiap-input-text" placeholder="0">
                                    <span class="aiap-field-unit"><?php echo esc_html( __( '/ day (0–50)', 'ai-auto-poster' ) ); ?></span>
                                </div>
                            </div>
                            <div class="aiap-field">
                                <label class="aiap-field-label"><?php echo esc_html( __( 'Allowed time ranges (post only here)', 'ai-auto-poster' ) ); ?></label>
                                <div class="aiap-allowed-list" data-aiap-allowed-list></div>
                                <button type="button" class="button aiap-allowed-add-empty" data-aiap-allowed-add-empty><?php echo esc_html( __( '+ Add time range', 'ai-auto-poster' ) ); ?></button>
                                <span class="aiap-field-hint"><?php echo esc_html( __( 'If empty, all 24 hours are allowed. Format: HH:MM – HH:MM. Crossing midnight (e.g. 22:00 – 07:00) is allowed.', 'ai-auto-poster' ) ); ?></span>
                            </div>
                            <div class="aiap-field">
                                <label class="aiap-field-label"><?php echo esc_html( __( 'Minimum interval between posts', 'ai-auto-poster' ) ); ?></label>
                                <div class="aiap-counts-row">
                                    <input type="number" name="task_min_interval_minutes" value="" min="0" max="720" step="1" class="aiap-input-text" placeholder="0">
                                    <span class="aiap-field-unit"><?php echo esc_html( __( 'minutes (0 = no minimum)', 'ai-auto-poster' ) ); ?></span>
                                </div>
                                <span class="aiap-field-hint"><?php echo esc_html( __( 'If you cannot fit "posts per day" with this interval, fewer posts will be scheduled.', 'ai-auto-poster' ) ); ?></span>
                            </div>
                        </div>
                        <div class="aiap-schedule-fields" data-aiap-schedule-fields>
                        <div class="aiap-field">
                            <label class="aiap-field-label"><?php echo esc_html( __( 'Schedule', 'ai-auto-poster' ) ); ?></label>
                            <div class="aiap-sched-grid">
                                <?php foreach ( $days as $dk ) : ?>
                                    <div class="aiap-sched-row">
                                        <span class="aiap-sched-day"><?php echo esc_html( self::resolve_dynamic_key( $dk ) ); ?></span>
                                        <input type="text" name="schedule[<?php echo esc_attr( $dk ); ?>]" class="aiap-input-text" placeholder="09:00, 14:00">
                                    </div>
                                <?php endforeach; ?>
                            </div>
                            <span class="aiap-field-hint"><?php echo esc_html( __( 'Comma-separated times. Empty = skip that day.', 'ai-auto-poster' ) ); ?></span>
                        </div>
                        </div>
                            </div>
                        </details>
                        <div class="aiap-field">
                            <label class="aiap-checkbox-row">
                                <input type="checkbox" name="task_enabled" value="1" checked>
                                <span><strong><?php echo esc_html( __( 'Enable this task', 'ai-auto-poster' ) ); ?></strong></span>
                            </label>
                        </div>
                    </div>
                </div>
                <p class="submit">
                    <button type="button" class="aiap-btn js-aiap-cancel-add-task" data-confirm-message="<?php echo esc_attr( __( 'Discard changes?', 'ai-auto-poster' ) ); ?>"><span class="dashicons dashicons-no" aria-hidden="true"></span> <?php echo esc_html( __( 'Cancel', 'ai-auto-poster' ) ); ?></button>
                    <button type="submit" class="button button-primary aiap-btn-primary"><span class="dashicons dashicons-saved" aria-hidden="true"></span> <?php echo esc_html( __( 'Save', 'ai-auto-poster' ) ); ?></button>
                </p>
            </form>
        </div>
        <?php
    }


    // ================================================================
    // ログ (unchanged)
    // ================================================================
    private static function render_logs() {
        // 旧「実行ログ(テキスト)」表示は WP_List_Table ベースの「生成投稿一覧」に置き換え。
        // 既存の AIAP_Logger は内部記録用としてはそのまま残す(エラー解析や cron 監視で使うため)が、
        // 管理画面 UI からは AI 生成された投稿の一覧 + 編集 / 削除アクションを提供する。

        // 一括削除アクションの処理 (WP_List_Table の bulk action)
        if ( isset( $_REQUEST['action'] ) || isset( $_REQUEST['action2'] ) ) {
            self::handle_generated_posts_bulk_action();
        }

        if ( ! class_exists( 'AIAP_Generated_Posts_Table' ) ) {
            require_once AIAP_PLUGIN_DIR . 'admin/class-aiap-generated-posts-table.php';
        }
        $table = new AIAP_Generated_Posts_Table();
        $table->prepare_items();
        $total = isset( $table->_pagination_args['total_items'] ) ? (int) $table->_pagination_args['total_items'] : 0;

        // Free プランの月次スキップ件数(実行ログ機能から流用): 上限到達時の警告は維持する。
        $skip_count_this_month = 0;
        if ( ! AIAP_License::is_pro() ) {
            $logs = AIAP_Logger::get_all();
            $current_month = wp_date( 'Y-m' );
            foreach ( $logs as $log ) {
                $key = $log['message_key'] ?? '';
                if ( $key !== 'log_free_limit_reached_skip_cron' ) continue;
                $log_time = (string) ( $log['time'] ?? '' );
                if ( $log_time === '' ) continue;
                if ( substr( $log_time, 0, 7 ) === $current_month ) $skip_count_this_month++;
            }
        }
        ?>
        <header class="aiap-provider-hero">
            <?php /* Phase 12: 旧版は「History / Generation history / Generated posts」と
                   3 つの異なる訳語が同じ画面内に混在していた。すべて「Post history / 投稿履歴」に統一。 */ ?>
            <span class="aiap-provider-hero-eyebrow"><?php echo esc_html( __( 'Post history', 'ai-auto-poster' ) ); ?></span>
            <h2 class="aiap-provider-hero-title"><?php echo esc_html( __( 'Post history', 'ai-auto-poster' ) ); ?></h2>
            <p class="aiap-provider-hero-sub"><?php echo esc_html( __( 'Posts created by this plugin. View or edit from here.', 'ai-auto-poster' ) ); ?></p>
        </header>

        <?php if ( $skip_count_this_month > 0 ) : ?>
            <div class="aiap-banner aiap-banner-warning">
                <span class="dashicons dashicons-warning" aria-hidden="true"></span>
                <span class="aiap-banner-text">
                    <strong><?php
                        /* translators: %d = number of times cron was skipped this month due to free plan limit */
                        echo esc_html( sprintf( _n( 'This month: %d cron run skipped due to monthly limit.', 'This month: %d cron runs skipped due to monthly limit.', $skip_count_this_month, 'ai-auto-poster' ), $skip_count_this_month ) );
                    ?></strong>
                </span>
                <a href="<?php echo esc_url( AIAP_License::upgrade_url() ); ?>" class="aiap-banner-cta">
                    <span class="dashicons dashicons-star-filled" aria-hidden="true"></span>
                    <?php echo esc_html( __( 'Upgrade', 'ai-auto-poster' ) ); ?>
                </a>
            </div>
        <?php endif; ?>

        <div class="aiap-section aiap-logs-table-section">
            <div class="aiap-section-header">
                <?php /* Phase 12: section title「Generated posts」は hero と統一して削除。カウントだけ残す。 */ ?>
                <div class="aiap-section-title-row">
                    <span class="aiap-section-count">
                        <?php
                        /* translators: %d: number of posts */
                        if ( $total > 100 ) {
                            /* translators: %d: total number of posts */
                            printf( esc_html__( 'Showing latest 100 of %d posts.', 'ai-auto-poster' ), (int) $total );
                        } else {
                            /* translators: %d: number of posts */
                            printf( esc_html__( 'Posts: %d', 'ai-auto-poster' ), (int) $total );
                        }
                        ?>
                    </span>
                </div>
            </div>
            <div class="aiap-section-body">
                <form method="get">
                    <?php
                    // ページ情報を維持するため page パラメータを引き継ぐ
                    if ( isset( $_REQUEST['page'] ) ) {
                        printf( '<input type="hidden" name="page" value="%s" />', esc_attr( sanitize_key( wp_unslash( $_REQUEST['page'] ) ) ) );
                    }
                    if ( isset( $_REQUEST['tab'] ) ) {
                        printf( '<input type="hidden" name="tab" value="%s" />', esc_attr( sanitize_key( wp_unslash( $_REQUEST['tab'] ) ) ) );
                    }
                    wp_nonce_field( self::NONCE );
                    $table->display();
                    ?>
                </form>
            </div>
        </div>
        <?php
    }

    // ================================================================
    // 実行ログ (Server-side execution log; AIAP_Logger の中身を表示)
    // ================================================================
    private static function render_run_logs() {
        $logs  = AIAP_Logger::get_all();
        $count = is_array( $logs ) ? count( $logs ) : 0;

        // ステータス → 表示ラベル + バッジ用クラス
        $status_meta = array(
            'success' => array( 'label' => __( 'Success', 'ai-auto-poster' ), 'class' => 'is-success' ),
            'error'   => array( 'label' => __( 'Error',   'ai-auto-poster' ), 'class' => 'is-error'   ),
            'info'    => array( 'label' => __( 'Info',    'ai-auto-poster' ), 'class' => 'is-info'    ),
            'warning' => array( 'label' => __( 'Warning', 'ai-auto-poster' ), 'class' => 'is-warning' ),
        );

        $clear_url = wp_nonce_url(
            add_query_arg(
                array( 'action' => 'aiap_clear_logs' ),
                admin_url( 'admin-post.php' )
            ),
            self::NONCE
        );
        ?>
        <header class="aiap-provider-hero">
            <span class="aiap-provider-hero-eyebrow"><?php echo esc_html( __( 'Diagnostics', 'ai-auto-poster' ) ); ?></span>
            <h2 class="aiap-provider-hero-title"><?php echo esc_html( __( 'Execution log', 'ai-auto-poster' ) ); ?></h2>
            <p class="aiap-provider-hero-sub"><?php echo esc_html( __( 'Server-side execution log for cron runs, generation requests, and errors. Entries older than 7 days are automatically removed.', 'ai-auto-poster' ) ); ?></p>
        </header>

        <div class="aiap-section aiap-run-logs-section">
            <div class="aiap-section-header">
                <?php /* Phase 12: section title「Execution log」は hero title と完全に重複していたので削除。
                       カウントバッジと Clear ボタンだけ残す。 */ ?>
                <div class="aiap-section-title-row">
                    <span class="aiap-section-count"><?php echo esc_html( (string) $count ); ?></span>
                </div>
                <?php if ( $count > 0 ) : ?>
                <div class="aiap-section-actions">
                    <a href="<?php echo esc_url( $clear_url ); ?>"
                       class="button button-secondary aiap-btn-danger-outline"
                       onclick="return confirm('<?php echo esc_js( __( 'Clear all execution log entries? This cannot be undone.', 'ai-auto-poster' ) ); ?>');">
                        <span class="dashicons dashicons-trash" aria-hidden="true"></span>
                        <?php echo esc_html( __( 'Clear log', 'ai-auto-poster' ) ); ?>
                    </a>
                </div>
                <?php endif; ?>
            </div>
            <div class="aiap-section-body">
                <?php if ( $count === 0 ) : ?>
                    <p class="aiap-run-logs-empty"><?php echo esc_html( __( 'No execution log entries yet.', 'ai-auto-poster' ) ); ?></p>
                <?php else : ?>
                    <ul class="aiap-run-logs-list">
                        <?php foreach ( $logs as $log ) :
                            if ( ! is_array( $log ) ) continue;

                            $status   = isset( $log['status'] ) ? (string) $log['status'] : 'info';
                            $meta     = isset( $status_meta[ $status ] ) ? $status_meta[ $status ] : array(
                                'label' => $status,
                                'class' => 'is-info',
                            );
                            $time_str = isset( $log['time'] ) ? (string) $log['time'] : '';
                            $message  = self::log_message( $log );
                        ?>
                        <li class="aiap-run-logs-item <?php echo esc_attr( $meta['class'] ); ?>">
                            <div class="aiap-run-logs-meta">
                                <span class="aiap-run-logs-badge"><?php echo esc_html( $meta['label'] ); ?></span>
                                <span class="aiap-run-logs-time"><?php echo esc_html( $time_str ); ?></span>
                            </div>
                            <div class="aiap-run-logs-msg"><?php echo esc_html( $message ); ?></div>
                        </li>
                        <?php endforeach; ?>
                    </ul>
                <?php endif; ?>
            </div>
        </div>
        <?php
    }

    /**
     * 「生成投稿一覧」テーブルの一括操作ハンドラ。
     *
     * WP_List_Table の bulk action は同じ画面に POST/GET で戻ってくるので、
     * render_logs() の冒頭で呼び出して処理する。現状は 'delete' のみサポート。
     * 削除は WordPress 標準の wp_delete_post( $id, true ) で完全削除する(ゴミ箱は経由しない)。
     */
    private static function handle_generated_posts_bulk_action() {
        $action = '';
        if ( ! empty( $_REQUEST['action'] ) && $_REQUEST['action'] !== '-1' ) {
            $action = sanitize_key( wp_unslash( $_REQUEST['action'] ) );
        } elseif ( ! empty( $_REQUEST['action2'] ) && $_REQUEST['action2'] !== '-1' ) {
            $action = sanitize_key( wp_unslash( $_REQUEST['action2'] ) );
        }
        if ( $action !== 'delete' ) {
            return;
        }
        if ( ! current_user_can( 'manage_options' ) ) {
            return;
        }
        check_admin_referer( self::NONCE );

        $ids = isset( $_REQUEST['post_ids'] ) ? (array) wp_unslash( $_REQUEST['post_ids'] ) : array();
        $deleted = 0;
        foreach ( $ids as $id ) {
            $post_id = (int) $id;
            if ( $post_id <= 0 ) {
                continue;
            }
            // 安全装置: AI Auto Poster が生成した投稿(_aiap_generated=1) のみ削除可能
            if ( get_post_meta( $post_id, '_aiap_generated', true ) !== '1' ) {
                continue;
            }
            if ( wp_delete_post( $post_id, true ) ) {
                $deleted++;
            }
        }
        if ( $deleted > 0 ) {
            /* translators: %d = number of deleted posts */
            $msg = sprintf( _n( 'Deleted %d post.', 'Deleted %d posts.', $deleted, 'ai-auto-poster' ), $deleted );
            // 同じ画面にリダイレクトして二重送信を防ぐ + メッセージ表示
            $redirect = add_query_arg(
                array(
                    'page'      => sanitize_key( wp_unslash( $_REQUEST['page'] ?? '' ) ),
                    'tab'       => sanitize_key( wp_unslash( $_REQUEST['tab'] ?? 'logs' ) ),
                    'aiap_msg'  => rawurlencode( $msg ),
                    'aiap_type' => 'success',
                ),
                admin_url( 'admin.php' )
            );
            wp_safe_redirect( $redirect );
            exit;
        }
    }

    /**
     * 個別の生成投稿削除ハンドラ (admin-post.php 経由)。
     * テーブル行の Actions 列にある削除ボタンから呼ばれる。
     */
    public static function handle_delete_generated_post() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html( __( 'Unauthorized', 'ai-auto-poster' ) ) );
        }
        $post_id = isset( $_GET['post_id'] ) ? (int) $_GET['post_id'] : 0;
        if ( $post_id <= 0 ) {
            wp_die( esc_html( __( 'Invalid post ID', 'ai-auto-poster' ) ) );
        }
        check_admin_referer( 'aiap_delete_generated_post_' . $post_id );

        // 安全装置: AI Auto Poster が生成した投稿(_aiap_generated=1) のみ削除可能
        if ( get_post_meta( $post_id, '_aiap_generated', true ) !== '1' ) {
            wp_die( esc_html( __( 'This post was not created by AI Auto Poster.', 'ai-auto-poster' ) ) );
        }

        $deleted  = wp_delete_post( $post_id, true );
        $msg_key  = $deleted ? 'success' : 'error';
        $msg_text = $deleted
            ? __( 'Post deleted.', 'ai-auto-poster' )
            : __( 'Failed to delete post.', 'ai-auto-poster' );

        $redirect = add_query_arg(
            array(
                'page'      => 'ai-auto-poster',
                'tab'       => 'logs',
                'aiap_msg'  => rawurlencode( $msg_text ),
                'aiap_type' => $msg_key,
            ),
            admin_url( 'admin.php' )
        );
        wp_safe_redirect( $redirect );
        exit;
    }



    // ================================================================
    // 通知・ハンドラ
    // ================================================================
    private static function render_notices() {
        if ( ! empty( $_GET['aiap_msg'] ) ) {
            $msg     = sanitize_text_field( rawurldecode( wp_unslash( $_GET['aiap_msg'] ) ) );
            $raw     = sanitize_key( wp_unslash( $_GET['aiap_type'] ?? 'success' ) );
            $allowed = array( 'success', 'error', 'warning' );
            $type    = in_array( $raw, $allowed, true ) ? $raw : 'success';
            printf( '<div class="notice notice-%s is-dismissible"><p>%s</p></div>', esc_attr( $type ), esc_html( $msg ) );
        }
    }

    private static function verify() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html( __( 'Unauthorized', 'ai-auto-poster' ) ) );
        }
        check_admin_referer( self::NONCE );
    }

    public static function ajax_refresh_models() {
        self::ajax_verify();

        $provider = sanitize_key( wp_unslash( $_POST['provider'] ?? '' ) );
        $text_providers  = array( 'claude', 'openai', 'gemini', 'mistral', 'groq', 'deepseek', 'openrouter' );
        $image_providers = array( 'dalle3', 'imagen' );
        $is_image = in_array( $provider, $image_providers, true );
        if ( ! in_array( $provider, $text_providers, true ) && ! $is_image ) {
            /* translators: %s = provider key (e.g. "openai") */
            wp_send_json_error( array( 'message' => sprintf( __( 'Unsupported provider: %s', 'ai-auto-poster' ), $provider ) ) );
        }

        // DB保存済みのAPIキーを使ってモデル一覧を取得
        // (テストボタン成功時にキーは自動保存されるため、通常はキーが存在する)
        $models = AIAP_AI_Client::fetch_models_from_api( $provider );
        if ( is_wp_error( $models ) ) {
            $msg = $models->get_error_message();
            // APIキー未設定の場合、先にテストで保存するよう案内
            if ( $models->get_error_code() === 'no_api_key' ) {
                $msg = __( 'Please test your API key before fetching models.', 'ai-auto-poster' );
            }
            wp_send_json_error( array( 'message' => $msg ) );
        }
        AIAP_AI_Client::cache_models( $provider, $models );

        // 現在選択中のモデルが新リストに無ければ、先頭のモデルに切替して通知
        $model_field = $provider . '_model';
        if ( $is_image ) {
            $settings = AIAP_Image_Client::get_settings();
        } else {
            $settings = AIAP_AI_Client::get_settings();
        }
        $current = $settings[ $model_field ] ?? '';
        $notice = '';
        $auto_switched = false;
        if ( $current !== '' && ! isset( $models[ $current ] ) ) {
            $new_model = array_key_first( $models );
            $settings[ $model_field ] = $new_model;
            $auto_switched = true;
            /* translators: 1: previous model identifier, 2: replacement model identifier */
            $notice = sprintf( __( '"%1$s" is no longer available. Switched to "%2$s".', 'ai-auto-poster' ), $current, $new_model );

            // 画像プロバイダの場合、新モデルがサポートしないスタイルが選ばれていたら
            // そのプロバイダの既定スタイル(先頭)にフォールバックする
            if ( $is_image ) {
                $style_field = $provider . '_style';
                $saved_style = $settings[ $style_field ] ?? '';
                $style_list  = AIAP_Image_Client::get_provider_styles( $provider, $new_model );
                if ( ! empty( $saved_style ) && ! empty( $style_list ) && ! isset( $style_list[ $saved_style ] ) ) {
                    $settings[ $style_field ] = array_key_first( $style_list );
                }
            }

            if ( $is_image ) {
                AIAP_Image_Client::update_settings( $settings );
            } else {
                AIAP_AI_Client::update_settings( $settings );
            }
        }

        // 画像プロバイダの場合は、新しいモデルに対するスタイル一覧も返す
        // (JSがモデル変更時にスタイルselectを動的更新するために使用)
        $styles_payload = null;
        if ( $is_image ) {
            $current_model = $settings[ $model_field ] ?? '';
            $style_list = AIAP_Image_Client::get_provider_styles( $provider, $current_model );
            $style_field = $provider . '_style';
            $styles_payload = array(
                'options' => self::format_styles_for_js( $style_list ),
                'current' => $settings[ $style_field ] ?? '',
            );
        }

        wp_send_json_success( array(
            'models'        => $models,
            'current'       => $settings[ $model_field ] ?? '',
            'auto_switched' => $auto_switched,
            'notice'        => $notice,
            'styles'        => $styles_payload,
        ) );
    }

    /**
     * スタイル定義をJS用の単純な配列形式に整形
     * [ { value, label } ] の形で返す
     */
    public static function format_styles_for_js( $style_list ) {
        $out  = array();
        foreach ( $style_list as $key => $def ) {
            $label = ! empty( $def['label_key'] ) ? self::resolve_dynamic_key( $def['label_key'] ) : ( $def['label_ja'] ?? $key );
            if ( ! empty( $def['icon'] ) ) {
                $label = $def['icon'] . ' ' . $label;
            }
            $out[] = array( 'value' => $key, 'label' => $label );
        }
        return $out;
    }

    /**
     * 指定した画像プロバイダ×モデルに対応するスタイル選択肢を返す軽量エンドポイント
     * ユーザーがモデルselectを変更した瞬間に呼ばれ、クライアントがスタイル欄を動的に更新する
     */
    public static function ajax_styles_for_model() {
        self::ajax_verify();

        $provider = sanitize_key( wp_unslash( $_POST['provider'] ?? '' ) );
        $model    = sanitize_text_field( wp_unslash( $_POST['model'] ?? '' ) );
        $style    = sanitize_text_field( wp_unslash( $_POST['style'] ?? '' ) );
        $use_provider = isset( $_POST['use_provider'] ) && absint( wp_unslash( $_POST['use_provider'] ?? 0 ) ) === 1;

        $image_providers = array( 'dalle3', 'imagen', 'stability', 'flux' );
        if ( ! in_array( $provider, $image_providers, true ) ) {
            /* translators: %s = image provider key */
            wp_send_json_error( array( 'message' => sprintf( __( 'Unsupported provider: %s', 'ai-auto-poster' ), $provider ) ) );
        }

        $style_list = AIAP_Image_Client::get_provider_styles( $provider, $model );

        wp_send_json_success( array(
            'provider' => $provider,
            'model'    => $model,
            'styles'   => self::format_styles_for_js( $style_list ),
            'has_any'  => ! empty( $style_list ),
        ) );
    }

    /**
     * 自動投稿の ON/OFF を切り替える(ダッシュボードのトグルから呼ばれる)
     */
    public static function ajax_toggle_enabled() {
        self::ajax_verify();

        $on = isset( $_POST['on'] ) && absint( wp_unslash( $_POST['on'] ?? 0 ) ) === 1 ? 1 : 0;

        $s = AIAP_AI_Client::get_settings();
        $s['enabled'] = $on;
        AIAP_AI_Client::update_settings( $s );

        // OFF にした場合はキューもクリア(再開時に古いキューが動かないよう)
        if ( ! $on && class_exists( 'AIAP_Scheduler' ) ) {
            delete_option( AIAP_Scheduler::OPTION_RUN_QUEUE );
        }

        AIAP_Scheduler::reschedule();

        wp_send_json_success( array(
            'enabled' => $on,
            'label'   => self::resolve_dynamic_key( $on ? 'enabled' : 'disabled' ),
            'msg'     => self::resolve_dynamic_key( $on ? 'auto_posting_turned_on' : 'auto_posting_turned_off' ),
        ) );
    }

    public static function ajax_test_provider() {
        self::ajax_verify();

        $kind     = sanitize_key( wp_unslash( $_POST['kind'] ?? 'text' ) );
        $provider = sanitize_key( wp_unslash( $_POST['provider'] ?? '' ) );
        $api_key  = sanitize_text_field( wp_unslash( $_POST['api_key'] ?? '' ) );
        $model    = sanitize_text_field( wp_unslash( $_POST['model'] ?? '' ) );
        $style    = sanitize_text_field( wp_unslash( $_POST['style'] ?? '' ) );
        $use_provider = isset( $_POST['use_provider'] ) && absint( wp_unslash( $_POST['use_provider'] ?? 0 ) ) === 1;

        // '*' のみ(マスク表示)は空扱いにする
        if ( $api_key !== '' && $api_key === str_repeat( '*', strlen( $api_key ) ) ) {
            $api_key = '';
        }

        // 入力欄が空ならDB保存済みのキーを使う(used_saved_key で UI に通知)
        $used_saved_key = false;
        if ( empty( $api_key ) ) {
            if ( $kind === 'text' ) {
                $ai = AIAP_AI_Client::get_settings();
                if ( isset( self::$text_provider_key_field[ $provider ] ) ) {
                    $api_key = $ai[ self::$text_provider_key_field[ $provider ] ] ?? '';
                }
            } elseif ( $kind === 'image' ) {
                $im = AIAP_Image_Client::get_settings();
                if ( isset( self::$image_provider_key_field[ $provider ] ) ) {
                    $api_key = $im[ self::$image_provider_key_field[ $provider ] ] ?? '';
                }
            }
            if ( ! empty( $api_key ) ) {
                $used_saved_key = true;
            }
        }

        if ( empty( $api_key ) ) {
            wp_send_json_error( array( 'message' => __( 'Enter a key first', 'ai-auto-poster' ) ) );
        }

        $result = $kind === 'image'
            ? self::test_image_provider( $provider, $api_key, $model )
            : self::test_text_provider( $provider, $api_key, $model );

        // テスト失敗時の処理。
        // ただし quota_exhausted (credit_balance_too_low / insufficient_quota 等) は
        // APIキー自体は有効である可能性が高いため、キー保存と verified 登録を許可する。
        // これがないと、課金切れの瞬間に「使用するプロバイダ」のトグルが落ちて復旧不能になる。
        $key_likely_valid_despite_error = false;
        if ( is_wp_error( $result ) ) {
            $err_data = $result->get_error_data();
            if ( is_array( $err_data ) && ! empty( $err_data['key_likely_valid'] ) ) {
                $key_likely_valid_despite_error = true;
            } else {
                wp_send_json_error( array(
                    'message'        => $result->get_error_message(),
                    'used_saved_key' => $used_saved_key,
                ) );
            }
        }

        // テスト成功 (もしくは quota_exhausted で「キー有効・残高なし」) → APIキーをDBに保存
        $key_saved = false;
        if ( $api_key !== '' ) {
            if ( $kind === 'text' && isset( self::$text_provider_key_field[ $provider ] ) ) {
                $s = AIAP_AI_Client::get_settings();
                $s[ self::$text_provider_key_field[ $provider ] ] = $api_key;
                if ( $model !== '' && isset( $s[ $provider . '_model' ] ) ) {
                    $s[ $provider . '_model' ] = $model;
                }
                // Bug fix (audit r4): get_providers() returns a list of provider keys
                // (values, not associative). array_keys() would produce numeric indices
                // [0..6] and in_array() would always be false, silently disabling the
                // "use this provider" toggle on test-success. Match against values directly.
                if ( $use_provider && in_array( $provider, AIAP_AI_Client::get_providers(), true ) ) {
                    $s['provider'] = $provider;
                }
                AIAP_AI_Client::update_settings( $s );
                $key_saved = true;
            } elseif ( $kind === 'image' && isset( self::$image_provider_key_field[ $provider ] ) ) {
                $im = AIAP_Image_Client::get_settings();
                $im[ self::$image_provider_key_field[ $provider ] ] = $api_key;
                if ( $model !== '' && isset( $im[ $provider . '_model' ] ) ) {
                    $im[ $provider . '_model' ] = $model;
                }
                if ( $style !== '' ) {
                    $style_field = $provider . '_style';
                    $current_model = $model !== '' ? $model : ( $im[ $provider . '_model' ] ?? '' );
                    $allowed_styles = array_keys( AIAP_Image_Client::get_provider_styles( $provider, $current_model ) );
                    if ( in_array( $style, $allowed_styles, true ) ) {
                        $im[ $style_field ] = $style;
                    }
                }
                if ( $use_provider && in_array( $provider, array_keys( AIAP_Image_Client::get_sources() ), true ) ) {
                    $im['image_source'] = $provider;
                }
                AIAP_Image_Client::update_settings( $im );
                $key_saved = true;
            }

            // テスト完了フラグを記録
            if ( $key_saved ) {
                $verified = get_option( 'aiap_verified_providers', array() );
                $verified[ $provider ] = time();
                update_option( 'aiap_verified_providers', $verified, false );
            }
        }

        // quota_exhausted の場合は「キーは有効、残高/上限が問題」と明示する
        // 成功扱いだが、UI で警告として表示できるよう warning フラグを付ける
        if ( $key_likely_valid_despite_error ) {
            wp_send_json_success( array(
                'message'        => $result->get_error_message(),
                'warning'        => true,
                'key_saved'      => $key_saved,
                'provider'       => $provider,
                'used_saved_key' => $used_saved_key,
            ) );
        }

        wp_send_json_success( array(
            'message'        => __( 'OK', 'ai-auto-poster' ),
            'key_saved'      => $key_saved,
            'provider'       => $provider,
            'used_saved_key' => $used_saved_key,
        ) );
    }

    private static function test_text_provider( $provider, $api_key, $model ) {
        // テストでは /v1/models 系の GET エンドポイントを使う。
        // 理由:
        //   - /v1/messages や /v1/chat/completions は実推論を走らせ課金される。
        //     クレジット残高が枯渇していると、APIキー自体は有効でも 400
        //     (credit_balance_too_low / insufficient_quota 等) で弾かれ、
        //     キー検証が成立しない。
        //   - /v1/models 系は課金されず、APIキーの正当性のみを検証できる。
        //   - 選択モデルの利用可否は、ユーザが「モデル更新」した時点でリスト不在を
        //     検出できるので、テストではキー検証だけに専念する。

        if ( $provider === 'claude' ) {
            $response = wp_remote_get( 'https://api.anthropic.com/v1/models', array(
                'timeout' => 20,
                'headers' => array(
                    'x-api-key'         => $api_key,
                    'anthropic-version' => '2023-06-01',
                ),
            ) );
            return self::api_test_result( $response, array( 200 ) );
        }

        if ( $provider === 'gemini' ) {
            $url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' . rawurlencode( $api_key );
            $response = wp_remote_get( $url, array( 'timeout' => 20 ) );
            return self::api_test_result( $response, array( 200 ) );
        }

        // OpenAI 互換系 (/v1/models で一覧取得可能なもの)
        $models_endpoints = array(
            'openai'     => 'https://api.openai.com/v1/models',
            'mistral'    => 'https://api.mistral.ai/v1/models',
            'groq'       => 'https://api.groq.com/openai/v1/models',
            'deepseek'   => 'https://api.deepseek.com/v1/models',
            'openrouter' => 'https://openrouter.ai/api/v1/models',
        );
        if ( empty( $models_endpoints[ $provider ] ) ) {
            return new WP_Error( 'invalid_provider', __( 'Invalid provider', 'ai-auto-poster' ) );
        }

        $headers = array( 'Authorization' => 'Bearer ' . $api_key );
        if ( $provider === 'openrouter' ) {
            $headers['HTTP-Referer'] = home_url();
            $headers['X-Title']      = 'AI Auto Poster';
        }

        $response = wp_remote_get( $models_endpoints[ $provider ], array(
            'timeout' => 20,
            'headers' => $headers,
        ) );
        return self::api_test_result( $response, array( 200 ) );
    }

    private static function test_image_provider( $provider, $api_key, $model ) {
        switch ( $provider ) {
            case 'dalle3':
                // OpenAI /v1/models でキーの有効性を確認
                // 加えて、選択モデルが結果に含まれているかを軽く確認する(廃止モデル検出)
                $response = wp_remote_get( 'https://api.openai.com/v1/models', array(
                    'timeout' => 20,
                    'headers' => array( 'Authorization' => 'Bearer ' . $api_key ),
                ) );
                $key_check = self::api_test_result( $response, array( 200 ) );
                if ( is_wp_error( $key_check ) ) {
                    return $key_check;
                }
                return self::verify_image_model_available( $response, $model, 'openai' );
            case 'imagen':
                // Gemini /v1beta/models でキーの有効性を確認
                // 加えて、選択モデルが結果に含まれているかを軽く確認する(廃止モデル検出)
                $response = wp_remote_get( 'https://generativelanguage.googleapis.com/v1beta/models?key=' . rawurlencode( $api_key ), array( 'timeout' => 20 ) );
                $key_check = self::api_test_result( $response, array( 200 ) );
                if ( is_wp_error( $key_check ) ) {
                    return $key_check;
                }
                return self::verify_image_model_available( $response, $model, 'gemini' );
            case 'stability':
                $response = wp_remote_get( 'https://api.stability.ai/v1/user/account', array(
                    'timeout' => 20,
                    'headers' => array( 'Authorization' => 'Bearer ' . $api_key ),
                ) );
                return self::api_test_result( $response, array( 200 ) );
            case 'unsplash':
                $response = wp_remote_get( add_query_arg( array( 'query' => 'test', 'per_page' => 1, 'client_id' => $api_key ), 'https://api.unsplash.com/search/photos' ), array( 'timeout' => 20 ) );
                return self::api_test_result( $response, array( 200 ) );
            case 'pixabay':
                $response = wp_remote_get( add_query_arg( array( 'key' => $api_key, 'q' => 'test', 'per_page' => 3 ), 'https://pixabay.com/api/' ), array( 'timeout' => 20 ) );
                return self::api_test_result( $response, array( 200 ) );
            case 'pexels':
                $response = wp_remote_get( add_query_arg( array( 'query' => 'test', 'per_page' => 1 ), 'https://api.pexels.com/v1/search' ), array(
                    'timeout' => 20,
                    'headers' => array( 'Authorization' => $api_key ),
                ) );
                return self::api_test_result( $response, array( 200 ) );
            case 'flux':
                $response = wp_remote_get( 'https://fal.ai/api/keys/current', array(
                    'timeout' => 20,
                    'headers' => array( 'Authorization' => 'Key ' . $api_key ),
                ) );
                return self::api_test_result( $response, array( 200 ) );
        }
        return new WP_Error( 'test_not_available', __( 'Test is not available for this provider', 'ai-auto-poster' ) );
    }

    /**
     * APIキーテストのレスポンスから、選択モデルがそのプロバイダで現在利用可能か検証する。
     *
     * APIキー自体は OK でも、選択モデルが廃止されている場合がある (例: Imagen で
     * imagen-X-fast-generate-001 が v1beta から消える等)。この場合、画像生成時に
     * 「model not found」エラーが出るが、ユーザはなぜ失敗するか分からない。
     *
     * 本関数はキーテストで取得した /models 一覧と選択モデルを突き合わせ、
     * 不在ならテスト結果としてエラーを返してユーザに「リフレッシュしてモデル一覧を更新する」
     * 手順を案内する。
     *
     * @param array  $response wp_remote_get の戻り値 (api_test_result が success と判定した後)
     * @param string $model    検証対象のモデルID(空ならスキップ)
     * @param string $shape    'openai' (data[].id) | 'gemini' (models[].name)
     * @return true|WP_Error 利用可能なら true、不在なら WP_Error
     */
    private static function verify_image_model_available( $response, $model, $shape ) {
        // モデル未指定はスキップ(キー有効性のみで OK)
        if ( ! is_string( $model ) || $model === '' ) {
            return true;
        }
        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );
        if ( ! is_array( $data ) ) {
            // パース失敗時は判定不能のため pass(キーテストは通っているので OK 扱い)
            return true;
        }

        $found = false;
        if ( $shape === 'openai' && ! empty( $data['data'] ) && is_array( $data['data'] ) ) {
            foreach ( $data['data'] as $m ) {
                if ( ( $m['id'] ?? '' ) === $model ) {
                    $found = true;
                    break;
                }
            }
        } elseif ( $shape === 'gemini' && ! empty( $data['models'] ) && is_array( $data['models'] ) ) {
            foreach ( $data['models'] as $m ) {
                $name = isset( $m['name'] ) ? (string) $m['name'] : '';
                // Gemini API は "models/<id>" 形式で返す
                if ( strpos( $name, 'models/' ) === 0 ) {
                    $name = substr( $name, 7 );
                }
                if ( $name === $model ) {
                    $found = true;
                    break;
                }
            }
        } else {
            // 想定外のレスポンス形状: 判定せず pass
            return true;
        }

        if ( ! $found ) {
            return new WP_Error(
                'model_not_available',
                sprintf(
                    /* translators: %s = selected image model id */
                    __( 'API key OK, but selected model "%s" is not available. Click the refresh button next to the model selector to update the list.', 'ai-auto-poster' ),
                    $model
                )
            );
        }
        return true;
    }

    private static function api_test_result( $response, $success_codes ) {
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = wp_remote_retrieve_response_code( $response );
        if ( in_array( $code, $success_codes, true ) ) {
            return true;
        }
        $body = wp_remote_retrieve_body( $response );
        $data = json_decode( $body, true );
        // PHP 8 互換: $data が null や $data['error'] が string の場合、
        // 直接の連鎖アクセス '$data[error][message] ?? $data[message] ?? ...' は
        // 「Trying to access array offset on value of type null/string」警告を出す。
        // is_array で守ってから個別に isset で確認する。
        $msg = '';
        if ( is_array( $data ) ) {
            if ( isset( $data['error']['message'] ) ) {
                $msg = $data['error']['message'];
            } elseif ( isset( $data['error'] ) && is_string( $data['error'] ) ) {
                $msg = $data['error'];
            } elseif ( isset( $data['message'] ) ) {
                $msg = $data['message'];
            } elseif ( isset( $data['detail'] ) ) {
                $msg = $data['detail'];
            }
        }
        if ( $msg === '' || $msg === null ) {
            $msg = $body;
        }
        if ( is_array( $msg ) ) {
            $msg = wp_json_encode( $msg );
        }
        $msg = $msg ? wp_strip_all_tags( (string) $msg ) : 'API test failed.';

        // クレジット枯渇 / 月次上限 / 課金未設定など「課金状態」起因のエラーは、
        // APIキー自体は有効である可能性が高い。専用の分類関数で先に判定する。
        // (build_api_error_wp_error と整合させ、テスト経路でも同じ判定を使う)
        $is_quota_exhausted = false;
        $kind               = 'unknown';
        if ( class_exists( 'AIAP_AI_Client' ) ) {
            $is_quota_exhausted = AIAP_AI_Client::is_quota_exhausted_error( $msg );
            $classified         = AIAP_AI_Client::classify_api_error( $code, $msg );
            $kind               = $classified['kind'];
        }

        $hint = '';
        if ( $is_quota_exhausted ) {
            // credit_balance_too_low / insufficient_quota / spending cap 等。
            // キー自体は通っている可能性が高いことを明示する。
            $hint = __( 'API credit balance / quota problem (the API key itself may be valid)', 'ai-auto-poster' );
        } elseif ( in_array( $code, array( 401, 403 ), true ) ) {
            $hint = __( 'API key or permission problem', 'ai-auto-poster' );
        } elseif ( $code === 404 ) {
            $hint = __( 'Model or endpoint not found', 'ai-auto-poster' );
        } elseif ( $code === 429 ) {
            $hint = __( 'Rate limit or quota problem', 'ai-auto-poster' );
        } elseif ( $code >= 500 ) {
            $hint = __( 'Provider server error', 'ai-auto-poster' );
        } elseif ( $code >= 400 ) {
            $hint = __( 'Request setting problem', 'ai-auto-poster' );
        }

        $display = $hint ? sprintf( 'HTTP %d: %s - %s', $code, $hint, $msg ) : sprintf( 'HTTP %d: %s', $code, $msg );

        // モデル関連の問題が疑われる場合は「リフレッシュ推奨」を末尾に添える。
        // 主用途: テキストAI のテストで、ユーザが選択中のモデルがプロバイダ側で
        // 廃止されている時 (404 や 400 で "model" を含むエラー) に、対処法を提示する。
        // 画像側は verify_image_model_available() で別経路の案内を出すので、
        // ここでは API キーテスト自体が失敗したケースをカバーする。
        // ただし quota_exhausted の時はモデル問題ではないので Tip を付けない。
        $is_model_issue = ! $is_quota_exhausted && (
            ( $code === 404 )
            || ( $code >= 400 && $code < 500 && stripos( (string) $msg, 'model' ) !== false )
        );
        if ( $is_model_issue ) {
            /* translators: 1: original error message, 2: tip text appended after an em-dash separator */
            $display = sprintf( __( '%1$s — %2$s', 'ai-auto-poster' ), $display, __( 'Tip: Click the refresh button next to the model selector to update the available model list.', 'ai-auto-poster' ) );
        }

        return new WP_Error( 'api_test_failed', $display, array(
            'http_code' => (int) $code,
            'kind'      => $kind,
            // quota_exhausted 時は呼び出し側でキー保存を許可するためのフラグ。
            // 「キーは有効、ただ残高がない」というユーザ状態を救う。
            'key_likely_valid' => $is_quota_exhausted,
            'raw_message' => $msg,
        ) );
    }

    public static function handle_save_settings() {
        self::verify();
        $s = AIAP_AI_Client::get_settings();
        $is_pro = AIAP_License::is_pro();
        // enabled はダッシュボードのトグルで管理(設定画面では触らない・現状値を維持)
        $posted_status        = sanitize_text_field( wp_unslash( $_POST['post_status'] ?? '' ) );
        $s['post_status']     = in_array( $posted_status, array('draft','pending','publish'), true ) ? $posted_status : 'draft';
        $s['post_category']   = absint( wp_unslash( $_POST['post_category'] ?? 0 ) );
        // Bug fix (audit r9 self-audit): post_author は absint だけだと存在しないユーザー ID を
        // 受け入れてしまう。設定後に管理者を削除した・user_id を手で書き換えた等のケースで、
        // wp_insert_post( ['post_author' => 999] ) が post_author を NULL のまま保存し、
        // フロントエンドで「by 」と作者欄が空になる事故が起きる。get_userdata でユーザーが
        // 存在することを確認し、存在しない場合は既存値(あれば)を維持、無ければ現在の管理者に
        // フォールバックする。
        $posted_author = absint( wp_unslash( $_POST['post_author'] ?? 1 ) );
        if ( $posted_author > 0 && get_userdata( $posted_author ) !== false ) {
            $s['post_author'] = $posted_author;
        } else {
            // 既存値が有効ならそれを維持。なければ現在の管理者にフォールバック。
            $existing_author = isset( $s['post_author'] ) ? (int) $s['post_author'] : 0;
            if ( $existing_author <= 0 || get_userdata( $existing_author ) === false ) {
                $s['post_author'] = (int) get_current_user_id();
            }
        }
        // 自動実行(cron)関連 — 投稿し損ねた予定の処理は無料、ただし最大件数は Pro のみ可変
        $s['cron_recover_missed'] = isset( $_POST['cron_recover_missed'] ) ? 1 : 0;
        if ( $is_pro ) {
            $s['cron_recover_max'] = max( 1, min( 5, absint( wp_unslash( $_POST['cron_recover_max'] ?? 3 ) ) ) );
        } else {
            // Free プラン: 1 件固定 (UI の disabled を bypass されても強制)
            $s['cron_recover_max'] = 1;
        }
        // 重複防止 — Pro のみフォーム反映。Free は既存値を保持(UI 非活性)
        if ( $is_pro ) {
            $s['dedup_enabled']   = isset( $_POST['dedup_enabled'] ) ? 1 : 0;
            $s['dedup_step1']     = isset( $_POST['dedup_step1'] ) ? 1 : 0;
            $s['dedup_step2']     = isset( $_POST['dedup_step2'] ) ? 1 : 0;
            $s['dedup_max_retry'] = max( 1, min( 10, absint( wp_unslash( $_POST['dedup_max_retry'] ?? 3 ) ) ) );
            $sens                 = sanitize_text_field( wp_unslash( $_POST['dedup_sensitivity'] ?? 'medium' ) );
            $s['dedup_sensitivity'] = in_array( $sens, array( 'low', 'medium', 'high' ), true ) ? $sens : 'medium';
        }
        // SEO 関連 — Pro のみフォーム反映。Free は既存値を保持(UI 非活性)
        $seo_fields = array('seo_enabled','seo_meta_desc','seo_heading','seo_focus_kw','seo_faq','seo_toc','seo_template','seo_img_alt','seo_img_webp','seo_internal_link','seo_auto_tag','seo_related','seo_schema','seo_ogp','seo_canonical','seo_plugin_link','seo_reading_time','seo_sitemap_ping','seo_llms_txt');
        if ( $is_pro ) {
            foreach ( $seo_fields as $f ) $s[ $f ] = isset( $_POST[ $f ] ) ? 1 : 0;
        }
        AIAP_AI_Client::update_settings( $s );
        AIAP_Scheduler::reschedule();
        self::redirect( 'settings', __( 'Settings saved.', 'ai-auto-poster' ) );
    }

    public static function handle_save_provider() {
        self::verify();
        $s   = AIAP_AI_Client::get_settings();
        $tab = sanitize_key( wp_unslash( $_POST['provider_tab'] ?? ( $s['provider'] ?? 'claude' ) ) );
        if ( ! in_array( $tab, AIAP_AI_Client::get_providers(), true ) ) $tab = 'claude';

        // APIキー: 空文字 or '*' のみ(マスク表示)は「未変更」扱いで既存値を保持。
        foreach ( array('claude_api_key','openai_api_key','gemini_api_key','mistral_api_key','groq_api_key','deepseek_api_key','openrouter_api_key') as $f ) {
            if ( isset( $_POST[ $f ] ) ) {
                $val = trim( (string) wp_unslash( $_POST[ $f ] ) );
                // '*' のみの値はマスク表示なので保存しない
                if ( $val !== '' && $val !== str_repeat( '*', strlen( $val ) ) ) {
                    $s[ $f ] = $val;
                }
            }
        }

        // 「使用するプロバイダ」の切替
        if ( ! empty( $_POST['provider'] ) ) {
            $posted_provider = sanitize_key( wp_unslash( $_POST['provider'] ) );
            if ( in_array( $posted_provider, AIAP_AI_Client::get_providers(), true ) ) {
                $s['provider'] = $posted_provider;
            }
        }
        foreach ( array('claude_model','openai_model','gemini_model','mistral_model','groq_model','deepseek_model','openrouter_model') as $f ) {
            if ( isset( $_POST[ $f ] ) ) $s[ $f ] = sanitize_text_field( wp_unslash( $_POST[ $f ] ) );
        }
        foreach ( AIAP_AI_Client::get_providers() as $provider ) {
            $custom = sanitize_text_field( wp_unslash( $_POST[ $provider . '_custom_model' ] ?? '' ) );
            // カスタムモデルは英数字・ハイフン・スラッシュ・コロン・ピリオドのみ、最大128文字
            if ( $custom !== '' && preg_match( '#^[A-Za-z0-9._/:\-]{1,128}$#', $custom ) && isset( $s[ $provider . '_model' ] ) ) {
                $s[ $provider . '_model' ] = $custom;
            }
        }
        AIAP_AI_Client::update_settings( $s );
        self::redirect( 'provider&sub=' . $tab, __( 'Settings saved.', 'ai-auto-poster' ) );
    }

    public static function handle_save_image_provider() {
        self::verify();
        $im  = AIAP_Image_Client::get_settings();
        $tab = sanitize_key( wp_unslash( $_POST['image_tab'] ?? 'dalle3' ) );
        if ( ! isset( self::$img_icons[ $tab ] ) ) $tab = 'dalle3';
        $src = array_keys( AIAP_Image_Client::get_sources() );
        if ( isset( $_POST['image_source'] ) ) {
            $posted_src = sanitize_text_field( wp_unslash( $_POST['image_source'] ) );
            if ( in_array( $posted_src, $src, true ) ) {
                $im['image_source'] = $posted_src;
            }
        }
        // APIキー: 空文字 or '*' のみ(マスク表示)は「未変更」扱い
        foreach ( array('image_openai_api_key','image_gemini_api_key','stability_api_key','flux_api_key','unsplash_access_key','pixabay_api_key','pexels_api_key') as $f ) {
            if ( isset( $_POST[ $f ] ) ) {
                $val = trim( (string) wp_unslash( $_POST[ $f ] ) );
                if ( $val !== '' && $val !== str_repeat( '*', strlen( $val ) ) ) {
                    $im[ $f ] = $val;
                }
            }
        }
        if ( isset( $_POST['stability_model'] ) ) $im['stability_model'] = sanitize_text_field( wp_unslash( $_POST['stability_model'] ) );
        if ( isset( $_POST['flux_model'] ) )       $im['flux_model']      = sanitize_text_field( wp_unslash( $_POST['flux_model'] ) );

        // 画像プロバイダ(dalle3/imagen)のモデル選択保存 (スタイル検証より前に確定させる)
        foreach ( array( 'dalle3', 'imagen' ) as $prov ) {
            $field = $prov . '_model';
            if ( isset( $_POST[ $field ] ) ) {
                $im[ $field ] = sanitize_text_field( wp_unslash( $_POST[ $field ] ) );
            }
        }

        // プロバイダごとのスタイル保存 (そのプロバイダの現在モデルがサポートするスタイルのみ受け入れる)
        foreach ( array( 'dalle3', 'imagen', 'stability', 'flux' ) as $prov ) {
            $field = $prov . '_style';
            if ( isset( $_POST[ $field ] ) ) {
                $val = sanitize_text_field( wp_unslash( $_POST[ $field ] ) );
                $current_model = $im[ $prov . '_model' ] ?? '';
                $allowed = array_keys( AIAP_Image_Client::get_provider_styles( $prov, $current_model ) );
                if ( in_array( $val, $allowed, true ) ) {
                    $im[ $field ] = $val;
                }
                // サポート外の値が送られてきた場合は保存せず既存値を維持
            }
        }

        // 仕様変更: 「デフォルト画像ソースとして使用する」トグルを廃止したため、
        // image_enabled は常に 1 に固定する。タスク/手動側の featured_image_mode が
        // 'on'/'off' の二択で個別に制御する。
        $im['image_enabled'] = 1;
        AIAP_Image_Client::update_settings( $im );
        self::redirect( 'image_provider&sub=' . $tab, __( 'Settings saved.', 'ai-auto-poster' ) );
    }

    public static function handle_save_image_settings() {
        self::verify();
        $im = AIAP_Image_Client::get_settings();
        // image_preset は UI ヒント。許可リストで検証。
        $allowed_presets = array( 'blog', 'sns', 'lite', 'custom' );
        $posted_preset   = sanitize_text_field( wp_unslash( $_POST['image_preset'] ?? 'custom' ) );
        $im['image_preset'] = in_array( $posted_preset, $allowed_presets, true ) ? $posted_preset : 'custom';

        // image_format / image_quality は許可リストで厳密検証
        $posted_format  = sanitize_text_field( wp_unslash( $_POST['image_format'] ?? '16:9' ) );
        $posted_quality = sanitize_text_field( wp_unslash( $_POST['image_quality'] ?? 'low' ) );
        $im['image_format']    = in_array( $posted_format, self::$image_format_keys, true ) ? $posted_format : '16:9';
        $im['image_quality']   = in_array( $posted_quality, self::$image_quality_keys, true ) ? $posted_quality : 'low';
        $im['image_max_width'] = max( 100, min( 4096, absint( wp_unslash( $_POST['image_max_width'] ?? 1200 ) ) ) );
        $im['image_compress']  = max( 1, min( 100, absint( wp_unslash( $_POST['image_compress'] ?? 85 ) ) ) );
        AIAP_Image_Client::update_settings( $im );
        self::redirect( 'image_settings', __( 'Settings saved.', 'ai-auto-poster' ) );
    }

    public static function handle_save_system_prompt() {
        self::verify();
        $s = AIAP_AI_Client::get_settings();
        $posted_lang = sanitize_text_field( wp_unslash( $_POST['content_language'] ?? 'ja' ) );
        // 言語コードは get_language_options のキーと一致するもののみ受け入れる
        $valid_langs = array_keys( AIAP_AI_Client::get_language_options() );
        $s['content_language'] = in_array( $posted_lang, $valid_langs, true ) ? $posted_lang : 'ja';
        $s['content_length']   = isset( $_POST['content_length'] ) && wp_unslash( $_POST['content_length'] ) !== ''
            ? max( 0, min( 10000, absint( wp_unslash( $_POST['content_length'] ) ) ) )
            : 0;
        $s['system_prompt']    = sanitize_textarea_field( wp_unslash( $_POST['system_prompt'] ?? '' ) );
        AIAP_AI_Client::update_settings( $s );
        self::redirect( 'prompts', __( 'Settings saved.', 'ai-auto-poster' ) );
    }

    public static function handle_add_task() {
        self::verify();
        $prompt = sanitize_textarea_field( wp_unslash( $_POST['prompt'] ?? '' ) );
        if ( empty( $prompt ) ) { self::redirect( 'prompts', __( 'Prompt is required.', 'ai-auto-poster' ), 'error' ); return; }
        $language = sanitize_text_field( wp_unslash( $_POST['task_content_language'] ?? '' ) );
        $valid_langs = array_keys( AIAP_AI_Client::get_language_options() );
        if ( ! in_array( $language, $valid_langs, true ) ) {
            self::redirect( 'prompts', __( 'Content language is required.', 'ai-auto-poster' ), 'error' );
            return;
        }
        $schedule_raw = isset( $_POST['schedule'] ) ? wp_unslash( $_POST['schedule'] ) : array();
        $invalid_schedule = AIAP_Prompts::count_invalid_in_schedule_form( $schedule_raw );
        $schedule = AIAP_Prompts::parse_schedule_from_form( $schedule_raw );
        $length = isset( $_POST['task_content_length'] ) && wp_unslash( $_POST['task_content_length'] ) !== ''
            ? max( 0, min( 10000, absint( wp_unslash( $_POST['task_content_length'] ) ) ) )
            : 0;
        $image_mode = AIAP_Prompts::sanitize_image_mode( wp_unslash( $_POST['task_featured_image_mode'] ?? 'on' ) );
        // RSS 設定
        $rss_input = array(
            'rss_enabled'  => isset( $_POST['rss_enabled'] )  ? $_POST['rss_enabled']  : 0,
            'rss_feed_ids' => isset( $_POST['rss_feed_ids'] ) ? (array) wp_unslash( $_POST['rss_feed_ids'] ) : array(),
            'rss_count'    => isset( $_POST['rss_count'] )    ? $_POST['rss_count']    : 5,
            'rss_mode'     => isset( $_POST['rss_mode'] )     ? sanitize_text_field( wp_unslash( $_POST['rss_mode'] ) ) : 'pick',
            'rss_fallback' => isset( $_POST['rss_fallback'] ) ? sanitize_text_field( wp_unslash( $_POST['rss_fallback'] ) ) : 'skip',
        );
        $rss_normalized = AIAP_Prompts::get_rss_settings( $rss_input );
        // #47: ランダム投稿モード関連
        $random_mode      = AIAP_Prompts::sanitize_random_mode( $_POST['task_random_mode'] ?? 0 );
        $random_count_min = AIAP_Prompts::sanitize_random_count( $_POST['task_random_count_min'] ?? 0 );
        $random_count_max = AIAP_Prompts::sanitize_random_count( $_POST['task_random_count_max'] ?? 0 );
        if ( $random_count_max < $random_count_min ) { $random_count_max = $random_count_min; }
        // Phase 13: 旧 exclude_periods → 新 allowed_periods + min_interval_minutes
        $allowed_periods      = AIAP_Prompts::sanitize_allowed_periods( $_POST['task_allowed_periods'] ?? array() );
        $min_interval_minutes = AIAP_Prompts::sanitize_min_interval_minutes( $_POST['task_min_interval_minutes'] ?? 0 );
        $name             = sanitize_text_field( wp_unslash( $_POST['task_name'] ?? '' ) );
        AIAP_Prompts::add( $prompt, $schedule, isset( $_POST['task_enabled'] ) ? 1 : 0, $language, $length, $image_mode, $random_mode, $random_count_min, $random_count_max, $allowed_periods, $min_interval_minutes, $name );
        // RSS 設定を追加したタスクに反映（add() は ID を返さないため末尾タスクを更新）
        $all_tasks_after = AIAP_Prompts::get_all();
        $last_task = end( $all_tasks_after );
        if ( $last_task && isset( $last_task['id'] ) ) {
            AIAP_Prompts::update( $last_task['id'], $rss_normalized );
        }
        if ( $invalid_schedule > 0 ) {
            /* translators: %d = number of invalid schedule entries discarded */
            $msg = sprintf( __( 'Settings saved. %d invalid schedule time(s) were discarded. Use HH:MM (e.g. 09:00).', 'ai-auto-poster' ), $invalid_schedule );
            self::redirect( 'prompts', $msg, 'warning' );
            return;
        }
        self::redirect( 'prompts', __( 'Settings saved.', 'ai-auto-poster' ) );
    }

    public static function handle_edit_task() {
        self::verify();
        $task_id = sanitize_text_field( wp_unslash( $_POST['task_id'] ?? '' ) );
        $prompt  = sanitize_textarea_field( wp_unslash( $_POST['prompt'] ?? '' ) );
        if ( empty( $task_id ) || empty( $prompt ) ) {
            self::redirect( 'prompts', __( 'Prompt is required.', 'ai-auto-poster' ), 'error' );
            return;
        }
        $language = sanitize_text_field( wp_unslash( $_POST['task_content_language'] ?? '' ) );
        $valid_langs = array_keys( AIAP_AI_Client::get_language_options() );
        if ( ! in_array( $language, $valid_langs, true ) ) {
            self::redirect( 'prompts', __( 'Content language is required.', 'ai-auto-poster' ), 'error' );
            return;
        }
        $length = isset( $_POST['task_content_length'] ) && wp_unslash( $_POST['task_content_length'] ) !== ''
            ? max( 0, min( 10000, absint( wp_unslash( $_POST['task_content_length'] ) ) ) )
            : 0;
        $image_mode = AIAP_Prompts::sanitize_image_mode( wp_unslash( $_POST['task_featured_image_mode'] ?? 'on' ) );
        // RSS 設定
        $rss_input = array(
            'rss_enabled'  => isset( $_POST['rss_enabled'] )  ? $_POST['rss_enabled']  : 0,
            'rss_feed_ids' => isset( $_POST['rss_feed_ids'] ) ? (array) wp_unslash( $_POST['rss_feed_ids'] ) : array(),
            'rss_count'    => isset( $_POST['rss_count'] )    ? $_POST['rss_count']    : 5,
            'rss_mode'     => isset( $_POST['rss_mode'] )     ? sanitize_text_field( wp_unslash( $_POST['rss_mode'] ) ) : 'pick',
            'rss_fallback' => isset( $_POST['rss_fallback'] ) ? sanitize_text_field( wp_unslash( $_POST['rss_fallback'] ) ) : 'skip',
        );
        $rss_normalized = AIAP_Prompts::get_rss_settings( $rss_input );
        // #47: ランダム投稿モード関連
        $random_mode      = AIAP_Prompts::sanitize_random_mode( $_POST['task_random_mode'] ?? 0 );
        $random_count_min = AIAP_Prompts::sanitize_random_count( $_POST['task_random_count_min'] ?? 0 );
        $random_count_max = AIAP_Prompts::sanitize_random_count( $_POST['task_random_count_max'] ?? 0 );
        if ( $random_count_max < $random_count_min ) { $random_count_max = $random_count_min; }
        // Phase 13: 旧 exclude_periods → 新 allowed_periods + min_interval_minutes
        $allowed_periods      = AIAP_Prompts::sanitize_allowed_periods( $_POST['task_allowed_periods'] ?? array() );
        $min_interval_minutes = AIAP_Prompts::sanitize_min_interval_minutes( $_POST['task_min_interval_minutes'] ?? 0 );
        $schedule_raw = isset( $_POST['schedule'] ) ? wp_unslash( $_POST['schedule'] ) : array();
        $invalid_schedule = AIAP_Prompts::count_invalid_in_schedule_form( $schedule_raw );
        AIAP_Prompts::update( $task_id, array(
            'name'                 => sanitize_text_field( wp_unslash( $_POST['task_name'] ?? '' ) ),
            'prompt'               => $prompt,
            'schedule'             => AIAP_Prompts::parse_schedule_from_form( $schedule_raw ),
            'enabled'              => isset( $_POST['task_enabled'] ) ? 1 : 0,
            'content_language'     => $language,
            'content_length'       => $length,
            'featured_image_mode'  => $image_mode,
            'random_mode'          => $random_mode,
            'random_count_min'     => $random_count_min,
            'random_count_max'     => $random_count_max,
            'allowed_periods'      => $allowed_periods,
            'min_interval_minutes' => $min_interval_minutes,
            'rss_enabled'          => $rss_normalized['rss_enabled'],
            'rss_feed_ids'         => $rss_normalized['rss_feed_ids'],
            'rss_count'            => $rss_normalized['rss_count'],
            'rss_mode'             => $rss_normalized['rss_mode'],
            'rss_fallback'         => $rss_normalized['rss_fallback'],
        ) );
        if ( $invalid_schedule > 0 ) {
            /* translators: %d = number of invalid schedule entries discarded */
            $msg = sprintf( __( 'Settings saved. %d invalid schedule time(s) were discarded. Use HH:MM (e.g. 09:00).', 'ai-auto-poster' ), $invalid_schedule );
            self::redirect( 'prompts', $msg, 'warning' );
            return;
        }
        self::redirect( 'prompts', __( 'Settings saved.', 'ai-auto-poster' ) );
    }

    public static function handle_toggle_task() { self::verify(); AIAP_Prompts::toggle( sanitize_text_field( wp_unslash( $_POST['task_id'] ?? '' ) ) ); self::redirect( 'prompts', __( 'Settings saved.', 'ai-auto-poster' ) ); }
    public static function handle_delete_task() { self::verify(); AIAP_Prompts::delete( sanitize_text_field( wp_unslash( $_POST['task_id'] ?? '' ) ) ); self::redirect( 'prompts', __( 'Settings saved.', 'ai-auto-poster' ) ); }

    const USER_META_LAST_MANUAL_PROMPT     = 'aiap_last_manual_prompt';
    const USER_META_LAST_MANUAL_LANGUAGE   = 'aiap_last_manual_language';
    const USER_META_LAST_MANUAL_LENGTH     = 'aiap_last_manual_length';
    const USER_META_LAST_MANUAL_IMAGE_MODE = 'aiap_last_manual_image_mode';
    const USER_META_LAST_MANUAL_RSS_ENABLED  = 'aiap_last_manual_rss_enabled';
    const USER_META_LAST_MANUAL_RSS_FEED_IDS = 'aiap_last_manual_rss_feed_ids';
    const USER_META_LAST_MANUAL_RSS_COUNT    = 'aiap_last_manual_rss_count';
    const USER_META_LAST_MANUAL_RSS_MODE     = 'aiap_last_manual_rss_mode';
    const USER_META_LAST_MANUAL_RSS_FALLBACK = 'aiap_last_manual_rss_fallback';

    private static function read_manual_overrides() {
        $overrides = array();
        $uid = get_current_user_id();

        if ( isset( $_POST['manual_content_language'] ) ) {
            $lang = sanitize_text_field( wp_unslash( $_POST['manual_content_language'] ) );
            $valid = array_keys( AIAP_AI_Client::get_language_options() );
            if ( in_array( $lang, $valid, true ) ) {
                $overrides['content_language'] = $lang;
                update_user_meta( $uid, self::USER_META_LAST_MANUAL_LANGUAGE, $lang );
            }
        }

        if ( isset( $_POST['manual_content_length'] ) ) {
            $raw_len = wp_unslash( $_POST['manual_content_length'] );
            if ( $raw_len !== '' ) {
                $len = max( 0, min( 10000, absint( $raw_len ) ) );
                // 送信された値は常にユーザーメタに保存する(0=無制限を含む)
                update_user_meta( $uid, self::USER_META_LAST_MANUAL_LENGTH, $len );
                // overrides に積むのは len > 0 のときだけ(0 はオーバーライドではなく既定値使用)
                if ( $len > 0 ) {
                    $overrides['content_length'] = $len;
                }
            }
        }

        // RSS 設定（Quick Post 専用）
        // フォームに manual_rss_enabled が来ていない場合は触らない（user_meta も更新しない）
        // Quick Post フォームから来た場合のみ RSS 設定を正規化＆保存する。
        // manual_rss_form_submitted 隠しフィールドが確実な判定。
        // フォールバック: いずれかの RSS フィールドが送られていれば旧挙動でも保存する。
        $has_manual_rss_field = isset( $_POST['manual_rss_form_submitted'] )
            || isset( $_POST['manual_rss_enabled'] )
            || isset( $_POST['manual_rss_feed_ids'] )
            || isset( $_POST['manual_rss_count'] )
            || isset( $_POST['manual_rss_mode'] )
            || isset( $_POST['manual_rss_fallback'] );
        if ( $has_manual_rss_field ) {
            $rss_input = array(
                'rss_enabled'  => isset( $_POST['manual_rss_enabled'] ) ? 1 : 0,
                'rss_feed_ids' => isset( $_POST['manual_rss_feed_ids'] ) ? (array) wp_unslash( $_POST['manual_rss_feed_ids'] ) : array(),
                'rss_count'    => isset( $_POST['manual_rss_count'] ) ? $_POST['manual_rss_count'] : 5,
                'rss_mode'     => isset( $_POST['manual_rss_mode'] ) ? sanitize_text_field( wp_unslash( $_POST['manual_rss_mode'] ) ) : 'pick',
                'rss_fallback' => isset( $_POST['manual_rss_fallback'] ) ? sanitize_text_field( wp_unslash( $_POST['manual_rss_fallback'] ) ) : 'generate',
            );
            $rss_normalized = AIAP_Prompts::get_rss_settings( $rss_input );
            $overrides['manual_rss_settings'] = $rss_normalized;
            // user_meta に最後の値を保存（次回画面復元用、フォーム入力のあった値だけ）
            update_user_meta( $uid, self::USER_META_LAST_MANUAL_RSS_ENABLED,  (int) $rss_normalized['rss_enabled'] );
            update_user_meta( $uid, self::USER_META_LAST_MANUAL_RSS_FEED_IDS, $rss_normalized['rss_feed_ids'] );
            update_user_meta( $uid, self::USER_META_LAST_MANUAL_RSS_COUNT,    (int) $rss_normalized['rss_count'] );
            update_user_meta( $uid, self::USER_META_LAST_MANUAL_RSS_MODE,     $rss_normalized['rss_mode'] );
            update_user_meta( $uid, self::USER_META_LAST_MANUAL_RSS_FALLBACK, $rss_normalized['rss_fallback'] );
        }

        return $overrides;
    }

    // ========== Stepwise manual run (AJAX) ==========
    const RUN_STATE_TRANSIENT_PREFIX = 'aiap_run_state_';
    const RUN_STATE_TTL = 600; // 10 minutes

    private static function run_state_key() {
        return self::RUN_STATE_TRANSIENT_PREFIX . get_current_user_id();
    }

    private static function get_run_state() {
        $state = get_transient( self::run_state_key() );
        return is_array( $state ) ? $state : array();
    }

    private static function set_run_state( array $state ) {
        set_transient( self::run_state_key(), $state, self::RUN_STATE_TTL );
    }

    private static function clear_run_state() {
        delete_transient( self::run_state_key() );
    }

    private static function ajax_verify() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Unauthorized', 'ai-auto-poster' ) ), 403 );
        }
        check_ajax_referer( self::NONCE, 'nonce' );
    }

    /**
     * Bug fix (audit r9 self-audit): 長時間処理(AI 生成 + 画像 retry)の途中で
     * PHP の max_execution_time に達してプロセスが殺されないようにする。
     *
     * 観察された症状:
     *   - ユーザーが画像 502 で「サーバ障害対応中 1/3」のログを見たが
     *     2/3 と 3/3 のログがどこにも書かれていなかった。
     *   - Logger::add は即 update_option するため、書かれていない =
     *     プロセス自体がそこに到達する前に殺されている、と判断できる。
     *
     * 旧コードは set_time_limit / ignore_user_abort をどこでも呼んでいなかった。
     * デフォルトの max_execution_time は PHP 5.3 以降 30 秒。共用ホスティング
     * では 30〜60 秒に絞られていることが多い。一方で:
     *   - wp_remote_post( ..., array('timeout' => 120) ) を呼んでいる
     *   - call_with_retry が最大 4 回試行 + sleep(1+2+3) する
     *   - dedup retry や AI generate retry でさらに繰り返される
     * これが PHP 制限と衝突して、リトライ完了前にプロセスが死ぬ。
     *
     * 対策:
     *   - set_time_limit(0) で実行時間制限を解除(ホストが許可していない場合は @ で握る)
     *   - ignore_user_abort(true) でクライアントが切断しても処理継続
     *   - wp_raise_memory_limit('cron') でメモリ上限を引き上げ(画像処理対策)
     */
    private static function raise_long_running_limits() {
        if ( function_exists( 'set_time_limit' ) ) {
            // 共用ホスティングで disable_functions に入っていることがあるため warning を抑制。
            @set_time_limit( 0 );
        }
        if ( function_exists( 'ignore_user_abort' ) ) {
            @ignore_user_abort( true );
        }
        if ( function_exists( 'wp_raise_memory_limit' ) ) {
            wp_raise_memory_limit( 'cron' );
        }
    }

    /**
     * Ajaxハンドラで Fatal Error が起きた時、最後にログを残して JSONエラーで返す
     * register_shutdown_function で登録する
     */
    public static function fatal_error_handler() {
        $err = error_get_last();
        if ( ! $err ) return;
        // Fatal系のみ反応
        $fatal_types = array( E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR );
        if ( ! in_array( $err['type'], $fatal_types, true ) ) return;

        $msg = sprintf(
            /* translators: 1: PHP error message, 2: file basename, 3: line number */
            __( 'PHP Fatal: %1$s in %2$s:%3$d', 'ai-auto-poster' ),
            $err['message'],
            basename( $err['file'] ),
            $err['line']
        );

        if ( class_exists( 'AIAP_Logger' ) ) {
            AIAP_Logger::add( 'error', 'log_manual_fatal', array( $msg ) );
        }

        // すでにヘッダ送信済みなら応答できない
        if ( headers_sent() ) return;
        if ( ! function_exists( 'wp_send_json_error' ) ) return;

        self::clear_run_state();

        // 500を返さずJSONエラーとして応答(クライアントがちゃんと受け取れる)
        if ( function_exists( 'http_response_code' ) ) {
            http_response_code( 200 );
        }
        wp_send_json_error( array( 'step' => 'ai', 'message' => $msg ) );
    }

    public static function ajax_run_now_start() {
        register_shutdown_function( array( __CLASS__, 'fatal_error_handler' ) );
        self::raise_long_running_limits();
        self::ajax_verify();

        // Free プラン: 月10投稿の上限に達していたら拒否
        if ( ! AIAP_License::can_post_more() ) {
            wp_send_json_error( array(
                'step'    => 'limit',
                'message' => AIAP_License::limit_reached_message(),
                'limit'   => AIAP_License::FREE_MONTHLY_LIMIT,
                'count'   => AIAP_License::monthly_post_count(),
                'upgrade' => AIAP_License::upgrade_url(),
            ) );
        }

        $prompt = sanitize_textarea_field( wp_unslash( $_POST['manual_prompt'] ?? '' ) );
        $overrides = self::read_manual_overrides();
        // 手動投稿のアイキャッチ override (on/off) を effective 値に解決して
        // overrides['image_enabled_effective'] に積む。AI プロンプト側と image step の双方で同じ値を使う。
        // これがないと AI に「画像を生成する前提のプロンプト」が送られ、
        // 本文に「次の画像を見ると...」のような文言が混入する可能性がある。
        $featured_image_mode = AIAP_Prompts::sanitize_image_mode( wp_unslash( $_POST['manual_featured_image_mode'] ?? 'on' ) );
        // 言語・長さと同様、次回 Quick Post を開いた時の初期値復元のため user_meta に保存する
        update_user_meta( get_current_user_id(), self::USER_META_LAST_MANUAL_IMAGE_MODE, $featured_image_mode );

        $im_settings_qs = AIAP_Image_Client::get_settings();
        $overrides['image_enabled_effective'] = AIAP_Image_Client::resolve_image_enabled(
            $featured_image_mode,
            $im_settings_qs['image_enabled'] ?? 0
        );

        // 画面のプロンプト欄の状態(空/入力あり)をそのまま user_meta に保存
        // これで次回画面を開いた時も同じ状態(空欄なら空欄)で再現される
        update_user_meta( get_current_user_id(), self::USER_META_LAST_MANUAL_PROMPT, $prompt );

        // プロンプト空欄の場合、次に予定されている有効タスクから取得
        $task_id = '';
        if ( $prompt === '' ) {
            $next_task = AIAP_Prompts::find_next_scheduled_task();
            if ( ! $next_task || empty( $next_task['prompt'] ) ) {
                wp_send_json_error( array( 'message' => __( 'No available task. Enter a prompt or add a task.', 'ai-auto-poster' ) ) );
            }
            $prompt = (string) $next_task['prompt'];
            // タスク単位の content_language / content_length は、
            // フォームで明示的に指定されていない場合のみフォールバックとして使う
            if ( ! isset( $overrides['content_language'] ) && ! empty( $next_task['content_language'] ) ) {
                $overrides['content_language'] = $next_task['content_language'];
            }
            if ( ! isset( $overrides['content_length'] ) && ! empty( $next_task['content_length'] ) ) {
                $overrides['content_length'] = (int) $next_task['content_length'];
            }
            // RSS フォールバック（B 案）:
            // フォーム manual_rss_enabled=1 なら overrides['manual_rss_settings'] を使う（既設定済み）
            // それ以外なら、タスクの RSS 設定にフォールバック
            $form_rss = $overrides['manual_rss_settings'] ?? null;
            if ( empty( $form_rss['rss_enabled'] ) ) {
                $task_rss = AIAP_Prompts::get_rss_settings( $next_task );
                $overrides['manual_rss_settings'] = $task_rss;
            }
            // task_id を保持(post step で _aiap_task_id postmeta と increment_used に使う)
            $task_id = isset( $next_task['id'] ) ? (string) $next_task['id'] : '';
            AIAP_Logger::add( 'info', 'log_manual_empty_prompt', array( $task_id ));
        }

        // Bug fix (audit r9 self-audit, #39): 取りこぼしのキュー追加は「投稿後」に移動。
        // 旧実装はここ (投稿前) で enqueue_missed_for_manual を呼んでいたが、ユーザー仕様は
        // 「投稿しようとした際に、取りこぼしがあったら、今の投稿後を終えてから、取りこぼし
        // を処理する」。つまり現在分の投稿が確定してから取りこぼしをキューに積むのが正しい。
        // 失敗時は積まない (次回手動実行のタイミングで改めて積まれる)。
        // 実際の enqueue は ajax_run_now_step の post step 成功直後で行う (L4045 付近)。

        // ===== Quick Post の RSS フェーズ =====
        $qp_rss_settings   = isset( $overrides['manual_rss_settings'] ) && is_array( $overrides['manual_rss_settings'] )
            ? $overrides['manual_rss_settings']
            : AIAP_Prompts::get_rss_settings( array() );
        $qp_rss_used_articles = array();
        if ( $qp_rss_settings['rss_enabled'] === 1 ) {
            $qp_is_pro = class_exists( 'AIAP_License' ) && AIAP_License::is_pro();
            if ( ! $qp_is_pro ) {
                AIAP_Logger::add( 'info', 'log_rss_skipped_free_plan', array( 'src_manual', '-' ) );
            } else {
                $qp_candidates = AIAP_RSS_Feeds::pick_unused( $qp_rss_settings['rss_feed_ids'], $qp_rss_settings['rss_count'] );
                if ( empty( $qp_candidates ) ) {
                    if ( $qp_rss_settings['rss_fallback'] === 'skip' ) {
                        AIAP_Logger::add( 'info', 'log_rss_skipped_no_articles', array( 'src_manual', '-' ) );
                        wp_send_json_error( array(
                            'step'    => 'rss',
                            'message' => __( 'No fresh RSS articles available; post skipped per task setting.', 'ai-auto-poster' ),
                        ) );
                    }
                    AIAP_Logger::add( 'info', 'log_rss_fallback_to_normal', array( 'src_manual', '-' ) );
                } else {
                    $overrides['rss_articles']  = $qp_candidates;
                    $overrides['rss_mode']      = $qp_rss_settings['rss_mode'];
                    $qp_rss_used_articles       = $qp_candidates;
                }
            }
        }
        // 内部キー manual_rss_settings は run_state には引き継がない（不要）が、
        // overrides は run_state に保存されるためそのまま残ってもよい。
        // ===== /Quick Post の RSS フェーズ =====

        // オリジナルのプロンプトと上書きは run_state に保持して次リトライでも使う
        // featured_image_mode から計算した effective 値は $overrides['image_enabled_effective'] に
        // 含まれているため、state に別途持たせる必要はない。
        // 取りこぼしのキュー追加は finalize_run (全 step 完了時) で行うため、
        // state に recovered_count を持たせる必要はなくなった (#40)。
        self::set_run_state( array(
            'original_prompt'   => $prompt,
            'overrides'         => $overrides,
            'task_id'           => $task_id,
            'rss_used_articles' => $qp_rss_used_articles,
            'rss_settings'      => $qp_rss_settings,
        ) );

        // 1回だけAI生成+dedup判定
        // server_retry_count は JS 側で「サーバエラーで何回連続リトライしたか」を保持し、
        // 同じ start エンドポイントへ再投稿してくる(dedup ループ前段)ので、毎回読み取る。
        $server_retry_count = max( 0, absint( wp_unslash( $_POST['server_retry_count'] ?? 0 ) ) );
        self::do_single_generate_and_check( $prompt, 1, 0, $server_retry_count );
    }

    /**
     * 手動実行のリトライ用Ajax
     * クライアントが retry_needed=true を受けたら再度このエンドポイントを叩く
     */
    public static function ajax_run_now_retry() {
        register_shutdown_function( array( __CLASS__, 'fatal_error_handler' ) );
        self::raise_long_running_limits();
        self::ajax_verify();

        $state = self::get_run_state();
        if ( empty( $state['original_prompt'] ) ) {
            wp_send_json_error( array( 'message' => __( 'Run state expired. Please retry.', 'ai-auto-poster' ) ) );
        }

        $attempt             = max( 1, absint( wp_unslash( $_POST['attempt'] ?? 1 ) ) );
        $dedup_attempts      = max( 0, absint( wp_unslash( $_POST['dedup_attempts'] ?? 0 ) ) );
        $retry_prompt        = sanitize_textarea_field( wp_unslash( $_POST['retry_prompt'] ?? '' ) );
        // server_retry_count: JS が「これまでサーバエラーで何回リトライしたか」を保持して送ってくる。
        // 0=初回、1=1 回目のリトライ実行中、… のサイクル番号。PHP は受け取ったらログのラベルにのみ使う。
        $server_retry_count  = max( 0, absint( wp_unslash( $_POST['server_retry_count'] ?? 0 ) ) );

        $use_prompt = $retry_prompt !== '' ? $retry_prompt : $state['original_prompt'];
        self::do_single_generate_and_check( $use_prompt, $attempt, $dedup_attempts, $server_retry_count );
    }

    /**
     * 1回だけAI生成 + dedup判定 を実行し、結果をJSONで返す
     * リトライが必要なら retry_needed=true で返す(クライアントが再度Ajaxを投げる)
     */
    private static function do_single_generate_and_check( $current_prompt, $attempt, $dedup_attempts, $server_retry_count = 0 ) {
        $state = self::get_run_state();
        $original_prompt = isset( $state['original_prompt'] ) ? $state['original_prompt'] : $current_prompt;
        $overrides       = isset( $state['overrides'] ) ? $state['overrides'] : array();

        $settings      = AIAP_AI_Client::get_settings();
        // Bug fix (audit r4): mirror Pro gate from AIAP_Poster::run_single() so manual
        // runs do not silently exercise dedup (a Pro feature) on Free plan when the
        // saved dedup_enabled=1 was carried over from a previous Pro subscription.
        $dedup_enabled = ! empty( $settings['dedup_enabled'] )
            && ( ! class_exists( 'AIAP_License' ) || AIAP_License::is_pro() );
        $max_retry     = max( 1, (int) ( $settings['dedup_max_retry'] ?? 3 ) );

        // AI生成(1回だけ)。サーバエラーリトライは JS 側で別 AJAX として呼び直すことで実現する
        // (画面に「サーバ障害対応中 N/M」進捗を表示するため)。
        // 永続エラー(課金枯渇/認証/入力不正)は retryable=false で即終了させる。
        // 一時エラー(レート制限/サーバ一時障害)は retryable=true で JS にリトライさせる。
        // is_manual=true: 自動cronがセットする現在日時overrideを参照しない
        // (手動実行は投稿し損ね処理の対象ではないため、cronと並走時の混入を防ぐ)
        $result = AIAP_AI_Client::generate( $current_prompt, $overrides, true );

        if ( is_wp_error( $result ) ) {
            // エラー種別を判定して、JS にリトライ可否を伝える
            $err_data    = $result->get_error_data();
            $is_retryable = is_array( $err_data ) && array_key_exists( 'retryable', $err_data )
                ? (bool) $err_data['retryable']
                : false;
            $kind         = is_array( $err_data ) && isset( $err_data['kind'] ) ? (string) $err_data['kind'] : 'unknown';
            $task_label   = AIAP_Prompts::format_log_label( $state['task_id'] ?? '' );

            $max_retry_ai = AIAP_AI_Client::GENERATE_MAX_RETRY;
            if ( $is_retryable && $server_retry_count >= $max_retry_ai ) {
                $is_retryable = false;
            }

            // 生のレスポンス本文(raw)はログに含めない: AI 出力にユーザー入力や潜在的な機密が
            // 紛れ込むリスクがあるため、format_error_for_log でヒント+API エラー本文のみ記録する。
            //
            // Bug fix (audit r9 self-audit, #36): リトライ中の中間ログのレベルを
            // 'warning' に統一する。旧実装は admin.php (UI) で 'error'、poster.php (cron) で
            // 'info'、image-client.php (画像) で 'info' と経路ごとにバラバラだった。
            // リトライ中の途中失敗は「まだ復旧の見込みあり」なので 'info' は弱すぎ、
            // 一方 'error' は最終失敗 (log_server_error_failed) と紛らわしい。'warning' が妥当。
            $attempts_so_far = $server_retry_count + 1;
            if ( $is_retryable ) {
                AIAP_Logger::add( 'warning', 'log_server_error_retry', array(
                    'src_manual', $attempts_so_far, $max_retry_ai + 1,
                    AIAP_AI_Client::format_error_for_log( $result ), $task_label,
                ) );
            } else {
                AIAP_Logger::add( 'error', 'log_server_error_failed', array(
                    'src_manual', $attempts_so_far,
                    AIAP_AI_Client::format_error_for_log( $result ), $task_label,
                ) );
            }

            if ( ! $is_retryable ) {
                // 永続エラー or 上限到達: 即終了。run_state もクリア。
                self::clear_run_state();
                wp_send_json_error( array(
                    'step'      => 'ai',
                    'message'   => $result->get_error_message(),
                    'retryable' => false,
                    'kind'      => $kind,
                ) );
            }

            // 一時エラー: run_state は維持して JS にリトライ判断を委ねる
            // Bug fix (audit r9 self-audit, #34/#35): バックオフ秒数を PHP 側で計算して渡す。
            // 手動実行のリトライは「PHP 側で sleep + 4 回試行」ではなく「JS が retryable=true
            // を受けて別 AJAX で投げ直す」設計のため、PHP 内 sleep が効かない経路。
            // ここでは AI_Client::compute_retry_sleep_seconds() (PHP・JS 共通の真実の供給源)
            // で次回までの待機秒数を計算し、JS には next_sleep_seconds として渡す。
            // JS は受け取った値をそのまま setTimeout に渡すだけで、独自にバックオフ計算しない。
            $retry_after_value = is_array( $err_data ) && isset( $err_data['retry_after'] ) ? (int) $err_data['retry_after'] : 0;
            $next_sleep        = AIAP_AI_Client::compute_retry_sleep_seconds( $server_retry_count + 1, $retry_after_value );
            wp_send_json_error( array(
                'step'               => 'ai',
                'message'            => $result->get_error_message(),
                'retryable'          => true,
                'kind'               => $kind,
                'next_sleep_seconds' => $next_sleep,
            ) );
        }

        // dedup無効 なら即通過
        if ( ! $dedup_enabled ) {
            self::set_run_state( array_merge( $state, array(
                'result'   => $result,
                'prompt'   => $original_prompt,
                'post_id'  => 0,
                'warnings' => array(),
            ) ) );
            wp_send_json_success( array(
                'retry_needed'   => false,
                'next_step'      => 'post',
                'message'        => __( 'Creating post…', 'ai-auto-poster' ),
                'dedup_attempts' => $dedup_attempts,
            ) );
        }

        // 類似チェック
        $dedup_result = AIAP_Dedup::check( $result['title'], $result['content'] );

        // 非類似 → そのまま採用
        if ( ! $dedup_result['is_similar'] ) {
            if ( $attempt > 1 ) {
                AIAP_Logger::add( 'info', 'log_dedup_passed', array(
                    'src_manual', $attempt, $dedup_result['method'],
                ) );
            }
            self::set_run_state( array_merge( $state, array(
                'result'   => $result,
                'prompt'   => $original_prompt,
                'post_id'  => 0,
                'warnings' => array(),
            ) ) );
            wp_send_json_success( array(
                'retry_needed'   => false,
                'next_step'      => 'post',
                'message'        => __( 'Creating post…', 'ai-auto-poster' ),
                'dedup_attempts' => $dedup_attempts,
            ) );
        }

        // 類似 かつ リトライ上限 → 諦めて採用(最後の生成を投稿)
        // max_retry は「リトライ最大回数」なので、試行回数が max_retry を超えたら打ち切る(最大 max_retry+1 回試行)。
        if ( $attempt > $max_retry ) {
            AIAP_Logger::add( 'info', 'log_dedup_max_reached', array( 'src_manual', $max_retry ) );
            $dedup_attempts++;
            self::set_run_state( array_merge( $state, array(
                'result'   => $result,
                'prompt'   => $original_prompt,
                'post_id'  => 0,
                'warnings' => array( __( 'Could not fully avoid similarity. Posted with the latest generation.', 'ai-auto-poster' ) ),
            ) ) );
            wp_send_json_success( array(
                'retry_needed'   => false,
                'next_step'      => 'post',
                'message'        => __( 'Creating post…', 'ai-auto-poster' ),
                'dedup_attempts' => $dedup_attempts,
            ) );
        }

        // 類似 → クライアントにリトライ要求
        $dedup_attempts++;
        AIAP_Logger::add( 'info', 'log_dedup_retry', array(
            'src_manual', $attempt, $max_retry,
            $dedup_result['method'],
            mb_substr( $dedup_result['reason'], 0, 80 ),
        ) );

        $recent       = AIAP_Dedup::get_recent_posts();
        $retry_prompt = AIAP_Dedup::build_retry_prompt(
            $original_prompt,
            $dedup_result['reason'],
            $recent,
            $result
        );

        wp_send_json_success( array(
            'retry_needed'   => true,
            'kind'           => 'dedup',  // JS が「サーバエラーリトライ」と区別するため
            'next_attempt'   => $attempt + 1,
            'retry_prompt'   => $retry_prompt,
            'dedup_attempts' => $dedup_attempts,
            'reason'         => $dedup_result['reason'],
        ) );
    }

    public static function ajax_run_now_step() {
        // Bug fix (audit r9 self-audit): post / image / seo の各 step 実行中に
        // PHP fatal が起きた場合の防御。旧実装は ajax_run_now_start と _retry にしか
        // 登録していなかったため、step ハンドラでクラッシュすると:
        //   - ログに何も残らない
        //   - run_state が transient に残ったまま (clear_run_state されない)
        //   - クライアントは応答無しのままタイムアウト or fetch reject になる
        // start / retry と同じ shutdown フックを step にも入れる。
        register_shutdown_function( array( __CLASS__, 'fatal_error_handler' ) );
        // Bug fix (audit r9 self-audit): 画像 step は call_with_retry で最大 4 回試行 +
        // sleep(1+2+3) するため、PHP の max_execution_time に到達して途中で殺される
        // ケースが報告されている (例: 1/3 のログだけ残って 2/3 と 3/3 が失われる)。
        self::raise_long_running_limits();
        self::ajax_verify();
        $step  = sanitize_key( wp_unslash( $_POST['step'] ?? '' ) );
        $state = self::get_run_state();
        if ( empty( $state ) ) {
            wp_send_json_error( array( 'message' => __( 'State expired. Please retry.', 'ai-auto-poster' ) ) );
        }

        switch ( $step ) {
            case 'post':
                $result = $state['result'];
                $ai     = AIAP_AI_Client::get_settings();

                // 共通の投稿作成処理に委譲(自動経路の AIAP_Poster::run_single と同じ挙動)
                // 経路固有postmeta: _aiap_manual=1 (手動投稿の識別)
                //                   _aiap_task_id=<id> (空欄→次タスク補充時のみ)
                $extra_meta = array( '_aiap_manual' => 1 );
                if ( ! empty( $state['task_id'] ) ) {
                    $extra_meta['_aiap_task_id'] = $state['task_id'];
                }
                // RSS: pick モードは AI 応答の selected_rss_index で 1 件確定、digest は全件
                $rss_used = isset( $state['rss_used_articles'] ) && is_array( $state['rss_used_articles'] ) ? $state['rss_used_articles'] : array();
                $rss_mode = isset( $state['rss_settings']['rss_mode'] ) ? (string) $state['rss_settings']['rss_mode'] : 'pick';
                $rss_consumed = array();
                if ( ! empty( $rss_used ) ) {
                    if ( $rss_mode === 'pick' ) {
                        $rss_idx = isset( $result['selected_rss_index'] ) ? (int) $result['selected_rss_index'] : 0;
                        if ( $rss_idx < 1 || $rss_idx > count( $rss_used ) ) {
                            AIAP_Logger::add( 'warning', 'log_rss_pick_index_invalid', array( 'src_manual', $rss_idx, count( $rss_used ) ) );
                            $rss_idx = 1;
                        }
                        $rss_consumed = array( $rss_used[ $rss_idx - 1 ] );
                    } else {
                        $rss_consumed = $rss_used;
                    }
                    $rss_urls     = array();
                    $rss_guids    = array();
                    $rss_feed_ids = array();
                    foreach ( $rss_consumed as $a ) {
                        $rss_urls[]     = isset( $a['link'] )    ? (string) $a['link']    : '';
                        $rss_guids[]    = isset( $a['guid'] )    ? (string) $a['guid']    : '';
                        $rss_feed_ids[] = isset( $a['feed_id'] ) ? (string) $a['feed_id'] : '';
                    }
                    $extra_meta['_aiap_rss_source_urls']  = $rss_urls;
                    $extra_meta['_aiap_rss_source_guids'] = $rss_guids;
                    $extra_meta['_aiap_rss_feed_ids']     = $rss_feed_ids;
                    $extra_meta['_aiap_rss_mode']         = $rss_mode;
                }

                $pid = AIAP_Poster::create_post_from_result(
                    $result,
                    $ai,
                    'src_manual',
                    '',
                    $extra_meta
                );

                if ( is_wp_error( $pid ) ) {
                    AIAP_Logger::add( 'error', 'log_manual_post_error', array( $pid->get_error_message() ) );
                    self::clear_run_state();
                    wp_send_json_error( array( 'step' => 'post', 'message' => $pid->get_error_message() ) );
                }

                // 投稿作成直後にログへ記録する。
                // 画像生成やSEOで止まっても、手動投稿で記事が作成された事実は残す。
                // RSS を使った場合はサマリを末尾に付与した拡張版キーを使う。
                $manual_rss_summary = ! empty( $rss_consumed )
                    ? AIAP_RSS_Composer::build_log_summary( $rss_consumed, $rss_mode )
                    : '';
                if ( $manual_rss_summary !== '' ) {
                    AIAP_Logger::add( 'success', 'log_manual_post_created_with_rss', array(
                        $pid, wp_strip_all_tags( (string) $result['title'] ), $manual_rss_summary,
                    ) );
                } else {
                    AIAP_Logger::add( 'success', 'log_manual_post_created', array(
                        $pid, wp_strip_all_tags( (string) $result['title'] ),
                    ) );
                }

                // 空欄→次タスク補充ケースでは使用回数を加算(自動経路と整合)
                if ( ! empty( $state['task_id'] ) && class_exists( 'AIAP_Prompts' ) ) {
                    AIAP_Prompts::increment_used( $state['task_id'] );
                }
                // RSS: 消費した記事を使用済み履歴に追加
                if ( ! empty( $rss_consumed ) ) {
                    foreach ( $rss_consumed as $a ) {
                        if ( ! empty( $a['feed_id'] ) && ! empty( $a['guid'] ) ) {
                            AIAP_RSS_State::mark_used( $a['feed_id'], $a['guid'], (int) $pid );
                        }
                    }
                }

                $state['post_id'] = (int) $pid;
                $state['slug']    = get_post_field( 'post_name', $pid );
                $state['ai']      = $ai;
                self::set_run_state( $state );

                // 取りこぼしのキュー追加は、画像 / SEO まで含めた全 step 完了後 (finalize_run 内)
                // で行う。仕様「今の投稿後を終えてから、取りこぼしを処理する」の「投稿後」は
                // 投稿エンティティ作成直後ではなく、画像 + SEO まで含めた完全な処理完了を指す。
                // (#39 で post step に置いていたが、ユーザー指摘で #40 で finalize_run に移動)

                // SEO の有効判定は AIAP_SEO::apply() 側に委譲(常に seo step へ進む)。
                // image step は手動側の featured_image_mode が effective OFF の場合に飛ばす。
                // (フロント UI に「アイキャッチを生成しています…」の偽完了表示が出るのを防ぐ)
                // effective 値は ajax_run_now_start で計算済みのものを overrides 経由で読む。
                if ( ! empty( $state['overrides']['image_enabled_effective'] ) ) {
                    wp_send_json_success( array( 'next_step' => 'image', 'message' => __( 'Generating featured image…', 'ai-auto-poster' ), 'post_id' => $pid ) );
                }
                wp_send_json_success( array( 'next_step' => 'seo', 'message' => __( 'Applying SEO settings…', 'ai-auto-poster' ), 'post_id' => $pid ) );
                break; // Bug fix (audit r9): wp_send_json_* は exit するので到達しないが、防御的に break を置く

            case 'image':
                // Bug fix (audit r9): クライアントが順序を破って image step を直接叩いた場合、
                // post step で詰めるはずの post_id / slug が state に無い可能性がある。
                // undefined index 経由のエラーで JSON レスポンスが壊れる前にここで弾く。
                if ( empty( $state['post_id'] ) ) {
                    wp_send_json_error( array( 'message' => __( 'Out-of-order step. Please retry.', 'ai-auto-poster' ) ) );
                }
                $pid    = (int) $state['post_id'];
                $result = $state['result'];
                $slug   = $state['slug'] ?? '';
                // image_enabled / image_source の判定は AIAP_Image_Client::generate_and_attach() 内部に委譲。
                // 設定により無効化されている場合は false が返り、is_wp_error() で false 扱いとなりログは出ない。
                $preset_ip        = isset( $result['image_prompt'] ) ? (string) $result['image_prompt'] : '';
                $user_prompt_full = isset( $state['original_prompt'] ) ? (string) $state['original_prompt'] : '';
                $article_body     = isset( $result['content'] ) ? (string) $result['content'] : '';
                // effective 値は ajax_run_now_start で計算済みのものを overrides 経由で読む。
                $effective_on = ! empty( $state['overrides']['image_enabled_effective'] );
                $r = AIAP_Image_Client::generate_and_attach( $pid, $result['title'], $slug, $preset_ip, $user_prompt_full, $article_body, $effective_on, 'src_manual' );
                if ( is_wp_error( $r ) ) {
                    AIAP_Logger::add( 'error', 'log_thumbnail_failed', array( 'src_manual', (string) $pid, AIAP_AI_Client::format_error_for_log( $r ) ) );
                    // warnings は UI 表示用なので短いヒントのみ。詳細は実行ログで確認できる。
                    /* translators: %s: underlying error message from the image provider */
                    $state['warnings'][] = sprintf( __( 'Featured image generation failed, but the post was created (%s)', 'ai-auto-poster' ), $r->get_error_message() );
                    self::set_run_state( $state );
                }
                wp_send_json_success( array(
                    'next_step' => 'seo',
                    'message'   => __( 'Applying SEO settings…', 'ai-auto-poster' ),
                    'warnings'  => $state['warnings'],
                ) );
                break; // Bug fix (audit r9): 同上、防御的 break

            case 'seo':
                // Bug fix (audit r9): post step を経ずに seo を叩かれたケースの防御
                if ( empty( $state['post_id'] ) ) {
                    wp_send_json_error( array( 'message' => __( 'Out-of-order step. Please retry.', 'ai-auto-poster' ) ) );
                }
                $pid    = (int) $state['post_id'];
                $result = $state['result'];
                $ai     = $state['ai'] ?? AIAP_AI_Client::get_settings();
                // seo_enabled の判定は AIAP_SEO::apply() 内部の早期リターンに委ねる
                AIAP_SEO::apply( $pid, $result, $ai );
                wp_send_json_success( self::finalize_run( $state ) );
                break; // Bug fix (audit r9): 同上、防御的 break

            default:
                wp_send_json_error( array( 'message' => __( 'Unknown step.', 'ai-auto-poster' ) ) );
        }
    }

    private static function finalize_run( array $state ) {
        $pid      = (int) $state['post_id'];
        $result   = $state['result'];
        $warnings = $state['warnings'] ?? array();
        if ( is_array( $warnings ) ) {
            $warnings = array_values( array_unique( array_map( 'strval', $warnings ) ) );
        } else {
            $warnings = array();
        }

        // Bug fix (audit r9 self-audit, #40): 取りこぼしのキュー追加は seo step 完了の
        // この時点で行う。仕様「投稿しようとした際に、取りこぼしがあったら、今の投稿後を
        // 終えてから、取りこぼしを処理する」の「投稿後」は post step (投稿エンティティ
        // 作成) ではなく、image + seo を含む全 step が完了した時点。失敗時はここに到達
        // しないため、取りこぼしは積まれない (次回手動実行で改めて積まれる)。設定 OFF・
        // Free 上限到達時は enqueue_missed_for_manual 内で 0 が返る。
        $recovered = 0;
        if ( class_exists( 'AIAP_Scheduler' ) ) {
            $recovered = (int) AIAP_Scheduler::enqueue_missed_for_manual();
        }
        if ( $recovered > 0 ) {
            /* translators: %d = number of missed schedules detected */
            $warnings[] = sprintf( __( '%d missed schedule(s) detected. They will be posted automatically every 5 minutes.', 'ai-auto-poster' ),  $recovered ) ;
        }

        self::clear_run_state();
        return array(
            'next_step' => 'done',
            /* translators: %d = post ID of the newly created post */
            'message'   => sprintf( __( 'Post created (ID: %d)', 'ai-auto-poster' ), $pid ),
            'post_id'   => $pid,
            'edit_url'  => get_edit_post_link( $pid, 'raw' ),
            'warnings'  => $warnings,
        );
    }

    public static function handle_clear_logs() { self::verify(); AIAP_Logger::clear(); self::redirect('run_logs',__( 'Settings saved.', 'ai-auto-poster' )); }

    private static function redirect( $tab, $msg, $type = 'success' ) {
        // URL 長による 414 (URI Too Long) を避けるため、エラーメッセージは 500 文字でクリップ。
        // rawurlencode 後はマルチバイト文字で 3〜9倍に膨らむが、500 字 + URL パラメータ程度なら
        // どの Web サーバ (Apache: 8KB / nginx: 4-8KB) でも問題なく収まる。
        if ( is_string( $msg ) && mb_strlen( $msg ) > 500 ) {
            $msg = mb_substr( $msg, 0, 497 ) . '…';
        }
        wp_safe_redirect( add_query_arg( array( 'page'=>self::MENU_SLUG, 'tab'=>$tab, 'aiap_msg'=>rawurlencode($msg), 'aiap_type'=>$type ), admin_url('admin.php') ) );
        exit;
    }

    /**
     * RSS フィードタブのレンダリング
     */
    public static function render_rss_feeds() {
        $is_pro = class_exists( 'AIAP_License' ) && AIAP_License::is_pro();
        $feeds  = AIAP_RSS_Feeds::get_all();
        $not_set_label    = __( 'Not set', 'ai-auto-poster' );
        $configured_label = __( 'Configured', 'ai-auto-poster' );
        ?>
        <div class="aiap-card aiap-rss-tab">
            <header class="aiap-provider-hero">
                <span class="aiap-provider-hero-eyebrow"><?php echo esc_html__( 'RSS Feeds', 'ai-auto-poster' ); ?></span>
                <h2 class="aiap-provider-hero-title"><?php echo esc_html__( 'Which news feeds should we read?', 'ai-auto-poster' ); ?></h2>
                <p class="aiap-provider-hero-sub"><?php echo esc_html__( 'Use news RSS as source material for AI generation.', 'ai-auto-poster' ); ?></p>
            </header>

            <?php if ( ! $is_pro ) : ?>
                <div class="aiap-pro-upsell">
                    <p><strong><?php echo esc_html__( 'This is a Pro feature.', 'ai-auto-poster' ); ?></strong></p>
                </div>
            <?php endif; ?>

            <fieldset class="aiap-rss-fieldset" <?php echo $is_pro ? '' : 'disabled'; ?>>
                <div class="aiap-provider-section-label"><?php echo esc_html__( 'Add Feed', 'ai-auto-poster' ); ?></div>
                <div class="aiap-rss-add-card">
                    <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-rss-feed-form">
                        <?php wp_nonce_field( self::NONCE ); ?>
                        <input type="hidden" name="action" value="aiap_save_rss_feed" />
                        <input type="hidden" name="aiap_rss_op" value="add" />
                        <div class="aiap-rss-add-grid">
                            <div class="aiap-rss-add-field">
                                <label class="aiap-rss-add-label"><?php echo esc_html__( 'Display Name', 'ai-auto-poster' ); ?></label>
                                <input type="text" name="name" class="aiap-input-text" placeholder="<?php echo esc_attr__( 'e.g. TechCrunch JP', 'ai-auto-poster' ); ?>" required />
                            </div>
                            <div class="aiap-rss-add-field">
                                <label class="aiap-rss-add-label"><?php echo esc_html__( 'RSS URL', 'ai-auto-poster' ); ?></label>
                                <input type="url" name="url" class="aiap-input-text" placeholder="https://example.com/feed.xml" required />
                            </div>
                        </div>
                        <div class="aiap-rss-add-row">
                            <label class="aiap-rss-add-toggle">
                                <input type="checkbox" name="enabled" value="1" checked />
                                <span><?php echo esc_html__( 'Enabled', 'ai-auto-poster' ); ?></span>
                            </label>
                            <div class="aiap-rss-add-actions">
                                <button type="button" class="button js-aiap-rss-test"><?php echo esc_html__( 'Test Connection', 'ai-auto-poster' ); ?></button>
                                <button type="submit" class="button button-primary"><?php echo esc_html__( 'Add Feed', 'ai-auto-poster' ); ?></button>
                            </div>
                        </div>
                        <span class="aiap-rss-test-result" aria-live="polite"></span>
                    </form>
                </div>

                <div class="aiap-provider-section-label"><?php echo esc_html__( 'Registered Feeds', 'ai-auto-poster' ); ?></div>
                <?php if ( empty( $feeds ) ) : ?>
                    <p class="aiap-rss-empty-state"><?php echo esc_html__( 'No feeds registered yet.', 'ai-auto-poster' ); ?></p>
                <?php else : ?>
                    <div class="aiap-rss-feed-list">
                    <?php foreach ( $feeds as $feed ) :
                        $feed_id      = isset( $feed['id'] )      ? $feed['id']      : '';
                        $name         = isset( $feed['name'] )    ? $feed['name']    : '';
                        $url          = isset( $feed['url'] )     ? $feed['url']     : '';
                        $enabled      = ! empty( $feed['enabled'] );
                        $last_fetched = isset( $feed['last_fetched_at'] ) ? $feed['last_fetched_at'] : '';
                        $last_error   = isset( $feed['last_error'] )      ? $feed['last_error']      : '';
                        $used_count   = AIAP_RSS_State::count_used( $feed_id );
                    ?>
                        <div class="aiap-rss-feed-card<?php echo $enabled ? ' is-enabled' : ' is-disabled'; ?>" data-feed-id="<?php echo esc_attr( $feed_id ); ?>">
                            <div class="aiap-rss-feed-card-head">
                                <span class="aiap-rss-feed-icon dashicons dashicons-rss" aria-hidden="true"></span>
                                <div class="aiap-rss-feed-titlebox">
                                    <strong class="aiap-rss-feed-name"><?php echo esc_html( $name ); ?></strong>
                                    <span class="aiap-rss-feed-url"><?php echo esc_html( $url ); ?></span>
                                </div>
                                <span class="aiap-provider-tile-status<?php echo $enabled ? ' is-configured' : ' is-empty'; ?>" aria-label="<?php echo esc_attr( $enabled ? $configured_label : $not_set_label ); ?>">
                                    <?php echo esc_html( $enabled ? $configured_label : $not_set_label ); ?>
                                </span>
                                <label class="aiap-toggle-switch" title="<?php echo esc_attr__( 'Enabled', 'ai-auto-poster' ); ?>" aria-label="<?php echo esc_attr__( 'Enabled', 'ai-auto-poster' ); ?>">
                                    <input type="checkbox" class="js-aiap-rss-toggle-enabled" <?php checked( $enabled ); ?> />
                                    <span class="aiap-toggle-slider"></span>
                                </label>
                            </div>
                            <div class="aiap-rss-feed-card-body">
                                <div class="aiap-rss-feed-meta">
                                    <span class="aiap-rss-feed-meta-item">
                                        <span class="aiap-rss-feed-meta-label"><?php echo esc_html__( 'Last fetched', 'ai-auto-poster' ); ?></span>
                                        <span class="aiap-rss-feed-meta-value"><?php echo esc_html( $last_fetched !== '' ? $last_fetched : '—' ); ?></span>
                                    </span>
                                    <span class="aiap-rss-feed-meta-item">
                                        <span class="aiap-rss-feed-meta-label"><?php echo esc_html__( 'Used', 'ai-auto-poster' ); ?></span>
                                        <span class="aiap-rss-feed-meta-value"><?php
                                            /* translators: 1: used count, 2: limit */
                                            echo esc_html( sprintf( __( '%1$d / %2$d', 'ai-auto-poster' ), $used_count, AIAP_RSS_State::HISTORY_PER_FEED ) );
                                        ?></span>
                                    </span>
                                    <?php if ( $last_error !== '' ) : ?>
                                        <span class="aiap-rss-feed-meta-item aiap-rss-feed-meta-item-error">
                                            <span class="aiap-rss-feed-meta-label"><?php echo esc_html__( 'Last error', 'ai-auto-poster' ); ?></span>
                                            <span class="aiap-rss-feed-meta-value"><?php echo esc_html( $last_error ); ?></span>
                                        </span>
                                    <?php endif; ?>
                                </div>
                                <div class="aiap-rss-feed-actions">
                                    <button type="button" class="aiap-btn aiap-btn-icon-only js-aiap-rss-edit" title="<?php echo esc_attr__( 'Edit', 'ai-auto-poster' ); ?>" aria-label="<?php echo esc_attr__( 'Edit', 'ai-auto-poster' ); ?>">
                                        <span class="dashicons dashicons-edit" aria-hidden="true"></span>
                                    </button>
                                    <button type="button" class="aiap-btn aiap-btn-icon-only js-aiap-rss-test" data-url="<?php echo esc_attr( $url ); ?>" title="<?php echo esc_attr__( 'Test Connection', 'ai-auto-poster' ); ?>" aria-label="<?php echo esc_attr__( 'Test Connection', 'ai-auto-poster' ); ?>">
                                        <span class="dashicons dashicons-update" aria-hidden="true"></span>
                                    </button>
                                    <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-rss-inline-form">
                                        <?php wp_nonce_field( self::NONCE ); ?>
                                        <input type="hidden" name="action" value="aiap_delete_rss_feed" />
                                        <input type="hidden" name="feed_id" value="<?php echo esc_attr( $feed_id ); ?>" />
                                        <button type="submit" class="aiap-btn aiap-btn-icon-only aiap-btn-danger-icon js-aiap-rss-delete-confirm" title="<?php echo esc_attr__( 'Delete', 'ai-auto-poster' ); ?>" aria-label="<?php echo esc_attr__( 'Delete', 'ai-auto-poster' ); ?>">
                                            <span class="dashicons dashicons-trash" aria-hidden="true"></span>
                                        </button>
                                    </form>
                                </div>
                                <span class="aiap-rss-test-result" aria-live="polite"></span>
                            </div>
                            <!--
                                編集フォーム: 初期は hidden、.js-aiap-rss-edit クリックで
                                親カードに .is-editing が付与され、CSS で表示／既存ビュー(head/body)を非表示にする。
                                送信は通常の POST → handle_save_rss_feed (op=update) → リダイレクト。
                            -->
                            <form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="aiap-rss-feed-edit-form aiap-rss-feed-form">
                                <?php wp_nonce_field( self::NONCE ); ?>
                                <input type="hidden" name="action" value="aiap_save_rss_feed" />
                                <input type="hidden" name="aiap_rss_op" value="update" />
                                <input type="hidden" name="feed_id" value="<?php echo esc_attr( $feed_id ); ?>" />
                                <div class="aiap-rss-add-grid">
                                    <div class="aiap-rss-add-field">
                                        <label class="aiap-rss-add-label"><?php echo esc_html__( 'Display Name', 'ai-auto-poster' ); ?></label>
                                        <input type="text" name="name" class="aiap-input-text" value="<?php echo esc_attr( $name ); ?>" required />
                                    </div>
                                    <div class="aiap-rss-add-field">
                                        <label class="aiap-rss-add-label"><?php echo esc_html__( 'RSS URL', 'ai-auto-poster' ); ?></label>
                                        <input type="url" name="url" class="aiap-input-text" value="<?php echo esc_attr( $url ); ?>" required />
                                    </div>
                                </div>
                                <div class="aiap-rss-add-row">
                                    <label class="aiap-rss-add-toggle">
                                        <input type="checkbox" name="enabled" value="1" <?php checked( $enabled ); ?> />
                                        <span><?php echo esc_html__( 'Enabled', 'ai-auto-poster' ); ?></span>
                                    </label>
                                    <div class="aiap-rss-add-actions">
                                        <button type="button" class="button js-aiap-rss-edit-cancel"><?php echo esc_html__( 'Cancel', 'ai-auto-poster' ); ?></button>
                                        <button type="submit" class="button button-primary"><?php echo esc_html__( 'Save', 'ai-auto-poster' ); ?></button>
                                    </div>
                                </div>
                            </form>
                        </div>
                    <?php endforeach; ?>
                    </div>
                <?php endif; ?>
            </fieldset>
        </div>
        <?php
    }

    // ================================================================
    // RSS フィード AJAX / admin-post ハンドラ
    // ================================================================

    /**
     * RSS フィードの追加・編集
     */
    public static function handle_save_rss_feed() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Insufficient permissions.', 'ai-auto-poster' ) );
        }
        check_admin_referer( self::NONCE );
        if ( ! class_exists( 'AIAP_License' ) || ! AIAP_License::is_pro() ) {
            wp_die( esc_html__( 'This is a Pro feature.', 'ai-auto-poster' ) );
        }
        $op = isset( $_POST['aiap_rss_op'] ) ? sanitize_key( wp_unslash( $_POST['aiap_rss_op'] ) ) : 'add';
        $name    = isset( $_POST['name'] )    ? sanitize_text_field( wp_unslash( $_POST['name'] ) )    : '';
        $url     = isset( $_POST['url'] )     ? esc_url_raw( wp_unslash( $_POST['url'] ) )             : '';
        $enabled = ! empty( $_POST['enabled'] ) ? 1 : 0;

        if ( $op === 'add' ) {
            $result = AIAP_RSS_Feeds::add( array(
                'name'    => $name,
                'url'     => $url,
                'enabled' => $enabled,
            ) );
            if ( is_wp_error( $result ) ) {
                $back = add_query_arg( array(
                    'page'              => self::MENU_SLUG,
                    'tab'               => 'rss_feeds',
                    'aiap_rss_error'    => rawurlencode( $result->get_error_message() ),
                ), admin_url( 'admin.php' ) );
                wp_safe_redirect( $back );
                exit;
            }
        } elseif ( $op === 'update' ) {
            $feed_id = isset( $_POST['feed_id'] ) ? sanitize_key( wp_unslash( $_POST['feed_id'] ) ) : '';
            $result  = AIAP_RSS_Feeds::update( $feed_id, array(
                'name'    => $name,
                'url'     => $url,
                'enabled' => $enabled,
            ) );
            if ( is_wp_error( $result ) ) {
                $back = add_query_arg( array(
                    'page'              => self::MENU_SLUG,
                    'tab'               => 'rss_feeds',
                    'aiap_rss_error'    => rawurlencode( $result->get_error_message() ),
                ), admin_url( 'admin.php' ) );
                wp_safe_redirect( $back );
                exit;
            }
        }

        wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=rss_feeds' ) );
        exit;
    }

    /**
     * RSS フィードの削除（履歴も連動削除）
     */
    public static function handle_delete_rss_feed() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Insufficient permissions.', 'ai-auto-poster' ) );
        }
        check_admin_referer( self::NONCE );
        if ( ! class_exists( 'AIAP_License' ) || ! AIAP_License::is_pro() ) {
            wp_die( esc_html__( 'This is a Pro feature.', 'ai-auto-poster' ) );
        }
        $feed_id = isset( $_POST['feed_id'] ) ? sanitize_key( wp_unslash( $_POST['feed_id'] ) ) : '';

        // 削除結果はリダイレクト先でエラー処理する余地があるが、現状はログを残さずに一覧へ戻す。
        // 設定変更系の操作ログは実行ログのスコープ外のため出力しない。
        AIAP_RSS_Feeds::delete( $feed_id );
        wp_safe_redirect( admin_url( 'admin.php?page=' . self::MENU_SLUG . '&tab=rss_feeds' ) );
        exit;
    }

    /**
     * RSS フィードの有効/無効トグル
     */
    public static function ajax_rss_toggle_enabled() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'ai-auto-poster' ) ) );
        }
        check_ajax_referer( self::NONCE );
        if ( ! class_exists( 'AIAP_License' ) || ! AIAP_License::is_pro() ) {
            wp_send_json_error( array( 'message' => __( 'This is a Pro feature.', 'ai-auto-poster' ) ) );
        }
        $feed_id = isset( $_POST['feed_id'] ) ? sanitize_key( wp_unslash( $_POST['feed_id'] ) ) : '';
        $enabled = ! empty( $_POST['enabled'] ) ? 1 : 0;

        $result = AIAP_RSS_Feeds::update( $feed_id, array( 'enabled' => $enabled ) );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
        }
        wp_send_json_success( array( 'enabled' => $enabled ) );
    }

    /**
     * RSS フィード接続テスト
     */
    public static function ajax_test_rss_feed() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'ai-auto-poster' ) ) );
        }
        check_ajax_referer( self::NONCE );
        if ( ! class_exists( 'AIAP_License' ) || ! AIAP_License::is_pro() ) {
            wp_send_json_error( array( 'message' => __( 'This is a Pro feature.', 'ai-auto-poster' ) ) );
        }
        $url = isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '';
        $result = AIAP_RSS_Feeds::test_connection( $url );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array(
                'message' => sprintf(
                    /* translators: %s = error message */
                    __( 'Connection failed: %s', 'ai-auto-poster' ),
                    $result->get_error_message()
                ),
            ) );
        }
        wp_send_json_success( array(
            'message' => sprintf(
                /* translators: %d = item count */
                __( 'Connection successful: %d items found', 'ai-auto-poster' ),
                (int) $result['item_count']
            ),
            'sample'  => $result['sample_title'],
        ) );
    }

    /**
     * RSS 履歴のみ削除（フィード自体は残す）
     */
    public static function ajax_rss_purge_history() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'ai-auto-poster' ) ) );
        }
        check_ajax_referer( self::NONCE );
        if ( ! class_exists( 'AIAP_License' ) || ! AIAP_License::is_pro() ) {
            wp_send_json_error( array( 'message' => __( 'This is a Pro feature.', 'ai-auto-poster' ) ) );
        }
        $feed_id = isset( $_POST['feed_id'] ) ? sanitize_key( wp_unslash( $_POST['feed_id'] ) ) : '';
        $feed    = AIAP_RSS_Feeds::get_by_id( $feed_id );
        if ( ! $feed ) {
            wp_send_json_error( array( 'message' => __( 'Feed not found.', 'ai-auto-poster' ) ) );
        }
        // 設定変更系の操作ログは実行ログのスコープ外のため、ログには出力しない。
        $purged = AIAP_RSS_State::purge_feed( $feed_id );
        wp_send_json_success( array( 'purged' => $purged ) );
    }

    // ================================================================
    // タスクカードのアコーディオンセクション用 helper
    // ================================================================

    /**
     * タスク配列から画像セクションのサマリ文字列を生成する
     *
     * @param array $task
     * @return string 空文字 or 「— Always attach」「— Don't attach」等
     */
    private static function build_image_section_summary( $task ) {
        $mode = AIAP_Prompts::get_image_mode( $task );
        if ( $mode === 'on' ) {
            return '— ' . __( 'Always attach', 'ai-auto-poster' );
        }
        return '— ' . __( "Don't attach", 'ai-auto-poster' );
    }

    /**
     * タスク配列から RSS セクションのサマリ文字列を生成する
     *
     * @param array $task
     * @return string 空文字 or 「— Yahoo IT, NHK Main / 5 items / pick」
     */
    private static function build_rss_section_summary( $task ) {
        $rss = AIAP_Prompts::get_rss_settings( $task );
        if ( $rss['rss_enabled'] !== 1 ) {
            return '';
        }
        // フィード名解決（先頭 3 つまで、残りは「他 N フィード」）
        $feed_names = array();
        if ( class_exists( 'AIAP_RSS_Feeds' ) ) {
            $all_feeds = AIAP_RSS_Feeds::get_all();
            $feed_map  = array();
            foreach ( $all_feeds as $f ) {
                if ( isset( $f['id'] ) ) {
                    $feed_map[ $f['id'] ] = isset( $f['name'] ) ? (string) $f['name'] : '';
                }
            }
            foreach ( $rss['rss_feed_ids'] as $fid ) {
                if ( isset( $feed_map[ $fid ] ) && $feed_map[ $fid ] !== '' ) {
                    $feed_names[] = $feed_map[ $fid ];
                }
            }
        }
        $names_str = '';
        $total     = count( $feed_names );
        if ( $total === 0 ) {
            $names_str = '';
        } elseif ( $total <= 3 ) {
            $names_str = implode( ', ', $feed_names );
        } else {
            /* translators: 1: first feed names, 2: remaining count */
            $names_str = sprintf( __( '%1$s and %2$d more', 'ai-auto-poster' ), implode( ', ', array_slice( $feed_names, 0, 3 ) ), $total - 3 );
        }
        $mode_label = $rss['rss_mode'] === 'digest' ? __( 'digest', 'ai-auto-poster' ) : __( 'pick', 'ai-auto-poster' );
        /* translators: 1: feed names, 2: item count, 3: mode label */
        return '— ' . sprintf( __( '%1$s / %2$d items / %3$s', 'ai-auto-poster' ), $names_str, (int) $rss['rss_count'], $mode_label );
    }

    /**
     * タスク配列からスケジュールセクションのサマリ文字列を生成する
     *
     * @param array $task
     * @return string 「— Daily 09:00」等、既存の format_schedule_summary をラップ
     */
    private static function build_schedule_section_summary( $task ) {
        if ( ! is_array( $task ) ) {
            $task = array();
        }
        $summary = AIAP_Prompts::format_schedule_summary( $task );
        if ( $summary === '' || $summary === __( 'No schedule', 'ai-auto-poster' ) ) {
            return '— ' . __( 'unset', 'ai-auto-poster' );
        }
        return '— ' . $summary;
    }

    /**
     * セクションを既定で展開するかどうかを判定する
     *
     * 仕様変更（2026-05-13 UX 改善 v2）：
     * 編集モード・新規追加・Quick Post すべてで初期表示は折り畳み。
     * 設定済みかどうかは is_section_configured() で別途判定し、ヘッダに「設定済」バッジを出して区別する。
     *
     * @param string $section_key 'image' | 'rss' | 'schedule'
     * @param array  $task
     * @param bool   $is_new      新規追加フォームならtrue
     * @return bool
     */
    private static function is_section_expanded( $section_key, $task, $is_new = false ) {
        unset( $section_key, $task, $is_new ); // 引数は互換のため受け取るが現状常に false 返却
        return false;
    }

    /**
     * セクションが「設定済み」（既定値以外の値が入っている）かを判定する
     *
     * 折り畳んだ状態のヘッダに「設定済」バッジを表示するかの判定に使う。
     *
     * @param string $section_key 'image' | 'rss' | 'schedule'
     * @param array  $task
     * @return bool
     */
    private static function is_section_configured( $section_key, $task ) {
        if ( ! is_array( $task ) ) {
            $task = array();
        }
        switch ( $section_key ) {
            case 'image':
                // featured_image_mode は 'on' / 'off' の 2 値でいずれかが必ず設定されている。
                // 設定状態の概念がないので常に true (= サマリは常に表示する) とする。
                return true;
            case 'rss':
                $rss = AIAP_Prompts::get_rss_settings( $task );
                return $rss['rss_enabled'] === 1;
            case 'schedule':
                if ( ! empty( $task['random_mode'] ) ) {
                    return true;
                }
                $schedule = isset( $task['schedule'] ) && is_array( $task['schedule'] ) ? $task['schedule'] : array();
                foreach ( $schedule as $times ) {
                    if ( is_array( $times ) ) {
                        foreach ( $times as $t ) {
                            if ( trim( (string) $t ) !== '' ) {
                                return true;
                            }
                        }
                    }
                }
                return false;
        }
        return false;
    }
}
