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

/**
 * タスク → AI生成 → WordPress投稿 の一連の処理
 */
class AIAP_Poster {

    /**
     * 1タスクあたりの AI 呼び出し回数上限(レート制限保護)。
     * dedup_max_retry(リトライ最大回数、既定3 → 試行最大 4 回)× generate_with_retry 内部試行(GENERATE_MAX_RETRY=3 → 試行最大 4 回)で
     * 最悪 16 回まで膨らむ可能性がある。8 回程度を上限としておけば、dedup を 2 試行する間
     * 内部リトライが最大まで走っても収まり、レートリミット(429)を踏みにくい。
     */
    const AI_CALL_HARD_CAP = 8;

    /**
     * 今日の全タスクを実行（cron / 自動実行用）
     *
     * @return array 各タスクの結果 array( 'task_id' => int|WP_Error, ... )
     */
    public static function run() {
        $tasks = AIAP_Prompts::get_today_tasks();

        if ( empty( $tasks ) ) {
            AIAP_Logger::add( 'info', 'log_no_tasks_today', array( AIAP_Prompts::get_today_label()) );
            return array();
        }

        $results = array();
        foreach ( $tasks as $task ) {
            $results[ $task['id'] ] = self::run_single( $task, false );
        }

        return $results;
    }

    /**
     * AI生成結果から投稿を作成する共通処理
     *
     * 自動実行(run_single)と手動実行(ajax_run_now_step) の両方から呼ばれる。
     * 投稿ステータス検証 / カテゴリ / 投稿者 / 標準postmeta(_aiap_generated, _aiap_provider) の付与方針を
     * 1か所に集約することで、両経路で常に同じ挙動を保証する。
     *
     * @param array      $result      AIAP_AI_Client::generate() の戻り値 (title, content, slug 等)
     * @param array      $settings    AIAP_AI_Client::get_settings() の戻り値
     * @param string     $source      ログ用ソースキー ('src_manual' | 'src_auto')
     * @param string     $task_label  ログ表示用のタスクラベル (省略可)
     * @param array      $extra_meta  経路固有の追加postmeta (例: array( '_aiap_manual' => 1 ))
     * @param array|null $post_date   array( 'date_str' => 'Y-m-d', 'time_str' => 'H:i' ) を渡すとバックデート
     * @return int|WP_Error 投稿ID or エラー
     */
    public static function create_post_from_result( $result, $settings, $source, $task_label = '', $extra_meta = array(), $post_date = null ) {
        // タイトルまたは本文が空ならエラーで止める(空投稿の事故防止)
        $title_check   = trim( wp_strip_all_tags( (string) ( $result['title'] ?? '' ) ) );
        $content_check = trim( wp_strip_all_tags( (string) ( $result['content'] ?? '' ) ) );
        if ( $title_check === '' || $content_check === '' ) {
            $msg = __( 'AI response title or content is empty; aborted post creation', 'ai-auto-poster' );
            AIAP_Logger::add( 'error', 'log_empty_content_with_task', array( $source, $msg, $task_label ) );
            return new WP_Error( 'empty_content', $msg );
        }

        // slug 未指定時のフォールバック(手動実行で result.slug が無いケースに備える)
        $slug = isset( $result['slug'] ) ? $result['slug'] : 'post-' . wp_date( 'Ymd-His' );

        // Bug fix (audit r9 self-audit): post_title は MySQL の VARCHAR(255) なので、
        // それを超える長さの文字列を渡すと MySQL レベルで切り詰められる。マルチバイト
        // (UTF-8 で 3 バイト) の途中で切れると壊れた文字 (\xE3\x81 だけ等) が混入し、
        // wp_kses 経由でも修復できない。さらに DB のフルバイトカウントは utf8mb4 で
        // 765 バイト枠 (≒ 191 文字) なので、絵文字込みで 200 文字を超える AI 出力に
        // 対しても安全に収まるよう、保守的に 180 文字で打ち切る。
        $title_raw   = wp_strip_all_tags( $result['title'] );
        $title_capped = mb_substr( $title_raw, 0, 180, 'UTF-8' );

        $post_data = array(
            'post_title'    => $title_capped,
            'post_content'  => wp_kses_post( $result['content'] ),
            'post_name'     => sanitize_title( $slug ),
            'post_status'   => in_array( $settings['post_status'], array( 'draft', 'publish', 'pending' ), true ) ? $settings['post_status'] : 'draft',
            'post_author'   => (int) $settings['post_author'],
            'post_category' => $settings['post_category'] ? array( (int) $settings['post_category'] ) : array(),
        );

        // バックデート指示があれば post_date / post_date_gmt を上書き
        if ( is_array( $post_date ) && ! empty( $post_date['date_str'] ) && ! empty( $post_date['time_str'] ) ) {
            $backdate_local         = $post_date['date_str'] . ' ' . $post_date['time_str'] . ':00';
            $post_data['post_date'] = $backdate_local;
            try {
                $tz                         = function_exists( 'wp_timezone' ) ? wp_timezone() : new DateTimeZone( date_default_timezone_get() );
                $dt                         = new DateTimeImmutable( $backdate_local, $tz );
                $dt_utc                     = $dt->setTimezone( new DateTimeZone( 'UTC' ) );
                $post_data['post_date_gmt'] = $dt_utc->format( 'Y-m-d H:i:s' );
            } catch ( Exception $e ) {
                // 失敗時は WP に任せる(post_date_gmt 未指定 → WPが自動計算)
            }
        }

        $post_id = wp_insert_post( $post_data, true );

        if ( is_wp_error( $post_id ) ) {
            AIAP_Logger::add( 'error', 'log_post_creation_failed', array( $source, $post_id->get_error_message() ) );
            return $post_id;
        }

        // Free プランの月次投稿カウンタを増加(Pro でも累積記録するが上限はかからない)
        AIAP_License::increment_post_count();

        // 標準postmeta(両経路共通)
        update_post_meta( $post_id, '_aiap_generated', 1 );
        update_post_meta( $post_id, '_aiap_provider', $settings['provider'] );

        // 経路固有postmeta(_aiap_manual, _aiap_task_id 等)
        if ( is_array( $extra_meta ) ) {
            foreach ( $extra_meta as $meta_key => $meta_value ) {
                if ( $meta_value === null || $meta_value === '' ) {
                    continue;
                }
                update_post_meta( $post_id, $meta_key, $meta_value );
            }
        }

        return (int) $post_id;
    }

    /**
     * 指定されたタスクを1件実行
     *
     * @param array|string $task_or_id タスク配列 or タスクID
     * @param bool         $is_manual  手動実行かどうか
     * @return int|WP_Error 投稿ID or エラー
     */
    public static function run_single( $task_or_id, $is_manual = true ) {
        // キー文字列を保持(ログ表示時に Logger 側で展開される)
        $source = $is_manual ? 'src_manual' : 'src_auto';

        // IDが渡された場合はタスクを取得
        if ( is_string( $task_or_id ) ) {
            $task = AIAP_Prompts::get_by_id( $task_or_id );
            if ( ! $task ) {
                $err = new WP_Error( 'task_not_found', __( 'Task not found', 'ai-auto-poster' ) );
                AIAP_Logger::add( 'error', 'log_source_error', array( $source, $err->get_error_message() ) );
                return $err;
            }
        } else {
            $task = $task_or_id;
        }

        if ( empty( $task['prompt'] ) ) {
            $err = new WP_Error( 'empty_prompt', __( 'Task prompt is empty', 'ai-auto-poster' ) );
            AIAP_Logger::add( 'error', 'log_source_error', array( $source, $err->get_error_message() ) );
            return $err;
        }

        $task_label = AIAP_Prompts::format_log_label( $task );

        if ( empty( $task['content_language'] ) ) {
            AIAP_Logger::add( 'error', 'log_task_missing_language', array( $source, $task_label ) );
            return new WP_Error( 'missing_language', __( 'Task has no content language set; skipped.', 'ai-auto-poster' ) );
        }

        $settings      = AIAP_AI_Client::get_settings();
        // Bug fix (audit r4): dedup is a Pro feature. handle_save_settings only writes
        // dedup_enabled when is_pro(), but a user who was Pro and downgraded retains a
        // saved dedup_enabled=1, which would silently keep using AI credits on Free.
        // Mirror what effective_recover_max() does for cron_recover_max — gate at runtime.
        $dedup_enabled = ! empty( $settings['dedup_enabled'] )
            && ( ! class_exists( 'AIAP_License' ) || AIAP_License::is_pro() );
        $max_retry     = max( 1, (int) ( $settings['dedup_max_retry'] ?? 3 ) );
        $current_prompt = $task['prompt'];

        // タスク固有の言語・文字数(設定があればグローバル値を上書き)
        $task_overrides = array();
        if ( ! empty( $task['content_language'] ) ) {
            $task_overrides['content_language'] = $task['content_language'];
        }
        if ( ! empty( $task['content_length'] ) ) {
            $task_overrides['content_length'] = (int) $task['content_length'];
        }

        // featured_image_mode (inherit/on/off) からこのタスクで実際にアイキャッチを付けるかを決定。
        // 値は generate_and_attach の override に使うほか、AI プロンプト生成側にも伝える(image_enabled_effective)。
        $image_mode              = AIAP_Prompts::get_image_mode( $task );
        $image_settings          = AIAP_Image_Client::get_settings();
        $effective_image_enabled = AIAP_Image_Client::resolve_image_enabled( $image_mode, $image_settings['image_enabled'] ?? 0 );
        $task_overrides['image_enabled_effective'] = $effective_image_enabled;

        // ===== RSS フェーズ =====
        // rss_enabled かつ Pro なら、選択フィードから未使用記事を取得して task_overrides に追加する。
        // 候補 0 件の時は rss_fallback に従って skip/generate を分岐。
        // Pro ゲートを実行時にも適用する（Pro→Free ダウングレード対策）。
        $rss_settings    = AIAP_Prompts::get_rss_settings( $task );
        $rss_used_articles = array();
        if ( $rss_settings['rss_enabled'] === 1 ) {
            $is_pro = class_exists( 'AIAP_License' ) && AIAP_License::is_pro();
            if ( ! $is_pro ) {
                AIAP_Logger::add( 'info', 'log_rss_skipped_free_plan', array( $source, $task_label ) );
            } else {
                $candidates = AIAP_RSS_Feeds::pick_unused( $rss_settings['rss_feed_ids'], $rss_settings['rss_count'] );
                if ( empty( $candidates ) ) {
                    if ( $rss_settings['rss_fallback'] === 'skip' ) {
                        AIAP_Logger::add( 'info', 'log_rss_skipped_no_articles', array( $source, $task_label ) );
                        return new WP_Error( 'rss_no_articles', __( 'No fresh RSS articles available; post skipped per task setting.', 'ai-auto-poster' ) );
                    }
                    AIAP_Logger::add( 'info', 'log_rss_fallback_to_normal', array( $source, $task_label ) );
                    // フォールバック時は RSS なしで通常生成（task_overrides に何も追加しない）
                } else {
                    $task_overrides['rss_articles'] = $candidates;
                    $task_overrides['rss_mode']     = $rss_settings['rss_mode'];
                    $rss_used_articles              = $candidates;
                }
            }
        }
        // ===== /RSS フェーズ =====

        // AI生成(dedup有効時はリトライループ)
        // max_retry(=dedup_max_retry) は「リトライ最大回数」。試行は最大 (max_retry + 1) 回(初回 + リトライ N 回)。
        //
        // API 呼び出し回数の安全上限:
        //   generate_with_retry は内部で最大 GENERATE_MAX_RETRY+1(=4)回 AI を呼ぶ。
        //   dedup ループは最大 max_retry+1(=4)回まわるので、最悪ケースで 16 回 AI を呼ぶ可能性がある。
        //   レート制限に当たりやすいため、累積 API 呼び出し回数を AI_CALL_HARD_CAP で制限する。
        $result        = null;
        $attempt       = 0;
        $recent        = $dedup_enabled ? AIAP_Dedup::get_recent_posts() : array();
        $dedup_result  = null;
        $ai_call_count = 0;
        $hard_cap      = self::AI_CALL_HARD_CAP;

        // 試行回数が max_retry を超えるまで(つまり最大 max_retry+1 回試行する)
        while ( $attempt <= $max_retry ) {
            $attempt++;

            // 累積 AI 呼び出し回数が上限を超えそうなら dedup ループを中断
            // (最低 1 回は呼ぶ前提なので $attempt===1 ではこの分岐に入らない)
            if ( $ai_call_count >= $hard_cap ) {
                AIAP_Logger::add( 'error', 'log_server_error_failed', array(
                    $source, $ai_call_count, '', $task_label,
                ) );
                break;
            }

            $result = AIAP_AI_Client::generate_with_retry( $current_prompt, function( $ai_attempt, $err ) use ( $source, $task_label, &$ai_call_count ) {
                $ai_call_count++;
                // Bug fix (audit r9 self-audit, #36/#37): レベルを 'warning' に統一し、
                // 分母を MAX_RETRY+1 (=4) にする。旧実装は 'info' + 分母 MAX_RETRY (=3) で:
                //   - 経路ごとにレベルがバラバラだった (UI=error, cron=info, image=info)
                //   - 「3/3」表示で「リトライ終了」と読めるが実際は 4 回目試行が残る、という UI 混乱
                AIAP_Logger::add( 'warning', 'log_server_error_retry', array(
                    $source, $ai_attempt, AIAP_AI_Client::GENERATE_MAX_RETRY + 1,
                    AIAP_AI_Client::format_error_for_log( $err ), $task_label,
                ));
            }, $task_overrides, $is_manual );
            // 成功時は generate_with_retry のコールバックは呼ばれないため、
            // ここで成功分(初回試行)を 1 カウントする。コールバックでカウント済みのリトライ分と合計する。
            $ai_call_count++;

            if ( is_wp_error( $result ) ) {
                AIAP_Logger::add( 'error', 'log_server_error_failed', array(
                    $source, $ai_call_count,
                    AIAP_AI_Client::format_error_for_log( $result ), $task_label,
                ));
                return $result;
            }

            if ( ! $dedup_enabled ) {
                break;
            }

            $dedup_result = AIAP_Dedup::check( $result['title'], $result['content'] );

            if ( empty( $dedup_result['is_similar'] ) ) {
                if ( $attempt > 1 ) {
                    AIAP_Logger::add( 'info', 'log_dedup_passed', array(
                        $source, $attempt, $dedup_result['method'],
                    ));
                }
                break;
            }

            $reason_text = is_string( $dedup_result['reason'] ?? '' ) ? (string) $dedup_result['reason'] : '';

            // Bug fix (audit r9): UI ラベル「Max retries」とクラスコメント L181 のとおり、
            // dedup_max_retry は「リトライ最大回数」を意味する (=初回1回 + max_retry 回 = 試行 max_retry+1 回)。
            // 旧条件 `$attempt >= $max_retry` だと max_retry=3 のとき iteration 3 で break してしまい、
            // 試行 3 回しか走っていなかった (初回 + リトライ 2 回)。`$attempt > $max_retry` に変更して
            // 初回 + max_retry 回 = 4 回まで試行できるようにする。
            if ( $attempt > $max_retry ) {
                AIAP_Logger::add( 'info', 'log_dedup_max_reached', array( $source, $max_retry ) );
                break;
            }

            AIAP_Logger::add( 'info', 'log_dedup_retry', array(
                $source, $attempt, $max_retry,
                $dedup_result['method'], mb_substr( $reason_text, 0, 80 ),
            ));

            $current_prompt = AIAP_Dedup::build_retry_prompt(
                $task['prompt'],
                $reason_text,
                $recent,
                $result
            );
        }
        // 投稿作成(共通処理)
        // バックデート: cron取りこぼし拾い時に予定時刻で投稿されたように見せる
        // (自動経由 & override が新しい場合のみ適用。取りこぼし復元時は必ず予定日時を使用)
        $post_date = null;
        $schedule_run_key = '';
        if ( ! $is_manual && class_exists( 'AIAP_Scheduler' ) ) {
            $override = get_option( AIAP_Scheduler::OPTION_OVERRIDE_POSTDATE, array() );
            if ( is_array( $override )
                && isset( $override['task_id'] )
                && $override['task_id'] === $task['id']
                // 古い情報(5分以上前にセット)は無視(誤適用防止)
                && isset( $override['set_at'] )
                && ( time() - (int) $override['set_at'] ) < 300
            ) {
                if ( ! empty( $override['run_key'] ) ) {
                    $schedule_run_key = (string) $override['run_key'];
                }
                if ( ! empty( $override['date_str'] ) && ! empty( $override['time_str'] ) ) {
                    // 取りこぼし復元時は必ず本来の予定日時を post_date に使う。
                    // 記事内容が朝/昼/夜の予定に紐づくため、現在時刻で公開すると時刻と内容が不一致になる。
                    $post_date = array(
                        'date_str' => $override['date_str'],
                        'time_str' => $override['time_str'],
                    );
                }
            }
        }

        // RSS 使用記事の決定 (pick モードは AI の selected_rss_index、digest モードは全件)
        $rss_consumed = array();
        if ( ! empty( $rss_used_articles ) ) {
            if ( $rss_settings['rss_mode'] === 'pick' ) {
                $index = isset( $result['selected_rss_index'] ) ? (int) $result['selected_rss_index'] : 0;
                if ( $index < 1 || $index > count( $rss_used_articles ) ) {
                    AIAP_Logger::add( 'warning', 'log_rss_pick_index_invalid', array(
                        $source, $index, count( $rss_used_articles ),
                    ) );
                    $index = 1;
                }
                $rss_consumed = array( $rss_used_articles[ $index - 1 ] );
            } else {
                $rss_consumed = $rss_used_articles;
            }
        }

        $rss_extra_meta = array();
        if ( ! empty( $rss_consumed ) ) {
            $urls     = array();
            $guids    = array();
            $feed_ids = array();
            foreach ( $rss_consumed as $a ) {
                $urls[]     = isset( $a['link'] )    ? (string) $a['link']    : '';
                $guids[]    = isset( $a['guid'] )    ? (string) $a['guid']    : '';
                $feed_ids[] = isset( $a['feed_id'] ) ? (string) $a['feed_id'] : '';
            }
            $rss_extra_meta = array(
                '_aiap_rss_source_urls'  => $urls,
                '_aiap_rss_source_guids' => $guids,
                '_aiap_rss_feed_ids'     => $feed_ids,
                '_aiap_rss_mode'         => $rss_settings['rss_mode'],
            );
        }

        $post_id = self::create_post_from_result(
            $result,
            $settings,
            $source,
            $task_label,
            array_merge(
                array(
                    '_aiap_task_id'          => $task['id'],
                    '_aiap_schedule_run_key' => $schedule_run_key,
                    '_aiap_scheduled_date'   => is_array( $post_date ) && ! empty( $post_date['date_str'] ) ? $post_date['date_str'] : '',
                    '_aiap_scheduled_time'   => is_array( $post_date ) && ! empty( $post_date['time_str'] ) ? $post_date['time_str'] : '',
                ),
                $rss_extra_meta
            ),
            $post_date
        );

        if ( is_wp_error( $post_id ) ) {
            return $post_id;
        }

        // 使用回数をインクリメント
        AIAP_Prompts::increment_used( $task['id'] );

        // RSS: 消費した記事を使用済み履歴に追加
        if ( ! empty( $rss_consumed ) ) {
            foreach ( $rss_consumed as $a ) {
                if ( ! empty( $a['feed_id'] ) && ! empty( $a['guid'] ) ) {
                    AIAP_RSS_State::mark_used( $a['feed_id'], $a['guid'], (int) $post_id );
                }
            }
        }

        // アイキャッチ画像の生成・設定
        // image_enabled / image_source の判定は AIAP_Image_Client::generate_and_attach() 内部に委譲。
        // 設定により無効化されている場合は false が返り、is_wp_error() で false 扱いとなりログは出ない。
        $preset_ip        = isset( $result['image_prompt'] ) ? (string) $result['image_prompt'] : '';
        $user_prompt_full = isset( $task['prompt'] ) ? (string) $task['prompt'] : '';
        $article_body     = isset( $result['content'] ) ? (string) $result['content'] : '';
        $image_result     = AIAP_Image_Client::generate_and_attach( $post_id, $result['title'], $result['slug'], $preset_ip, $user_prompt_full, $article_body, $effective_image_enabled, $source );
        if ( is_wp_error( $image_result ) ) {
            AIAP_Logger::add( 'error', 'log_thumbnail_failed', array(
                $source, $post_id, AIAP_AI_Client::format_error_for_log( $image_result ),
            ));
        }

        // SEO処理を適用
        AIAP_SEO::apply( $post_id, $result, $settings );

        $log_task_label = $task_label;
        if ( is_array( $post_date ) && ! empty( $post_date['date_str'] ) && ! empty( $post_date['time_str'] ) ) {
            $log_task_label .= ' / 対象: ' . $post_date['date_str'] . ' ' . $post_date['time_str'];
        }

        // 投稿作成ログ: RSS を使った場合はサマリを末尾に付与した拡張版キーを使う
        $rss_log_summary = ! empty( $rss_consumed )
            ? AIAP_RSS_Composer::build_log_summary( $rss_consumed, $rss_settings['rss_mode'] )
            : '';
        if ( $rss_log_summary !== '' ) {
            AIAP_Logger::add( 'success', 'log_post_created_with_rss', array(
                $source, $post_id, wp_strip_all_tags( (string) $result['title'] ), $log_task_label, $settings['provider'], $rss_log_summary,
            ) );
        } else {
            AIAP_Logger::add( 'success', 'log_post_created', array(
                $source, $post_id, wp_strip_all_tags( (string) $result['title'] ), $log_task_label, $settings['provider'],
            ) );
        }

        return $post_id;
    }

    /**
     * 今日のタスクを全実行（手動実行の空欄時に使用）
     *
     * Free プラン上限到達時は、残り枠ぶんだけ実行して残りはスキップする。
     * スキップ件数はログに記録される。
     *
     * @param string $source ログ用ソース名
     * @return int 実行件数
     */
    public static function run_today( $source = 'manual' ) {
        $tasks = AIAP_Prompts::get_today_tasks();
        $count = 0;
        $skipped = 0;

        foreach ( $tasks as $task ) {
            // 1件ごとに残り枠を確認(Pro なら常に true)
            if ( ! AIAP_License::can_post_more() ) {
                $skipped++;
                continue;
            }
            $result = self::run_single( $task, true );
            if ( ! is_wp_error( $result ) ) {
                $count++;
            }
        }

        if ( $skipped > 0 ) {
            AIAP_Logger::add( 'info', 'log_free_limit_reached_skip_cron' );
        }

        return $count;
    }
}
