<?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 new WP_Error( 'no_api_key', __( 'Gemini API key is required for Imagen.', 'ai-auto-poster' ) );
        }

        $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キーフィールド一覧
     */
    private static function get_key_fields() {
        return array( 'image_openai_api_key', 'image_gemini_api_key', 'stability_api_key', 'flux_api_key', 'unsplash_access_key', 'pixabay_api_key', 'pexels_api_key' );
    }

    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();
        switch ( $source ) {
            case 'dalle3':    return ! empty( $im['image_openai_api_key'] );
            case 'imagen':    return ! empty( $im['image_gemini_api_key'] );
            case 'stability': return ! empty( $im['stability_api_key'] );
            case 'flux':      return ! empty( $im['flux_api_key'] );
            case 'unsplash':  return ! empty( $im['unsplash_access_key'] );
            case 'pixabay':   return ! empty( $im['pixabay_api_key'] );
            case 'pexels':    return ! empty( $im['pexels_api_key'] );
        }
        return false;
    }

    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: ' . mb_substr( $plain_article, 0, 900 ) . '.';
        }

        $plain_user_prompt = trim( wp_strip_all_tags( (string) $user_prompt ) );
        if ( $plain_user_prompt !== '' ) {
            $parts[] = 'Additional direction: ' . 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 !== '' && 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'] ) ) {
            // Bug fix (audit r9 self-audit): SSRF 防御。
            // 旧実装は wp_remote_get + 任意 URL で、攻撃者が API レスポンスを偽装できる
            // 経路 (悪意の API キー、改ざんされたプロキシ、Stock 系 API の改竄) で
            //   - file:///etc/passwd 読み出し
            //   - http://127.0.0.1:8080/admin 内部サービス叩き
            //   - http://169.254.169.254/latest/meta-data/ クラウドメタデータ盗取
            // が可能だった。
            // 1. スキームを http/https に限定
            // 2. 解決後の IP がプライベート/予約レンジに入る場合は拒否
            // 3. wp_safe_remote_get を使い、WP の追加防御 (リダイレクト先のホスト検証等) を効かせる
            $image_url = (string) $image_result['url'];
            if ( ! preg_match( '#^https?://#i', $image_url ) ) {
                return new WP_Error( 'invalid_image_url',
                    /* translators: %s = scheme found in URL */
                    sprintf( __( 'Image URL scheme not allowed: %s', 'ai-auto-poster' ), wp_parse_url( $image_url, PHP_URL_SCHEME ) ?: 'unknown' ) );
            }
            $host = wp_parse_url( $image_url, PHP_URL_HOST );
            if ( ! $host ) {
                return new WP_Error( 'invalid_image_url', __( 'Image URL host is missing.', 'ai-auto-poster' ) );
            }
            // ホストが直接 IP リテラルで書かれているケースをまず弾く (gethostbynamel を経由せずに済む)
            if ( filter_var( $host, FILTER_VALIDATE_IP ) !== false ) {
                if ( filter_var( $host, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) {
                    return new WP_Error( 'invalid_image_url', __( 'Image URL points to a private or reserved IP address.', 'ai-auto-poster' ) );
                }
            } else {
                // ホスト名 → IP 解決して private/reserved レンジを除外。gethostbynamel は失敗時 false。
                // 注: TOCTOU 攻撃 (DNS Rebinding) は完全には防げないが、純粋な内部 IP 直叩きは塞げる。
                $resolved = function_exists( 'gethostbynamel' ) ? @gethostbynamel( $host ) : false;
                if ( is_array( $resolved ) ) {
                    foreach ( $resolved as $ip ) {
                        if ( filter_var( $ip, FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE ) === false ) {
                            return new WP_Error( 'invalid_image_url', __( 'Image URL resolves to a private or reserved IP address.', 'ai-auto-poster' ) );
                        }
                    }
                }
            }
            $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;
    }
}
