<?php

namespace WPForms\Integrations\Stripe\Admin;

use WPForms\Admin\Payments\BuilderReturnNotice;
use WPForms\Integrations\Stripe\Api\DomainManager;
use WPForms\Integrations\Stripe\Api\WebhooksManager;
use WPForms\Integrations\Stripe\Helpers;
use WPForms\Integrations\Stripe\WebhooksHealthCheck;
use WPForms\Vendor\Stripe\Account;

/**
 * Stripe Connect functionality.
 *
 * @since 1.8.2
 */
class Connect {

	/**
	 * WPForms Stripe OAuth URL.
	 *
	 * @since 1.8.2
	 */
	const WPFORMS_URL = 'https://wpforms.com/oauth/stripe-connect';

	/**
	 * Stripe live/test account objects.
	 *
	 * @since 1.8.2
	 *
	 * @var array
	 */
	protected $accounts = [];

	/**
	 * Webhooks manager.
	 *
	 * @since 1.8.4
	 *
	 * @var WebhooksManager
	 */
	private $webhooks_manager;

	/**
	 * Domain manager.
	 *
	 * @since 1.8.6
	 *
	 * @var DomainManager
	 */
	private $domain_manager;

	/**
	 * Initialize.
	 *
	 * @since 1.8.2
	 *
	 * @return Connect
	 */
	public function init() {

		$this->webhooks_manager = new WebhooksManager();
		$this->domain_manager   = new DomainManager();

		$this->hooks();

		return $this;
	}

	/**
	 * Hooks.
	 *
	 * @since 1.8.2
	 */
	private function hooks() {

		add_action( 'admin_init', [ $this, 'handle_oauth_handshake' ] );
		add_action( 'admin_init', [ $this, 'maybe_queue_builder_return_notice' ], 20 );
	}

	/**
	 * Queue the "Return to your form" success notice after the user completes
	 * the Stripe Connect flow that they started from the form builder.
	 *
	 * @since 1.10.1.1
	 */
	public function maybe_queue_builder_return_notice(): void {

		BuilderReturnNotice::maybe_queue_oauth( 'stripe', __( 'Stripe', 'wpforms-lite' ) );
	}

	/**
	 * Handle Stripe Connect OAuth handshake and save Stripe keys.
	 *
	 * @since 1.8.2
	 */
	public function handle_oauth_handshake(): void {

		// Handle disconnect requests.
		if ( $this->is_valid_disconnect_request() ) {
			$this->handle_disconnect();

			return;
		}

		if ( ! $this->is_valid_handshake_request() ) {
			return;
		}

		$state = isset( $_GET['state'] ) ? sanitize_text_field( wp_unslash( $_GET['state'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( empty( $state ) ) {
			return;
		}

		$credentials   = $this->fetch_stripe_credentials( $state );
		$required_keys = [ 'stripe_user_id', 'stripe_publishable_key', 'access_token', 'refresh_token', 'live_mode' ];

		if ( 0 !== count( array_diff( $required_keys, array_keys( $credentials ) ) ) ) {
			return;
		}

		$mode = empty( $credentials['live_mode'] ) ? 'test' : 'live';

		$this->set_connected_user_id( $credentials['stripe_user_id'], $mode );
		$this->set_current_mode( $mode );

		// In case of switching accounts existing account data needs to be cleared.
		unset( $this->accounts[ $mode ] );

		Helpers::set_stripe_key( $credentials['stripe_publishable_key'], 'publishable', $mode );
		Helpers::set_stripe_key( $credentials['access_token'], 'secret', $mode );

		$this->update_account_meta( $credentials['stripe_user_id'], $mode );
		$this->set_connected_account_country( $mode );
		$this->webhooks_manager->connect();
		$this->domain_manager->validate();

		$settings_url   = $this->get_payments_settings_url( false );
		$return_form_id = isset( $_GET['wpforms_return_form_id'] ) ? absint( $_GET['wpforms_return_form_id'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( $return_form_id > 0 ) {
			$settings_url = add_query_arg(
				[
					'wpforms_connected'      => 'stripe',
					'wpforms_return_form_id' => $return_form_id,
				],
				$settings_url
			);
		}

		wp_safe_redirect( $settings_url );
		exit;
	}

	/**
	 * Validates if the current handshake request is valid.
	 *
	 * @since 1.9.5
	 */
	private function is_valid_handshake_request(): bool {

		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_stripe_connect' ) ) {
			return false;
		}

		if ( ! isset( $_GET['stripe_connect'] ) || $_GET['stripe_connect'] !== 'complete' ) {
			return false;
		}

		if ( ! wpforms_current_user_can() ) {
			return false;
		}

		return true;
	}

	/**
	 * Validates if the current disconnect request is valid.
	 *
	 * @since 1.9.8
	 *
	 * @return bool
	 */
	private function is_valid_disconnect_request(): bool {

		if ( ! isset( $_GET['_wpnonce'] ) || ! wp_verify_nonce( sanitize_key( $_GET['_wpnonce'] ), 'wpforms_stripe_disconnect' ) ) {
			return false;
		}

		if ( ! wpforms_current_user_can() ) {
			return false;
		}

		return true;
	}

	/**
	 * Fetch Stripe credentials from https://wpforms.com.
	 *
	 * @since 1.8.2
	 *
	 * @param string $state Anonymous autogenerated ID to safely fetch Stripe credentials.
	 *
	 * @return array
	 */
	protected function fetch_stripe_credentials( $state ) {

		$response = wp_remote_post(
			self::WPFORMS_URL,
			[
				'body' => [
					'action' => 'credentials',
					'state'  => $state,
				],
			]
		);

		if ( is_wp_error( $response ) ) {
			return [];
		}

		$body = wpforms_json_decode( wp_remote_retrieve_body( $response ), true );

		return is_array( $body ) ? $body : [];
	}

	/**
	 * Fetch Stripe Account from Stripe.
	 *
	 * @since 1.8.2
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 *
	 * @return null|Account
	 */
	protected function fetch_stripe_account( $mode = '' ) {

		$api_key = Helpers::get_stripe_key( 'secret', $mode );

		if ( ! $api_key ) {
			return null;
		}

		try {
			$account = Account::retrieve( null, sanitize_text_field( $api_key ) );
		} catch ( \Exception $e ) {
			$account = null;
		}

		return $account;
	}

	/**
	 * Update connected account meta.
	 *
	 * @since 1.8.2
	 *
	 * @param string $account_id Account ID.
	 * @param string $mode       Stripe mode (e.g. 'live' or 'test').
	 */
	public function update_account_meta( $account_id = '', $mode = '' ) {

		if ( ! $mode ) {
			$mode = Helpers::get_stripe_mode();
		}

		// Stripe API has limited update method for live accounts only.
		if ( $mode !== 'live' ) {
			return;
		}

		if ( ! $account_id ) {
			$account_id = $this->get_connected_user_id( $mode );
		}

		// Return early if no connected account.
		if ( ! $account_id ) {
			return;
		}

		$licence_type = wpforms_get_license_type();

		$metadata = [
			'wpforms_stripe'  => Helpers::is_addon_active() ? 'addon' : 'core',
			'wpforms_type'    => wpforms()->is_pro() ? 'pro' : 'lite',
			'wpforms_license' => $licence_type ? $licence_type : 'lite',
		];

		try {
			Account::update( $account_id, [ 'metadata' => $metadata ], Helpers::get_auth_opts() );
		} catch ( \Exception $e ) {
			wpforms_log(
				'Unable to update connected Stripe account meta.',
				$e->getMessage(),
				[
					'type' => [ 'payment', 'error' ],
				]
			);
		}
	}

	/**
	 * Generate random alphanumeric token string.
	 * Token length is always 32 chars.
	 *
	 * @since 1.8.2
	 *
	 * @return string
	 */
	public function generate_random_token() {

		$random = false;

		if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
			$strong_result = false; // This has been added as argument #2 ($strong_result) cannot be passed by reference.
			$random        = openssl_random_pseudo_bytes( 16, $strong_result );
		}

		if ( $random === false ) {
			return md5( wp_rand() );
		}

		return bin2hex( $random );
	}

	/**
	 * Set fetched Stripe Account for caching purposes.
	 *
	 * @since 1.8.2
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 */
	protected function set_connected_account( $mode = '' ) {

		$user_id = $this->get_connected_user_id( $mode );

		if ( ! $user_id ) {
			return;
		}

		$account                 = $this->fetch_stripe_account( $mode );
		$this->accounts[ $mode ] = null;

		if ( ! isset( $account->id ) || $account->id !== $user_id ) {
			return;
		}

		$this->accounts[ $mode ] = $account;
	}

	/**
	 * Get cached Stripe Account or fetch it from Stripe.
	 *
	 * @since 1.8.2
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 *
	 * @return null|Account
	 */
	public function get_connected_account( $mode = '' ) {

		$mode = Helpers::validate_stripe_mode( $mode );

		if ( empty( $this->accounts ) || ( is_array( $this->accounts ) && ! array_key_exists( $mode, $this->accounts ) ) ) {
			$this->set_connected_account( $mode );
		}

		if ( ! empty( $this->accounts[ $mode ] ) ) {
			return $this->accounts[ $mode ];
		}

		return null;
	}

	/**
	 * Save connected user id to an option.
	 *
	 * @since 1.8.2
	 *
	 * @param string $user_id User id to set.
	 * @param string $mode    Stripe mode (e.g. 'live' or 'test').
	 *
	 * @return bool
	 */
	protected function set_connected_user_id( $user_id, $mode = '' ) {

		$mode = Helpers::validate_stripe_mode( $mode );

		return update_option( "wpforms_stripe_{$mode}_connect_user_id", sanitize_text_field( $user_id ) );
	}

	/**
	 * Get saved Stripe Connect user id from DB.
	 *
	 * @since 1.8.2
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 *
	 * @return string
	 */
	public function get_connected_user_id( $mode = '' ) {

		$mode    = Helpers::validate_stripe_mode( $mode );
		$user_id = get_option( "wpforms_stripe_{$mode}_connect_user_id", '' );

		/**
		 * User ID associated with the Stripe account.
		 *
		 * @since 1.8.2
		 *
		 * @param string $user_id User ID.
		 */
		return (string) apply_filters( 'wpforms_stripe_admin_connect_get_connected_user_id', $user_id ); // phpcs:ignore WPForms.PHP.ValidateHooks.InvalidHookName
	}

	/**
	 * Get Stripe Account name.
	 *
	 * @since 1.8.2
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 *
	 * @return string
	 */
	public function get_connected_account_name( $mode = '' ) {

		$account = $this->get_connected_account( $mode );

		if ( isset( $account->display_name ) ) {
			return $account->display_name;
		}

		if ( isset( $account->settings, $account->settings->dashboard->display_name ) ) {
			return $account->settings->dashboard->display_name;
		}

		return '';
	}

	/**
	 * Set Stripe mode.
	 *
	 * @since 1.8.4
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 */
	private function set_current_mode( $mode ) {

		$key              = 'stripe-test-mode';
		$settings         = (array) get_option( 'wpforms_settings', [] );
		$settings[ $key ] = $mode === 'test';

		update_option( 'wpforms_settings', $settings );
	}

	/**
	 * Set Stripe Account country.
	 *
	 * @since 1.8.2
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 */
	private function set_connected_account_country( $mode = '' ) {

		$account = $this->get_connected_account( $mode );

		if ( ! isset( $account->country ) ) {
			return;
		}

		update_option( "wpforms_stripe_{$mode}_account_country", strtolower( $account->country ) );
	}

	/**
	 * Get Stripe Connect button URL.
	 *
	 * @since 1.8.2
	 * @since 1.10.1.1 Added `$return_form_id` parameter to round-trip the
	 *                  source form ID through the OAuth callback.
	 *
	 * @param string $mode           Stripe mode (e.g. 'live' or 'test').
	 * @param int    $return_form_id Optional. Form ID the user came from in the
	 *                               builder. Carried through the OAuth callback
	 *                               so the Payments settings page can show a
	 *                               "Return to your form" notice.
	 *
	 * @return string
	 */
	public function get_connect_with_stripe_url( $mode = '', int $return_form_id = 0 ) {

		$mode         = Helpers::validate_stripe_mode( $mode );
		$settings_url = $this->get_payments_settings_url();

		if ( $return_form_id > 0 ) {
			$settings_url = add_query_arg( 'wpforms_return_form_id', $return_form_id, $settings_url );
		}

		return add_query_arg(
			[
				'action'    => 'init',
				'live_mode' => absint( $mode === 'live' ),
				'state'     => $this->generate_random_token(),
				'site_url'  => rawurlencode( $settings_url ),
			],
			self::WPFORMS_URL
		);
	}

	/**
	 * Get "Payments" settings page URL.
	 *
	 * @since 1.8.2
	 * @since 1.9.8 Added `$include_nonce` and `$action` parameters to allow more flexible settings.
	 *
	 * @param bool   $include_nonce Whether to include nonce in the URL.
	 * @param string $action        Action to be used for nonce verification.
	 *
	 * @return string
	 */
	private function get_payments_settings_url( bool $include_nonce = true, string $action = 'wpforms_stripe_connect' ): string {

		$url = Helpers::get_settings_page_url();

		if ( $include_nonce ) {
			$url = add_query_arg( '_wpnonce', wp_create_nonce( $action ), $url );
		}

		return $url;
	}

	/**
	 * Get Stripe disconnect URL.
	 *
	 * @since 1.9.8
	 *
	 * @param string $mode Stripe mode (e.g. 'live' or 'test').
	 *
	 * @return string
	 */
	public function get_disconnect_stripe_url( string $mode ): string {

		$mode   = Helpers::validate_stripe_mode( $mode );
		$action = 'wpforms_stripe_disconnect';

		return add_query_arg(
			[
				'action'    => $action,
				'live_mode' => absint( $mode === 'live' ),
			],
			$this->get_payments_settings_url( true, $action )
		);
	}

	/**
	 * Disconnect from Stripe.
	 *
	 * @since 1.9.8
	 */
	private function handle_disconnect(): void {

		$mode = Helpers::get_stripe_mode();

		$response = wp_remote_post(
			self::WPFORMS_URL,
			[
				'body'    => [
					'action'         => 'deauthorize',
					'secret_key'     => Helpers::get_stripe_key( 'secret', $mode ),
					'stripe_user_id' => $this->get_connected_user_id( $mode ),
					'live_mode'      => absint( $mode === 'live' ),
				],
				'timeout' => 15, // Handle case with slow connection.
			]
		);

		if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
			return;
		}

		// Cleanup saved options.
		$this->disconnect_cleanup();

		// Disconnect webhooks.
		$this->webhooks_manager->disconnect();

		wp_safe_redirect( $this->get_payments_settings_url( false ) );
		exit;
	}

	/**
	 * Cleanup after disconnecting from Stripe.
	 *
	 * @since 1.9.8
	 */
	private function disconnect_cleanup(): void {

		$mode = Helpers::get_stripe_mode();

		// Clean keys.
		Helpers::set_stripe_key( '', 'publishable', $mode );
		Helpers::set_stripe_key( '', 'secret', $mode );

		// Clean user ID and other saved options.
		delete_option( "wpforms_stripe_{$mode}_connect_user_id" );
		delete_option( "wpforms_stripe_{$mode}_account_country" );
		delete_option( DomainManager::STATUS_OPTION );
		delete_option( WebhooksHealthCheck::ENDPOINT_OPTION );

		// Clean cached account.
		unset( $this->accounts[ $mode ] );
	}
}
