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

/**
 * 管理画面の AJAX エンドポイント (モデル取得・プロバイダ接続テスト)。
 *
 * モデル一覧の更新、スタイル選択肢の取得、自動投稿の有効切替、各 AI/画像
 * プロバイダの API キー接続テストを担う AJAX ハンドラ群 (Phase 3 ステップ5)。
 *
 * nonce 検証は共有ヘルパ AIAP_Admin::ajax_verify() を、プロバイダ→キー field の
 * 対応表のうち画像系は本クラス内の静的プロパティを、テキスト系は
 * AIAP_Admin::$text_provider_key_field を参照する。フック登録は AIAP_Admin::init()。
 */
class AIAP_Admin_Ajax {

    private static function get_provider_key_field( $kind, $provider ) {
        if ( $kind === 'image' ) {
            return class_exists( 'AIAP_Image_Client' ) ? AIAP_Image_Client::get_key_field( $provider ) : '';
        }
        return AIAP_AI_Providers::get_key_field( $provider );
    }

    private static function get_saved_provider_key( $kind, $provider ) {
        $key_field = self::get_provider_key_field( $kind, $provider );
        if ( $key_field === '' ) {
            return '';
        }
        $settings = $kind === 'image' ? AIAP_Image_Client::get_settings() : AIAP_AI_Client::get_settings();
        return isset( $settings[ $key_field ] ) ? (string) $settings[ $key_field ] : '';
    }

    private static function save_tested_provider_credentials( $kind, $provider, $api_key, $model, $style, $use_provider ) {
        $key_field = self::get_provider_key_field( $kind, $provider );
        if ( $key_field === '' ) {
            return false;
        }

        if ( $kind === 'text' ) {
            $settings = AIAP_AI_Client::get_settings();
            $settings[ $key_field ] = $api_key;
            $model_field = AIAP_AI_Providers::get_model_field( $provider );
            if ( $model !== '' && $model_field !== '' && isset( $settings[ $model_field ] ) ) {
                $settings[ $model_field ] = $model;
            }
            if ( $use_provider && in_array( $provider, AIAP_AI_Providers::get_providers(), true ) ) {
                $settings['provider'] = $provider;
            }
            AIAP_AI_Client::update_settings( $settings );
            return true;
        }

        $settings = AIAP_Image_Client::get_settings();
        $settings[ $key_field ] = $api_key;
        if ( $model !== '' && isset( $settings[ $provider . '_model' ] ) ) {
            $settings[ $provider . '_model' ] = $model;
        }
        if ( $style !== '' ) {
            $style_field = $provider . '_style';
            $current_model = $model !== '' ? $model : ( $settings[ $provider . '_model' ] ?? '' );
            $allowed_styles = array_keys( AIAP_Image_Styles::get_provider_styles( $provider, $current_model ) );
            if ( in_array( $style, $allowed_styles, true ) ) {
                $settings[ $style_field ] = $style;
            }
        }
        if ( $use_provider && in_array( $provider, array_keys( AIAP_Image_Client::get_sources() ), true ) ) {
            $settings['image_source'] = $provider;
        }
        AIAP_Image_Client::update_settings( $settings );
        return true;
    }

    // 画像 AI プロバイダ → APIキー保存先 field 名は AIAP_Admin::$image_provider_key_field に集約。


    public static function ajax_weather_geocode() {
        AIAP_Admin::ajax_verify();

        $location = sanitize_text_field( wp_unslash( $_POST['location'] ?? '' ) );
        if ( ! class_exists( 'AIAP_Weather_Client' ) ) {
            wp_send_json_error( array( 'message' => __( 'Weather function is not available.', 'ai-auto-poster' ) ), 500 );
        }

        $result = AIAP_Weather_Client::geocode_location( $location );
        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
        }

        wp_send_json_success( $result );
    }

    public static function ajax_refresh_models() {
        AIAP_Admin::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_Providers::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_Providers::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 ] ) ) {
            // Bug fix: 旧実装は array_key_first($models) に切り替えていたが、
            // モデル一覧の先頭は API 応答順 / アルファベット順依存で「推奨」とは限らず、
            // 例えば Anthropic で `claude-haiku-*` が先頭に来てユーザーが意図せず軽量モデルに
            // 切り替えられる事故があり得た。Registry の `default` (= プラグイン作者が
            // リリース時に「推奨」と決めたモデル) が新リストに存在すればそれを優先し、
            // 無ければ array_key_first にフォールバックする。
            $registry_default = $is_image
                ? ( class_exists( 'AIAP_Model_Registry' ) ? AIAP_Model_Registry::default_image_model( $provider ) : '' )
                : ( class_exists( 'AIAP_Model_Registry' ) ? AIAP_Model_Registry::default_text_model( $provider ) : '' );
            if ( $registry_default !== '' && isset( $models[ $registry_default ] ) ) {
                $new_model = $registry_default;
            } else {
                $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_Styles::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 );
            }
        }

        // 保存時に即再チェック: 切替後の保存中モデルを今取得したモデル一覧と突き合わせ、
        // discontinued ステータスを即時更新する。これをしないと週次 cron 実行まで
        // 古い警告 notice が残り続ける (handle_save_provider と対称)。
        if ( class_exists( 'AIAP_Model_Sync' ) ) {
            $key_field = $is_image
                ? ( class_exists( 'AIAP_Image_Client' ) ? AIAP_Image_Client::get_key_field( $provider ) : '' )
                : AIAP_AI_Providers::get_key_field( $provider );
            $api_key = $key_field !== '' ? (string) ( $settings[ $key_field ] ?? '' ) : '';
            $model   = (string) ( $settings[ $model_field ] ?? '' );
            AIAP_Model_Sync::refresh_status_for( $provider, $api_key, $model );
        }

        // 画像プロバイダの場合は、新しいモデルに対するスタイル一覧も返す
        // (JSがモデル変更時にスタイルselectを動的更新するために使用)
        $styles_payload = null;
        if ( $is_image ) {
            $current_model = $settings[ $model_field ] ?? '';
            $style_list = AIAP_Image_Styles::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'] ) ? AIAP_Admin_View::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() {
        AIAP_Admin::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_Styles::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() {
        AIAP_Admin::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'   => AIAP_Admin_View::resolve_dynamic_key( $on ? 'enabled' : 'disabled' ),
            'msg'     => AIAP_Admin_View::resolve_dynamic_key( $on ? 'auto_posting_turned_on' : 'auto_posting_turned_off' ),
        ) );
    }

    public static function ajax_test_provider() {
        AIAP_Admin::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;

        // 入力欄が空ならDB保存済みのキーを使う(used_saved_key で UI に通知)
        // (新方式 value="" では '*' のマスク値が POST されないため、空チェックだけで足りる)
        $used_saved_key = false;
        if ( empty( $api_key ) ) {
            $api_key = self::get_saved_provider_key( $kind, $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 !== '' ) {
            // 暗号化基盤が利用可能か事前確認。encrypt_keys() は失敗時に空文字に置換する
            // 安全側の挙動だが、呼び出し側で気付かないと「成功表示なのに DB は空」になる。
            $crypto_ok = AIAP_Crypto::ensure_available();
            if ( is_wp_error( $crypto_ok ) ) {
                wp_send_json_error( array( 'message' => $crypto_ok->get_error_message() ), 500 );
            }
            $key_saved = self::save_tested_provider_credentials( $kind, $provider, $api_key, $model, $style, $use_provider );

            // テスト完了フラグを記録
            if ( $key_saved ) {
                $verified = AIAP_Settings::verified_providers();
                $verified[ $provider ] = time();
                AIAP_Settings::set_verified_providers( $verified );
            }
        }

        // 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' ) {
            $r = AIAP_Http::get( 'https://api.anthropic.com/v1/models', array(
                'x-api-key'         => $api_key,
                'anthropic-version' => '2023-06-01',
            ) );
            return self::api_test_result( $r, array( 200 ) );
        }

        if ( $provider === 'gemini' ) {
            $url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' . rawurlencode( $api_key );
            $r = AIAP_Http::get( $url );
            return self::api_test_result( $r, array( 200 ) );
        }

        // OpenAI 互換系 (/v1/models で一覧取得可能なもの)
        if ( $provider === 'openai' ) {
            $models_url = 'https://api.openai.com/v1/models';
        } else {
            $endpoints = AIAP_AI_Providers::get_openai_compatible_endpoints();
            $models_url = isset( $endpoints[ $provider ]['models_url'] ) ? $endpoints[ $provider ]['models_url'] : '';
        }
        if ( $models_url === '' ) {
            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';
        }

        $r = AIAP_Http::get( $models_url, $headers );
        return self::api_test_result( $r, array( 200 ) );
    }

    private static function test_image_provider( $provider, $api_key, $model ) {
        switch ( $provider ) {
            case 'dalle3':
                // OpenAI /v1/models でキーの有効性を確認
                // 加えて、選択モデルが結果に含まれているかを軽く確認する(廃止モデル検出)
                $r = AIAP_Http::get( 'https://api.openai.com/v1/models', AIAP_Http::bearer( $api_key ) );
                $key_check = self::api_test_result( $r, array( 200 ) );
                if ( is_wp_error( $key_check ) ) {
                    return $key_check;
                }
                return self::verify_image_model_available( $r, $model, 'openai' );
            case 'imagen':
                // Gemini /v1beta/models でキーの有効性を確認
                // 加えて、選択モデルが結果に含まれているかを軽く確認する(廃止モデル検出)
                $r = AIAP_Http::get( 'https://generativelanguage.googleapis.com/v1beta/models?key=' . rawurlencode( $api_key ) );
                $key_check = self::api_test_result( $r, array( 200 ) );
                if ( is_wp_error( $key_check ) ) {
                    return $key_check;
                }
                return self::verify_image_model_available( $r, $model, 'gemini' );
            case 'stability':
                $r = AIAP_Http::get( 'https://api.stability.ai/v1/user/account', AIAP_Http::bearer( $api_key ) );
                return self::api_test_result( $r, array( 200 ) );
            case 'unsplash':
                $r = AIAP_Http::get( add_query_arg( array( 'query' => 'test', 'per_page' => 1, 'client_id' => $api_key ), 'https://api.unsplash.com/search/photos' ) );
                return self::api_test_result( $r, array( 200 ) );
            case 'pixabay':
                $r = AIAP_Http::get( add_query_arg( array( 'key' => $api_key, 'q' => 'test', 'per_page' => 3 ), 'https://pixabay.com/api/' ) );
                return self::api_test_result( $r, array( 200 ) );
            case 'pexels':
                $r = AIAP_Http::get( add_query_arg( array( 'query' => 'test', 'per_page' => 1 ), 'https://api.pexels.com/v1/search' ), array(
                    'Authorization' => $api_key,
                ) );
                return self::api_test_result( $r, array( 200 ) );
            case 'flux':
                $r = AIAP_Http::get( 'https://fal.ai/api/keys/current', array(
                    'Authorization' => 'Key ' . $api_key,
                ) );
                return self::api_test_result( $r, 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( $r, $model, $shape ) {
        // モデル未指定はスキップ(キー有効性のみで OK)
        if ( ! is_string( $model ) || $model === '' ) {
            return true;
        }
        $data = is_array( $r['json'] ?? null ) ? $r['json'] : array();
        if ( empty( $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( $r, $success_codes ) {
        if ( is_wp_error( $r ) ) {
            return $r;
        }
        $code = (int) $r['code'];
        if ( in_array( $code, $success_codes, true ) ) {
            return true;
        }
        $body = $r['body'];
        $data = $r['json'];
        // 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_Errors::is_quota_exhausted_error( $msg );
            $classified         = AIAP_AI_Errors::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
            /* translators: 1: HTTP status code, 2: short hint, 3: raw error message */
            ? sprintf( __( 'HTTP %1$d: %2$s - %3$s', 'ai-auto-poster' ), $code, $hint, $msg )
            /* translators: 1: HTTP status code, 2: raw error message */
            : sprintf( __( 'HTTP %1$d: %2$s', 'ai-auto-poster' ), $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,
        ) );
    }

    /**
     * 投稿完了通知のテスト送信 (= 設定画面の「送信テスト」ボタンから呼ばれる)。
     *
     * 保存済みの設定値 (通知先メアド + SMTP) を使ってテストメールを送信する。
     * SMTP プラグインが検出されている場合はそちらに委譲され、未検出かつ
     * 独自 SMTP 設定がある場合は AIAP_Notifier::configure_phpmailer が PHPMailer を上書きする。
     */
    public static function ajax_notify_test() {
        AIAP_Admin::ajax_verify();

        if ( ! class_exists( 'AIAP_Notifier' ) ) {
            wp_send_json_error( array( 'message' => __( 'Notifier is not available.', 'ai-auto-poster' ) ) );
        }

        // フォーム値で検証 (= 保存されていない、画面で入力中の値をそのまま使う)
        $normalized = AIAP_Notifier::normalize_input( $_POST );
        if ( is_wp_error( $normalized ) ) {
            wp_send_json_error( array( 'message' => $normalized->get_error_message() ) );
        }

        // パスワード処理:
        // - フォームに値が入っている (= ユーザーが見える形で確認しながらテスト) → そのまま平文で使う
        // - フォームが空 (= ユーザーが何も触っていない) → DB の暗号化値を復号して使う
        // どちらも runtime override の smtp_password には「平文」をセットする。configure_phpmailer 側で
        // is_encrypted 判定により「暗号化済みなら復号、平文ならそのまま」と振り分けるので二重復号は起きない。
        $raw_pw = $normalized['smtp_password_input'];
        unset( $normalized['smtp_password_input'] );
        if ( $raw_pw === '' ) {
            $stored = get_option( AIAP_Notifier::OPTION_KEY, array() );
            $encrypted = isset( $stored['smtp_password'] ) ? (string) $stored['smtp_password'] : '';
            if ( $encrypted !== '' && class_exists( 'AIAP_Crypto' ) && AIAP_Crypto::is_encrypted( $encrypted ) ) {
                $decrypted = AIAP_Crypto::decrypt( $encrypted );
                $normalized['smtp_password'] = is_string( $decrypted ) ? $decrypted : '';
            } else {
                $normalized['smtp_password'] = $encrypted;
            }
        } else {
            $normalized['smtp_password'] = $raw_pw;
        }

        // 一時的に runtime override をセット → テスト送信 → 必ずクリア
        AIAP_Notifier::set_runtime_override( $normalized );
        try {
            $result = AIAP_Notifier::send_test_email();
        } finally {
            AIAP_Notifier::clear_runtime_override();
        }

        if ( is_wp_error( $result ) ) {
            wp_send_json_error( array( 'message' => $result->get_error_message() ) );
        }

        /* translators: %s: recipient email address */
        $msg = sprintf( __( 'Test email sent to %s.', 'ai-auto-poster' ), (string) $result );
        wp_send_json_success( array( 'message' => $msg, 'recipient' => (string) $result ) );
    }
}
