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

class Yatoon_SF_API {

    const DEFAULT_GRAPH_VERSION = 'v25.0';

    /**
     * Set by verify_page_token()/get_fb_photos()/get_fb_feed_text_posts()
     * and verify_ig_token()/get_ig_media_direct() right before they return
     * a failure, so callers (Yatoon_SF_Sync) can tell a genuine dead token
     * apart from a transient hiccup and message/track it accordingly.
     * Defaults to false (transient) - callers should only ever show a
     * "go regenerate your token" message when this is explicitly true.
     */
    public static $last_fb_error_definitive = false;
    public static $last_ig_error_definitive = false;

    public static function get_graph_version() {
        $version = get_option( 'yatoon_sf_graph_version', self::DEFAULT_GRAPH_VERSION );
        $version = is_string( $version ) ? trim( $version ) : self::DEFAULT_GRAPH_VERSION;

        if ( preg_match( '/^\d+\.\d+$/', $version ) ) {
            $version = 'v' . $version;
        }

        if ( ! preg_match( '/^v\d+\.\d+$/', $version ) ) {
            $version = self::DEFAULT_GRAPH_VERSION;
        }

        return apply_filters( 'yatoon_sf_graph_version', $version );
    }

    private static function graph_base() {
        return 'https://graph.facebook.com/' . self::get_graph_version() . '/';
    }

    private static function ig_graph_base() {
        return 'https://graph.instagram.com/' . self::get_graph_version() . '/';
    }

    /**
     * Log a debug message, but only when WP_DEBUG is enabled, and never
     * with a raw access token included in the message.
     */
    private static function debug_log( $message ) {
        if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
            error_log( 'Yatoon Social Feed: ' . $message ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
        }
    }

    private static function graph_error_message( $data, $fallback = 'Facebook API request failed.' ) {
        if ( ! isset( $data['error'] ) || ! is_array( $data['error'] ) ) {
            return $fallback;
        }

        $error = $data['error'];
        $parts = array();
        if ( ! empty( $error['message'] ) ) $parts[] = $error['message'];
        if ( ! empty( $error['type'] ) )    $parts[] = 'Type: ' . $error['type'];
        if ( ! empty( $error['code'] ) )    $parts[] = 'Code: ' . $error['code'];
        if ( ! empty( $error['error_subcode'] ) ) $parts[] = 'Subcode: ' . $error['error_subcode'];

        return ! empty( $parts ) ? implode( ' | ', $parts ) : $fallback;
    }

    /**
     * True only when the Graph API response unambiguously means the
     * token itself is invalid/expired/revoked - Meta's OAuthException
     * code 190 (subcodes like 460 password changed, 463 session
     * expired, 467 invalid token, etc.).
     *
     * Everything else - rate limiting (codes 4, 17, 32, 613), transient
     * service errors (codes 1, 2), HTTP 5xx/429, timeouts, DNS hiccups -
     * is NOT this, and must never be reported to the site owner as "your
     * token expired, go regenerate it." A token that's actually still
     * valid can't be revived by simply re-pasting the same string, so
     * if re-saving the same token "fixes" a reported expiry, that's
     * proof the original failure was one of these transient cases, not
     * a real expiry - this is exactly the bug pattern this method exists
     * to stop.
     */
    private static function is_definitive_auth_failure( $data ) {
        if ( ! isset( $data['error'] ) || ! is_array( $data['error'] ) ) {
            return false;
        }
        $error = $data['error'];
        return isset( $error['type'] ) && 'OAuthException' === $error['type']
            && isset( $error['code'] ) && 190 === (int) $error['code'];
    }

    /**
     * True when an HTTP-level response code (no parsed error body,
     * or in addition to one) itself indicates a transient condition
     * worth retrying: rate limiting or a server-side hiccup at Meta.
     */
    private static function is_retryable_http_code( $code ) {
        return in_array( (int) $code, array( 429, 500, 502, 503, 504 ), true );
    }

    /**
     * wp_remote_get() with one automatic retry for transient failures
     * only (network-level errors, or HTTP 429/5xx). A genuine 4xx/200
     * response - including one carrying an OAuthException - is returned
     * immediately on the first try; retrying wouldn't change an actual
     * invalid-token response, only waste time.
     */
    private static function remote_get_with_retry( $url, $args ) {
        $resp = wp_remote_get( $url, $args );

        $should_retry = is_wp_error( $resp ) || self::is_retryable_http_code( wp_remote_retrieve_response_code( $resp ) );

        if ( $should_retry ) {
            usleep( 700000 ); // 0.7s - long enough to clear a momentary blip, short enough not to hurt cron/AJAX timeouts
            $resp = wp_remote_get( $url, $args );
        }

        return $resp;
    }

    /* ---------------------------------------------------------------
     *  Verify Page token + fetch full Page profile
     * ------------------------------------------------------------- */
    public static function verify_page_token( $token ) {
        self::$last_fb_error_definitive = false;
        $token = trim( $token );
        $url = self::graph_base() . 'me?' . http_build_query( array(
            'fields'       => 'id,name,about,fan_count,picture{url},cover',
            'access_token' => $token,
        ) );
        $resp = self::remote_get_with_retry( $url, array( 'timeout' => 15, 'sslverify' => true ) );
        if ( is_wp_error( $resp ) ) {
            self::debug_log( 'verify_page_token request error: ' . $resp->get_error_message() );
            return null;
        }
        $code = wp_remote_retrieve_response_code( $resp );
        $body = wp_remote_retrieve_body( $resp );
        $data = json_decode( $body, true );
        if ( $code !== 200 || isset( $data['error'] ) ) {
            $msg = isset( $data['error']['message'] ) ? $data['error']['message'] : 'HTTP ' . $code;
            self::debug_log( 'verify_page_token failed: ' . $msg );
            self::$last_fb_error_definitive = self::is_definitive_auth_failure( $data );
            return null;
        }
        return $data;
    }

    /* ---------------------------------------------------------------
     *  Find Instagram Business account linked to a FB Page
     *  Returns full profile: id, username, followers, biography, profile_picture
     * ------------------------------------------------------------- */
    public static function get_ig_account( $page_id, $page_token ) {
        $url = self::graph_base() . $page_id . '?' . http_build_query( array(
            'fields'       => 'instagram_business_account{id,username,followers_count,biography,profile_picture_url,website}',
            'access_token' => $page_token,
        ) );
        $resp = self::remote_get_with_retry( $url, array( 'timeout' => 15, 'sslverify' => true ) );
        if ( is_wp_error( $resp ) ) {
            self::debug_log( 'get_ig_account request error: ' . $resp->get_error_message() );
            return null;
        }
        $data = json_decode( wp_remote_retrieve_body( $resp ), true );
        if ( isset( $data['instagram_business_account'] ) ) {
            return $data['instagram_business_account'];
        }
        return null;
    }

    /* ---------------------------------------------------------------
     *  Fetch Facebook Page photos
     *  Note: only the first page of results (up to $limit items) is
     *  requested; the `paging.next` cursor is not followed. This is
     *  intentional for a display widget - it keeps sync fast and avoids
     *  hammering the Graph API - but means very high-volume Pages will
     *  only ever have their most recent photos considered.
     * ------------------------------------------------------------- */
    public static function get_fb_photos( $page_id, $page_token, $limit = 100 ) {
        self::$last_fb_error_definitive = false;
        $url = self::graph_base() . $page_id . '/photos?' . http_build_query( array(
            'type'         => 'uploaded',
            'fields'       => 'id,images,picture,link,name,created_time',
            'access_token' => $page_token,
            'limit'        => $limit,
        ) );
        $resp = self::remote_get_with_retry( $url, array( 'timeout' => 20, 'sslverify' => true ) );
        if ( is_wp_error( $resp ) ) {
            self::debug_log( 'get_fb_photos request error: ' . $resp->get_error_message() );
            return new WP_Error( 'yatoon_sf_fb_request_failed', $resp->get_error_message() );
        }
        $code = wp_remote_retrieve_response_code( $resp );
        $data = json_decode( wp_remote_retrieve_body( $resp ), true );
        if ( $code !== 200 || isset( $data['error'] ) ) {
            $msg = self::graph_error_message( $data, 'Facebook API request failed with HTTP ' . $code . '.' );
            self::debug_log( 'get_fb_photos API error: ' . $msg );
            self::$last_fb_error_definitive = self::is_definitive_auth_failure( $data );
            return new WP_Error( 'yatoon_sf_fb_api_error', $msg );
        }
        return isset( $data['data'] ) ? $data['data'] : array();
    }

    public static function get_fb_feed_text_posts( $page_id, $page_token, $limit = 50 ) {
        self::$last_fb_error_definitive = false;
        $url = self::graph_base() . $page_id . '/feed?' . http_build_query( array(
            'fields'       => 'id,message,permalink_url,created_time,full_picture',
            'access_token' => $page_token,
            'limit'        => $limit,
        ) );
        $resp = self::remote_get_with_retry( $url, array( 'timeout' => 20, 'sslverify' => true ) );
        if ( is_wp_error( $resp ) ) {
            self::debug_log( 'get_fb_feed_text_posts request error: ' . $resp->get_error_message() );
            return new WP_Error( 'yatoon_sf_fb_feed_request_failed', $resp->get_error_message() );
        }
        $code = wp_remote_retrieve_response_code( $resp );
        $data = json_decode( wp_remote_retrieve_body( $resp ), true );
        if ( $code !== 200 || isset( $data['error'] ) ) {
            $msg = self::graph_error_message( $data, 'Facebook feed API request failed with HTTP ' . $code . '.' );
            self::debug_log( 'get_fb_feed_text_posts API error: ' . $msg );
            self::$last_fb_error_definitive = self::is_definitive_auth_failure( $data );
            return new WP_Error( 'yatoon_sf_fb_feed_api_error', $msg );
        }
        return isset( $data['data'] ) ? $data['data'] : array();
    }

    /* ---------------------------------------------------------------
     *  Fetch Facebook Page videos.
     *  Note: only the first page of results is requested, same
     *  trade-off as get_fb_photos() above. The video file itself is
     *  referenced directly from Meta's CDN (not downloaded/cached
     *  locally like photo thumbnails are) - video files are far larger
     *  than images, and re-hosting them would multiply this site's
     *  storage and bandwidth use for little benefit, since Meta's CDN
     *  already serves them reliably.
     * ------------------------------------------------------------- */
    public static function get_fb_videos( $page_id, $page_token, $limit = 25 ) {
        self::$last_fb_error_definitive = false;
        $url = self::graph_base() . $page_id . '/videos?' . http_build_query( array(
            'fields'       => 'id,source,picture,permalink_url,description,created_time',
            'access_token' => $page_token,
            'limit'        => $limit,
        ) );
        $resp = self::remote_get_with_retry( $url, array( 'timeout' => 25, 'sslverify' => true ) );
        if ( is_wp_error( $resp ) ) {
            self::debug_log( 'get_fb_videos request error: ' . $resp->get_error_message() );
            return new WP_Error( 'yatoon_sf_fb_videos_request_failed', $resp->get_error_message() );
        }
        $code = wp_remote_retrieve_response_code( $resp );
        $data = json_decode( wp_remote_retrieve_body( $resp ), true );
        if ( $code !== 200 || isset( $data['error'] ) ) {
            $msg = self::graph_error_message( $data, 'Facebook videos API request failed with HTTP ' . $code . '.' );
            self::debug_log( 'get_fb_videos API error: ' . $msg );
            self::$last_fb_error_definitive = self::is_definitive_auth_failure( $data );
            return new WP_Error( 'yatoon_sf_fb_videos_api_error', $msg );
        }
        return isset( $data['data'] ) ? $data['data'] : array();
    }

    /* ---------------------------------------------------------------
     *  Instagram Login API (graph.instagram.com) - direct IG token.
     *  Used because the Page-linked instagram_business_account lookup
     *  requires Advanced Access permissions that need Meta App Review;
     *  this direct-token path works for any Instagram Business/Creator
     *  account using only Standard Access.
     * ------------------------------------------------------------- */
    public static function verify_ig_token( $ig_token ) {
        self::$last_ig_error_definitive = false;
        $ig_token = trim( $ig_token );
        $url = self::ig_graph_base() . 'me?' . http_build_query( array(
            'fields'       => 'id,username,name,followers_count,biography,profile_picture_url',
            'access_token' => $ig_token,
        ) );
        $resp = self::remote_get_with_retry( $url, array( 'timeout' => 15, 'sslverify' => true ) );
        if ( is_wp_error( $resp ) ) {
            self::debug_log( 'verify_ig_token request error: ' . $resp->get_error_message() );
            return null;
        }
        $data = json_decode( wp_remote_retrieve_body( $resp ), true );
        if ( isset( $data['error'] ) ) {
            $msg = isset( $data['error']['message'] ) ? $data['error']['message'] : 'unknown error';
            self::debug_log( 'verify_ig_token failed: ' . $msg );
            self::$last_ig_error_definitive = self::is_definitive_auth_failure( $data );
            return null;
        }
        return $data;
    }

    /**
     * Note: only the first page of results is requested (no `paging.next`
     * cursor following) - same trade-off as get_fb_photos() above.
     */
    public static function get_ig_media_direct( $ig_token, $limit = 100 ) {
        self::$last_ig_error_definitive = false;
        $url = self::ig_graph_base() . 'me/media?' . http_build_query( array(
            'fields'       => 'id,media_type,media_url,thumbnail_url,permalink,caption,timestamp',
            'access_token' => $ig_token,
            'limit'        => $limit,
        ) );
        $resp = self::remote_get_with_retry( $url, array( 'timeout' => 20, 'sslverify' => true ) );
        if ( is_wp_error( $resp ) ) {
            self::debug_log( 'get_ig_media_direct request error: ' . $resp->get_error_message() );
            return new WP_Error( 'yatoon_sf_ig_request_failed', $resp->get_error_message() );
        }
        $data = json_decode( wp_remote_retrieve_body( $resp ), true );
        if ( isset( $data['error'] ) ) {
            $msg = self::graph_error_message( $data, 'Instagram API request failed.' );
            self::debug_log( 'get_ig_media_direct API error: ' . $msg );
            self::$last_ig_error_definitive = self::is_definitive_auth_failure( $data );
            return new WP_Error( 'yatoon_sf_ig_api_error', $msg );
        }
        return isset( $data['data'] ) ? $data['data'] : array();
    }

    /* ---------------------------------------------------------------
     *  Format follower count nicely: 1234 -> "1.2K"
     * ------------------------------------------------------------- */
    public static function format_followers( $count ) {
        $count = (int) $count;
        if ( $count >= 1000000 ) return round( $count / 1000000, 1 ) . 'M';
        if ( $count >= 1000 )    return round( $count / 1000, 1 ) . 'K';
        return (string) $count;
    }
}
