<?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';

    public 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' ),
    );

    public 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() で取得
    public 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() で取得
    public static $image_format_keys = array( '16:9', '1:1', '9:16', '4:3' );

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

    // テキスト AI プロバイダ → APIキー保存先 field 名
    // 画像 AI/ストック プロバイダ → APIキー保存先 field 名
    // 旧 AIAP_Admin_Ajax::$image_provider_key_field (private) を public 集約。
    // 同じ表が admin-view-provider.php の $img_keys ローカル変数にもあり、
    // そちらも本配列を参照する形に整理する (CLAUDE.md 重複排除)。
    // テキスト AI プロバイダ → モデル名保存先 field 名
    public static function init() {
        add_action( 'admin_menu', array( __CLASS__, 'register_menu' ) );
        // Phase 3: フォーム保存ハンドラは AIAP_Admin_Controller へ分離
        $controller_actions = array( 'save_recover','save_output','save_weather','save_avoid_dup','save_dedup','save_seo','save_provider','save_image_provider','save_image_settings','save_system_prompt','save_notify','add_task','edit_task','toggle_task','delete_task' );
        foreach ( $controller_actions as $a ) add_action( 'admin_post_aiap_' . $a, array( 'AIAP_Admin_Controller', 'handle_' . $a ) );
        $actions = array( '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( 'AIAP_Admin_Ajax', 'ajax_test_provider' ) );
        add_action( 'wp_ajax_aiap_run_now_start', array( 'AIAP_Admin_Runner', 'ajax_run_now_start' ) );
        add_action( 'wp_ajax_aiap_run_now_retry', array( 'AIAP_Admin_Runner', 'ajax_run_now_retry' ) );
        add_action( 'wp_ajax_aiap_run_now_step',  array( 'AIAP_Admin_Runner', 'ajax_run_now_step' ) );
        // ページ遷移後の復帰用: 進行中の手動実行があるかを返す軽量 endpoint。
        add_action( 'wp_ajax_aiap_run_now_status', array( 'AIAP_Admin_Runner', 'ajax_run_now_status' ) );
        // ブラウザ離脱時のキャンセル用: navigator.sendBeacon でクライアントから呼ばれる。
        // 中断された run_state を即座に clear して、再実行をブロックしないようにする。
        add_action( 'wp_ajax_aiap_run_now_cancel', array( 'AIAP_Admin_Runner', 'ajax_run_now_cancel' ) );
        add_action( 'wp_ajax_aiap_run_now_clear', array( 'AIAP_Admin_Runner', 'ajax_run_now_clear' ) );
        add_action( 'wp_ajax_aiap_refresh_models', array( 'AIAP_Admin_Ajax', 'ajax_refresh_models' ) );
        add_action( 'wp_ajax_aiap_weather_geocode', array( 'AIAP_Admin_Ajax', 'ajax_weather_geocode' ) );
        add_action( 'wp_ajax_aiap_styles_for_model', array( 'AIAP_Admin_Ajax', 'ajax_styles_for_model' ) );
        add_action( 'wp_ajax_aiap_toggle_enabled', array( 'AIAP_Admin_Ajax', 'ajax_toggle_enabled' ) );
        add_action( 'wp_ajax_aiap_notify_test',    array( 'AIAP_Admin_Ajax', 'ajax_notify_test' ) );
        add_action( 'admin_post_aiap_save_rss_feed',     array( 'AIAP_Admin_RSS', 'handle_save_rss_feed' ) );
        add_action( 'admin_post_aiap_delete_rss_feed',   array( 'AIAP_Admin_RSS', 'handle_delete_rss_feed' ) );
        add_action( 'wp_ajax_aiap_rss_toggle_enabled',   array( 'AIAP_Admin_RSS', 'ajax_rss_toggle_enabled' ) );
        add_action( 'wp_ajax_aiap_test_rss_feed',        array( 'AIAP_Admin_RSS', 'ajax_test_rss_feed' ) );
        add_action( 'wp_ajax_aiap_rss_purge_history',    array( 'AIAP_Admin_RSS', '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( 'AIAP_Admin_View', 'maybe_render_auth_key_warning' ) );
        // 定期同期 (AIAP_Model_Sync) で検出したプロバイダ側の廃止済みモデルを警告する。
        add_action( 'admin_notices', array( 'AIAP_Admin_View', 'maybe_render_discontinued_model_warning' ) );
    }

    public static function register_menu() {
        add_menu_page( 'AI Auto Poster', 'AI Auto Poster', 'manage_options', self::MENU_SLUG, array( 'AIAP_Admin_View', '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( 'AIAP_Admin_Pages', '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( 'AIAP_Admin_Pages', '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 ) {
        // 5 ファイル分割した管理画面 CSS。cascade 順: tokens → base → components → pages → responsive
        $css_files = array(
            'aiap-admin-tokens'      => 'assets/css/admin-tokens.css',
            'aiap-admin-base'        => 'assets/css/admin-base.css',
            'aiap-admin-components'  => 'assets/css/admin-components.css',
            'aiap-admin-pages'        => 'assets/css/admin-pages.css',
            'aiap-admin-pages-modern' => 'assets/css/admin-pages-modern.css',
            'aiap-admin-responsive'   => 'assets/css/admin-responsive.css',
        );

        // プラグイン一覧画面では Upgrade リンク用のスタイルだけ読み込んで終了
        if ( $hook === 'plugins.php' ) {
            $prev_handle = '';
            foreach ( $css_files as $handle => $rel ) {
                $path = AIAP_PLUGIN_DIR . $rel;
                $ver  = file_exists( $path ) ? AIAP_VERSION . '.' . filemtime( $path ) : AIAP_VERSION;
                $deps = $prev_handle ? array( $prev_handle ) : array();
                wp_enqueue_style( $handle, AIAP_PLUGIN_URL . $rel, $deps, $ver );
                $prev_handle = $handle;
            }
            return;
        }
        if ( strpos( $hook, self::MENU_SLUG ) === false ) return;

        // ファイル更新時刻をバージョン文字列に含めることで、AIAP_VERSION を変えなくてもアセットが更新される
        $prev_handle = '';
        foreach ( $css_files as $handle => $rel ) {
            $path = AIAP_PLUGIN_DIR . $rel;
            $ver  = file_exists( $path ) ? AIAP_VERSION . '.' . filemtime( $path ) : AIAP_VERSION;
            $deps = $prev_handle ? array( $prev_handle ) : array();
            wp_enqueue_style( $handle, AIAP_PLUGIN_URL . $rel, $deps, $ver );
            $prev_handle = $handle;
        }

        $js_path = AIAP_PLUGIN_DIR . 'assets/admin.js';
        $js_ver  = file_exists( $js_path ) ? AIAP_VERSION . '.' . filemtime( $js_path ) : AIAP_VERSION;
        wp_enqueue_script( 'aiap-admin', AIAP_PLUGIN_URL . 'assets/admin.js', array(), $js_ver, true );

        // モダンテーマ用の body class を付与 (admin-tokens.css の .aiap-theme-modern body セレクタが効くようにする)
        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' ),
            // RSS フィード機能の JS 側確認ダイアログ・フォールバックメッセージ。
            // (以前ハードコードされていた英語文言を i18n 経由に統一)
            'rss_purge_history_confirm' => __( 'Purge history for this feed?', 'ai-auto-poster' ),
            /* translators: %d = number of purged GUID entries */
            'rss_purged_result'         => __( 'Purged %d guids.', 'ai-auto-poster' ),
            'rss_delete_feed_confirm'   => __( 'Delete this feed and its history?', 'ai-auto-poster' ),
            // RSS フィードのトグル変更時 (AJAX 成功後) に status pill を切り替える際の表示文言
            'rss_status_configured'     => __( 'Configured', 'ai-auto-poster' ),
            'rss_status_not_set'        => __( 'Not set', 'ai-auto-poster' ),
            'unknown_error'             => __( 'Unknown error', 'ai-auto-poster' ),
            // Bug fix (audit r13): admin.js が AIAP_I18N 経由でなく直接英語フォールバック
            // していた箇所 (L838, L1125, L2227) を本キー経由に統一。
            'load_failed'               => __( 'Load failed', 'ai-auto-poster' ),
            'error_generic'             => __( 'Error', 'ai-auto-poster' ),
            'weather_enter_location'    => __( 'Enter a location name.', 'ai-auto-poster' ),
            'weather_lookup_running'    => __( 'Searching location...', 'ai-auto-poster' ),
            'weather_lookup_success'    => __( 'Coordinates were set. You can save now.', 'ai-auto-poster' ),
            'weather_lookup_not_found'  => __( 'Could not find the location. Enter the location again.', 'ai-auto-poster' ),
            // 別メニュー遷移後の復帰時に表示する「進行中」警告 (initRunNow のページロード時チェックで使用)
            /* translators: %d = seconds since the previous Run Now was started */
            'run_in_progress_resume'    => __( 'A previous Run Now is still in progress (started %d sec ago). Please wait for it to finish before retrying.', 'ai-auto-poster' ),
        ) );
    }

    // ========== 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();
        // 既存ユーザーで API キーを設定済みの場合はウィザード不要。
        // キー名は $text_provider_key_field の value と一致させる必要がある。
        foreach ( array( 'claude_api_key', 'openai_api_key', 'gemini_api_key' ) as $k ) {
            if ( ! empty( $s[ $k ] ) ) return false;
        }
        return true;
    }

    /**
     * ウィザード POST: タスク作成 + API キー保存 + プラグイン有効化
     */
    public static function handle_wizard_complete() {
        // check_admin_referer は内部で wp_unslash 経由の nonce 検証を行い、
        // 失敗時に -1 を die() する。他の admin ハンドラと挙動を統一する。
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html__( 'Insufficient permissions.', 'ai-auto-poster' ) );
        }
        check_admin_referer( self::NONCE );

        $prompt   = trim( wp_unslash( $_POST['wiz_prompt'] ?? '' ) );
        $freq     = sanitize_key( wp_unslash( $_POST['wiz_freq'] ?? 'daily' ) );
        $provider = sanitize_key( wp_unslash( $_POST['wiz_provider'] ?? 'claude' ) );
        $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 !== '' ) {
            // 暗号化基盤チェック (AJAX 側と同じパターン)。失敗時は wp_die で確実にユーザーに通知。
            // (既存の `err=prompt` リダイレクトは表示処理が無いため silent fail になる。
            // 暗号化失敗は気付かないと「保存できたつもりで空」になる重大な UX バグなので
            // ここでは silent fail させない。)
            $crypto_ok = AIAP_Crypto::ensure_available();
            if ( is_wp_error( $crypto_ok ) ) {
                wp_die(
                    esc_html( $crypto_ok->get_error_message() ),
                    esc_html__( 'API key cannot be saved', 'ai-auto-poster' ),
                    array( 'back_link' => true )
                );
            }
            $s = AIAP_AI_Client::get_settings();
            $key_field = AIAP_AI_Providers::get_key_field( $provider );
            if ( $key_field ) {
                $s[ $key_field ] = $apikey;
                $s['provider']   = $provider;
                // update_settings は内部で update_encrypted() を呼び、API キーを暗号化保存する。
                // 旧実装は update_option() を直接呼んでいたため平文保存になっていた。
                AIAP_AI_Client::update_settings( $s );
            }
        }

        // プラグイン有効化 + ウィザード完了フラグ
        $s = AIAP_AI_Client::get_settings();
        $s['enabled'] = 1;
        AIAP_AI_Client::update_settings( $s );
        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;
    }

    /**
     * 「ウィザードをスキップ」 → 完了フラグだけ立てて通常画面へ
     * nonce 検証で CSRF 経由でフラグが立つのを防ぐ。
     */
    public static function handle_wizard_skip() {
        if ( empty( $_GET['wizard_skip'] ) ) return;
        if ( ! current_user_can( 'manage_options' ) ) return;
        // nonce 検証 (失敗時は wp_die で停止)。アクション名は wp_nonce_url 側と一致させる。
        check_admin_referer( 'aiap_wizard_skip' );
        update_option( self::OPTION_WIZARD_DONE, 1, false );
    }

    /**
     * 「生成投稿一覧」テーブルの一括操作ハンドラ。
     *
     * WP_List_Table の bulk action は同じ画面に POST/GET で戻ってくるので、
     * render_logs() の冒頭で呼び出して処理する。現状は 'delete' のみサポート。
     * 削除は WordPress 標準の wp_delete_post( $id, true ) で完全削除する(ゴミ箱は経由しない)。
     */
    public 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 );
            $type = 'success';
        } else {
            // 削除0件 (既に削除済み・安全装置でスキップ等)。個別削除ハンドラと同様、
            // 件数に関わらず必ずリダイレクトして二重送信 (PRG) を防ぎ、結果を通知する。
            $msg  = __( 'No posts were deleted.', 'ai-auto-poster' );
            $type = 'error';
        }
        // 同じ画面にリダイレクトして二重送信を防ぐ + メッセージ表示
        $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' => $type,
            ),
            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;
    }



    // ================================================================
    // 通知・ハンドラ
    // ================================================================
    public static function verify() {
        if ( ! current_user_can( 'manage_options' ) ) {
            wp_die( esc_html( __( 'Unauthorized', 'ai-auto-poster' ) ) );
        }
        check_admin_referer( self::NONCE );
    }

    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';

    public 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' );
    }

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

    public 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 ) && AIAP_Compat::mb_strlen( $msg ) > 500 ) {
            $msg = AIAP_Compat::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;
    }

}
