<?php

declare(strict_types=1);

class TwitterClient
{
    private CacheInterface $cache;
    private string $authorization;
    private $data;

    public function __construct(CacheInterface $cache)
    {
        $this->cache = $cache;

        $data = $this->cache->get('twitter') ?? [];
        $this->data = $data;

        $this->authorization = 'AAAAAAAAAAAAAAAAAAAAAGHtAgAAAAAA%2Bx7ILXNILCqkSGIzy6faIHZ9s3Q%3DQy97w6SIrzE7lQwPJEYQBsArEE2fC25caFwRBvAGi456G09vGR';
        $this->tw_consumer_key = '3nVuSoBZnx6U4vzUxf5w';
        $this->tw_consumer_secret = 'Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys';
        $this->oauth_token = ''; //Fill here
        $this->oauth_token_secret = ''; //Fill here
    }

    private function getOauthAuthorization(
        $oauth_token,
        $oauth_token_secret,
        $method = 'GET',
        $url = '',
        $body = '',
        $timestamp = null,
        $oauth_nonce = null
    ) {
        if (!$url) {
            return '';
        }
        $method = strtoupper($method);
        $parseUrl = parse_url($url);
        $link = $parseUrl['scheme'] . '://' . $parseUrl['host'] . $parseUrl['path'];
        parse_str($parseUrl['query'], $query_params);
        if ($body) {
            parse_str($body, $body_params);
            $query_params = array_merge($query_params, $body_params);
        }
        $payload = [
            'oauth_version' => '1.0',
            'oauth_signature_method' => 'HMAC-SHA1',
            'oauth_consumer_key' => $this->tw_consumer_key,
            'oauth_token' => $oauth_token,
            'oauth_nonce' => $oauth_nonce ? $oauth_nonce : implode('', array_fill(0, 3, strval(time()))),
            'oauth_timestamp' => $timestamp ? $timestamp : time(),
        ];
        $payload = array_merge($payload, $query_params);
        ksort($payload);

        $url_parts = parse_url($url);
        $url_parts['query'] = http_build_query($payload, '', '&', PHP_QUERY_RFC3986);
        $base_url = $url_parts['scheme'] . '://' . $url_parts['host'] . $url_parts['path'];
        $signature_base_string = strtoupper($method) . '&' . rawurlencode($base_url) . '&' . rawurlencode($url_parts['query']);
        $hmac_key = $this->tw_consumer_secret . '&' . $oauth_token_secret;
        $hex_signature = hash_hmac('sha1', $signature_base_string, $hmac_key, true);
        $signature = base64_encode($hex_signature);

        $header_params = [
            'oauth_version' => '1.0',
            'oauth_token' => $oauth_token,
            'oauth_nonce' => $payload['oauth_nonce'],
            'oauth_timestamp' => $payload['oauth_timestamp'],
            'oauth_signature' => $signature,
            'oauth_consumer_key' => $this->tw_consumer_key,
            'oauth_signature_method' => 'HMAC-SHA1',
        ];
        // ksort($header_params);
        $header_values = [];
        foreach ($header_params as $key => $value) {
            $header_values[] = rawurlencode($key) . '="' . (is_int($value) ? $value : rawurlencode($value)) . '"';
        }
        return 'OAuth realm="http://api.twitter.com/", ' . implode(', ', $header_values);
    }

    private function extractTweetAndUsersFromGraphQL($timeline)
    {
        if (isset($timeline->data->user)) {
            $result = $timeline->data->user->result;
            $instructions = $result->timeline_v2->timeline->instructions;
        } elseif (isset($timeline->data->user_result)) {
            $result = $timeline->data->user_result->result->timeline_response;
            $instructions = $result->timeline->instructions;
        }

        if (isset($result->__typename) && $result->__typename === 'UserUnavailable') {
            throw new \Exception('UserUnavailable');
        }

        if (isset($timeline->data->list)) {
            $result = $timeline->data->list->timeline_response;
            $instructions = $result->timeline->instructions;
        }

        if (!isset($result) && !isset($instructions)) {
            throw new \Exception('Unable to fetch user/list timeline');
        }

        $instructionTypes = [
            'TimelineAddEntries',
            'TimelineClearCache',
            'TimelinePinEntry', // unclear purpose, maybe pinned tweet?
        ];
        if (!isset($instructions[1]) && isset($timeline->data->user)) {
            throw new \Exception('The account exists but has not tweeted yet?');
        }

        $entries = null;
        foreach ($instructions as $instruction) {
            $instructionType = '';
            if (isset($instruction->type)) {
                $instructionType = $instruction->type;
            } else {
                $instructionType = $instruction->__typename;
            }

            if ($instructionType === 'TimelineAddEntries') {
                $entries = $instruction->entries;
                break;
            }
        }
        if (!$entries) {
            throw new \Exception(sprintf('Unable to find time line tweets in: %s', implode(',', array_column($instructions, 'type'))));
        }

        $tweets = [];
        $userIds = [];
        foreach ($entries as $entry) {
            $entryType = '';

            if (isset($entry->content->entryType)) {
                $entryType = $entry->content->entryType;
            } else {
                $entryType = $entry->content->__typename;
            }

            if ($entryType !== 'TimelineTimelineItem') {
                continue;
            }

            if (isset($timeline->data->user)) {
                if (!isset($entry->content->itemContent->tweet_results->result)) {
                    continue;
                }

                if (isset($entry->content->itemContent->promotedMetadata)) {
                    continue;
                }

                $tweets[] = $entry->content->itemContent->tweet_results->result;

                $userIds[] = $entry->content->itemContent->tweet_results->result->core->user_results->result;
            } else {
                if (!isset($entry->content->content->tweetResult->result->legacy)) {
                    continue;
                }

                // Filter out any advertise tweet
                if (isset($entry->content->content->tweetPromotedMetadata)) {
                    continue;
                }

                $tweets[] = $entry->content->content->tweetResult->result;

                $userIds[] = $entry->content->content->tweetResult->result->core->user_result->result;
            }
        }

        return (object) [
            'userIds' => $userIds,
            'tweets' => $tweets,
        ];
    }

    private function extractTweetFromSearch($searchResult)
    {
        return $searchResult->statuses;
    }

    public function fetchUserTweets(string $screenName): \stdClass
    {
        $this->fetchGuestToken();
        try {
            $userInfo = $this->fetchUserInfoByScreenName($screenName);
        } catch (HttpException $e) {
            if ($e->getCode() === 403) {
                $this->data['guest_token'] = null;
                $this->fetchGuestToken();
                $userInfo = $this->fetchUserInfoByScreenName($screenName);
            } else {
                throw $e;
            }
        }

        $timeline = $this->fetchTimeline($userInfo->rest_id);
        // try {
        //     // $timeline = $this->fetchTimelineUsingSearch($screenName);
        // } catch (HttpException $e) {
        //     if ($e->getCode() === 403) {
        //         $this->data['guest_token'] = null;
        //         $this->fetchGuestToken();
        //         // $timeline = $this->fetchTimelineUsingSearch($screenName);
        //         $timeline = $this->fetchTimeline($userInfo->rest_id);
        //     } else {
        //         throw $e;
        //     }
        // }

        // $tweets = $this->extractTweetFromSearch($timeline);
        $tweets = $this->extractTweetAndUsersFromGraphQL($timeline)->tweets;

        return (object) [
            'user_info' => $userInfo,
            'tweets' => $tweets,
        ];
    }

    public function fetchListTweets($query, $operation = '')
    {
        $id = '';
        $this->fetchGuestToken();
        if ($operation == 'By list') {
            try {
                $listInfo = $this->fetchListInfoBySlug($query['screenName'], $query['listSlug']);
                $id = $listInfo->id_str;
            } catch (HttpException $e) {
                if ($e->getCode() === 403) {
                    $this->data['guest_token'] = null;
                    $this->fetchGuestToken();
                    $listInfo = $this->fetchListInfoBySlug($query['screenName'], $query['listSlug']);
                    $id = $listInfo->id_str;
                } else {
                    throw $e;
                }
            }
        } else if ($operation == 'By list ID') {
            $id = $query['listId'];
        } else {
            throw new \Exception('Unknown operation to make list tweets');
        }

        try {
            $timeline = $this->fetchListTimeline($id);
        } catch (HttpException $e) {
            if ($e->getCode() === 403) {
                $this->data['guest_token'] = null;
                $this->fetchGuestToken();
                $timeline = $this->fetchListTimeline($id);
            } else {
                throw $e;
            }
        }

        $data = $this->extractTweetAndUsersFromGraphQL($timeline);

        return $data;
    }

    private function fetchGuestToken(): void
    {
        if (isset($this->data['guest_token'])) {
            return;
        }
        $url = 'https://api.twitter.com/1.1/guest/activate.json';
        $response = getContents($url, $this->createHttpHeaders(), [CURLOPT_POST => true]);
        $guest_token = json_decode($response)->guest_token;
        $this->data['guest_token'] = $guest_token;

        $this->cache->set('twitter', $this->data);
    }

    private function fetchUserInfoByScreenName(string $screenName)
    {
        if (isset($this->data[$screenName])) {
            return $this->data[$screenName];
        }
        $variables = [
            'screen_name' => $screenName,
            'withHighlightedLabel' => true
        ];
        $url = sprintf(
            'https://twitter.com/i/api/graphql/hc-pka9A7gyS3xODIafnrQ/UserByScreenName?variables=%s',
            urlencode(json_encode($variables))
        );
        $response = Json::decode(getContents($url, $this->createHttpHeaders()), false);
        if (isset($response->errors)) {
            // Grab the first error message
            throw new \Exception(sprintf('From twitter api: "%s"', $response->errors[0]->message));
        }
        $userInfo = $response->data->user;
        $this->data[$screenName] = $userInfo;

        $this->cache->set('twitter', $this->data);
        return $userInfo;
    }

    private function fetchTimeline($userId)
    {
        $variables = [
            'autoplay_enabled' => true,
            'count' => 40,
            'includeEditControl' => true,
            'includeEditPerspective' => false,
            'includeHasBirdwatchNotes' => false,
            'includeTweetImpression' => true,
            'includeTweetVisibilityNudge' => true,
            'rest_id' => $userId
        ];
        $features = [
            'android_graphql_skip_api_media_color_palette' => true,
            'blue_business_profile_image_shape_enabled' => true,
            'creator_subscriptions_subscription_count_enabled' => true,
            'creator_subscriptions_tweet_preview_api_enabled' => true,
            'freedom_of_speech_not_reach_fetch_enabled' => true,
            'longform_notetweets_consumption_enabled' => true,
            'longform_notetweets_inline_media_enabled' => true,
            'longform_notetweets_rich_text_read_enabled' => true,
            'subscriptions_verification_info_enabled' => true,
            'super_follow_badge_privacy_enabled' => true,
            'super_follow_exclusive_tweet_notifications_enabled' => true,
            'super_follow_tweet_api_enabled' => true,
            'super_follow_user_api_enabled' => true,
            'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled' => true,
            'tweetypie_unmention_optimization_enabled' => true,
            'unified_cards_ad_metadata_container_dynamic_card_content_query_enabled' => true,
        ];
        $url = sprintf(
            'https://api.twitter.com/graphql/3JNH4e9dq1BifLxAa3UMWg/UserWithProfileTweetsQueryV2?variables=%s&features=%s',
            urlencode(json_encode($variables)),
            urlencode(json_encode($features))
        );
        $oauth = $this->getOauthAuthorization($this->oauth_token, $this->oauth_token_secret, 'GET', $url);
        $response = Json::decode(getContents($url, $this->createHttpHeaders($oauth)), false);
        return $response;
    }

    private function fetchTimelineUsingSearch($screenName)
    {
        $params = [
            'q' => 'from:' . $screenName,
            'modules' => 'status',
            'result_type' => 'recent'
        ];
        $response = $this->search($params);
        return $response;
    }

    public function search($queryParam)
    {
         $url = sprintf(
             'https://api.twitter.com/1.1/search/tweets.json?%s',
             http_build_query($queryParam)
         );
        $oauth = $this->getOauthAuthorization($this->oauth_token, $this->oauth_token_secret, 'GET', $url);
        $response = Json::decode(getContents($url, $this->createHttpHeaders($oauth)), false);
        return $response;
    }

    private function fetchListInfoBySlug($screenName, $listSlug)
    {
        if (isset($this->data[$screenName . '-' . $listSlug])) {
            return $this->data[$screenName . '-' . $listSlug];
        }

        $features = [
            'android_graphql_skip_api_media_color_palette' => false,
            'blue_business_profile_image_shape_enabled' => false,
            'creator_subscriptions_subscription_count_enabled' => false,
            'creator_subscriptions_tweet_preview_api_enabled' => true,
            'freedom_of_speech_not_reach_fetch_enabled' => false,
            'graphql_is_translatable_rweb_tweet_is_translatable_enabled' => false,
            'hidden_profile_likes_enabled' => false,
            'highlights_tweets_tab_ui_enabled' => false,
            'interactive_text_enabled' => false,
            'longform_notetweets_consumption_enabled' => true,
            'longform_notetweets_inline_media_enabled' => false,
            'longform_notetweets_richtext_consumption_enabled' => true,
            'longform_notetweets_rich_text_read_enabled' => false,
            'responsive_web_edit_tweet_api_enabled' => false,
            'responsive_web_enhance_cards_enabled' => false,
            'responsive_web_graphql_exclude_directive_enabled' => true,
            'responsive_web_graphql_skip_user_profile_image_extensions_enabled' => false,
            'responsive_web_graphql_timeline_navigation_enabled' => false,
            'responsive_web_media_download_video_enabled' => false,
            'responsive_web_text_conversations_enabled' => false,
            'responsive_web_twitter_article_tweet_consumption_enabled' => false,
            'responsive_web_twitter_blue_verified_badge_is_enabled' => true,
            'rweb_lists_timeline_redesign_enabled' => true,
            'spaces_2022_h2_clipping' => true,
            'spaces_2022_h2_spaces_communities' => true,
            'standardized_nudges_misinfo' => false,
            'subscriptions_verification_info_enabled' => true,
            'subscriptions_verification_info_reason_enabled' => true,
            'subscriptions_verification_info_verified_since_enabled' => true,
            'super_follow_badge_privacy_enabled' => false,
            'super_follow_exclusive_tweet_notifications_enabled' => false,
            'super_follow_tweet_api_enabled' => false,
            'super_follow_user_api_enabled' => false,
            'tweet_awards_web_tipping_enabled' => false,
            'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled' => false,
            'tweetypie_unmention_optimization_enabled' => false,
            'unified_cards_ad_metadata_container_dynamic_card_content_query_enabled' => false,
            'verified_phone_label_enabled' => false,
            'vibe_api_enabled' => false,
            'view_counts_everywhere_api_enabled' => false
        ];
        $variables = [
            'screenName' => $screenName,
            'listSlug' => $listSlug
        ];

        $url = sprintf(
            'https://twitter.com/i/api/graphql/-kmqNvm5Y-cVrfvBy6docg/ListBySlug?variables=%s&features=%s',
            urlencode(json_encode($variables)),
            urlencode(json_encode($features))
        );

        $response = Json::decode(getContents($url, $this->createHttpHeaders()), false);
        if (isset($response->errors)) {
            // Grab the first error message
            throw new \Exception(sprintf('From twitter api: "%s"', $response->errors[0]->message));
        }
        $listInfo = $response->data->user_by_screen_name->list;
        $this->data[$screenName . '-' . $listSlug] = $listInfo;

        $this->cache->set('twitter', $this->data);
        return $listInfo;
    }

    private function fetchListTimeline($listId)
    {
        $features = [
            'android_graphql_skip_api_media_color_palette' => false,
            'blue_business_profile_image_shape_enabled' => false,
            'creator_subscriptions_subscription_count_enabled' => false,
            'creator_subscriptions_tweet_preview_api_enabled' => true,
            'freedom_of_speech_not_reach_fetch_enabled' => false,
            'graphql_is_translatable_rweb_tweet_is_translatable_enabled' => false,
            'hidden_profile_likes_enabled' => false,
            'highlights_tweets_tab_ui_enabled' => false,
            'interactive_text_enabled' => false,
            'longform_notetweets_consumption_enabled' => true,
            'longform_notetweets_inline_media_enabled' => false,
            'longform_notetweets_richtext_consumption_enabled' => true,
            'longform_notetweets_rich_text_read_enabled' => false,
            'responsive_web_edit_tweet_api_enabled' => false,
            'responsive_web_enhance_cards_enabled' => false,
            'responsive_web_graphql_exclude_directive_enabled' => true,
            'responsive_web_graphql_skip_user_profile_image_extensions_enabled' => false,
            'responsive_web_graphql_timeline_navigation_enabled' => false,
            'responsive_web_media_download_video_enabled' => false,
            'responsive_web_text_conversations_enabled' => false,
            'responsive_web_twitter_article_tweet_consumption_enabled' => false,
            'responsive_web_twitter_blue_verified_badge_is_enabled' => true,
            'rweb_lists_timeline_redesign_enabled' => true,
            'spaces_2022_h2_clipping' => true,
            'spaces_2022_h2_spaces_communities' => true,
            'standardized_nudges_misinfo' => false,
            'subscriptions_verification_info_enabled' => true,
            'subscriptions_verification_info_reason_enabled' => true,
            'subscriptions_verification_info_verified_since_enabled' => true,
            'super_follow_badge_privacy_enabled' => false,
            'super_follow_exclusive_tweet_notifications_enabled' => false,
            'super_follow_tweet_api_enabled' => false,
            'super_follow_user_api_enabled' => false,
            'tweet_awards_web_tipping_enabled' => false,
            'tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled' => false,
            'tweetypie_unmention_optimization_enabled' => false,
            'unified_cards_ad_metadata_container_dynamic_card_content_query_enabled' => false,
            'verified_phone_label_enabled' => false,
            'vibe_api_enabled' => false,
            'view_counts_everywhere_api_enabled' => false
        ];
        $variables = [
            'rest_id' => $listId,
            'count' => 20
        ];

        $url = sprintf(
            'https://api.twitter.com/graphql/BbGLL1ZfMibdFNWlk7a0Pw/ListTimeline?variables=%s&features=%s',
            urlencode(json_encode($variables)),
            urlencode(json_encode($features))
        );
        $oauth = $this->getOauthAuthorization($this->oauth_token, $this->oauth_token_secret, 'GET', $url);
        $response = Json::decode(getContents($url, $this->createHttpHeaders($oauth)), false);
        return $response;
    }

    private function createHttpHeaders($oauth = null): array
    {
        $headers = [
            'authorization' => sprintf('Bearer %s', $this->authorization),
            'x-guest-token' => $this->data['guest_token'] ?? null,
        ];
        if (isset($oauth)) {
            $headers['authorization'] = $oauth;
            unset($headers['x-guest-token']);
        }
        foreach ($headers as $key => $value) {
            $headers2[] = sprintf('%s: %s', $key, $value);
        }
        return $headers2;
    }
}