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

/**
 * AIモデルの定期同期 (Phase 1: プロバイダ API ベース)
 *
 * 動作:
 *  - 週次 cron (aiap_weekly = 604800 秒) で API キーが保存されている各プロバイダの /models を取得
 *  - 取得結果は AIAP_AI_Providers の transient キャッシュ (TTL=1週、cron 周期と同期) に保存
 *  - 保存中のモデルが取得結果に含まれていなければ "discontinued" として記録
 *  - 管理画面で記録された discontinued をユーザーに警告 (admin_notices)
 *
 * 重要 (Bug fix の経緯): この cron は以下を「やらない」:
 *  - AIAP_Model_Registry::default_text_model() / default_image_model() の値は変更しない。
 *    既定モデルはプラグインのリリースごとにコード (class-aiap-model-registry.php) で更新する。
 *  - 保存中設定 (aiap_settings / aiap_image_settings) の自動更新もここでは行わない。
 *    切替は (a) 管理画面 Refresh model list (AJAX) (b) 設定 Save (handle_save_provider)
 *    の 2 箇所で「discontinued なら Registry default に切替」というロジックで行う。
 *
 * 設計方針:
 *  - 外部インフラ非依存 (既存の AI API のみを利用)
 *  - API エラー時は黙ってスキップ (次回 cron でリトライ)。"discontinued" 判定は
 *    取得が成功した場合に限るため、一時的なネットワーク障害で誤検知しない
 *  - API キー削除時は当該プロバイダの discontinued ステータスを明示クリアする
 *    (旧実装は残存し続けて古い警告が消えなかった)
 *  - cron 経路のみで動作。フロント/管理画面の通常リクエストでは API を叩かない
 */
class AIAP_Model_Sync {

    const CRON_HOOK   = 'aiap_model_sync';
    const STATUS_OPT  = 'aiap_model_sync_status';
    // Bug fix (致命): 旧実装は SCHEDULE='weekly' を使っていたが WordPress 標準には
    // 'weekly' スケジュールは無く (hourly / twicedaily / daily のみ)、本プラグインも
    // cron_schedules フィルタで 'aiap_five_minutes' しか追加していないため、
    // wp_schedule_event('weekly', ...) は登録失敗し model sync cron は一度も起動しなかった。
    // 自前で 'aiap_weekly' を cron_schedules に追加して登録を成立させる
    // (Scheduler の aiap_five_minutes と同パターン)。
    const SCHEDULE    = 'aiap_weekly';
    // wp_remote_get タイムアウト (バックグラウンド処理なので長めに)
    const FETCH_TIMEOUT = 15;

    /**
     * フックを登録する (plugins_loaded から呼ばれる)
     */
    public static function init() {
        add_filter( 'cron_schedules', array( __CLASS__, 'add_weekly_schedule' ) );
        add_action( self::CRON_HOOK, array( __CLASS__, 'run_sync' ) );
        // セーフティネット: cron 登録が消えたら admin 経路で再登録 (Scheduler と同パターン)
        add_action( 'admin_init', array( __CLASS__, 'ensure_scheduled' ) );
    }

    /**
     * cron_schedules フィルタ: 'aiap_weekly' (= 7 日) を追加登録する。
     *
     * WordPress 標準には weekly が存在しないため自前で追加する。
     */
    public static function add_weekly_schedule( $schedules ) {
        if ( ! is_array( $schedules ) ) {
            $schedules = array();
        }
        $schedules['aiap_weekly'] = array(
            'interval' => WEEK_IN_SECONDS,
            'display'  => __( 'AI Auto Poster (weekly model sync)', 'ai-auto-poster' ),
        );
        return $schedules;
    }

    /**
     * cron 未登録なら登録する。
     */
    public static function ensure_scheduled() {
        if ( ! wp_next_scheduled( self::CRON_HOOK ) ) {
            // 既存の AIAP_CRON_HOOK (5分間隔の本処理) と干渉しないように、初回は1時間後にずらす。
            wp_schedule_event( time() + HOUR_IN_SECONDS, self::SCHEDULE, self::CRON_HOOK );
        }
    }

    /**
     * プラグイン有効化時のフック登録
     *
     * Bug fix: register_activation_hook 経由で本メソッドが走るタイミングでは、
     * init() (plugins_loaded フック) は同リクエストで既に走っているが、後発の
     * activation 経路でも cron_schedules フィルタが確実に登録された状態にするため
     * ここで明示的に add_filter する (重複登録は WP が冪等に扱う)。
     */
    public static function activate() {
        add_filter( 'cron_schedules', array( __CLASS__, 'add_weekly_schedule' ) );
        self::ensure_scheduled();
    }

    /**
     * プラグイン無効化時のフック解除 + 状態クリア
     */
    public static function deactivate() {
        wp_clear_scheduled_hook( self::CRON_HOOK );
        // 状態 option は残す (再有効化時に直近の警告状態を保持するため)。
        // 完全な削除は aiap_uninstall_cleanup_site() (ai-auto-poster.php) で実施。
    }

    /**
     * 同期実行 (cron から呼ばれる)
     *
     * 各プロバイダの /models を取得し、transient と discontinued ステータスを更新する。
     */
    public static function run_sync() {
        if ( ! class_exists( 'AIAP_AI_Client' ) ) {
            return;
        }

        $settings = AIAP_AI_Client::get_settings();
        if ( ! is_array( $settings ) ) {
            return;
        }

        $status = get_option( self::STATUS_OPT, array() );
        if ( ! is_array( $status ) ) {
            $status = array();
        }

        // テキストプロバイダ + 画像プロバイダのうち、モデル選択 UI を持つもの
        // (dalle3 / imagen は API キーが image-client 側に保存されているため別途処理)
        $text_providers = AIAP_AI_Providers::get_providers();

        foreach ( $text_providers as $provider ) {
            self::sync_text_provider( $provider, $settings, $status );
        }

        // 画像プロバイダ (dalle3 / imagen は AIAP_Image_Client::get_settings() の API キーを使う)
        if ( class_exists( 'AIAP_Image_Client' ) ) {
            $image_settings = AIAP_Image_Client::get_settings();
            if ( is_array( $image_settings ) ) {
                self::sync_image_provider( 'dalle3', $image_settings, $status );
                self::sync_image_provider( 'imagen', $image_settings, $status );
            }
        }

        update_option( self::STATUS_OPT, $status, false );
    }

    /**
     * テキストプロバイダ 1 つを同期する
     */
    private static function sync_text_provider( $provider, $settings, &$status ) {
        $key_field   = AIAP_AI_Providers::get_key_field( $provider );
        $model_field = AIAP_AI_Providers::get_model_field( $provider );
        if ( $key_field === '' || $model_field === '' ) {
            return;
        }
        self::sync_provider_common( $provider, $settings, $key_field, $model_field, $status );
    }

    /**
     * 画像プロバイダ 1 つを同期する (dalle3 / imagen)
     */
    private static function sync_image_provider( $provider, $image_settings, &$status ) {
        // 画像プロバイダの API キー保存先は image-client の image_*_api_key 系
        $key_field = class_exists( 'AIAP_Image_Client' ) ? AIAP_Image_Client::get_key_field( $provider ) : '';
        if ( $key_field === '' ) {
            return;
        }
        $model_field = $provider . '_model';
        self::sync_provider_common( $provider, $image_settings, $key_field, $model_field, $status );
    }

    /**
     * Dedup: text/image 共通の同期処理。
     *
     * 旧実装は sync_text_provider / sync_image_provider の双方で「キー取得 → 空チェック →
     * fetch_models_from_api → 失敗時黙ってスキップ → 成功なら cache + discontinued 判定 +
     * status 更新」を独立に書いていた (CLAUDE.md 重複排除)。
     *
     * @param string $provider     'claude'/'openai'/.../'dalle3'/'imagen' 等
     * @param array  $settings     対応する設定配列 (AI 設定 or 画像設定)
     * @param string $key_field    api_key の保存先キー名
     * @param string $model_field  model の保存先キー名
     * @param array  $status       (by ref) 更新先の状態テーブル
     */
    private static function sync_provider_common( $provider, $settings, $key_field, $model_field, array &$status ) {
        $api_key     = isset( $settings[ $key_field ] ) ? (string) $settings[ $key_field ] : '';
        $saved_model = isset( $settings[ $model_field ] ) ? (string) $settings[ $model_field ] : '';

        // Bug fix: API キー未設定なら同期不能 = もうそのプロバイダを使っていない。
        // 旧実装は既存ステータスを残していたため、過去に記録された discontinued_model が
        // 永続して管理画面警告が出続けていた。キー削除 = 警告対象外として明示的にクリアする。
        if ( $api_key === '' ) {
            if ( isset( $status[ $provider ] ) ) {
                unset( $status[ $provider ] );
            }
            return;
        }

        $models = AIAP_AI_Providers::fetch_models_from_api( $provider );
        if ( is_wp_error( $models ) || ! is_array( $models ) || empty( $models ) ) {
            // 取得失敗時は何もしない (誤検知防止)。次回 cron でリトライ。
            return;
        }

        // 取得成功 → transient キャッシュを更新
        AIAP_AI_Providers::cache_models( $provider, $models );

        // discontinued 判定: 保存中モデルが /models 応答に含まれていなければ discontinued
        $entry = array(
            'last_check'         => time(),
            'discontinued_model' => null,
        );
        if ( $saved_model !== '' && ! isset( $models[ $saved_model ] ) ) {
            $entry['discontinued_model'] = $saved_model;
        }
        $status[ $provider ] = $entry;
    }

    /**
     * 単一プロバイダの discontinued ステータスを即時に再評価する。
     *
     * 週次 cron (run_sync) を待たずに最新状態へ更新するための公開エントリ。
     * 設定保存 (handle_save_provider) や AJAX のモデル一覧更新 (ajax_refresh_models)
     * から呼び、それらが直前に切替・キャッシュ更新した結果を STATUS_OPT に反映する。
     *
     * cron 経路 (sync_provider_common) と判定基準を揃えるため、照合元は
     * AIAP_AI_Providers::get_models_for() (= 動的取得済 transient 優先、無ければ
     * Registry 既定) とする。API は叩かない (保存処理の遅延・誤検知を避ける。
     * 保存/AJAX 経路は直前に cache_models() でキャッシュを更新済み)。
     *
     * @param string $provider    プロバイダキー (claude/openai/.../dalle3/imagen)
     * @param string $api_key      当該プロバイダの API キー (空ならステータスをクリア)
     * @param string $saved_model  切替後の保存中モデル ID
     */
    public static function refresh_status_for( $provider, $api_key, $saved_model ) {
        if ( ! is_string( $provider ) || $provider === '' ) {
            return;
        }
        $status = get_option( self::STATUS_OPT, array() );
        if ( ! is_array( $status ) ) {
            $status = array();
        }

        $api_key     = is_string( $api_key ) ? $api_key : '';
        $saved_model = is_string( $saved_model ) ? $saved_model : '';

        // API キー未設定 = もうそのプロバイダを使っていない。sync_provider_common と
        // 同じく、過去の discontinued 記録が残らないよう当該エントリをクリアする。
        if ( $api_key === '' ) {
            if ( isset( $status[ $provider ] ) ) {
                unset( $status[ $provider ] );
                update_option( self::STATUS_OPT, $status, false );
            }
            return;
        }

        $models = AIAP_AI_Providers::get_models_for( $provider );
        if ( ! is_array( $models ) || empty( $models ) ) {
            // 照合元が無い場合は誤検知を避けて何もしない (次回 cron で再評価)。
            return;
        }

        $entry = array(
            'last_check'         => time(),
            'discontinued_model' => null,
        );
        if ( $saved_model !== '' && ! isset( $models[ $saved_model ] ) ) {
            $entry['discontinued_model'] = $saved_model;
        }
        $status[ $provider ] = $entry;
        update_option( self::STATUS_OPT, $status, false );
    }

    /**
     * 管理画面で警告すべき discontinued エントリを返す
     *
     * @return array provider => discontinued_model (string)
     */
    public static function get_discontinued() {
        $status = get_option( self::STATUS_OPT, array() );
        if ( ! is_array( $status ) ) {
            return array();
        }
        $out = array();
        foreach ( $status as $provider => $entry ) {
            if ( ! is_array( $entry ) ) {
                continue;
            }
            $model = isset( $entry['discontinued_model'] ) ? (string) $entry['discontinued_model'] : '';
            if ( $model !== '' ) {
                $out[ $provider ] = $model;
            }
        }
        return $out;
    }
}
