<?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 等)

     */

    /**

     * AI 生成系プロバイダ (dalle3 / imagen / stability / flux) のリクエストログ

     *

     * 旧実装は 4 箇所で `image_size / image_format / image_style` の isset 抽出を

     * 同形にコピペしていた。ここに集約 (CLAUDE.md 重複排除)。

     */

    private static function log_ai_image_request( $provider, $model, $prompt, $settings ) {

        // flux のみ画像サイズキーが flux_image_size であり、グローバル image_size をフォールバック先にする

        $size_key = ( $provider === 'flux' && isset( $settings['flux_image_size'] ) ) ? 'flux_image_size' : 'image_size';

        // Bug fix: 旧実装は廃止された 'image_style' グローバルキーを参照しており、

        // 結果としてログの image_style カラムが常に空になっていた。プロバイダ別の

        // *_style キー (dalle3_style / imagen_style / stability_style / flux_style) を

        // 動的解決し、該当キーが無い場合のみ空文字を出す。

        $style_key   = $provider . '_style';

        $image_style = isset( $settings[ $style_key ] ) ? (string) $settings[ $style_key ] : '';

        self::log_request_payload(

            $provider,

            $model,

            $prompt,

            array(

                'image_size'   => isset( $settings[ $size_key ] )    ? $settings[ $size_key ]    : ( $settings['image_size'] ?? '' ),

                'image_format' => isset( $settings['image_format'] ) ? $settings['image_format'] : '',

                'image_style'  => $image_style,

            )

        );

    }



    /**

     * ストック検索系プロバイダ (unsplash / pixabay / pexels / openverse) のリクエストログ

     */

    private static function log_stock_request( $provider, $query, $settings ) {

        self::log_request_payload(

            $provider,

            '',

            $query,

            array(

                'orientation' => isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : 'landscape',

            )

        );

    }



    private static function log_request_payload( $provider, $model, $payload, $params = array() ) {

        if ( ! class_exists( 'AIAP_Logger' ) ) {

            return;

        }

        // 機密性 (CLAUDE.md L25): payload (= prompt / query) は全文保存せず

        // 長さ + 先頭プレビュー (200 文字) のみとする。誤って機密情報が含まれた

        // 場合の被害を限定する。デバッグには十分な情報量。

        $payload_str = is_string( $payload ) ? $payload : (string) $payload;

        AIAP_Logger::add(

            'info',

            'log_image_request_payload',

            array(),

            array(

                'provider'     => $provider,

                'model'        => $model,

                'payload_len'  => AIAP_Compat::mb_strlen( $payload_str ),

                'payload_head' => AIAP_Compat::mb_substr( $payload_str, 0, 200 ),

                'parameters'   => $params,

            )

        );

    }



    /**

     * 画像 orientation 値をプロバイダ仕様にマップする

     *

     * 統一されていない 3 プロバイダの仕様を 1 箇所に集約 (CLAUDE.md「共通化」)。

     * 旧実装は Pixabay が squarish を horizontal に強制し、Pexels は landscape のまま

     * 通すなど挙動が非整合だった。

     *

     * @param string $orientation WP 内部値 ('landscape' / 'portrait' / 'squarish')

     * @param string $target      'unsplash' / 'pixabay' / 'pexels'

     * @return string プロバイダ別の orientation 値 (空文字なら指定省略を意味する)

     */

    private static function map_orientation( $orientation, $target ) {

        $orientation = ( $orientation === '' || $orientation === null ) ? 'landscape' : (string) $orientation;

        switch ( $target ) {

            case 'pixabay':

                // Pixabay API は orientation=all|horizontal|vertical の 3 値。square は API 非対応のため all。

                if ( $orientation === 'portrait' ) { return 'vertical'; }

                if ( $orientation === 'landscape' ) { return 'horizontal'; }

                return 'all'; // squarish や未知値

            case 'pexels':

                // Pexels API は landscape|portrait|square

                if ( $orientation === 'squarish' ) { return 'square'; }

                if ( in_array( $orientation, array( 'landscape', 'portrait' ), true ) ) {

                    return $orientation;

                }

                return 'landscape';

            case 'unsplash':

            default:

                // Unsplash API は landscape|portrait|squarish をそのまま受ける

                return in_array( $orientation, array( 'landscape', 'portrait', 'squarish' ), true ) ? $orientation : 'landscape';

        }

    }



    // ================================================================

    // 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_ai_image_request(

            'dalle3',

            ! empty( $settings['dalle3_model'] ) ? (string) $settings['dalle3_model'] : 'dall-e-3',

            $prompt,

            $settings

        );



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



        if ( empty( $api_key ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'OpenAI' );

        }



        $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', '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_ai_image_request(

            'imagen',

            ! empty( $settings['imagen_model'] ) ? (string) $settings['imagen_model'] : AIAP_Image_Client::IMAGEN_FALLBACK_PRIORITY[0],

            $prompt,

            $settings

        );



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



        if ( empty( $api_key ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'Gemini' );

        }



        $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', '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_ai_image_request(

            'stability',

            ! empty( $settings['stability_model'] ) ? (string) $settings['stability_model'] : 'sd3.5-medium',

            $prompt,

            $settings

        );



        if ( empty( $settings['stability_api_key'] ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'Stability AI' );

        }



        $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";



        // 他プロバイダと統一して AIAP_Http 経由で呼ぶ。multipart/form-data なので post_raw を使う。

        $result = AIAP_Http::post_raw(

            $url,

            $body,

            array(

                'Authorization' => 'Bearer ' . $settings['stability_api_key'],

                'Content-Type'  => 'multipart/form-data; boundary=' . $boundary,

                'Accept'        => 'application/json',

            ),

            120

        );



        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( 'stability_error', __( 'Stability AI', 'ai-auto-poster' ), $result['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_stock_request( 'unsplash', $query, $settings );



        if ( empty( $settings['unsplash_access_key'] ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'Unsplash' );

        }



        $url = add_query_arg( array(

            'query'    => $query,

            'per_page' => 1,

            'orientation' => self::map_orientation( isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : '', 'unsplash' ),

            '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', '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_stock_request( 'pixabay', $query, $settings );



        if ( empty( $settings['pixabay_api_key'] ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'Pixabay' );

        }



        $url = add_query_arg( array(

            'key'       => $settings['pixabay_api_key'],

            'q'         => $query,

            'per_page'  => 3,

            'image_type' => 'photo',

            'orientation' => self::map_orientation( isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : '', 'pixabay' ),

            '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', '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_stock_request( 'pexels', $query, $settings );



        if ( empty( $settings['pexels_api_key'] ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'Pexels' );

        }



        $url = add_query_arg( array(

            'query'    => $query,

            'per_page' => 1,

            'orientation' => self::map_orientation( isset( $settings['stock_orientation'] ) ? $settings['stock_orientation'] : '', 'pexels' ),

        ), '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', '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_ai_image_request(

            'flux',

            ! empty( $settings['flux_model'] ) ? (string) $settings['flux_model'] : 'flux-2-pro',

            $prompt,

            $settings

        );



        if ( empty( $settings['flux_api_key'] ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'Fal.ai' );

        }



        $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', '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_stock_request( 'openverse', $query, $settings );



        // 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', '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 メディア登録 ==========



}

