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

/**
 * AIモデルの単一情報源 (Single Source of Truth)
 *
 * 各プロバイダの既定モデル / 既定モデル選択肢 / reasoning 判定 を一元管理する。
 * AI Client / Image Client / 管理画面 のいずれもここを参照することで、
 * モデルID の重複定義と保守漏れを防ぐ。
 *
 * 既定値の更新方針:
 *  - alias (例: mistral-small-latest, deepseek-chat) が提供されているプロバイダは
 *    そのまま採用し、プロバイダ側で世代を自動追従させる。
 *  - alias が無いプロバイダ (Anthropic / OpenAI / Gemini / Groq 等) はモデル世代を
 *    直接指定する必要があるため、リリースサイクルでこのファイルだけ更新する。
 *  - 日付サフィックス付き ID (例: claude-sonnet-4-6-20260217) は陳腐化に弱いため、
 *    プロバイダが alias を受理する場合は alias を優先する。
 */
class AIAP_Model_Registry {

    /**
     * テキスト生成 AI プロバイダの定義
     *
     * 各エントリ:
     *   'default' => 既定モデル ID
     *   'models'  => array(
     *       'model_id' => array(
     *           'label_suffix' => UI 表示用の英語ラベル (空文字なら ID のみ表示)
     *           'reasoning'    => true なら reasoning 系 (max_completion_tokens 必須等)
     *       )
     *   )
     *
     * @return array
     */
    public static function text_providers() {
        return array(
            'claude' => array(
                'default' => 'claude-sonnet-4-6',
                'models'  => array(
                    'claude-opus-4-7'           => array( 'label_suffix' => 'High quality',     'reasoning' => false ),
                    'claude-sonnet-4-6'         => array( 'label_suffix' => 'Recommended',      'reasoning' => false ),
                    'claude-haiku-4-5-20251001' => array( 'label_suffix' => 'Fast & affordable','reasoning' => false ),
                ),
            ),
            'openai' => array(
                // GPT-5 系への移行は別途検証が必要なため、当面は GPT-4.1 系を既定にする。
                // (reasoning モデル特有のレイテンシ・コスト・content_length 整合の確認が必要)
                'default' => 'gpt-4.1-mini',
                'models'  => array(
                    'gpt-4.1-mini' => array( 'label_suffix' => 'Recommended',                'reasoning' => false ),
                    'gpt-4.1'      => array( 'label_suffix' => 'High quality',               'reasoning' => false ),
                    'gpt-4.1-nano' => array( 'label_suffix' => 'Fastest & most affordable',  'reasoning' => false ),
                    'gpt-4o'       => array( 'label_suffix' => '',                           'reasoning' => false ),
                    'gpt-4o-mini'  => array( 'label_suffix' => '',                           'reasoning' => false ),
                    'o3-mini'      => array( 'label_suffix' => 'Reasoning',                  'reasoning' => true  ),
                    // GPT-5 系も選択肢として用意 (Reasoning モデル)。既定にはしない。
                    'gpt-5'        => array( 'label_suffix' => 'GPT-5 (Reasoning)',          'reasoning' => true  ),
                    'gpt-5-mini'   => array( 'label_suffix' => 'GPT-5 mini (Reasoning)',     'reasoning' => true  ),
                ),
            ),
            'gemini' => array(
                // 2.5-flash は 2026-05 時点で stable 維持。3.1 系は preview を含むため既定は安全側に。
                'default' => 'gemini-2.5-flash',
                'models'  => array(
                    'gemini-2.5-flash'        => array( 'label_suffix' => 'Recommended',  'reasoning' => false ),
                    'gemini-2.5-pro'          => array( 'label_suffix' => 'High quality', 'reasoning' => false ),
                    'gemini-3.1-flash-lite'   => array( 'label_suffix' => 'New gen — fast', 'reasoning' => false ),
                    'gemini-3.1-pro-preview'  => array( 'label_suffix' => 'New gen — high quality (preview)', 'reasoning' => false ),
                    // gemini-2.0-flash は 2026-06-01 にシャットダウン予定のため削除。
                ),
            ),
            'mistral' => array(
                'default' => 'mistral-small-latest',
                'models'  => array(
                    'mistral-small-latest'  => array( 'label_suffix' => 'Recommended',  'reasoning' => false ),
                    'mistral-medium-latest' => array( 'label_suffix' => '',             'reasoning' => false ),
                    'mistral-large-latest'  => array( 'label_suffix' => 'High quality', 'reasoning' => false ),
                    'codestral-latest'      => array( 'label_suffix' => 'Code',         'reasoning' => false ),
                    'magistral-medium-latest' => array( 'label_suffix' => 'Reasoning',  'reasoning' => true ),
                ),
            ),
            'groq' => array(
                // Llama 4 Maverick は 2026-02-20 廃止済。Scout を既定に。
                'default' => 'meta-llama/llama-4-scout-17b-16e-instruct',
                'models'  => array(
                    'meta-llama/llama-4-scout-17b-16e-instruct' => array( 'label_suffix' => 'Recommended (Llama 4)', 'reasoning' => false ),
                    'llama-3.3-70b-versatile'                   => array( 'label_suffix' => 'Llama 3.3',             'reasoning' => false ),
                    'llama-3.1-8b-instant'                      => array( 'label_suffix' => 'Fastest',               'reasoning' => false ),
                    'gemma2-9b-it'                              => array( 'label_suffix' => '',                      'reasoning' => false ),
                    // mixtral-8x7b-32768 は Mistral 側で世代交代済のため削除。
                ),
            ),
            'deepseek' => array(
                // deepseek-chat / deepseek-reasoner は 2026-07-24 廃止予定のため V4 系に移行。
                'default' => 'deepseek-v4-flash',
                'models'  => array(
                    'deepseek-v4-flash' => array( 'label_suffix' => 'Recommended',  'reasoning' => false ),
                    'deepseek-v4-pro'   => array( 'label_suffix' => 'High quality / Reasoning', 'reasoning' => true ),
                ),
            ),
            'openrouter' => array(
                // OpenRouter はバージョン無し ID で世代追従する設計のため既存値維持。
                'default' => 'anthropic/claude-sonnet-4',
                'models'  => array(
                    'anthropic/claude-sonnet-4' => array( 'label_suffix' => '', 'reasoning' => false ),
                    'openai/gpt-4.1-mini'       => array( 'label_suffix' => '', 'reasoning' => false ),
                    'google/gemini-2.5-flash'   => array( 'label_suffix' => '', 'reasoning' => false ),
                    'meta-llama/llama-4-scout'  => array( 'label_suffix' => '', 'reasoning' => false ),
                    'mistralai/mistral-large'   => array( 'label_suffix' => '', 'reasoning' => false ),
                    'deepseek/deepseek-v4'      => array( 'label_suffix' => '', 'reasoning' => false ),
                ),
            ),
        );
    }

    /**
     * 画像生成 AI プロバイダの定義
     *
     * @return array
     */
    public static function image_providers() {
        return array(
            'dalle3' => array(
                // dall-e-2 / dall-e-3 は 2026-05-12 にシャットダウン済。gpt-image-1 に既定移行。
                'default' => 'gpt-image-1',
                'models'  => array(
                    'gpt-image-1'      => array( 'label_suffix' => 'Recommended' ),
                    'gpt-image-1-mini' => array( 'label_suffix' => 'Fast & affordable' ),
                ),
            ),
            'imagen' => array(
                'default' => 'imagen-4.0-generate-001',
                'models'  => array(
                    'imagen-4.0-generate-001'      => array( 'label_suffix' => 'Recommended' ),
                    'imagen-4.0-fast-generate-001' => array( 'label_suffix' => 'Fast' ),
                    'imagen-3.0-generate-002'      => array( 'label_suffix' => '' ),
                    'imagen-3.0-generate-001'      => array( 'label_suffix' => '' ),
                    'imagen-3.0-fast-generate-001' => array( 'label_suffix' => '' ),
                ),
            ),
            'stability' => array(
                'default' => 'sd3.5-medium',
                'models'  => array(
                    'sd3.5-medium'      => array( 'label_suffix' => 'Recommended' ),
                    'sd3.5-large'       => array( 'label_suffix' => 'High quality' ),
                    'sd3.5-large-turbo' => array( 'label_suffix' => 'Fast' ),
                ),
            ),
            'flux' => array(
                'default' => 'flux-2-pro',
                'models'  => array(
                    'flux-2-pro' => array( 'label_suffix' => 'Recommended' ),
                    'flux-2-dev' => array( 'label_suffix' => 'Affordable' ),
                    'flux-2-max' => array( 'label_suffix' => 'Highest quality' ),
                ),
            ),
        );
    }

    /**
     * 指定プロバイダの既定テキストモデルを返す
     */
    public static function default_text_model( $provider ) {
        $providers = self::text_providers();
        return isset( $providers[ $provider ]['default'] ) ? $providers[ $provider ]['default'] : '';
    }

    /**
     * 指定プロバイダの既定画像モデルを返す
     */
    public static function default_image_model( $provider ) {
        $providers = self::image_providers();
        return isset( $providers[ $provider ]['default'] ) ? $providers[ $provider ]['default'] : '';
    }

    /**
     * 指定プロバイダのテキストモデル選択肢を返す
     *
     * @return array model_id => label
     */
    public static function text_model_options( $provider ) {
        return self::build_model_options( self::text_providers(), $provider );
    }

    /**
     * 指定プロバイダの画像モデル選択肢を返す
     *
     * @return array model_id => label
     */
    public static function image_model_options( $provider ) {
        return self::build_model_options( self::image_providers(), $provider );
    }

    /**
     * 画像プロバイダの ID 配列を返す (admin の UI セレクト用)
     *
     * @param string $provider
     * @return array model_id の配列
     */
    public static function image_model_ids( $provider ) {
        $providers = self::image_providers();
        $models    = isset( $providers[ $provider ]['models'] ) ? $providers[ $provider ]['models'] : array();
        return array_keys( $models );
    }

    /**
     * モデルが reasoning 系か (max_completion_tokens 必須、思考トークン消費等)
     *
     * レジストリに無いモデルについては OpenAI 系 (gpt-5* / o[1-9]*) のパターンマッチで
     * フォールバック判定する。これは fetch_models_from_api() で動的に取得したモデルや、
     * ユーザーがカスタム入力したモデル ID にも対応するため。
     *
     * @param string $provider プロバイダキー (任意。空文字なら全プロバイダから探索)
     * @param string $model    モデル ID
     * @return bool
     */
    public static function is_reasoning_model( $provider, $model ) {
        if ( ! is_string( $model ) || $model === '' ) {
            return false;
        }
        $providers = self::text_providers();

        // 指定プロバイダ配下で完全一致を探す
        if ( $provider !== '' && isset( $providers[ $provider ]['models'][ $model ] ) ) {
            return ! empty( $providers[ $provider ]['models'][ $model ]['reasoning'] );
        }

        // プロバイダ未指定なら全プロバイダから探索
        if ( $provider === '' ) {
            foreach ( $providers as $p ) {
                if ( isset( $p['models'][ $model ] ) ) {
                    return ! empty( $p['models'][ $model ]['reasoning'] );
                }
            }
        }

        // フォールバック: 静的リストに無い reasoning モデル (動的取得 / ユーザー入力 /
        // OpenRouter のプロバイダ接頭辞付き slug) をパターンで判定する。
        // OpenRouter の "vendor/model" 形式に対応するため、判定は接頭辞を除いた
        // 末尾セグメントに対して行う (例 "deepseek/deepseek-r1" → "deepseek-r1")。
        $bare = $model;
        $slash = strrpos( $bare, '/' );
        if ( $slash !== false ) {
            $bare = substr( $bare, $slash + 1 );
        }

        // OpenAI 系: "gpt-5*" (画像系 "gpt-5-image" 等は除外)、"o1/o3/o4..." 始まり。
        if ( preg_match( '/^gpt-5/', $bare ) && strpos( $bare, 'image' ) === false ) {
            return true;
        }
        if ( preg_match( '/^o[1-9](\b|[-\d])/', $bare ) ) {
            return true;
        }
        // 非 OpenAI 系の代表的な reasoning モデル名パターン:
        //   - DeepSeek: deepseek-r1*, deepseek-reasoner, *deepseek-r1* (distill 含む)
        //   - Mistral: magistral* (magistral-small/medium、日付版)
        //   - Qwen: qwq*, qwen*-reasoning / qwen*-thinking
        //   - OpenAI OSS: gpt-oss*
        // 通常モデル (deepseek-v4-flash, mistral-small, qwen2.5 等) を誤検出しないよう、
        // reasoning を示す語幹 (r1 / reasoner / magistral / qwq / oss / thinking) を要求する。
        if ( preg_match( '/(?:^|[-\/])deepseek-r1\b|deepseek-reasoner|^magistral|(?:^|[-\/])qwq\b|gpt-oss|-(?:reasoning|thinking)\b/i', $bare ) ) {
            return true;
        }
        return false;
    }

    /**
     * OpenAI 互換 chat completions API で `response_format: { type: "json_object" }` を
     * 受け付けるかを判定する。
     *
     * Bug fix: 旧実装は build_openai_chat_body で全モデルに一律 response_format を送って
     * いた。OpenAI o1 ファミリーは現状この指定を受け付けず 400 を返すため、reasoning
     * モデルでも下記条件で出し分ける:
     *   - o1 系 (o1, o1-mini, o1-preview, ...) → false
     *   - それ以外の reasoning (o3*, o4*, gpt-5*, deepseek-v4-pro, magistral-medium-latest) → true
     *   - chat 系 (gpt-4*, claude*, gemini* など) → true
     *
     * @param string $provider 'openai' / 'openrouter' / 'mistral' 等。空文字なら無視。
     * @param string $model    モデル ID。空文字や未指定なら true(後方互換)。
     * @return bool
     */
    public static function supports_json_response_format( $provider, $model ) {
        if ( ! is_string( $model ) || $model === '' ) {
            return true;
        }
        $lower = strtolower( $model );
        // OpenRouter 経由は "openai/o1-mini" 形式なので接頭辞を剥がして判定する
        $bare = preg_replace( '#^[a-z0-9_-]+/#', '', $lower );
        // o1 ファミリーのみ非対応 (o2 は存在しないので含めない、o3+ は対応)
        if ( preg_match( '/^o1(\b|[-\d])/', $bare ) ) {
            return false;
        }
        return true;
    }

    /**
     * 内部用: models 配列から「ID => 'ID — suffix'」形式の選択肢配列を生成
     */
    private static function build_model_options( $providers, $provider ) {
        $models = isset( $providers[ $provider ]['models'] ) ? $providers[ $provider ]['models'] : array();
        $result = array();
        foreach ( $models as $id => $meta ) {
            $suffix = isset( $meta['label_suffix'] ) ? (string) $meta['label_suffix'] : '';
            $result[ $id ] = $suffix !== '' ? sprintf( '%s — %s', $id, $suffix ) : $id;
        }
        return $result;
    }
}
