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

/**
 * RSS 使用済み履歴 + 予約（reservation）管理
 *
 * フィードごとに最新 HISTORY_PER_FEED 件の guid を FIFO で保持する。
 * 並行採用レース対策として「予約」機構を併設:
 *   - pick_unused() で候補を選んだ瞬間に reserved に書き込む
 *   - 投稿成功時に mark_used() が reserved を消して used_guids に移す
 *   - 失敗時は RESERVATION_TTL_SECONDS で自然失効
 *
 * aiap_rss_state option を独占管理する（直接 get_option を呼ばないこと）。
 *
 * 構造:
 *   array(
 *       'feed_xxx' => array(
 *           'used_guids' => array( 'guid1', 'guid2', ... ),
 *           'reserved'   => array(
 *               'guid_A' => 1715766600,  // 失効 UNIX 秒
 *               'guid_B' => 1715766900,
 *           ),
 *       ),
 *       ...
 *   )
 */
class AIAP_RSS_State {

    const OPTION_KEY        = 'aiap_rss_state';
    const HISTORY_PER_FEED  = 500;
    const RESERVATION_TTL_SECONDS = 600;  // 10 分（cron + 手動 step 全体の最大想定時間）
    const LOCK_KEY                = 'rss_reserve';

    /**
     * 全状態を取得
     *
     * @return array
     */
    public static function get_all() {
        $state = get_option( self::OPTION_KEY, array() );
        return is_array( $state ) ? $state : array();
    }

    /**
     * 全状態を保存（autoload=no）
     *
     * @param array $state
     */
    private static function save_all( array $state ) {
        update_option( self::OPTION_KEY, $state, false );
    }

    /**
     * 指定フィードで guid が使用済みか
     *
     * @param string $feed_id
     * @param string $guid
     * @return bool
     */
    public static function is_used( $feed_id, $guid ) {
        if ( $feed_id === '' || $guid === '' ) {
            return false;
        }
        $state = self::get_all();
        if ( ! isset( $state[ $feed_id ]['used_guids'] ) || ! is_array( $state[ $feed_id ]['used_guids'] ) ) {
            return false;
        }
        return in_array( $guid, $state[ $feed_id ]['used_guids'], true );
    }

    /**
     * 指定フィードで guid が現在予約中か（TTL 内）
     *
     * 失効した予約は false を返すだけで、option 書き込みはしない（lazy prune は
     * 次回 reserve() 呼び出し時にまとめて行う）。
     *
     * @param string $feed_id
     * @param string $guid
     * @return bool
     */
    public static function is_reserved( $feed_id, $guid ) {
        if ( $feed_id === '' || $guid === '' ) {
            return false;
        }
        $state = self::get_all();
        if ( ! isset( $state[ $feed_id ]['reserved'] ) || ! is_array( $state[ $feed_id ]['reserved'] ) ) {
            return false;
        }
        if ( ! isset( $state[ $feed_id ]['reserved'][ $guid ] ) ) {
            return false;
        }
        return (int) $state[ $feed_id ]['reserved'][ $guid ] > time();
    }

    /**
     * guid 配列を指定 TTL で予約する（AIAP_Lock で保護）
     *
     * - used_guids に既存の guid は予約しない（冪等）
     * - 失効済み reserved エントリは prune する
     * - ロック取得失敗時は warning ログ + そのまま続行（既存挙動と整合）
     *
     * @param string $feed_id
     * @param array  $guid_list
     * @param int    $ttl_seconds
     */
    public static function reserve( $feed_id, array $guid_list, $ttl_seconds = self::RESERVATION_TTL_SECONDS ) {
        if ( $feed_id === '' || empty( $guid_list ) ) {
            return;
        }
        $ttl_seconds = max( 1, (int) $ttl_seconds );

        $lock_token = false;
        if ( class_exists( 'AIAP_Lock' ) ) {
            // TTL=10s, 10 回 × 100ms = 最大 1 秒待機（reserve は短時間操作）
            $lock_token = AIAP_Lock::acquire_with_retry( self::LOCK_KEY, 10, 10, 100000 );
        }

        try {
            $state = self::get_all();
            if ( ! isset( $state[ $feed_id ] ) || ! is_array( $state[ $feed_id ] ) ) {
                $state[ $feed_id ] = array( 'used_guids' => array(), 'reserved' => array() );
            }
            if ( ! isset( $state[ $feed_id ]['used_guids'] ) || ! is_array( $state[ $feed_id ]['used_guids'] ) ) {
                $state[ $feed_id ]['used_guids'] = array();
            }
            if ( ! isset( $state[ $feed_id ]['reserved'] ) || ! is_array( $state[ $feed_id ]['reserved'] ) ) {
                $state[ $feed_id ]['reserved'] = array();
            }

            // 失効済み prune
            $now = time();
            foreach ( $state[ $feed_id ]['reserved'] as $g => $exp ) {
                if ( (int) $exp <= $now ) {
                    unset( $state[ $feed_id ]['reserved'][ $g ] );
                }
            }

            // 新規予約
            $expires_at = $now + $ttl_seconds;
            $used_map   = array_flip( $state[ $feed_id ]['used_guids'] );
            foreach ( $guid_list as $guid ) {
                $guid = (string) $guid;
                if ( $guid === '' ) {
                    continue;
                }
                if ( isset( $used_map[ $guid ] ) ) {
                    continue;  // 既に使用済みなら予約不要
                }
                $state[ $feed_id ]['reserved'][ $guid ] = $expires_at;
            }

            self::save_all( $state );
        } finally {
            if ( $lock_token !== false && class_exists( 'AIAP_Lock' ) ) {
                AIAP_Lock::release( self::LOCK_KEY, $lock_token );
            }
        }
    }

    /**
     * 単一 guid の予約を解除する（mark_used 内部から呼ばれる）
     *
     * ロックは取らない（1 guid の操作なので競合時は失効 TTL に任せる）。
     *
     * @param string $feed_id
     * @param string $guid
     */
    public static function release_reservation( $feed_id, $guid ) {
        if ( $feed_id === '' || $guid === '' ) {
            return;
        }
        $state = self::get_all();
        if ( ! isset( $state[ $feed_id ]['reserved'][ $guid ] ) ) {
            return;
        }
        unset( $state[ $feed_id ]['reserved'][ $guid ] );
        // reserved が空配列になったら整理
        if ( empty( $state[ $feed_id ]['reserved'] ) ) {
            unset( $state[ $feed_id ]['reserved'] );
        }
        self::save_all( $state );
    }

    /**
     * guid を使用済みとしてマーク（FIFO 500 件、冪等）
     *
     * @param string $feed_id
     * @param string $guid
     * @param int    $post_id   将来の拡張用（現状未使用、postmeta 側で post→guid を辿るため）
     */
    public static function mark_used( $feed_id, $guid, $post_id = 0 ) {
        if ( $feed_id === '' || $guid === '' ) {
            return;
        }
        $state = self::get_all();
        if ( ! isset( $state[ $feed_id ] ) || ! is_array( $state[ $feed_id ] ) ) {
            $state[ $feed_id ] = array( 'used_guids' => array() );
        }
        if ( ! isset( $state[ $feed_id ]['used_guids'] ) || ! is_array( $state[ $feed_id ]['used_guids'] ) ) {
            $state[ $feed_id ]['used_guids'] = array();
        }
        // 冪等性
        if ( in_array( $guid, $state[ $feed_id ]['used_guids'], true ) ) {
            return;
        }
        $state[ $feed_id ]['used_guids'][] = $guid;
        // FIFO トリム
        $count = count( $state[ $feed_id ]['used_guids'] );
        if ( $count > self::HISTORY_PER_FEED ) {
            $state[ $feed_id ]['used_guids'] = array_slice(
                $state[ $feed_id ]['used_guids'],
                $count - self::HISTORY_PER_FEED
            );
        }
        self::save_all( $state );

        // 予約済みだった場合は予約も解除する（mark_used が「予約 → 確定」のゴール）
        self::release_reservation( $feed_id, $guid );

        unset( $post_id ); // 将来拡張のための予約引数（PHP の "未使用引数" 警告対策）
    }

    /**
     * 指定フィードの履歴をすべて削除（フィード削除時、または UI からの履歴クリア時に呼ぶ）
     *
     * @param string $feed_id
     * @return int 削除した guid 件数（0 ならエントリ無し）
     */
    public static function purge_feed( $feed_id ) {
        if ( $feed_id === '' ) {
            return 0;
        }
        $state = self::get_all();
        if ( ! isset( $state[ $feed_id ]['used_guids'] ) ) {
            return 0;
        }
        $count = count( $state[ $feed_id ]['used_guids'] );
        unset( $state[ $feed_id ] );
        self::save_all( $state );
        return $count;
    }

    /**
     * 指定フィードの使用済み件数を返す（UI 表示用）
     *
     * @param string $feed_id
     * @return int
     */
    public static function count_used( $feed_id ) {
        if ( $feed_id === '' ) {
            return 0;
        }
        $state = self::get_all();
        return isset( $state[ $feed_id ]['used_guids'] ) && is_array( $state[ $feed_id ]['used_guids'] )
            ? count( $state[ $feed_id ]['used_guids'] )
            : 0;
    }
}
