<?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 {

    // 画像 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 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 ] ) ) {
            $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 );
            }
        }

        // 画像プロバイダの場合は、新しいモデルに対するスタイル一覧も返す
        // (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;

        // '*' のみ(マスク表示)は空扱いにする
        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( AIAP_Admin::$text_provider_key_field[ $provider ] ) ) {
                    $api_key = $ai[ AIAP_Admin::$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( AIAP_Admin::$text_provider_key_field[ $provider ] ) ) {
                $s = AIAP_AI_Client::get_settings();
                $s[ AIAP_Admin::$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_Providers::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_Styles::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 = 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 で一覧取得可能なもの)
        $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';
        }

        $r = AIAP_Http::get( $models_endpoints[ $provider ], $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,
        ) );
    }
}
