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

/**
 * 画像プロバイダ呼び出し層: AI生成 (DALL-E/Imagen/Stability/Flux) + ストック (Unsplash/Pixabay/Pexels/Openverse)
 *
 * 元 includes/class-aiap-image-client.php から分離。
 * 共通基盤 (get_settings, resolve_imagen_model 等) は親クラス AIAP_Image_Client にあるため直接参照。
 * スタイル一覧は AIAP_Image_Styles を参照。
 */
class AIAP_Image_Providers {

    // ================================================================
    // Request payload logging helper
    // ================================================================

    /**
     * 画像 API への送信内容を AIAP_Logger に記録する共通ヘルパ。
     *
     * 対象は本クラス内の全 8 画像取得関数 (AI 生成 4 + ストック検索 4):
     *   call_dalle3 / call_imagen / call_stability / call_flux  (AI 生成)
     *   call_unsplash / call_pixabay / call_pexels / call_openverse (ストック検索)
     *
     * テキスト AI 側 (AIAP_AI_Client::generate) の AI 送信ログと同じ目的の正式ログ機能。
     * 外部 API に何を送ったかを後からデバッグできるようにする。
     *
     * 機密性 (claude.md L25): API キー / Authorization ヘッダはここに含めない。
     * (認証は AIAP_Http で HTTP ヘッダ経由のため、payload には API キーが入らない。
     *  ただし Unsplash 等で URL クエリに client_id を載せる経路があるため、それは
     *  $payload に入れず、各 call 側で API キーを除外した値を渡すこと。)
     *
     * @param string $provider プロバイダ識別子 (dalle3 / imagen / stability / flux /
     *                          unsplash / pixabay / pexels / openverse)
     * @param string $model    モデル名 (AI 生成系) または '' (ストック検索系)
     * @param string $payload  AI 生成系は prompt、ストック検索系は query
     * @param array  $params   その他のパラメータ (size / quality / orientation 等)
     */
    private static function log_request_payload( $provider, $model, $payload, $params = array() ) {
        if ( ! class_exists( 'AIAP_Logger' ) ) {
            return;
        }
        AIAP_Logger::add(
            'info',
            'log_image_request_payload',
            array(),
            array(
                'provider'   => $provider,
                'model'      => $model,
                'payload'    => $payload,
                'parameters' => $params,
            )
        );
    }

    // ================================================================
    // Retry helper
    // ================================================================

    /**
     * 画像 API 呼び出し用のリトライラッパー。
     *
     * テキスト AI 側の AIAP_AI_Client::generate_with_retry() と同じ仕様で動く:
     * - 最大 IMAGE_GENERATE_MAX_RETRY 回まで試行
     * - 失敗時、WP_Error の error_data を見てリトライ可否を判断
     *   - retryable=false (永続エラー: 課金枯渇/認証/入力不正) は即返却
     *   - 既存の is_retryable_http_code でも判定 (後方互換)
     * - retry_after があればそれを尊重 (上限 30 秒)、無ければ線形バックオフ (1, 2 秒)
     * - 各リトライ前に $on_retry($attempt, $err) を呼ぶ (ログ用)
     *
     * @param callable $fn        実 API 呼び出しを行う無引数関数。array(成功) or WP_Error(失敗) を返す
     * @param callable|null $on_retry function(int $attempt, WP_Error $err): void
     * @return array|WP_Error 最後の成功結果、または最終失敗の WP_Error
     */
    public static function call_with_retry( $fn, $on_retry = null ) {
        $result = null;
        // IMAGE_GENERATE_MAX_RETRY は「リトライ最大回数」。よって試行は最大 (N+1) 回(初回 + リトライ N 回)。
        for ( $attempt = 1; $attempt <= AIAP_Image_Client::IMAGE_GENERATE_MAX_RETRY + 1; $attempt++ ) {
            // Bug fix (audit r9 self-audit): 各 attempt の頭で実行時間制限をリセットする。
            // set_time_limit(0) は呼ぶたびにカウントを 0 に戻す効果がある。
            // 上位 (ajax_run_now_step / Scheduler::run) で既に呼んでいるが、共用ホストで
            // disable_functions により無視されていた場合や、PHP-FPM の request_terminate_timeout
            // が PHP の制限とは別軸で効いている場合の保険。各リトライの前に無制限へリセットすれば、
            // 少なくとも個々の API 呼び出しは max_execution_time 切れで殺されにくくなる。
            if ( function_exists( 'set_time_limit' ) ) {
                @set_time_limit( 0 );
            }

            $result = call_user_func( $fn );
            if ( ! is_wp_error( $result ) ) {
                return $result;
            }

            $err_data    = $result->get_error_data();
            $http_code   = is_array( $err_data ) && isset( $err_data['http_code'] ) ? (int) $err_data['http_code'] : 0;
            $retry_after = is_array( $err_data ) && isset( $err_data['retry_after'] ) ? (int) $err_data['retry_after'] : 0;

            // 永続エラー(retryable=false)は即返却。
            if ( is_array( $err_data ) && array_key_exists( 'retryable', $err_data ) && ! $err_data['retryable'] ) {
                return $result;
            }

            // retryable キーが無い経路の後方互換チェック。
            // http_code=0(HTTP 通信失敗)はリトライ可能扱い。
            if ( $http_code !== 0 && ! AIAP_AI_Errors::is_retryable_http_code( $http_code ) ) {
                return $result;
            }

            // 最後の試行はリトライ前処理(コールバック・sleep)をスキップ。
            if ( $attempt <= AIAP_Image_Client::IMAGE_GENERATE_MAX_RETRY ) {
                if ( is_callable( $on_retry ) ) {
                    call_user_func( $on_retry, $attempt, $result );
                }
                // 待機秒数は AI_Client::compute_retry_sleep_seconds() に集約 (#35)。
                sleep( AIAP_AI_Errors::compute_retry_sleep_seconds( $attempt, $retry_after ) );
            }
        }
        return $result;
    }


    // ================================================================
    // Format & capabilities helpers
    // ================================================================

    /**
     * 投稿者が選んだ image_format から「向き」(landscape/square/portrait) を返す。
     *
     * Bug fix (audit r7): モデルが希望サイズをサポートしていない場合の代替選定で使う。
     *
     * @param string $format '16:9'|'4:3'|'1:1'|'9:16'
     * @return string 'landscape'|'square'|'portrait'
     */
    private static function format_to_orientation( $format ) {
        switch ( $format ) {
            case '1:1':  return 'square';
            case '9:16': return 'portrait';
            case '16:9':
            case '4:3':
            default:     return 'landscape';
        }
    }

    /**
     * DALL-E / GPT-Image 用に、ユーザー選択の format に最も近いサポート済み size を返す。
     *
     * Bug fix (audit r7): 旧コードはユーザー指定 image_size がモデルのサポート外なら無条件で
     * sizes[0] にフォールバックしていた。GPT-Image-1 の sizes[0] は '1024x1024' (正方形) なので、
     * ユーザーが 16:9 を選んでも常に正方形画像が生成される問題があった。同じ「向き」のサイズが
     * モデルにあればそれを返し、無ければ正方形 → 何でも、の順で代替する。
     *
     * @param array  $caps   get_model_capabilities の戻り値
     * @param string $format 投稿者が選んだ format
     * @return string         'WxH' 形式の size 文字列
     */
    private static function pick_dalle_size_by_format( $caps, $format ) {
        if ( empty( $caps['sizes'] ) ) {
            return '1024x1024';
        }
        $orientation = self::format_to_orientation( $format );

        $by_orientation = array( 'landscape' => array(), 'square' => array(), 'portrait' => array() );
        foreach ( $caps['sizes'] as $sz ) {
            if ( $sz === 'auto' ) continue;
            if ( ! preg_match( '/^(\d+)x(\d+)$/', $sz, $m ) ) continue;
            $w = (int) $m[1]; $h = (int) $m[2];
            if ( $w > $h )       $by_orientation['landscape'][] = $sz;
            elseif ( $w < $h )   $by_orientation['portrait'][]  = $sz;
            else                 $by_orientation['square'][]    = $sz;
        }

        if ( ! empty( $by_orientation[ $orientation ] ) ) {
            return $by_orientation[ $orientation ][0];
        }
        if ( ! empty( $by_orientation['square'] ) ) {
            return $by_orientation['square'][0];
        }
        // どれにも該当しない (auto しか無い等) のときは API 既定に任せる
        foreach ( $caps['sizes'] as $sz ) {
            if ( $sz !== 'auto' ) return $sz;
        }
        return '1024x1024';
    }

    /**
     * プロバイダ×モデルごとの機能サポートマトリクス
     *
     * 各モデルがAPIで受け付けるパラメータを一元管理する。スタイル欄の表示/非表示、
     * size/quality の範囲、response_format のサポート可否などをここで判定する。
     *
     * @param string $provider プロバイダキー (dalle3, stability, imagen, flux)
     * @param string $model    モデルID (空なら設定から取得)
     * @return array {
     *     @type array  $styles          スタイル選択肢。空配列ならスタイル非対応モデル。
     *     @type array  $sizes           受け付ける size 値
     *     @type array  $quality_values  受け付ける quality 値
     *     @type bool   $response_format response_format パラメータを指定可能か
     *     @type string $family          API系統の識別子 (dall-e / gpt-image / その他)
     * }
     */
    public static function get_model_capabilities( $provider, $model = '' ) {
        if ( $model === '' ) {
            $settings = AIAP_Image_Client::get_settings();
            $model    = $settings[ $provider . '_model' ] ?? '';
        }

        if ( $provider === 'dalle3' ) {
            // GPT Image系: style / response_format を受け付けない、size・qualityが別レンジ
            if ( strpos( $model, 'gpt-image' ) === 0 ) {
                return array(
                    'styles'          => array(),
                    'sizes'           => array( '1024x1024', '1536x1024', '1024x1536', 'auto' ),
                    'quality_values'  => array( 'low', 'medium', 'high', 'auto' ),
                    'response_format' => false,
                    'family'          => 'gpt-image',
                );
            }
            // dall-e-3 (既定): style (natural/vivid) / response_format サポート
            return array(
                'styles'          => array(
                    'natural' => array( 'icon' => '🌿', 'label_key' => 'imgstyle_natural' ),
                    'vivid'   => array( 'icon' => '🎨', 'label_key' => 'imgstyle_vivid' ),
                ),
                'sizes'           => array( '1024x1024', '1792x1024', '1024x1792' ),
                'quality_values'  => array( 'standard', 'hd' ),
                'response_format' => true,
                'family'          => 'dall-e-3',
            );
        }

        if ( $provider === 'stability' ) {
            return array(
                'styles'          => AIAP_Image_Styles::get_stability_style_list(),
                'sizes'           => array(),
                'quality_values'  => array(),
                'response_format' => false,
                'family'          => 'stability',
            );
        }

        if ( $provider === 'imagen' || $provider === 'flux' ) {
            return array(
                'styles'          => AIAP_Image_Styles::get_generic_style_list(),
                'sizes'           => array(),
                'quality_values'  => array(),
                'response_format' => false,
                'family'          => $provider,
            );
        }

        return array(
            'styles'          => array(),
            'sizes'           => array(),
            'quality_values'  => array(),
            'response_format' => false,
            'family'          => $provider,
        );
    }


    // ================================================================
    // API calls (provider-specific)
    // ================================================================

    /**
     * DALL-E 3 (OpenAI)
     * APIキーは画像AIプロバイダ設定を使用する
     */
    public static function call_dalle3( $prompt, $settings ) {
        self::log_request_payload(
            'dalle3',
            ! empty( $settings['dalle3_model'] ) ? (string) $settings['dalle3_model'] : 'dall-e-3',
            $prompt,
            array(
                'image_size'   => isset( $settings['image_size'] )   ? $settings['image_size']   : '',
                'image_format' => isset( $settings['image_format'] ) ? $settings['image_format'] : '',
                'image_style'  => isset( $settings['image_style'] )  ? $settings['image_style']  : '',
            )
        );

        $api_key = $settings['image_openai_api_key'] ?? '';

        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', __( 'OpenAI API key is required for OpenAI image generation.', 'ai-auto-poster' ) );
        }

        $model = ! empty( $settings['dalle3_model'] ) ? $settings['dalle3_model'] : 'dall-e-3';
        $caps  = self::get_model_capabilities( 'dalle3', $model );
        $body  = array(
            'model'  => $model,
            'prompt' => $prompt,
            'n'      => 1,
        );

        // size: モデルごとにサポート範囲が違う
        // Bug fix (audit r7): 旧コードは未サポート時に sizes[0] にフォールバックしていた。
        // gpt-image-1 では sizes[0] = '1024x1024' (正方形) なので、ユーザーが 16:9 を選んでも
        // 強制的に正方形になっていた。pick_dalle_size_by_format で「同じ向き」の代替を選ぶ。
        $requested_size = isset( $settings['image_size'] ) ? $settings['image_size'] : '';
        $format         = isset( $settings['image_format'] ) ? $settings['image_format'] : '16:9';
        if ( ! empty( $caps['sizes'] ) && in_array( $requested_size, $caps['sizes'], true ) ) {
            $body['size'] = $requested_size;
        } else {
            $body['size'] = self::pick_dalle_size_by_format( $caps, $format );
        }

        // quality: モデルごとに許容値が違う
        if ( $caps['family'] === 'gpt-image' ) {
            $requested_quality = isset( $settings['dalle_quality'] ) ? $settings['dalle_quality'] : 'medium';
            $body['quality']   = in_array( $requested_quality, $caps['quality_values'], true ) ? $requested_quality : 'medium';
        } else {
            $requested_quality = isset( $settings['dalle_quality'] ) ? $settings['dalle_quality'] : 'standard';
            $body['quality']   = in_array( $requested_quality, $caps['quality_values'], true ) ? $requested_quality : 'standard';
        }

        // style: dall-e-3 のみサポート。GPT-Image系には絶対に送らない
        if ( ! empty( $caps['styles'] ) ) {
            $style_val = isset( $settings['dalle3_style'] ) ? $settings['dalle3_style'] : '';
            if ( isset( $caps['styles'][ $style_val ] ) ) {
                $body['style'] = $style_val;
            } else {
                // 保存値がサポート外なら既定のnaturalに
                $body['style'] = 'natural';
            }
        }

        // response_format: dall-e-3 のみ指定可能 (GPT-Image系は常にbase64)
        if ( ! empty( $caps['response_format'] ) ) {
            $body['response_format'] = 'url';
        }

        $result = AIAP_Http::post_json(
            'https://api.openai.com/v1/images/generations',
            $body,
            AIAP_Http::bearer( $api_key )
        );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $data = is_array( $result['json'] ) ? $result['json'] : array();
        if ( $result['code'] !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'dalle3_error', __( 'OpenAI image error:', 'ai-auto-poster' ), $result['response'] );
        }

        // レスポンス形式: dall-e-3 は url を返す、GPT-Image系は b64_json のみ
        if ( ! empty( $data['data'][0]['url'] ) ) {
            return array(
                'url'       => $data['data'][0]['url'],
                'source'    => 'dalle3',
                'extension' => 'png',
            );
        }
        if ( ! empty( $data['data'][0]['b64_json'] ) ) {
            return array(
                'base64'    => $data['data'][0]['b64_json'],
                'source'    => 'dalle3',
                'extension' => 'png',
            );
        }

        return new WP_Error( 'dalle3_no_data', __( 'OpenAI image API did not return image data.', 'ai-auto-poster' ) );
    }

    /**
     * Imagen (Google Gemini)
     * APIキーは画像AIプロバイダ設定を使用する
     */
    public static function call_imagen( $prompt, $settings ) {
        self::log_request_payload(
            'imagen',
            ! empty( $settings['imagen_model'] ) ? (string) $settings['imagen_model'] : AIAP_Image_Client::IMAGEN_FALLBACK_PRIORITY[0],
            $prompt,
            array(
                'image_size'   => isset( $settings['image_size'] )   ? $settings['image_size']   : '',
                'image_format' => isset( $settings['image_format'] ) ? $settings['image_format'] : '',
                'image_style'  => isset( $settings['image_style'] )  ? $settings['image_style']  : '',
            )
        );

        $api_key = $settings['image_gemini_api_key'] ?? '';

        if ( empty( $api_key ) ) {
            return new WP_Error( 'no_api_key', __( 'Gemini API key is required for Imagen.', 'ai-auto-poster' ) );
        }

        $saved_model = ! empty( $settings['imagen_model'] ) ? $settings['imagen_model'] : AIAP_Image_Client::IMAGEN_FALLBACK_PRIORITY[0];
        $resolved    = AIAP_Image_Client::resolve_imagen_model( $api_key, $saved_model );
        if ( is_wp_error( $resolved ) ) {
            return $resolved;
        }
        $model = $resolved;
        // 保存値と異なる場合のみ「フォールバック発動」をログに残す。保存値は書き換えない。
        if ( $model !== $saved_model && class_exists( 'AIAP_Logger' ) ) {
            AIAP_Logger::add( 'info', 'log_imagen_fallback', array( $saved_model, $model ) );
        }
        $url = 'https://generativelanguage.googleapis.com/v1beta/models/' . rawurlencode( $model )
             . ':predict?key=' . rawurlencode( $api_key );

        $aspect = isset( $settings['aspect_ratio'] ) ? $settings['aspect_ratio'] : '1:1';
        // Imagen 3 が受け付ける aspectRatio: "1:1", "3:4", "4:3", "9:16", "16:9"
        // プラグインの 4:3 もそのまま受け付ける
        $allowed_aspect = array( '1:1', '3:4', '4:3', '9:16', '16:9' );
        if ( ! in_array( $aspect, $allowed_aspect, true ) ) {
            $aspect = '1:1';
        }

        $result = AIAP_Http::post_json( $url, array(
            'instances' => array(
                array( 'prompt' => $prompt ),
            ),
            'parameters' => array(
                'sampleCount' => 1,
                'aspectRatio' => $aspect,
            ),
        ) );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $data = is_array( $result['json'] ) ? $result['json'] : array();
        if ( $result['code'] !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'imagen_error', __( 'Imagen error:', 'ai-auto-poster' ), $result['response'] );
        }

        if ( empty( $data['predictions'][0]['bytesBase64Encoded'] ) ) {
            return new WP_Error( 'imagen_no_data', __( 'Imagen did not return image data.', 'ai-auto-poster' ) );
        }

        return array(
            'base64'    => $data['predictions'][0]['bytesBase64Encoded'],
            'source'    => 'imagen',
            'extension' => 'png',
        );
    }

    /**
     * Stability AI
     */
    public static function call_stability( $prompt, $settings ) {
        self::log_request_payload(
            'stability',
            ! empty( $settings['stability_model'] ) ? (string) $settings['stability_model'] : 'sd3.5-medium',
            $prompt,
            array(
                'image_size'   => isset( $settings['image_size'] )   ? $settings['image_size']   : '',
                'image_format' => isset( $settings['image_format'] ) ? $settings['image_format'] : '',
                'image_style'  => isset( $settings['image_style'] )  ? $settings['image_style']  : '',
            )
        );

        if ( empty( $settings['stability_api_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'Stability AI API key is not set.', 'ai-auto-poster' ) );
        }

        $model = ! empty( $settings['stability_model'] ) ? $settings['stability_model'] : 'sd3.5-medium';

        $url = 'https://api.stability.ai/v2beta/stable-image/generate/sd3';

        $boundary = wp_generate_password( 24, false );
        $body  = '';
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"prompt\"\r\n\r\n";
        $body .= $prompt . "\r\n";
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"model\"\r\n\r\n";
        $body .= $model . "\r\n";
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"aspect_ratio\"\r\n\r\n";
        // Bug fix (audit r7): Stability AI v2beta SD3 がサポートする aspect_ratio は
        // "21:9, 16:9, 3:2, 5:4, 1:1, 4:5, 2:3, 9:16, 9:21" のみ。プラグインの '4:3' は
        // 非対応のため、近似横長として '3:2' に置換する (5:4 より 3:2 の方が映える)。
        $stability_aspect = isset( $settings['aspect_ratio'] ) ? $settings['aspect_ratio'] : '1:1';
        $stability_supported = array( '21:9', '16:9', '3:2', '5:4', '1:1', '4:5', '2:3', '9:16', '9:21' );
        if ( ! in_array( $stability_aspect, $stability_supported, true ) ) {
            // '4:3' 等の代替: 一旦 '3:2' に丸める (横長近似)。それ以外の未知値は '1:1' に。
            $stability_aspect = ( $stability_aspect === '4:3' ) ? '3:2' : '1:1';
        }
        $body .= $stability_aspect . "\r\n";
        $body .= "--{$boundary}\r\n";
        $body .= "Content-Disposition: form-data; name=\"output_format\"\r\n\r\n";
        $body .= "png\r\n";
        $body .= "--{$boundary}--\r\n";

        $response = wp_remote_post( $url, array(
            'timeout' => 120,
            'headers' => array(
                'Authorization' => 'Bearer ' . $settings['stability_api_key'],
                'Content-Type'  => 'multipart/form-data; boundary=' . $boundary,
                'Accept'        => 'application/json',
            ),
            'body' => $body,
        ) );

        if ( is_wp_error( $response ) ) {
            return $response;
        }

        $code = wp_remote_retrieve_response_code( $response );
        $data = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( ! is_array( $data ) ) { $data = array(); }

        if ( $code !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'stability_error', __( 'Stability AI error:', 'ai-auto-poster' ), $response );
        }

        if ( empty( $data['image'] ) ) {
            return new WP_Error( 'stability_no_data', __( 'Stability AI did not return image data.', 'ai-auto-poster' ) );
        }

        return array(
            'base64'    => $data['image'],
            'source'    => 'stability',
            'extension' => 'png',
        );
    }

    // ========== フリー素材 ==========

    /**
     * Unsplash
     */
    public static function call_unsplash( $query, $settings ) {
        self::log_request_payload(
            'unsplash',
            '',
            $query,
            array(
                'orientation' => isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : 'landscape',
            )
        );

        if ( empty( $settings['unsplash_access_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'Unsplash access key is not set.', 'ai-auto-poster' ) );
        }

        $url = add_query_arg( array(
            'query'    => $query,
            'per_page' => 1,
            'orientation' => isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : 'landscape',
            'client_id'   => $settings['unsplash_access_key'],
        ), 'https://api.unsplash.com/search/photos' );

        $result = AIAP_Http::get( $url, array(), 30 );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $data = is_array( $result['json'] ) ? $result['json'] : array();
        if ( $result['code'] !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'unsplash_error', __( 'Unsplash error:', 'ai-auto-poster' ), $result['response'] );
        }

        if ( empty( $data['results'][0]['urls']['regular'] ) ) {
            /* translators: %s = search query */
            return new WP_Error( 'unsplash_no_result', sprintf( __( 'No image found on Unsplash: %s', 'ai-auto-poster' ), $query ) );
        }

        // Unsplash の guideline に従い「ダウンロード追跡」を呼ぶ。
        // download_location には既に ?ixid=... が付与されているため
        // 文字列結合ではなく add_query_arg で client_id を追加する。
        if ( ! empty( $data['results'][0]['links']['download_location'] ) ) {
            $track_url = add_query_arg(
                array( 'client_id' => $settings['unsplash_access_key'] ),
                $data['results'][0]['links']['download_location']
            );
            AIAP_Http::get( $track_url, array(), 5 );
        }

        return array(
            'url'       => $data['results'][0]['urls']['regular'],
            'source'    => 'unsplash',
            'extension' => 'jpg',
            'credit'    => isset( $data['results'][0]['user']['name'] ) ? $data['results'][0]['user']['name'] : '',
        );
    }

    /**
     * Pixabay
     */
    public static function call_pixabay( $query, $settings ) {
        self::log_request_payload(
            'pixabay',
            '',
            $query,
            array(
                'orientation' => isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : 'landscape',
            )
        );

        if ( empty( $settings['pixabay_api_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'Pixabay API key is not set.', 'ai-auto-poster' ) );
        }

        $url = add_query_arg( array(
            'key'       => $settings['pixabay_api_key'],
            'q'         => $query,
            'per_page'  => 3,
            'image_type' => 'photo',
            'orientation' => ( isset( $settings['stock_orientation'] ) && $settings['stock_orientation'] === 'portrait' ) ? 'vertical' : 'horizontal',
            'safesearch' => 'true',
        ), 'https://pixabay.com/api/' );

        $result = AIAP_Http::get( $url, array(), 30 );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $data = is_array( $result['json'] ) ? $result['json'] : array();
        if ( $result['code'] !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'pixabay_error', __( 'Pixabay error:', 'ai-auto-poster' ), $result['response'] );
        }

        if ( empty( $data['hits'][0]['largeImageURL'] ) ) {
            /* translators: %s = search query */
            return new WP_Error( 'pixabay_no_result', sprintf( __( 'No image found on Pixabay: %s', 'ai-auto-poster' ), $query ) );
        }

        return array(
            'url'       => $data['hits'][0]['largeImageURL'],
            'source'    => 'pixabay',
            'extension' => 'jpg',
            'credit'    => isset( $data['hits'][0]['user'] ) ? $data['hits'][0]['user'] : '',
        );
    }

    /**
     * Pexels
     */
    public static function call_pexels( $query, $settings ) {
        self::log_request_payload(
            'pexels',
            '',
            $query,
            array(
                'orientation' => isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : 'landscape',
            )
        );

        if ( empty( $settings['pexels_api_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'Pexels API key is not set.', 'ai-auto-poster' ) );
        }

        $url = add_query_arg( array(
            'query'    => $query,
            'per_page' => 1,
            'orientation' => ( isset( $settings['stock_orientation'] ) && $settings['stock_orientation'] === 'squarish' ) ? 'square' : ( $settings['stock_orientation'] ?? 'landscape' ),
        ), 'https://api.pexels.com/v1/search' );

        $result = AIAP_Http::get( $url, array(
            'Authorization' => $settings['pexels_api_key'],
        ), 30 );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $data = is_array( $result['json'] ) ? $result['json'] : array();
        if ( $result['code'] !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'pexels_error', __( 'Pexels error:', 'ai-auto-poster' ), $result['response'] );
        }

        if ( empty( $data['photos'][0]['src']['large'] ) ) {
            /* translators: %s = search query */
            return new WP_Error( 'pexels_no_result', sprintf( __( 'No image found on Pexels: %s', 'ai-auto-poster' ), $query ) );
        }

        return array(
            'url'       => $data['photos'][0]['src']['large'],
            'source'    => 'pexels',
            'extension' => 'jpg',
            'credit'    => isset( $data['photos'][0]['photographer'] ) ? $data['photos'][0]['photographer'] : '',
        );
    }


    // ========== Flux (Fal.ai) ==========

    /**
     * Flux via Fal.ai API
     */
    public static function call_flux( $prompt, $settings ) {
        self::log_request_payload(
            'flux',
            ! empty( $settings['flux_model'] ) ? (string) $settings['flux_model'] : 'flux-2-pro',
            $prompt,
            array(
                'image_size'   => isset( $settings['flux_image_size'] ) ? $settings['flux_image_size'] : ( $settings['image_size'] ?? '' ),
                'image_format' => isset( $settings['image_format'] )    ? $settings['image_format']    : '',
                'image_style'  => isset( $settings['image_style'] )     ? $settings['image_style']     : '',
            )
        );

        if ( empty( $settings['flux_api_key'] ) ) {
            return new WP_Error( 'no_api_key', __( 'Fal.ai API key is not set', 'ai-auto-poster' ) );
        }

        $model = ! empty( $settings['flux_model'] ) ? $settings['flux_model'] : 'flux-2-pro';
        $size  = isset( $settings['flux_image_size'] ) ? $settings['flux_image_size'] : ( $settings['image_size'] ?? '1024x1024' );
        $parts = explode( 'x', $size );
        $width  = isset( $parts[0] ) ? (int) $parts[0] : 1024;
        $height = isset( $parts[1] ) ? (int) $parts[1] : 1024;

        $url = 'https://fal.run/fal-ai/' . rawurlencode( $model );

        // Bug fix (audit r9 spec audit): flux-2-pro は Fal.ai 公式ドキュメント上「zero-configuration」
        // と明記されており、num_inference_steps を受け付けるパラメータに含めていない。現状は
        // 未知パラメータとして無視されるが、将来 strict validation が入ると 400 で弾かれる。
        // flux-2-flex / flux-1.1-pro / flux-2-dev / flux-2-max / flux-1-* は num_inference_steps を
        // 仕様としてサポートしているため引き続き送信する。
        $body = array(
            'prompt'     => $prompt,
            'image_size' => array( 'width' => $width, 'height' => $height ),
            'num_images' => 1,
        );
        if ( strpos( $model, 'flux-2-pro' ) !== 0 ) {
            $body['num_inference_steps'] = isset( $settings['flux_steps'] ) ? (int) $settings['flux_steps'] : 25;
        }

        $result = AIAP_Http::post_json( $url, $body, array(
            'Authorization' => 'Key ' . $settings['flux_api_key'],
        ) );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $data = is_array( $result['json'] ) ? $result['json'] : array();
        if ( $result['code'] !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'flux_error', __( 'Flux API error:', 'ai-auto-poster' ), $result['response'] );
        }

        if ( empty( $data['images'][0]['url'] ) ) {
            return new WP_Error( 'flux_no_url', __( 'No image URL returned from Flux', 'ai-auto-poster' ) );
        }

        return array(
            'url'       => $data['images'][0]['url'],
            'source'    => 'flux',
            'extension' => 'png',
        );
    }

    // ========== Openverse ==========

    /**
     * Openverse (WordPress.org公式) - APIキー不要
     */
    public static function call_openverse( $query, $settings = array() ) {
        self::log_request_payload(
            'openverse',
            '',
            $query,
            array(
                'orientation' => isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : 'landscape',
            )
        );

        // Bug fix (audit r9): Openverse は aspect_ratio パラメータ (tall/wide/square) を
        // サポートしているが旧実装では未指定だった。ユーザーが image_format=16:9 を選んでも
        // Openverse からは縦長や正方形が返ってしまう問題を修正。
        $stock_or = isset( $settings['stock_orientation'] ) ? (string) $settings['stock_orientation'] : 'landscape';
        $aspect_map = array(
            'landscape' => 'wide',
            'portrait'  => 'tall',
            'squarish'  => 'square',
        );
        $aspect_ratio = isset( $aspect_map[ $stock_or ] ) ? $aspect_map[ $stock_or ] : 'wide';

        $url = add_query_arg( array(
            'q'            => $query,
            'page_size'    => 5,
            'license'      => 'by,by-sa,cc0',
            'mature'       => 'false',
            'aspect_ratio' => $aspect_ratio,
        ), 'https://api.openverse.org/v1/images/' );

        $result = AIAP_Http::get( $url, array(
            'User-Agent' => 'AI Auto Poster WordPress Plugin',
        ), 30 );

        if ( is_wp_error( $result ) ) {
            return $result;
        }

        $data = is_array( $result['json'] ) ? $result['json'] : array();
        if ( $result['code'] !== 200 ) {
            return AIAP_AI_Errors::build_api_error_wp_error( 'openverse_error', __( 'Openverse API error:', 'ai-auto-poster' ), $result['response'] );
        }

        if ( empty( $data['results'][0]['url'] ) ) {
            /* translators: %s = search query */
            return new WP_Error( 'openverse_no_result', sprintf( __( 'No image found on Openverse: %s', 'ai-auto-poster' ), $query ) );
        }

        $img = $data['results'][0];
        $ext = 'jpg';
        if ( preg_match( '/\.(png|webp|jpeg|jpg)$/i', $img['url'], $m ) ) {
            $ext = strtolower( $m[1] ) === 'jpeg' ? 'jpg' : strtolower( $m[1] );
        }

        return array(
            'url'       => $img['url'],
            'source'    => 'openverse',
            'extension' => $ext,
            'credit'    => isset( $img['creator'] ) ? $img['creator'] : '',
            'license'   => isset( $img['license'] ) ? $img['license'] : '',
        );
    }

    // ========== WordPress メディア登録 ==========

}
