<?php

if ( ! defined( 'ABSPATH' ) ) {

    exit;

}



/**

 * アイキャッチ画像取得のエントリポイント

 *

 * 取得元の選択・retry 制御・後処理 (リサイズ・最適化等) を担当する。

 * 実際の API 呼出本体 (DALL-E 3 / Imagen / Stability AI / Flux /

 * Unsplash / Pixabay / Pexels) は AIAP_Image_Providers に、

 * 画像スタイル (画風プロンプト) は AIAP_Image_Styles に分離されている。

 */

class AIAP_Image_Client {



    const OPTION_KEY = 'aiap_image_settings';



    /**

     * Imagen フォールバック優先順位

     *

     * 保存モデルが Google API の Imagen モデル一覧に含まれない場合、このリストを上から順に走査し、

     * 最初にマッチしたものを採用する。リストにマッチが無ければ、API が返した imagen-* モデルの

     * ID 文字列降順で 1 位を採用する(新モデルへの自動追従)。

     *

     * 値は実装時点での想定であり、Google 側のリリース状況に応じて更新する。

     */

    const IMAGEN_FALLBACK_PRIORITY = array(

        'imagen-4.0-generate-001',

        'imagen-4.0-fast-generate-001',

        'imagen-3.0-generate-002',

        'imagen-3.0-generate-001',

    );



    /**

     * 画像 API 呼び出しの最大試行回数(初回 + リトライ)。

     * テキスト AI 側の AIAP_AI_Client::GENERATE_MAX_RETRY と対称な仕様。

     */

    const IMAGE_GENERATE_MAX_RETRY = 3;



    /**

     * Featured image mode + global default → effective enabled boolean.

     *

     * タスク/手動の featured_image_mode ('on'|'off') から

     * 「実際にアイキャッチを付けるか」を一意に決定する。

     *

     * 仕様変更履歴:

     *   - 初期: 'inherit'/'on'/'off' の 3 値、グローバル「デフォルト画像ソースとして使用する」トグル併用

     *   - 旧:   グローバルトグル廃止、'inherit' は常時 true (画像を付ける) にフォールバック

     *   - 現:   'inherit' を完全廃止し 'on'/'off' の 2 値化 (旧 inherit データは migration で 'on' に変換)

     * 防御として未知値は 'on' 相当 (true) を返す。

     *

     * @param string $mode           'on' | 'off' (旧 'inherit' も true 扱い: 防御)

     * @param mixed  $global_enabled (廃止予定、互換のため引数は残す) 旧 image_enabled

     * @return bool

     */

    public static function resolve_image_enabled( $mode, $global_enabled = 1 ) {

        if ( $mode === 'off' ) { return false; }

        // 'on' および未知値 (防御) は「画像を付ける」を返す。

        return true;

    }



    /**

     * Imagen のモデル ID を解決する。

     *

     * 1. キャッシュ済みモデル一覧を読む

     * 2. キャッシュに saved_model があればそのまま返す

     * 3. 無ければ API から再取得 (cache を上書き) して再検査

     * 4. それでも無ければ IMAGEN_FALLBACK_PRIORITY を上から走査

     * 5. それでも無ければ API が返した imagen-* を ID 降順で並べた 1 位

     * 6. 候補 0 件なら WP_Error

     *

     * 保存値 ($settings['imagen_model']) は書き換えない(API の一時的な揺らぎで保存値を破壊しないため)。

     *

     * @param string $api_key      Gemini API key

     * @param string $saved_model  ユーザー保存値

     * @return string|WP_Error     利用可能なモデル ID

     */

    public static function resolve_imagen_model( $api_key, $saved_model ) {

        if ( empty( $api_key ) ) {

            return AIAP_AI_Errors::no_api_key_error( 'Gemini' );

        }



        $cached = class_exists( 'AIAP_AI_Client' ) ? AIAP_AI_Providers::get_models_for( 'imagen' ) : array();



        // ステップ 2: キャッシュにヒット

        if ( $saved_model !== '' && is_array( $cached ) && isset( $cached[ $saved_model ] ) ) {

            return $saved_model;

        }



        // ステップ 3: API から再取得して cache を更新

        $fresh = class_exists( 'AIAP_AI_Client' ) ? AIAP_AI_Providers::fetch_models_from_api( 'imagen' ) : array();

        if ( is_wp_error( $fresh ) ) {

            return $fresh;

        }

        if ( ! is_array( $fresh ) || empty( $fresh ) ) {

            return new WP_Error( 'imagen_no_models', __( 'No Imagen models are available.', 'ai-auto-poster' ) );

        }

        if ( class_exists( 'AIAP_AI_Client' ) ) {

            AIAP_AI_Providers::cache_models( 'imagen', $fresh );

        }



        // ステップ 4: 再取得結果に saved_model

        if ( $saved_model !== '' && isset( $fresh[ $saved_model ] ) ) {

            return $saved_model;

        }



        // ステップ 5: 優先順位リスト走査

        foreach ( self::IMAGEN_FALLBACK_PRIORITY as $candidate ) {

            if ( isset( $fresh[ $candidate ] ) ) {

                return $candidate;

            }

        }



        // ステップ 6: imagen-* ID の降順 1 位

        $imagen_ids = array_values( array_filter( array_keys( $fresh ), function ( $id ) {

            return strpos( $id, 'imagen-' ) === 0;

        } ) );

        if ( ! empty( $imagen_ids ) ) {

            rsort( $imagen_ids );

            return $imagen_ids[0];

        }



        return new WP_Error( 'imagen_no_models', __( 'No Imagen models are available.', 'ai-auto-poster' ) );

    }



    /**

     * APIキーフィールド一覧

     */

    public static function get_key_field( $source ) {
        $map = 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',
        );
        $source = (string) $source;
        return isset( $map[ $source ] ) ? $map[ $source ] : '';
    }

    public static function get_key_fields() {
        return array_values( array_filter( array_map( array( __CLASS__, 'get_key_field' ), array_keys( self::get_sources() ) ) ) );
    }


    const DEFAULT_IMAGE_SOURCE = 'dalle3';



    /**

     * 画像設定スキーマ (既定値)。

     *

     * モデル ID 既定値は AIAP_Model_Registry に集約 (単一情報源)。

     *

     * @return array<string,mixed>

     */

    public static function get_default_settings() {

        return array(

            'image_enabled'        => 1,

            'image_source'         => self::DEFAULT_IMAGE_SOURCE,

            // 共通オプション

            'image_size'           => '1024x1024',

            'aspect_ratio'         => '1:1',

            'image_format'         => '16:9',

            'image_quality'        => 'low',

            // Bug fix (audit r9 self-audit): image_preset を defaults に追加。

            // UI hidden input (aiap-image-preset) で送信され handle_save_image_settings で

            // 保存される値だが、旧 defaults に欠落していたため初回ロード時に undefined と

            // なるリスクがあった。表示時は format/quality/max_width/compress から計算で

            // 再判定されるため値は事実上 UI ヒントだが、整合性のため defaults に置く。

            'image_preset'         => 'custom',

            // Bug fix (audit r9 self-audit): 旧 defaults にあった 'image_style' (グローバル) は

            // コード中で一度も参照されない dead key だった。プロバイダ別 dalle3_style /

            // imagen_style / stability_style / flux_style が実態として使われている。

            // 紛らわしいので defaults から削除した。

            // プロバイダごとのスタイル

            'dalle3_style'         => 'natural',

            'imagen_style'         => 'none',

            'stability_style'      => 'none',

            'flux_style'           => 'none',

            // プロバイダごとのモデル (既定値は AIAP_Model_Registry に集約)

            // dall-e-2 / dall-e-3 は 2026-05-12 にシャットダウン済のため

            // 新規インストールでは gpt-image-1 系を既定にする (レジストリ側で管理)。

            'dalle3_model'         => AIAP_Model_Registry::default_image_model( 'dalle3' ),

            'imagen_model'         => AIAP_Model_Registry::default_image_model( 'imagen' ),

            // 画像後処理: 値はここで保持。トリガは SEO 設定の seo_img_webp に統合。

            'image_max_width'      => 1200,

            'image_compress'       => 85,

            // OpenAI image

            'image_openai_api_key' => '',

            // Google Imagen

            'image_gemini_api_key' => '',

            // Stability AI

            'stability_api_key'    => '',

            'stability_model'      => AIAP_Model_Registry::default_image_model( 'stability' ),

            // Flux (via Fal.ai)

            'flux_api_key'         => '',

            'flux_model'           => AIAP_Model_Registry::default_image_model( 'flux' ),

            // フリー素材

            'unsplash_access_key'  => '',

            'pixabay_api_key'      => '',

            'pexels_api_key'       => '',

        );

    }



    /**

     * 画像設定のリポジトリ。

     *

     * @return AIAP_Settings

     */

    public static function settings() {

        return new AIAP_Settings(

            self::OPTION_KEY,

            array( __CLASS__, 'get_default_settings' ),

            self::get_key_fields()

        );

    }



    /**

     * 保存済み画像設定への一度きりのデータマイグレーション。

     * レガシーキー検出時のみ DB へ書き込み、整理後の生配列を返す。

     *

     * @param array<string,mixed> $saved AIAP_Settings::raw() の戻り値

     * @return array<string,mixed>

     */

    private static function run_migrations( array $saved ) {

        // 既存データのマイグレーション: image_source='none' -> image_enabled=0 + デフォルトプロバイダ

        if ( isset( $saved['image_source'] ) && ! isset( $saved['image_enabled'] ) ) {

            if ( $saved['image_source'] === 'none' ) {

                $saved['image_enabled'] = 0;

                $saved['image_source']  = self::DEFAULT_IMAGE_SOURCE;

            } else {

                $saved['image_enabled'] = 1;

            }

        }



        // マイグレーション: 旧 image_webp=1 / image_optimize=1 -> SEO の seo_img_webp に統合

        // (画像後処理のトリガを SEO セクション一本化したため、既存設定を seo_img_webp=1 に移送する)

        // 一度きりの処理として、レガシーキーが存在する場合のみ実行 -> 削除して以降は走らない。

        $legacy_webp     = ! empty( $saved['image_webp'] );

        $legacy_optimize = ! empty( $saved['image_optimize'] );

        $has_legacy_keys = isset( $saved['image_webp'] ) || isset( $saved['image_optimize'] );



        if ( $has_legacy_keys ) {

            if ( $legacy_webp || $legacy_optimize ) {

                $ai_store    = AIAP_AI_Client::settings();

                $ai_settings = $ai_store->raw();

                $ai_settings['seo_enabled']  = 1;

                $ai_settings['seo_img_webp'] = 1;

                $ai_store->save( $ai_settings );

            }

            // レガシーキーを永久削除して以降のマイグレーションを抑止

            unset( $saved['image_webp'], $saved['image_optimize'] );

            self::settings()->save( $saved );

        }



        return $saved;

    }



    public static function get_settings() {

        // オプション I/O・既定値マージ・復号は AIAP_Settings に集約。

        // 画像設定固有のデータマイグレーションだけここで実行する。

        $saved = self::run_migrations( self::settings()->raw() );

        return AIAP_Crypto::decrypt_keys(

            wp_parse_args( $saved, self::get_default_settings() ),

            self::get_key_fields()

        );

    }



    public static function update_settings( $settings ) {

        // API キー保護 (audit r9) と暗号化・保存は AIAP_Settings::update_encrypted に集約。

        self::settings()->update_encrypted( is_array( $settings ) ? $settings : array() );

    }



    /**

     * 利用可能な画像ソース一覧

     */

    /**

     * 指定したソースで画像生成に必要な設定（APIキー等）が揃っているか確認

     *

     * @param string $source

     * @return bool

     */

    /**

     * 指定ソースに必要な API キーが設定されているか判定する。

     *

     * 第2引数 $settings に画像設定配列を渡すと、その配列を直接見る(DB を読まない)。

     * 省略時は `get_settings()` でDBからロード。保存処理中に「これから保存しようと

     * している値で判定したい」用途のため第2引数を提供している。

     *

     * @param string     $source 画像プロバイダキー

     * @param array|null $settings 画像設定配列(省略時は DB から取得)

     * @return bool

     */

    public static function has_required_credentials( $source, $settings = null ) {

        if ( $source === 'openverse' ) {

            return true; // APIキー不要

        }

        $im = is_array( $settings ) ? $settings : self::get_settings();

        $key_field = self::get_key_field( $source );
        return $key_field !== '' && ! empty( $im[ $key_field ] );
    }


    public static function get_sources() {

        return array(

            'dalle3'    => 'DALL-E 3 (OpenAI)',

            'imagen'    => 'Imagen (Google)',

            'stability' => 'Stability AI',

            'flux'      => 'Flux (Black Forest Labs)',

            'unsplash'  => 'Unsplash',

            'pixabay'   => 'Pixabay',

            'pexels'    => 'Pexels',

            'openverse' => 'Openverse',

        );

    }



    private static function normalize_generation_settings( $settings ) {

        $format = isset( $settings['image_format'] ) ? $settings['image_format'] : '16:9';

        // Bug fix (audit r7): 旧テーブルでは '4:3' の DALL-E size が 1024x1024 (正方形) となり、

        // ユーザーが「横長 4:3」を選んだ意図と矛盾していた。横長系として扱うため、近似横長

        // である 1792x1024 を割り当てる (GPT-Image-1 ではこの値はサポート外なので、

        // call_dalle3 内の pick_dalle_size_by_format で 1536x1024 に丸められる)。

        $sizes  = array(

            '16:9' => array( 'size' => '1792x1024', 'flux' => '1344x768',  'aspect' => '16:9', 'stock' => 'landscape' ),

            '1:1'  => array( 'size' => '1024x1024', 'flux' => '1024x1024', 'aspect' => '1:1',  'stock' => 'squarish' ),

            '9:16' => array( 'size' => '1024x1792', 'flux' => '768x1344',  'aspect' => '9:16', 'stock' => 'portrait' ),

            '4:3'  => array( 'size' => '1792x1024', 'flux' => '1024x768',  'aspect' => '4:3',  'stock' => 'landscape' ),

        );

        $map = isset( $sizes[ $format ] ) ? $sizes[ $format ] : $sizes['16:9'];



        $settings['image_size']        = $map['size'];

        $settings['flux_image_size']   = $map['flux'];

        $settings['aspect_ratio']      = $map['aspect'];

        $settings['stock_orientation'] = $map['stock'];



        $quality = isset( $settings['image_quality'] ) ? $settings['image_quality'] : 'low';

        if ( $quality === 'high' ) {

            $settings['dalle_quality'] = 'hd';

            $settings['flux_steps']    = 40;

        } elseif ( $quality === 'medium' ) {

            $settings['dalle_quality'] = 'standard';

            $settings['flux_steps']    = 25;

        } else {

            $settings['dalle_quality'] = 'standard';

            $settings['flux_steps']    = 15;

        }



        return $settings;

    }



    /**

     * 記事タイトルからアイキャッチ画像を生成/取得し、投稿に設定する

     *

     * @param int    $post_id    投稿ID

     * @param string $title      記事タイトル(キーワード/プロンプト生成用)

     * @return int|WP_Error      アタッチメントID or エラー

     */

    public static function generate_and_attach( $post_id, $title, $slug = '', $preset_image_prompt = '', $user_prompt = '', $article_content = '', $override_enabled = null, $run_source = 'src_auto' ) {

        $settings = self::get_settings();



        // アイキャッチ生成が無効ならスキップ (エラーではない)

        // 戻り値 false は「設定により無効化されている」ことを示す。

        // 呼び出し側では is_wp_error() のみを失敗扱いとし、false は無視する。

        //

        // $override_enabled が明示指定されていればそちらを優先(タスク/手動側の per-post 設定)。

        // null の場合のみグローバル設定にフォールバック。

        $effective_enabled = is_null( $override_enabled )

            ? ! empty( $settings['image_enabled'] )

            : (bool) $override_enabled;

        if ( ! $effective_enabled ) {

            return false;

        }



        $settings = self::normalize_generation_settings( $settings );

        $source   = $settings['image_source'];



        // image_source が 'none' の場合もスキップ扱い (image_enabled=1 でも source=none なら何もしない)

        if ( $source === 'none' ) {

            return false;

        }



        $is_ai = in_array( $source, array( 'dalle3', 'imagen', 'stability', 'flux' ), true );



        // AIが記事生成時に書いた image_prompt を最優先で使う(AI画像生成時のみ)

        if ( $is_ai && $preset_image_prompt !== '' ) {

            $query = $preset_image_prompt;

        } else {

            $query = self::generate_image_query( $title, $is_ai, $settings, $source, $user_prompt, $article_content );

        }



        if ( is_wp_error( $query ) ) {

            return $query;

        }



        // リトライ可能な API エラー(レート制限・サーバ一時障害)に備えて call_with_retry でラップ。

        // 永続エラー(課金枯渇/認証/入力不正)は WP_Error の retryable=false で即中断される。

        // テキスト AI 側の generate_with_retry と対称な挙動。

        $log_source = (string) $run_source;

        $on_retry = function ( $attempt, $err ) use ( $log_source ) {

            // Bug fix (audit r9 self-audit, #36/#37): レベルを 'warning' に統一し、

            // 分母を MAX_RETRY+1 (=4) にする (3 経路で食い違っていた retry log を統一)。

            AIAP_Logger::add( 'warning', 'log_image_retry', array(

                $log_source,

                $attempt,

                self::IMAGE_GENERATE_MAX_RETRY + 1,

                AIAP_AI_Errors::format_error_for_log( $err ),

            ) );

        };



        switch ( $source ) {

            case 'dalle3':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_dalle3( $query, $settings );

                }, $on_retry );

                break;

            case 'imagen':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_imagen( $query, $settings );

                }, $on_retry );

                break;

            case 'stability':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_stability( $query, $settings );

                }, $on_retry );

                break;

            case 'flux':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_flux( $query, $settings );

                }, $on_retry );

                break;

            case 'unsplash':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_unsplash( $query, $settings );

                }, $on_retry );

                break;

            case 'pixabay':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_pixabay( $query, $settings );

                }, $on_retry );

                break;

            case 'pexels':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_pexels( $query, $settings );

                }, $on_retry );

                break;

            case 'openverse':

                $image_result = AIAP_Image_Providers::call_with_retry( function () use ( $query, $settings ) {

                    return AIAP_Image_Providers::call_openverse( $query, $settings );

                }, $on_retry );

                break;

            default:

                /* translators: %s = image source key (e.g. dalle3) */

                return new WP_Error( 'invalid_source', sprintf( __( 'Invalid image source: %s', 'ai-auto-poster' ), $source ) );

        }



        if ( is_wp_error( $image_result ) ) {

            return $image_result;

        }



        return self::attach_image_to_post( $post_id, $image_result, $title, $slug );

    }



    /**

     * テキストAIを使って画像用プロンプト/キーワードを生成

     *

     * @param string $title 記事タイトル

     * @param bool   $is_ai AI画像生成向けか(true)、フリー素材検索向けか(false)

     * @return string|WP_Error

     */

    private static function generate_image_query( $title, $is_ai, $img_settings = array(), $source = '', $user_prompt = '', $article_content = '' ) {

        // 画像AIプロバイダが OpenAI 以外でも、画像プロンプト作成のために

        // テキストAI(OpenAI等)を呼ぶと、設定していないAIのエラーが表示される。

        // 画像側は画像AI設定だけで完結させるため、ここでは外部テキストAIを呼ばない。

        if ( $is_ai ) {

            return self::build_local_ai_image_prompt( $title, $img_settings, $source, $user_prompt, $article_content );

        }



        // Pexels / Unsplash / Pixabay / Openverse などの素材検索でも、

        // 検索キーワード作成のためにテキストAIは呼ばない。

        return self::build_local_stock_search_query( $title, $user_prompt, $article_content );

    }



    /**

     * AI画像生成用プロンプトをローカルで作成する。

     * 画像AI実行時に、別設定のテキストAIを呼ばないための処理。

     */

    private static function build_local_ai_image_prompt( $title, $img_settings = array(), $source = '', $user_prompt = '', $article_content = '' ) {

        $parts = array();

        $parts[] = 'Create a blog featured image with no text, no letters, no logo, and no watermark.';



        $plain_title = trim( wp_strip_all_tags( (string) $title ) );

        if ( $plain_title !== '' ) {

            $parts[] = 'Article title: ' . $plain_title . '.';

        }



        $plain_article = trim( wp_strip_all_tags( (string) $article_content ) );

        if ( $plain_article !== '' ) {

            $parts[] = 'Reflect this article atmosphere and concrete objects: ' . AIAP_Compat::mb_substr( $plain_article, 0, 900 ) . '.';

        }



        $plain_user_prompt = trim( wp_strip_all_tags( (string) $user_prompt ) );

        if ( $plain_user_prompt !== '' ) {

            $parts[] = 'Additional direction: ' . AIAP_Compat::mb_substr( $plain_user_prompt, 0, 600 ) . '.';

        }



        $style_text = '';

        if ( $source !== '' ) {

            $style_key = $source . '_style';

            $style_val = $img_settings[ $style_key ] ?? '';



            if ( $source === 'dalle3' ) {

                if ( $style_val === 'vivid' ) {

                    $style_text = 'vivid, colorful style';

                } elseif ( $style_val === 'natural' ) {

                    $style_text = 'natural, organic style';

                }

            } elseif ( $source === 'stability' ) {

                $phrase_map = array(

                    '3d-model'          => '3D model render',

                    'analog-film'       => 'analog film photography',

                    'anime'             => 'anime or manga style',

                    'cinematic'         => 'cinematic style',

                    'comic-book'        => 'comic book illustration',

                    'digital-art'       => 'digital art',

                    'enhance'           => 'enhanced detail',

                    'fantasy-art'       => 'fantasy art',

                    'isometric'         => 'isometric view style',

                    'line-art'          => 'line art',

                    'low-poly'          => 'low-poly 3D style',

                    'origami'           => 'origami folded paper style',

                    'photographic'      => 'photorealistic photography',

                    'pixel-art'         => 'pixel art style',

                    'tile-texture'      => 'seamless tile texture',

                );

                if ( isset( $phrase_map[ $style_val ] ) ) {

                    $style_text = $phrase_map[ $style_val ];

                }

            } else {

                $style_text = AIAP_Image_Styles::style_to_prompt_text( $style_val );

            }

        }



        if ( $style_text !== '' ) {

            $parts[] = 'Visual style: ' . $style_text . '.';

        }



        $parts[] = 'Use a clean composition suitable for a WordPress featured image.';



        return trim( implode( ' ', array_filter( $parts ) ) );

    }



    /**

     * フリー素材検索用のキーワードをローカルで生成する。

     * OpenAI/Claude/Gemini等のテキストAIは呼ばない。

     */

    private static function build_local_stock_search_query( $title, $user_prompt = '', $article_content = '' ) {

        $source_text = trim( wp_strip_all_tags( $title . ' ' . $article_content . ' ' . $user_prompt ) );



        $map = array(

            '自然' => 'nature', '森' => 'forest', '林' => 'forest', '木' => 'trees',

            '葉' => 'leaves', '花' => 'flowers', '桜' => 'cherry blossoms',

            '春' => 'spring', '夏' => 'summer', '秋' => 'autumn', '冬' => 'winter',

            '空' => 'sky', '雲' => 'clouds', '雨' => 'rain', '雪' => 'snow',

            '海' => 'sea', '川' => 'river', '山' => 'mountain',

            '朝' => 'morning', '夕' => 'sunset', '夜' => 'night',

            '文房具' => 'stationery', 'ノート' => 'notebook', '手帳' => 'notebook',

            '万年筆' => 'fountain pen', 'ペン' => 'pen', '鉛筆' => 'pencil',

            'タイプライター' => 'typewriter', 'パソコン' => 'laptop', 'PC' => 'laptop',

            'コーヒー' => 'coffee', '珈琲' => 'coffee', 'カップ' => 'coffee cup',

            '机' => 'desk', '暮らし' => 'lifestyle', '生活' => 'lifestyle',

            '散歩' => 'walking', '公園' => 'park', '庭' => 'garden',

        );



        $keywords = array();

        foreach ( $map as $jp => $en ) {

            if ( $source_text !== '' && AIAP_Compat::mb_stripos( $source_text, $jp ) !== false ) {

                $keywords[] = $en;

            }

        }



        if ( preg_match_all( '/[A-Za-z][A-Za-z0-9\\-]{2,}/', $source_text, $matches ) ) {

            foreach ( $matches[0] as $word ) {

                $word = strtolower( $word );

                if ( in_array( $word, array( 'the', 'and', 'for', 'with', 'this', 'that', 'from' ), true ) ) {

                    continue;

                }

                $keywords[] = $word;

            }

        }



        $keywords = array_values( array_unique( array_filter( $keywords ) ) );



        if ( empty( $keywords ) ) {

            return 'nature lifestyle';

        }



        return implode( ' ', array_slice( $keywords, 0, 5 ) );

    }



    // ========== AI画像生成 ==========



    /**

     * 画像をダウンロードしてメディアライブラリに登録し、アイキャッチに設定

     * @param int    $post_id

     * @param array  $image_result  url or base64 + source + extension

     * @param string $title

     * @return int|WP_Error アタッチメントID

     */

    private static function postprocess_image_file( $path, $filename, $ext, $settings, $do_post_process = false ) {

        if ( ! $do_post_process ) {

            return array( 'path' => $path, 'filename' => $filename, 'extension' => $ext );

        }



        $editor = wp_get_image_editor( $path );

        if ( is_wp_error( $editor ) ) {

            return array( 'path' => $path, 'filename' => $filename, 'extension' => $ext );

        }



        $size      = $editor->get_size();

        $max_width = max( 100, min( 4096, (int) ( $settings['image_max_width'] ?? 1200 ) ) );

        if ( ! empty( $size['width'] ) && $size['width'] > $max_width ) {

            $editor->resize( $max_width, null, false );

        }



        $quality = max( 1, min( 100, (int) ( $settings['image_compress'] ?? 85 ) ) );

        $editor->set_quality( $quality );



        // WebP 変換 (GD または Imagick がサポートする場合のみ)

        // Bug fix (audit r9): 旧コードは function_exists('imagewebp') (GD関数) だけを見ていたため、

        // Imagick バックエンドの環境では WebP delegate がインストールされていても変換がスキップ

        // されていた。WordPress 5.8+ の WP_Image_Editor::supports_mime_type() を優先的に使い、

        // 古い環境では GD/Imagick の能力を直接検査するフォールバックに切り替える。

        $can_webp = false;

        if ( method_exists( $editor, 'supports_mime_type' ) ) {

            $can_webp = (bool) $editor->supports_mime_type( 'image/webp' );

        } else {

            $can_webp = function_exists( 'imagewebp' )

                || ( class_exists( 'Imagick' ) && in_array( 'WEBP', (array) \Imagick::queryFormats(), true ) );

        }

        if ( $can_webp ) {

            $new_path = preg_replace( '/\.[^.]+$/', '.webp', $path );

            $saved    = $editor->save( $new_path, 'image/webp' );

            if ( ! is_wp_error( $saved ) && ! empty( $saved['path'] ) ) {

                wp_delete_file( $path );

                return array(

                    'path'      => $saved['path'],

                    'filename'  => basename( $saved['path'] ),

                    'extension' => 'webp',

                );

            }

        }



        // WebP 非対応環境: リサイズ + 圧縮のみ適用して同形式で保存

        $saved = $editor->save( $path );

        if ( is_wp_error( $saved ) || empty( $saved['path'] ) ) {

            return array( 'path' => $path, 'filename' => $filename, 'extension' => $ext );

        }



        return array(

            'path'      => $saved['path'],

            'filename'  => basename( $saved['path'] ),

            'extension' => $ext,

        );

    }



    private static function attach_image_to_post( $post_id, $image_result, $title, $slug = '' ) {

        require_once ABSPATH . 'wp-admin/includes/file.php';

        require_once ABSPATH . 'wp-admin/includes/media.php';

        require_once ABSPATH . 'wp-admin/includes/image.php';



        $ext = isset( $image_result['extension'] ) ? $image_result['extension'] : 'jpg';



        // ファイル名: 英語スラグベース

        $slug_part = ! empty( $slug ) ? sanitize_file_name( $slug ) : 'post-' . $post_id;

        $filename  = $slug_part . '-' . time() . '.' . $ext;



        if ( ! empty( $image_result['base64'] ) ) {

            $image_data = base64_decode( $image_result['base64'] );

        } elseif ( ! empty( $image_result['url'] ) ) {

            // SSRF 防御: 共通ヘルパ AIAP_Http::is_safe_external_url() に集約。

            // 旧実装は @gethostbynamel の失敗時 (DNS 解決不可) を素通りさせるバグがあった。

            // 共通ヘルパは DNS 解決失敗時を「拒否」に変更し、IPv6 (AAAA レコード) も

            // チェック対象に含める。CLAUDE.md「同じ問題が他にないか確認」観点で

            // RSS test_connection と挙動を統一する。

            $image_url = (string) $image_result['url'];

            if ( class_exists( 'AIAP_Http' ) ) {

                $safe = AIAP_Http::is_safe_external_url( $image_url );

                if ( is_wp_error( $safe ) ) {

                    return new WP_Error( 'invalid_image_url', $safe->get_error_message() );

                }

            }

            $dl = wp_safe_remote_get( $image_url, array(

                'timeout' => 60,

                // 画像サイズの上限: 16MB を超えるレスポンスは破棄(メモリ枯渇対策)

                'limit_response_size' => 16 * 1024 * 1024,

                // リダイレクト追跡時の上限を縛る (リダイレクトループや SSRF 多段リダイレクト対策)

                'redirection' => 3,

            ) );

            if ( is_wp_error( $dl ) ) {

                /* translators: %s = error message */

                return new WP_Error( 'download_failed', sprintf( __( 'Image download failed: %s', 'ai-auto-poster' ), $dl->get_error_message() ) );

            }

            $code = wp_remote_retrieve_response_code( $dl );

            if ( (int) $code !== 200 ) {

                /* translators: %d = HTTP status code */

                return new WP_Error( 'download_failed', sprintf( __( 'Image download failed: HTTP %d', 'ai-auto-poster' ), (int) $code ) );

            }

            // Content-Type で画像かどうか軽くチェック(error HTML を画像として保存しないため)

            $ctype = wp_remote_retrieve_header( $dl, 'content-type' );

            if ( $ctype && stripos( $ctype, 'image/' ) === false ) {

                /* translators: %s = Content-Type header value */

                return new WP_Error( 'download_not_image', sprintf( __( 'Downloaded content is not an image (Content-Type: %s)', 'ai-auto-poster' ), $ctype ) );

            }

            $image_data = wp_remote_retrieve_body( $dl );

        } else {

            return new WP_Error( 'no_image_data', __( 'No image data.', 'ai-auto-poster' ) );

        }



        if ( empty( $image_data ) ) {

            return new WP_Error( 'empty_image', __( 'Image data is empty.', 'ai-auto-poster' ) );

        }



        $upload_dir = wp_upload_dir();

        // uploads ディレクトリ自体が書き込み不可・存在しない場合のチェック

        if ( ! empty( $upload_dir['error'] ) ) {

            return new WP_Error( 'upload_dir_error',

                /* translators: %s = WordPress upload directory error message */

                sprintf( __( 'WordPress upload directory error: %s', 'ai-auto-poster' ), $upload_dir['error'] )

            );

        }

        if ( empty( $upload_dir['path'] ) ) {

            return new WP_Error( 'upload_dir_invalid', __( 'WordPress upload directory path is empty.', 'ai-auto-poster' ) );

        }

        $tmp_path   = $upload_dir['path'] . '/' . $filename;



        // WP_Filesystem を使って一時ファイルに書き込む(WordPress.org ガイドライン準拠)

        global $wp_filesystem;

        if ( ! function_exists( 'WP_Filesystem' ) ) {

            require_once ABSPATH . 'wp-admin/includes/file.php';

        }

        if ( ! WP_Filesystem() ) {

            return new WP_Error( 'write_failed', __( 'Failed to initialize WP_Filesystem.', 'ai-auto-poster' ) );

        }

        if ( ! $wp_filesystem->put_contents( $tmp_path, $image_data, FS_CHMOD_FILE ) ) {

            return new WP_Error( 'write_failed', __( 'Failed to write image file.', 'ai-auto-poster' ) );

        }



        // 一時ファイルから真の MIME を確認(マジックバイト判定)

        $check = wp_check_filetype_and_ext( $tmp_path, $filename );

        if ( ! empty( $check['proper_filename'] ) ) {

            // WordPress が修正したファイル名を採用(拡張子の自動修正)

            $new_path = $upload_dir['path'] . '/' . $check['proper_filename'];

            // rename() は WordPress.org Plugin Check で警告される。

            // WP_Filesystem::move() を使用して規約に準拠する。

            if ( $wp_filesystem->move( $tmp_path, $new_path, true ) ) {

                $tmp_path = $new_path;

                $filename = $check['proper_filename'];

                // Bug fix (audit r9): WordPress が拡張子を修正した場合は $ext も同期する。

                // これがないと後続の $mime_types[$ext] 検索で古い拡張子が使われ、

                // post_mime_type が実ファイルと食い違うアタッチメントが作成されてしまう

                // (例: AIが "png" と申告した実体 jpg ファイルが image/png で登録される)。

                if ( preg_match( '/\.([^.]+)$/', $filename, $m ) ) {

                    $new_ext = strtolower( $m[1] );

                    if ( $new_ext === 'jpeg' ) $new_ext = 'jpg';

                    $ext = $new_ext;

                }

            }

        }

        if ( empty( $check['type'] ) || stripos( (string) $check['type'], 'image/' ) !== 0 ) {

            wp_delete_file( $tmp_path );

            return new WP_Error( 'invalid_image_type', __( 'Downloaded file is not a valid image type.', 'ai-auto-poster' ) );

        }



        $settings  = self::normalize_generation_settings( self::get_settings() );



        // 後処理 (リサイズ + 圧縮 + WebP変換) のトリガは SEO セクションの seo_img_webp に統合。

        // seo_enabled=ON かつ seo_img_webp=ON のときのみ、画像設定の値で後処理を実行する。

        $ai_settings = AIAP_AI_Client::get_settings();

        $do_post_process = ! empty( $ai_settings['seo_enabled'] ) && ! empty( $ai_settings['seo_img_webp'] );



        $processed = self::postprocess_image_file( $tmp_path, $filename, $ext, $settings, $do_post_process );

        if ( is_wp_error( $processed ) ) {

            wp_delete_file( $tmp_path );

            return $processed;

        }

        $tmp_path = $processed['path'];

        $filename = $processed['filename'];

        $ext      = $processed['extension'];



        $mime_types = array(

            'png'  => 'image/png',

            'jpg'  => 'image/jpeg',

            'jpeg' => 'image/jpeg',

            'webp' => 'image/webp',

        );

        $mime = isset( $mime_types[ $ext ] ) ? $mime_types[ $ext ] : 'image/jpeg';



        $attachment = array(

            'post_mime_type' => $mime,

            'post_title'     => sanitize_text_field( $title ),

            'post_content'   => '',

            'post_status'    => 'inherit',

        );



        $attach_id = wp_insert_attachment( $attachment, $tmp_path, $post_id );



        if ( is_wp_error( $attach_id ) ) {

            wp_delete_file( $tmp_path );

            return $attach_id;

        }



        $attach_data = wp_generate_attachment_metadata( $attach_id, $tmp_path );

        wp_update_attachment_metadata( $attach_id, $attach_data );



        set_post_thumbnail( $post_id, $attach_id );



        if ( ! empty( $image_result['credit'] ) ) {

            update_post_meta( $post_id, '_aiap_image_credit', $image_result['credit'] );

        }

        update_post_meta( $post_id, '_aiap_image_source', $image_result['source'] );



        return $attach_id;

    }

}

