<?php

if ( ! defined( 'ABSPATH' ) ) {

    exit;

}



/**

 * RSS フィードライブラリ（CRUD + フェッチ + 未使用記事選別）

 *

 * aiap_rss_feeds option を独占管理する（直接 get_option を呼ばないこと）。

 *

 * 構造:

 *   array(

 *       array(

 *           'id'              => 'feed_xxx',

 *           'name'            => '表示名',

 *           'url'             => 'https://...',

 *           'enabled'         => 1,

 *           'created_at'      => 'YYYY-MM-DD HH:MM:SS',

 *           'last_fetched_at' => 'YYYY-MM-DD HH:MM:SS' or '',

 *           'last_error'      => '',

 *       ),

 *       ...

 *   )

 */

class AIAP_RSS_Feeds {



    const OPTION_KEY          = 'aiap_rss_feeds';

    const FEEDS_MAX           = 30;

    const COUNT_MAX           = 10;

    const CACHE_TTL_SECONDS   = 3600;

    const FETCH_TIMEOUT       = 15;



    /**

     * 全フィードを取得

     *

     * @return array

     */

    public static function get_all() {

        $feeds = get_option( self::OPTION_KEY, array() );

        return is_array( $feeds ) ? $feeds : array();

    }



    /**

     * 全フィードを保存（autoload=no）

     *

     * @param array $feeds

     */

    private static function save_all( array $feeds ) {
        update_option( self::OPTION_KEY, array_values( $feeds ), false );
    }

    private static function validate_feed_url( $url ) {
        $url = esc_url_raw( (string) $url );
        if ( $url === '' || ! function_exists( 'wp_http_validate_url' ) || ! wp_http_validate_url( $url ) ) {
            return new WP_Error( 'rss_invalid_url', __( 'Valid RSS URL is required.', 'ai-auto-poster' ) );
        }
        if ( class_exists( 'AIAP_Http' ) ) {
            $safe_url = AIAP_Http::is_safe_external_url( $url );
            if ( is_wp_error( $safe_url ) ) {
                return $safe_url;
            }
        }
        return $url;
    }


    /**

     * ID でフィードを取得

     *

     * @param string $feed_id

     * @return array|null

     */

    public static function get_by_id( $feed_id ) {

        if ( $feed_id === '' ) {

            return null;

        }

        foreach ( self::get_all() as $feed ) {

            if ( isset( $feed['id'] ) && $feed['id'] === $feed_id ) {

                return $feed;

            }

        }

        return null;

    }



    /**

     * フィードを追加（FEEDS_MAX 件まで）

     *

     * @param array $input { name, url, enabled }

     * @return string|WP_Error 新規 feed_id or エラー

     */

    public static function add( $input ) {

        $name    = isset( $input['name'] ) ? sanitize_text_field( (string) $input['name'] ) : '';

        $url     = isset( $input['url'] ) ? (string) $input['url'] : '';
        $enabled = ! empty( $input['enabled'] ) ? 1 : 0;



        if ( $name === '' ) {

            return new WP_Error( 'rss_invalid_name', __( 'Feed name is required.', 'ai-auto-poster' ) );

        }

        $url = self::validate_feed_url( $url );
        if ( is_wp_error( $url ) ) {
            return $url;
        }
        // SSRF 防止: 内部ネットワーク (127.0.0.1 / 169.254.169.254 等) への接続を弾く。

        // test_connection() と同じ防御を保存時にも適用する。保存後の URL は後で fetch_feed() で

        // 取得されるため、テスト接続を回避して保存だけ行うルートでも内部 IP を弾く必要がある。

        $feeds = self::get_all();

        if ( count( $feeds ) >= self::FEEDS_MAX ) {

            return new WP_Error( 'rss_feeds_max', sprintf(

                /* translators: %d = max feed count */

                __( 'Cannot register more than %d RSS feeds.', 'ai-auto-poster' ),

                self::FEEDS_MAX

            ) );

        }



        $id     = 'feed_' . uniqid();

        $feeds[] = array(

            'id'              => $id,

            'name'            => $name,

            'url'             => $url,

            'enabled'         => $enabled,

            'created_at'      => current_time( 'mysql' ),

            'last_fetched_at' => '',

            'last_error'      => '',

        );

        self::save_all( $feeds );

        return $id;

    }



    /**

     * フィードを更新（id は変更不可、created_at は保持）

     *

     * @param string $feed_id

     * @param array  $input    { name?, url?, enabled? }

     * @return true|WP_Error

     */

    public static function update( $feed_id, $input ) {

        $feeds = self::get_all();

        $found = false;

        foreach ( $feeds as &$feed ) {

            if ( ! isset( $feed['id'] ) || $feed['id'] !== $feed_id ) {

                continue;

            }

            $found = true;

            if ( isset( $input['name'] ) ) {

                $name = sanitize_text_field( (string) $input['name'] );

                if ( $name === '' ) {

                    return new WP_Error( 'rss_invalid_name', __( 'Feed name is required.', 'ai-auto-poster' ) );

                }

                $feed['name'] = $name;

            }

            if ( isset( $input['url'] ) ) {

                $url = self::validate_feed_url( $input['url'] );
                if ( is_wp_error( $url ) ) {
                    return $url;
                }
                // SSRF 防止 (add() と同じ防御を update() にも適用)

                $feed['url'] = $url;
            }

            if ( array_key_exists( 'enabled', $input ) ) {

                $feed['enabled'] = ! empty( $input['enabled'] ) ? 1 : 0;

            }

            break;

        }

        unset( $feed );

        if ( ! $found ) {

            return new WP_Error( 'rss_not_found', __( 'Feed not found.', 'ai-auto-poster' ) );

        }

        self::save_all( $feeds );

        return true;

    }



    /**

     * 取得成功時刻と直近エラーを更新

     *

     * @param string $feed_id

     * @param string $when    YYYY-MM-DD HH:MM:SS

     */

    public static function update_last_fetched( $feed_id, $when ) {

        $feeds = self::get_all();

        foreach ( $feeds as &$feed ) {

            if ( isset( $feed['id'] ) && $feed['id'] === $feed_id ) {

                $feed['last_fetched_at'] = $when;

                $feed['last_error']      = '';

                self::save_all( $feeds );

                return;

            }

        }

    }



    /**

     * 直近エラーを記録

     *

     * @param string $feed_id

     * @param string $message

     */

    public static function update_last_error( $feed_id, $message ) {

        $feeds = self::get_all();

        foreach ( $feeds as &$feed ) {

            if ( isset( $feed['id'] ) && $feed['id'] === $feed_id ) {

                $feed['last_error'] = sanitize_text_field( $message );

                self::save_all( $feeds );

                return;

            }

        }

    }



    /**

     * フィードを削除（履歴も連動削除）

     *

     * @param string $feed_id

     * @return true|WP_Error

     */

    public static function delete( $feed_id ) {

        $feeds = self::get_all();

        $found = false;

        $new   = array();

        foreach ( $feeds as $feed ) {

            if ( isset( $feed['id'] ) && $feed['id'] === $feed_id ) {

                $found = true;

                continue;

            }

            $new[] = $feed;

        }

        if ( ! $found ) {

            return new WP_Error( 'rss_not_found', __( 'Feed not found.', 'ai-auto-poster' ) );

        }

        self::save_all( $new );

        if ( class_exists( 'AIAP_RSS_State' ) ) {

            AIAP_RSS_State::purge_feed( $feed_id );

        }

        return true;

    }



    /**

     * 接続テスト（指定 URL の取得が成功するかと、先頭 1 件のサンプルを返す）

     *

     * @param string $url

     * @return array|WP_Error  array( 'item_count' => int, 'sample_title' => string ) or WP_Error

     */

    public static function test_connection( $url ) {

        $url = self::validate_feed_url( $url );
        if ( is_wp_error( $url ) ) {
            return $url;
        }
        // SSRF 防止: 内部ネットワーク (127.0.0.1 / 169.254.169.254 等) への接続を弾く。

        // 管理者 nonce で守られていても、解決先が内部 IP の URL を受け付けると

        // クラウドメタデータエンドポイント等への意図しない接続を許してしまう。

        // 接続テストはキャッシュを無視してリアルタイム取得したいので、TTL=0 を一時的に適用

        $ttl_filter = function () { return 0; };

        add_filter( 'wp_feed_cache_transient_lifetime', $ttl_filter );

        $rss = fetch_feed( $url );

        remove_filter( 'wp_feed_cache_transient_lifetime', $ttl_filter );



        if ( is_wp_error( $rss ) ) {

            return $rss;

        }

        $count  = $rss->get_item_quantity( 50 );

        $items  = $rss->get_items( 0, 1 );

        $sample = '';

        if ( ! empty( $items ) ) {

            $sample = wp_strip_all_tags( (string) $items[0]->get_title() );

            $sample = AIAP_Compat::mb_substr( $sample, 0, 100 );

        }

        return array(

            'item_count'   => (int) $count,

            'sample_title' => $sample,

        );

    }



    /**

     * 選択フィードから未使用記事を最大 $count 件、公開日新しい順で返す

     *

     * フィード単位の取得失敗は当該フィードのみスキップし、他フィードで継続する。

     * SimplePie の transient キャッシュ TTL を CACHE_TTL_SECONDS に一時設定。

     *

     * @param array $feed_ids 選択フィードの id 配列

     * @param int   $count    取得件数（1〜COUNT_MAX にクランプ）

     * @return array 記事配列。各要素は feed_id/feed_name/guid/title/description/link/pub_date/pub_ts を持つ

     */

    public static function pick_unused( array $feed_ids, $count ) {

        $count = (int) $count;

        if ( $count < 1 ) {

            $count = 1;

        }

        if ( $count > self::COUNT_MAX ) {

            $count = self::COUNT_MAX;

        }

        if ( empty( $feed_ids ) ) {

            return array();

        }



        $ttl_filter = function () { return self::CACHE_TTL_SECONDS; };

        $all_articles = array();



        foreach ( $feed_ids as $feed_id ) {

            $feed_id = (string) $feed_id;

            $feed    = self::get_by_id( $feed_id );

            if ( ! $feed || empty( $feed['enabled'] ) ) {
                continue;
            }

            $feed_url = self::validate_feed_url( isset( $feed['url'] ) ? (string) $feed['url'] : '' );
            if ( is_wp_error( $feed_url ) ) {
                AIAP_Logger::add( 'error', 'log_rss_fetch_failed', array(
                    $feed['name'], $feed_url->get_error_message(),
                ) );
                self::update_last_error( $feed_id, $feed_url->get_error_message() );
                continue;
            }

            add_filter( 'wp_feed_cache_transient_lifetime', $ttl_filter );
            $rss = fetch_feed( $feed_url );
            remove_filter( 'wp_feed_cache_transient_lifetime', $ttl_filter );


            if ( is_wp_error( $rss ) ) {

                AIAP_Logger::add( 'error', 'log_rss_fetch_failed', array(

                    $feed['name'], $rss->get_error_message(),

                ) );

                self::update_last_error( $feed_id, $rss->get_error_message() );

                continue;

            }



            self::update_last_fetched( $feed_id, current_time( 'mysql' ) );



            $max_items = $rss->get_item_quantity( 50 );

            foreach ( $rss->get_items( 0, $max_items ) as $item ) {

                $guid = trim( (string) $item->get_id() );

                if ( $guid === '' ) {

                    $guid = (string) $item->get_permalink();

                }

                if ( $guid === '' ) {

                    continue;

                }

                if ( class_exists( 'AIAP_RSS_State' ) ) {

                    if ( AIAP_RSS_State::is_used( $feed_id, $guid ) ) {

                        continue;

                    }

                    if ( AIAP_RSS_State::is_reserved( $feed_id, $guid ) ) {

                        // 他経路（並行 cron / Quick Post）が予約中の記事はスキップ。

                        // 並行採用レース対策（spec: 2026-05-15-concurrency-fixes-design）。

                        continue;

                    }

                }



                $pub_ts   = (int) $item->get_date( 'U' );

                $pub_date = $pub_ts > 0 ? wp_date( 'Y-m-d H:i:s', $pub_ts ) : '';



                $all_articles[] = array(

                    'feed_id'     => $feed_id,

                    'feed_name'   => isset( $feed['name'] ) ? $feed['name'] : '',

                    'guid'        => $guid,

                    'title'       => wp_strip_all_tags( (string) $item->get_title() ),

                    'description' => wp_strip_all_tags( (string) $item->get_description() ),

                    'link'        => esc_url_raw( (string) $item->get_permalink() ),

                    'pub_date'    => $pub_date,

                    'pub_ts'      => $pub_ts,

                );

            }

        }



        // 公開日の新しい順

        usort( $all_articles, function ( $a, $b ) {

            return $b['pub_ts'] <=> $a['pub_ts'];

        } );



        $picked = array_slice( $all_articles, 0, $count );



        // 採用候補を予約（並行する他経路が同じ記事を pick できないようにする）

        if ( ! empty( $picked ) && class_exists( 'AIAP_RSS_State' ) ) {

            $picked_by_feed = array();

            foreach ( $picked as $a ) {

                $fid  = isset( $a['feed_id'] ) ? (string) $a['feed_id'] : '';

                $guid = isset( $a['guid'] )    ? (string) $a['guid']    : '';

                if ( $fid === '' || $guid === '' ) {

                    continue;

                }

                $picked_by_feed[ $fid ][] = $guid;

            }

            foreach ( $picked_by_feed as $fid => $guids ) {

                AIAP_RSS_State::reserve( $fid, $guids );

            }

        }



        return $picked;

    }

}

