<?php

/**
 * SocialPromoter X (Twitter) Connector
 *
 * Handles OAuth 1.0a authentication and posting to the X/Twitter API v2.
 *
 * @package PublisherKits_SocialPromoter
 */
if ( !defined( 'ABSPATH' ) ) {
    exit;
}
/**
 * Class PKITS_SP_Connector_X
 */
class PKITS_SP_Connector_X implements PKITS_SP_Connector_Interface {
    // ----------------------------------------------------------------
    // Interface metadata methods
    // ----------------------------------------------------------------
    /** @return string */
    public function get_slug() {
        return 'x';
    }

    /** @return string */
    public function get_label() {
        return 'X (Twitter)';
    }

    /** @return string */
    public function get_badge() {
        return 'X';
    }

    /** @return string */
    public function get_tier() {
        return 'free';
    }

    /** @return int */
    public function get_char_limit() {
        return 280;
    }

    /** @return string */
    public function get_auth_type() {
        return 'oauth1';
    }

    /**
     * @return array
     */
    public function get_credential_fields() {
        return array(array(
            'id'    => 'consumer_key',
            'label' => 'Consumer Key (App ID)',
            'type'  => 'text',
        ), array(
            'id'    => 'consumer_secret',
            'label' => 'Consumer Secret',
            'type'  => 'password',
        ));
    }

    /** @return string */
    public function get_accounts_description() {
        return 'X posting uses the X API Pay Per Use model (~$0.01 per post). Check your credit balance at <a href="https://developer.twitter.com/en/portal/billing" target="_blank" rel="noopener noreferrer">developer.twitter.com/en/portal/billing</a> before posting and enable Auto Recharge to avoid interruptions.';
    }

    /**
     * X API base URL for OAuth.
     *
     * @var string
     */
    const OAUTH_BASE = 'https://api.twitter.com/oauth/';

    /**
     * X API v2 base URL.
     *
     * @var string
     */
    const API_BASE = 'https://api.twitter.com/2/';

    /**
     * Get the OAuth 1.0a request token and return the authorization URL.
     *
     * Step 1 of the OAuth 1.0a flow. Call this when the user clicks "Connect X Account".
     *
     * @param string $consumer_key    X App Consumer Key.
     * @param string $consumer_secret X App Consumer Secret.
     * @param string $callback_url    URL X will redirect to after authorization.
     * @return array|WP_Error Array with 'oauth_token' and 'auth_url', or WP_Error.
     */
    public function get_auth_url( $consumer_key, $consumer_secret, $callback_url ) {
        $url = self::OAUTH_BASE . 'request_token';
        $params = array(
            'oauth_callback' => $callback_url,
        );
        $header = $this->build_auth_header(
            'POST',
            $url,
            $params,
            $consumer_key,
            $consumer_secret
        );
        $response = wp_remote_post( $url, array(
            'headers' => array(
                'Authorization' => $header,
            ),
            'body'    => array(
                'oauth_callback' => $callback_url,
            ),
            'timeout' => 20,
        ) );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = wp_remote_retrieve_response_code( $response );
        $body = wp_remote_retrieve_body( $response );
        if ( 200 !== (int) $code ) {
            return new WP_Error('x_request_token_failed', 'X request token failed: ' . esc_html( $body ));
        }
        parse_str( $body, $parsed );
        if ( empty( $parsed['oauth_token'] ) || empty( $parsed['oauth_token_secret'] ) ) {
            return new WP_Error('x_token_parse_error', 'Could not parse X OAuth tokens from response.');
        }
        // Store the request token secret in a transient so we can use it on the callback.
        set_transient( 'pkits_sp_x_req_secret_' . $parsed['oauth_token'], $parsed['oauth_token_secret'], 15 * MINUTE_IN_SECONDS );
        return array(
            'oauth_token' => $parsed['oauth_token'],
            'auth_url'    => self::OAUTH_BASE . 'authorize?oauth_token=' . rawurlencode( $parsed['oauth_token'] ),
        );
    }

    /**
     * Exchange an OAuth verifier for access tokens.
     *
     * Step 3 of the OAuth 1.0a flow. Call this in the OAuth callback handler.
     *
     * @param string $consumer_key    X App Consumer Key.
     * @param string $consumer_secret X App Consumer Secret.
     * @param string $oauth_token     oauth_token from the callback query string.
     * @param string $oauth_verifier  oauth_verifier from the callback query string.
     * @return array|WP_Error Account data array, or WP_Error.
     */
    public function get_access_token(
        $consumer_key,
        $consumer_secret,
        $oauth_token,
        $oauth_verifier
    ) {
        $token_secret = get_transient( 'pkits_sp_x_req_secret_' . $oauth_token );
        if ( empty( $token_secret ) ) {
            return new WP_Error('x_missing_request_secret', 'OAuth session expired. Please try connecting again.');
        }
        delete_transient( 'pkits_sp_x_req_secret_' . $oauth_token );
        $url = self::OAUTH_BASE . 'access_token';
        $params = array(
            'oauth_token'    => $oauth_token,
            'oauth_verifier' => $oauth_verifier,
        );
        $header = $this->build_auth_header(
            'POST',
            $url,
            $params,
            $consumer_key,
            $consumer_secret,
            $oauth_token,
            $token_secret
        );
        $response = wp_remote_post( $url, array(
            'headers' => array(
                'Authorization' => $header,
            ),
            'body'    => array(
                'oauth_token'    => $oauth_token,
                'oauth_verifier' => $oauth_verifier,
            ),
            'timeout' => 20,
        ) );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = wp_remote_retrieve_response_code( $response );
        $body = wp_remote_retrieve_body( $response );
        if ( 200 !== (int) $code ) {
            return new WP_Error('x_access_token_failed', 'X access token failed: ' . esc_html( $body ));
        }
        parse_str( $body, $parsed );
        if ( empty( $parsed['oauth_token'] ) || empty( $parsed['oauth_token_secret'] ) ) {
            return new WP_Error('x_access_parse_error', 'Could not parse X access tokens.');
        }
        return array(
            'platform'            => 'x',
            'account_id'          => sanitize_text_field( $parsed['user_id'] ?? '' ),
            'screen_name'         => sanitize_text_field( $parsed['screen_name'] ?? '' ),
            'label'               => '@' . sanitize_text_field( $parsed['screen_name'] ?? $parsed['user_id'] ?? '' ),
            'access_token'        => $parsed['oauth_token'],
            'access_token_secret' => $parsed['oauth_token_secret'],
            'consumer_key'        => $consumer_key,
            'consumer_secret'     => $consumer_secret,
            'connected_at'        => gmdate( 'Y-m-d H:i:s' ),
        );
    }

    /**
     * Upload media to X and return the media_id.
     *
     * @param array $account    Account data.
     * @param array $image_data Image data from PKITS_SP_Image_Handler.
     * @return string|WP_Error media_id string or WP_Error.
     */
    /**
     * Upload media to X using multipart binary upload.
     *
     * Uses multipart/form-data with raw binary (the 'media' param) rather
     * than base64 encoding (the 'media_data' param). Binary upload is more
     * reliable and avoids encoding overhead on large images.
     *
     * @param array $account    Account data (with decrypted tokens).
     * @param array $image_data Image data from PKITS_SP_Image_Handler.
     * @return string|WP_Error media_id_string on success, or WP_Error.
     */
    public function upload_media( array $account, array $image_data ) {
        $upload_url = 'https://upload.twitter.com/1.1/media/upload.json';
        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
        $image_binary = file_get_contents( $image_data['local_path'] );
        if ( false === $image_binary ) {
            return new WP_Error('x_media_read_failed', 'Could not read image file.');
        }
        // Build multipart/form-data body with raw binary.
        $boundary = '----XMediaUpload' . wp_generate_password( 16, false );
        $body = '--' . $boundary . "\r\n";
        $body .= 'Content-Disposition: form-data; name="media"; filename="image.' . PKITS_SP_Image_Handler::mime_to_extension( $image_data['mime_type'] ) . '"' . "\r\n";
        $body .= 'Content-Type: ' . $image_data['mime_type'] . "\r\n\r\n";
        $body .= $image_binary . "\r\n";
        $body .= '--' . $boundary . '--' . "\r\n";
        // OAuth signature covers only OAuth header params for multipart uploads.
        $header = $this->build_auth_header(
            'POST',
            $upload_url,
            array(),
            $account['consumer_key'],
            $account['consumer_secret'],
            $account['access_token'],
            $account['access_token_secret']
        );
        $response = wp_remote_post( $upload_url, array(
            'headers' => array(
                'Authorization' => $header,
                'Content-Type'  => 'multipart/form-data; boundary=' . $boundary,
            ),
            'body'    => $body,
            'timeout' => 60,
        ) );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = wp_remote_retrieve_response_code( $response );
        $data = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( $code < 200 || $code >= 300 || empty( $data['media_id_string'] ) ) {
            $msg = ( isset( $data['errors'][0]['message'] ) ? $data['errors'][0]['message'] : wp_remote_retrieve_body( $response ) );
            return new WP_Error('x_media_upload_failed', 'X media upload failed (' . $code . '): ' . esc_html( $msg ));
        }
        return $data['media_id_string'];
    }

    /**
     * Post a tweet to X using the v2 API.
     *
     * @param array      $account    Account data (with decrypted tokens).
     * @param string     $text       Tweet text (max 280 characters).
     * @param string     $url        Post URL (unused - should already be in $text).
     * @param array|null $image_data Optional image data.
     * @return array|WP_Error
     */
    public function post(
        array $account,
        $text,
        $url = '',
        $image_data = null
    ) {
        $url = self::API_BASE . 'tweets';
        $payload = array(
            'text' => $text,
        );
        // Upload image if provided and attach media_id.
        $image_error = '';
        if ( !empty( $image_data ) ) {
            $media_id = $this->upload_media( $account, $image_data );
            if ( !is_wp_error( $media_id ) ) {
                $payload['media'] = array(
                    'media_ids' => array($media_id),
                );
            } else {
                // Keep the reason but continue posting without the image. It must
                // NOT go into $payload: the X v2 API rejects unknown JSON fields,
                // so injecting it there would fail the whole tweet. Surfaced on the
                // queue record via the result, like the other connectors.
                $image_error = $media_id->get_error_message();
            }
            PKITS_SP_Image_Handler::cleanup_temp( $image_data );
        }
        $body = wp_json_encode( $payload );
        // For API v2 with JSON body, OAuth signature covers only OAuth params.
        $header = $this->build_auth_header(
            'POST',
            $url,
            array(),
            $account['consumer_key'],
            $account['consumer_secret'],
            $account['access_token'],
            $account['access_token_secret']
        );
        $response = wp_remote_post( $url, array(
            'headers' => array(
                'Authorization' => $header,
                'Content-Type'  => 'application/json',
            ),
            'body'    => $body,
            'timeout' => 30,
        ) );
        if ( is_wp_error( $response ) ) {
            return $response;
        }
        $code = wp_remote_retrieve_response_code( $response );
        $body_decoded = json_decode( wp_remote_retrieve_body( $response ), true );
        if ( $code < 200 || $code >= 300 ) {
            $message = ( isset( $body_decoded['detail'] ) ? $body_decoded['detail'] : wp_remote_retrieve_body( $response ) );
            // Detect empty balance specifically and return a clearer message.
            if ( 402 === (int) $code ) {
                $message = 'Insufficient X API credits. Please top up your balance at developer.twitter.com/en/portal/billing.';
            }
            return new WP_Error('x_post_failed', 'X post failed (' . $code . '): ' . esc_html( $message ));
        }
        $result = array(
            'platform_post_id' => ( isset( $body_decoded['data']['id'] ) ? sanitize_text_field( $body_decoded['data']['id'] ) : '' ),
        );
        if ( '' !== $image_error ) {
            $result['_image_error'] = $image_error;
        }
        return $result;
    }

    /**
     * Build an OAuth 1.0a Authorization header.
     *
     * @param string $method          HTTP method (GET, POST).
     * @param string $url             Request URL (no query string).
     * @param array  $extra_params    Additional params to include in signature (query/body params).
     * @param string $consumer_key    App Consumer Key.
     * @param string $consumer_secret App Consumer Secret.
     * @param string $token           OAuth token (optional).
     * @param string $token_secret    OAuth token secret (optional).
     * @return string Authorization header value.
     */
    private function build_auth_header(
        $method,
        $url,
        array $extra_params,
        $consumer_key,
        $consumer_secret,
        $token = '',
        $token_secret = ''
    ) {
        $oauth_params = array(
            'oauth_consumer_key'     => $consumer_key,
            'oauth_nonce'            => md5( uniqid( (string) wp_rand(), true ) ),
            'oauth_signature_method' => 'HMAC-SHA1',
            'oauth_timestamp'        => (string) time(),
            'oauth_version'          => '1.0',
        );
        if ( !empty( $token ) ) {
            $oauth_params['oauth_token'] = $token;
        }
        // Merge all params for the signature base string.
        $all_params = array_merge( $oauth_params, $extra_params );
        ksort( $all_params );
        // Build percent-encoded parameter string.
        $param_parts = array();
        foreach ( $all_params as $key => $value ) {
            $param_parts[] = rawurlencode( $key ) . '=' . rawurlencode( $value );
        }
        $param_string = implode( '&', $param_parts );
        // Signature base string.
        $base_string = strtoupper( $method ) . '&' . rawurlencode( $url ) . '&' . rawurlencode( $param_string );
        // Signing key.
        $signing_key = rawurlencode( $consumer_secret ) . '&' . rawurlencode( $token_secret );
        // HMAC-SHA1 signature.
        $signature = base64_encode( hash_hmac(
            'sha1',
            $base_string,
            $signing_key,
            true
        ) );
        // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
        $oauth_params['oauth_signature'] = $signature;
        ksort( $oauth_params );
        // Build Authorization header.
        $header_parts = array();
        foreach ( $oauth_params as $key => $value ) {
            $header_parts[] = rawurlencode( $key ) . '="' . rawurlencode( $value ) . '"';
        }
        return 'OAuth ' . implode( ', ', $header_parts );
    }

}
