<?php
/**
 * Class WC_Payments_Apple_Pay_Registration
 *
 * Adapted from WooCommerce Stripe Gateway extension.
 *
 * @package WooCommerce\Payments
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

use WCPay\Logger;
use WCPay\Exceptions\API_Exception;
use WCPay\Tracker;

/**
 * WC_Payments_Apple_Pay_Registration class.
 */
class WC_Payments_Apple_Pay_Registration {

	const DOMAIN_ASSOCIATION_FILE_NAME = 'apple-developer-merchantid-domain-association';
	const DOMAIN_ASSOCIATION_FILE_DIR  = '.well-known';

	/**
	 * Client for making requests to the WooCommerce Payments API
	 *
	 * @var WC_Payments_API_Client
	 */
	private $payments_api_client;

	/**
	 * The WCPay account object.
	 *
	 * @var WC_Payments_Account
	 */
	private $account;

	/**
	 * WC_Payment_Gateway_WCPay instance.
	 *
	 * @var WC_Payment_Gateway_WCPay
	 */
	private $gateway;

	/**
	 * Current domain name.
	 *
	 * @var string
	 */
	private $domain_name;

	/**
	 * Stores Apple Pay domain verification issues.
	 *
	 * @var string
	 */
	private $apple_pay_verify_notice;

	/**
	 * Initialize class actions.
	 *
	 * @param WC_Payments_API_Client   $payments_api_client WooCommerce Payments API client.
	 * @param WC_Payments_Account      $account WooCommerce Payments account.
	 * @param WC_Payment_Gateway_WCPay $gateway WooCommerce Payments gateway.
	 */
	public function __construct( WC_Payments_API_Client $payments_api_client, WC_Payments_Account $account, WC_Payment_Gateway_WCPay $gateway ) {
		$this->domain_name             = str_replace( [ 'https://', 'http://' ], '', get_site_url() ); // @codingStandardsIgnoreLine
		$this->apple_pay_verify_notice = '';
		$this->payments_api_client     = $payments_api_client;
		$this->account                 = $account;
		$this->gateway                 = $gateway;

		add_action( 'init', [ $this, 'add_domain_association_rewrite_rule' ], 5 );
		add_action( 'woocommerce_woocommerce_payments_updated', [ $this, 'verify_domain_on_update' ] );
		add_action( 'init', [ $this, 'init' ] );
	}

	/**
	 * Initialize hooks.
	 *
	 * @return  void
	 */
	public function init() {
		add_action( 'admin_init', [ $this, 'verify_domain_on_domain_name_change' ] );
		add_filter( 'query_vars', [ $this, 'whitelist_domain_association_query_param' ], 10, 1 );
		add_action( 'parse_request', [ $this, 'parse_domain_association_request' ], 10, 1 );

		add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_error_notice' ] );
		add_action( 'woocommerce_woocommerce_payments_admin_notices', [ $this, 'display_live_account_notice' ] );
		add_action( 'add_option_woocommerce_woocommerce_payments_settings', [ $this, 'verify_domain_on_new_settings' ], 10, 2 );
		add_action( 'update_option_woocommerce_woocommerce_payments_settings', [ $this, 'verify_domain_on_updated_settings' ], 10, 2 );
	}

	/**
	 * Whether the gateway and Payment Request Button (prerequisites for Apple Pay) are enabled.
	 *
	 * @return bool Whether Apple Pay required settings are enabled.
	 */
	private function is_enabled() {
		return $this->gateway->is_enabled() && 'yes' === $this->gateway->get_option( 'payment_request' );
	}

	/**
	 * Whether the gateway and Payment Request Button were enabled in previous settings.
	 *
	 * @param array|null $prev_settings Gateway settings.
	 *
	 * @return bool Whether Apple Pay required settings are enabled.
	 */
	private function was_enabled( $prev_settings ) {
		$gateway_enabled         = 'yes' === ( $prev_settings['enabled'] ?? 'no' );
		$payment_request_enabled = 'yes' === ( $prev_settings['payment_request'] ?? 'no' );
		return $gateway_enabled && $payment_request_enabled;
	}

	/**
	 * Trigger Apple Pay registration upon domain name change.
	 */
	public function verify_domain_on_domain_name_change() {
		$verified_domain = $this->gateway->get_option( 'apple_pay_verified_domain' );
		if ( $this->domain_name !== $verified_domain ) {
			$this->verify_domain_if_configured();
		}
	}

	/**
	 * Verify domain upon plugin update only in case the domain association file has changed.
	 */
	public function verify_domain_on_update() {
		if ( ! $this->is_hosted_domain_association_file_up_to_date() ) {
			$this->verify_domain_if_configured();
		}
	}

	/**
	 * Vefifies if hosted domain association file is up to date
	 * with the file from the plugin directory.
	 *
	 * @return bool Whether file is up to date or not.
	 */
	private function is_hosted_domain_association_file_up_to_date() {
		// Contents of domain association file from plugin dir.
		$new_contents = @file_get_contents( WCPAY_ABSPATH . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME ); // @codingStandardsIgnoreLine
		// Get file contents from local path and remote URL and check if either of which matches.
		$fullpath        = untrailingslashit( ABSPATH ) . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
		$local_contents  = @file_get_contents( $fullpath ); // @codingStandardsIgnoreLine
		$url             = get_site_url() . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
		$response        = @wp_remote_get( $url ); // @codingStandardsIgnoreLine
		$remote_contents = @wp_remote_retrieve_body( $response ); // @codingStandardsIgnoreLine

		return $local_contents === $new_contents || $remote_contents === $new_contents;
	}

	/**
	 * Copies and overwrites domain association file.
	 *
	 * @return null|string Error message.
	 */
	private function copy_and_overwrite_domain_association_file() {
		$well_known_dir = untrailingslashit( ABSPATH ) . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR;
		$fullpath       = $well_known_dir . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;

		if ( ! is_dir( $well_known_dir ) && ! @mkdir( $well_known_dir, 0755 ) && ! is_dir( $well_known_dir ) ) { // @codingStandardsIgnoreLine
			return __( 'Unable to create domain association folder to domain root.', 'woocommerce-payments' );
		}

		if ( ! @copy( WCPAY_ABSPATH . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME, $fullpath ) ) { // @codingStandardsIgnoreLine
			return __( 'Unable to copy domain association file to domain root.', 'woocommerce-payments' );
		}
	}

	/**
	 * Updates the Apple Pay domain association file.
	 * Reports failure only if file isn't already being served properly.
	 */
	public function update_domain_association_file() {
		if ( $this->is_hosted_domain_association_file_up_to_date() ) {
			return;
		}

		$error_message = $this->copy_and_overwrite_domain_association_file();

		if ( isset( $error_message ) ) {
			$url = get_site_url() . '/' . self::DOMAIN_ASSOCIATION_FILE_DIR . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
			Logger::log(
				'Error: ' . $error_message . ' ' .
				/* translators: expected domain association file URL */
				sprintf( __( 'To enable Apple Pay, domain association file must be hosted at %s.', 'woocommerce-payments' ), $url )
			);
		} else {
			Logger::log( __( 'Domain association file updated.', 'woocommerce-payments' ) );
		}
	}

	/**
	 * Adds a rewrite rule for serving the domain association file from the proper location.
	 */
	public function add_domain_association_rewrite_rule() {
		$regex    = '^\\' . self::DOMAIN_ASSOCIATION_FILE_DIR . '\/' . self::DOMAIN_ASSOCIATION_FILE_NAME . '$';
		$redirect = 'index.php?' . self::DOMAIN_ASSOCIATION_FILE_NAME . '=1';

		add_rewrite_rule( $regex, $redirect, 'top' );
	}

	/**
	 * Add to the list of publicly allowed query variables.
	 *
	 * @param  array $query_vars - provided public query vars.
	 * @return array Updated public query vars.
	 */
	public function whitelist_domain_association_query_param( $query_vars ) {
		$query_vars[] = self::DOMAIN_ASSOCIATION_FILE_NAME;
		return $query_vars;
	}

	/**
	 * Serve domain association file when proper query param is provided.
	 *
	 * @param object $wp WordPress environment object.
	 */
	public function parse_domain_association_request( $wp ) {
		if (
			! isset( $wp->query_vars[ self::DOMAIN_ASSOCIATION_FILE_NAME ] ) ||
			'1' !== $wp->query_vars[ self::DOMAIN_ASSOCIATION_FILE_NAME ]
		) {
			return;
		}

		$path = WCPAY_ABSPATH . '/' . self::DOMAIN_ASSOCIATION_FILE_NAME;
		header( 'Content-Type: text/plain;charset=utf-8' );
		echo esc_html( @file_get_contents( $path ) ); // @codingStandardsIgnoreLine
		exit;
	}

	/**
	 * Returns the string representation of the current mode. One of:
	 *   - 'dev'
	 *   - 'test'
	 *   - 'live'
	 *
	 * @return string A string representation of the current mode.
	 */
	private function get_gateway_mode_string() {
		if ( $this->gateway->is_in_dev_mode() ) {
			return 'dev';
		} elseif ( $this->gateway->is_in_test_mode() ) {
			return 'test';
		}
		return 'live';
	}

	/**
	 * Processes the Apple Pay domain verification.
	 */
	public function register_domain_with_apple() {
		$error = null;
		try {
			$registration_response = $this->payments_api_client->register_domain_with_apple( $this->domain_name );

			if ( isset( $registration_response['id'] ) ) {
				$this->gateway->update_option( 'apple_pay_verified_domain', $this->domain_name );
				$this->gateway->update_option( 'apple_pay_domain_set', 'yes' );

				Logger::log( __( 'Your domain has been verified with Apple Pay!', 'woocommerce-payments' ) );
				Tracker::track_admin(
					'wcpay_apple_pay_domain_registration_success',
					[
						'domain' => $this->domain_name,
						'mode'   => $this->get_gateway_mode_string(),
					]
				);

				return;
			} elseif ( isset( $registration_response['error']['message'] ) ) {
				$error = $registration_response['error']['message'];
			}
		} catch ( API_Exception $e ) {
			$error = $e->getMessage();
		}

		// Display error message in notice.
		$this->apple_pay_verify_notice = $error;

		$this->gateway->update_option( 'apple_pay_verified_domain', $this->domain_name );
		$this->gateway->update_option( 'apple_pay_domain_set', 'no' );

		Logger::log( 'Error registering domain with Apple: ' . $error );
		Tracker::track_admin(
			'wcpay_apple_pay_domain_registration_failure',
			[
				'domain' => $this->domain_name,
				'reason' => $error,
				'mode'   => $this->get_gateway_mode_string(),
			]
		);
	}

	/**
	 * Process the Apple Pay domain verification if proper settings are configured.
	 */
	public function verify_domain_if_configured() {
		// If Payment Request Buttons are not enabled, or account is not live,
		// do not attempt to register domain.
		if ( ! $this->is_enabled() || ! $this->account->get_is_live() ) {
			return;
		}

		// Ensure that domain association file will be served.
		flush_rewrite_rules();

		// The rewrite rule method doesn't work if permalinks are set to Plain.
		// Create/update domain association file by copying it from the plugin folder as a fallback.
		$this->update_domain_association_file();

		// Register the domain with Apple Pay.
		$this->register_domain_with_apple();
	}

	/**
	 * Conditionally process the Apple Pay domain verification after settings are initially set.
	 *
	 * @param string $option   Option name.
	 * @param array  $settings Settings array.
	 */
	public function verify_domain_on_new_settings( $option, $settings ) {
		$this->verify_domain_on_updated_settings( [], $settings );
	}

	/**
	 * Conditionally process the Apple Pay domain verification after settings are updated.
	 *
	 * @param array $prev_settings Settings before update.
	 * @param array $settings      Settings after update.
	 */
	public function verify_domain_on_updated_settings( $prev_settings, $settings ) {
		// If Gateway or Payment Request Button weren't enabled, then might need to verify now.
		if ( ! $this->was_enabled( $prev_settings ) ) {
			$this->verify_domain_if_configured();
		}
	}

	/**
	 * Display warning notice explaining that the domain can't be registered without a live account.
	 */
	public function display_live_account_notice() {
		if ( ! $this->is_enabled() || $this->account->get_is_live() ) {
			return;
		}

		?>
		<div class="notice notice-warning apple-pay-message">
			<p>
				<strong><?php echo esc_html( 'Apple Pay:' ); ?></strong>
				<?php echo esc_html_e( 'Payment request buttons are enabled. To use Apple Pay, please use a live WooCommerce Payments account.', 'woocommerce-payments' ); ?>
			</p>
		</div>
		<?php
	}

	/**
	 * Display Apple Pay registration errors.
	 */
	public function display_error_notice() {
		if ( ! $this->is_enabled() || ! $this->account->get_is_live() ) {
			return;
		}

		$empty_notice = empty( $this->apple_pay_verify_notice );
		$domain_set   = $this->gateway->get_option( 'apple_pay_domain_set' );
		// Don't display error notice if verification notice is empty and
		// apple_pay_domain_set option equals to '' or 'yes'.
		if ( $empty_notice && 'no' !== $domain_set ) {
			return;
		}

		/**
		 * Apple pay is enabled by default and domain verification initializes
		 * when setting screen is displayed. So if domain verification is not set,
		 * something went wrong so lets notify user.
		 */
		$allowed_html                      = [
			'a' => [
				'href'  => [],
				'title' => [],
			],
		];
		$payment_request_button_text       = __( 'Payment request button:', 'woocommerce-payments' );
		$verification_failed_without_error = __( 'Apple Pay domain verification failed.', 'woocommerce-payments' );
		$verification_failed_with_error    = __( 'Apple Pay domain verification failed with the following error:', 'woocommerce-payments' );
		$check_log_text                    = sprintf(
			/* translators: 1) HTML anchor open tag 2) HTML anchor closing tag */
			esc_html__( 'Please check the %1$slogs%2$s for more details on this issue. Debug log must be enabled to see recorded logs.', 'woocommerce-payments' ),
			'<a href="' . admin_url( 'admin.php?page=wc-status&tab=logs' ) . '">',
			'</a>'
		);

		?>
		<div class="notice notice-error apple-pay-message">
			<?php if ( $empty_notice ) : ?>
				<p>
					<strong><?php echo esc_html( $payment_request_button_text ); ?></strong>
					<?php echo esc_html( $verification_failed_without_error ); ?>
				</p>
			<?php else : ?>
				<p>
					<strong><?php echo esc_html( $payment_request_button_text ); ?></strong>
					<?php echo esc_html( $verification_failed_with_error ); ?>
				</p>
				<p><i><?php echo wp_kses( make_clickable( esc_html( $this->apple_pay_verify_notice ) ), $allowed_html ); ?></i></p>
			<?php endif; ?>
			<p><?php echo $check_log_text; /* @codingStandardsIgnoreLine */ ?></p>
		</div>
		<?php
	}
}
