<?php

namespace Acms\Services\SocialLogin;

use Acms\Services\Facades\Session;

class Twitter extends Base
{
    const API_HOST = 'https://api.twitter.com';

    const SIGNATURE_METHOD = 'HMAC-SHA1';

    /**
     * @var string
     */
    protected $consumerKey;

    /**
     * @var string
     */
    protected $consumerSecret;

    /**
     * @var string
     */
    protected $version;

    /**
     * @var object
     */
    protected $me;

    /**
     * Twitter constructor.
     * @param string $consumerKey
     * @param string $consumerSecret
     */
    public function __construct(
        string $consumerKey,
        string $consumerSecret,
        string $version
    ) {
        $this->consumerKey = $consumerKey;
        $this->consumerSecret = $consumerSecret;
        $this->version = $version;
    }

    /**
     * @return string
     */
    protected function getCallbackUrl()
    {
        return acmsLink(array('bid' => BID, '_protocol' => 'http'), false) . 'callback/signin/twitter.html';
    }

    /**
     * @return string
     */
    protected function getRequestTokenUrl(): string
    {
        return self::API_HOST . '/oauth/request_token';
    }

    /**
     * Get Authenticate url
     * @param string $oauthToken
     *
     * @return string
     */
    public function getAuthenticateUrl(string $oauthToken)
    {
        return self::API_HOST . '/oauth/authenticate' . '?' . http_build_query(
            ['oauth_token' => $oauthToken],
            '',
            '&',
            PHP_QUERY_RFC3986
        );
    }

    /**
     * @return string
     */
    protected function getAccessTokenUrl()
    {
        return self::API_HOST . '/oauth/access_token';
    }

    /**
     * @return string
     */
    protected function getVerifyCredentialsUrl()
    {
        return self::API_HOST . '/1.1/account/verify_credentials.json';
    }

    /**
     * @param string $oauthToken
     *
     * @return array{
     *  request_token: string,
     *  request_token_secret: string
     * }
     * @throws \RuntimeException
     */
    public function obtainRequestToken(): array
    {
        $url = $this->getRequestTokenUrl();
        $params = [
            'oauth_callback' => $this->getCallbackUrl(),
            'oauth_consumer_key' => $this->consumerKey,
            'oauth_nonce' => md5(microtime() . mt_rand()),
            'oauth_signature_method' => self::SIGNATURE_METHOD,
            'oauth_timestamp' => time(),
            'oauth_version' => $this->version
        ];

        $ch = curl_init();

        $headers = [
            $this->createAuthorizationOAuthHeader($url, $params, 'POST')
        ];

        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($ch);

        if (curl_errno($ch)) {
            throw new \RuntimeException(curl_error($ch));
        }

        if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
            throw new \RuntimeException(json_decode($response)->errors[0]->message);
        }
        curl_close($ch);
        parse_str($response, $response);

        if ($response['oauth_callback_confirmed'] !== 'true') {
            throw new \RuntimeException('Callback URL not confirmed.');
        }

        return [
            'request_token' => $response['oauth_token'],
            'request_token_secret' => $response['oauth_token_secret']
        ];
    }

    /**
     * @param string $oauthVerifier
     * @param string $requestToken
     *
     * @return array{
     *  access_token: string,
     *  access_token_secret: string
     * }
     */
    public function obtainAccessToken(
        string $oauthVerifier,
        string $requestToken
    ): array {
        $session = Session::handle();
        $url = $this->getAccessTokenUrl();
        $params = [
            'oauth_consumer_key' => $this->consumerKey,
            'oauth_nonce' => md5(microtime() . mt_rand()),
            'oauth_signature_method' => self::SIGNATURE_METHOD,
            'oauth_timestamp' => time(),
            'oauth_token' => $requestToken,
            'oauth_version' => $this->version
        ];
        $postFields = [
            'oauth_verifier' => $oauthVerifier
        ];

        $headers = [
            $this->createAuthorizationOAuthHeader(
                $url,
                array_merge($params, $postFields),
                'POST',
                $session->get('twitter_request_token_secret')
            ),
            'Content-Type: application/x-www-form-urlencoded'
        ];

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_POST, true);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postFields, '', '&', PHP_QUERY_RFC3986));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($ch);

        if (curl_errno($ch)) {
            throw new \RuntimeException(curl_error($ch));
        }

        if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
            throw new \RuntimeException(json_decode($response)->errors[0]->message);
        }

        curl_close($ch);

        parse_str($response, $response);

        return [
            'access_token' => $response['oauth_token'],
            'access_token_secret' => $response['oauth_token_secret']
        ];
    }

    /**
     * ユーザー情報を取得
     *
     * @param string $accessToken
     * @param string $accessTokenSecret
     */
    public function setMe(string $accessToken, string $accessTokenSecret)
    {
        $url = $this->getVerifyCredentialsUrl();

        $params = [
            'oauth_consumer_key' => $this->consumerKey,
            'oauth_nonce' => md5(microtime() . mt_rand()),
            'oauth_signature_method' => self::SIGNATURE_METHOD,
            'oauth_timestamp' => time(),
            'oauth_token' => $accessToken,
            'oauth_version' => $this->version
        ];

        $headers = [
            $this->createAuthorizationOAuthHeader(
                $url,
                $params,
                'GET',
                $accessTokenSecret
            )
        ];

        $ch = curl_init();
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET');
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

        $response = curl_exec($ch);

        if (curl_errno($ch)) {
            throw new \RuntimeException(curl_error($ch));
        }

        if (curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
            throw new \RuntimeException(json_decode($response)->errors[0]->message);
        }

        curl_close($ch);

        $this->me = json_decode($response);
    }

    public function saveRequestTokens(?string $requestToken, ?string $requestTokenSecret)
    {
        $session = Session::handle();
        $session->delete('twitter_request_token');
        $session->delete('twitter_request_token_secret');
        if (!is_null($requestToken)) {
            $session->set('twitter_request_token', $requestToken);
        }
        if (!is_null($requestTokenSecret)) {
            $session->set('twitter_request_token_secret', $requestTokenSecret);
        }
        $session->save();
    }

    /**
     * /oauth/request_token エンドポイントから取得したリクエストトークンと
     * /oauth/authenticate エンドポイントから取得したトークンの比較を行う
     * @param string $oauthToken
     *
     * @return bool
     */
    public function verifyOAuthToken(string $oauthToken): bool
    {
        $session = Session::handle();
        return $oauthToken === $session->get('twitter_request_token');
    }

    /**
     * Authorization OAuth ヘッダの作成
     *
     * @param string $url
     * @param array<string, string|int> $params
     * @param string $httpMethod
     * @param string $oauthTokenSecret
     *
     * @return string
     */
    protected function createAuthorizationOAuthHeader(
        string $url,
        array $params = [],
        string $httpMethod = 'GET',
        string $oauthTokenSecret = ''
    ): string {
        $params['oauth_signature'] = $this->createSignature(
            $url,
            $params,
            $httpMethod,
            $oauthTokenSecret
        );
        return 'Authorization: OAuth ' . implode(', ', array_map(
            function (string $key, string $value) {
                return rawurlencode($key) . '=' . '"' . rawurlencode($value) . '"';
            },
            array_keys($params),
            array_values($params)
        ));
    }

    /**
     * Twitter OAuthのシグニチャを生成する
     * @see https://developer.twitter.com/en/docs/authentication/oauth-1-0a/creating-a-signature
     *
     * @param string $url
     * @param array<string, string|int> $params
     * @param string $httpMethod
     * @param string $oauthTokenSecret
     *
     * @return string
     */
    protected function createSignature(
        string $url,
        array $params,
        string $httpMethod = 'GET',
        string $oauthTokenSecret = ''
    ): string {
        uksort($params, 'strnatcmp');
        $base = implode(
            '&',
            array_map(
                'rawurlencode',
                [
                    strtoupper($httpMethod),
                    $url,
                    http_build_query(
                        $params,
                        '',
                        '&',
                        PHP_QUERY_RFC3986
                    )
                ]
            )
        );

        $key = implode('&', array_map('rawurlencode', [$this->consumerSecret, $oauthTokenSecret]));

        return base64_encode(hash_hmac('sha1', $base, $key, true));
    }

    /**
     * @return mixed
     */
    protected function getId()
    {
        return $this->me->id;
    }

    /**
     * @return mixed
     */
    protected function getEmail()
    {
        return $this->me->screen_name . '@example.com';
    }

    /**
     * @return mixed
     */
    protected function getName()
    {
        return $this->me->name;
    }

    /**
     * @return mixed
     */
    protected function getCode()
    {
        return $this->me->screen_name;
    }

    /**
     * @return mixed
     */
    protected function getIcon()
    {
        if (property_exists($this->me, 'profile_image_url_https')) {
            return $this->userIconFromUri($this->me->profile_image_url_https);
        }
        return '';
    }

    /**
     * @return mixed
     */
    protected function getUserKey()
    {
        return 'user_twitter_id';
    }
}
