<?php
/**
 * Updates the checkout behavior.
 *
 * @package EDD\Pro\Checkout
 * @copyright Copyright (c) <year>, Sandhills Development, LLC
 * @license https://opensource.org/licenses/gpl-2.0.php GNU Public License
 * @since 3.1.1
 */

namespace EDD\Pro\Checkout;

// Exit if accessed directly.
defined( 'ABSPATH' ) || exit;

use EDD\EventManagement\SubscriberInterface;

/**
 * Handles the GeoIP functionality.
 *
 * @since 3.1.1
 */
class GeoIP implements SubscriberInterface {

	/**
	 * The endpoint to use for the geolocation request.
	 *
	 * @var string
	 */
	private $endpoint = 'https://geo.easydigitaldownloads.com/v3/geolocate/json';

	/**
	 * Returns an array of events that this subscriber wants to listen to.
	 *
	 * @return array
	 */
	public static function get_subscribed_events() {
		if ( ! self::is_enabled() ) {
			return array();
		}

		return array(
			'wp_enqueue_scripts'           => 'enqueue',
			'wp_ajax_edd_pro_geoip'        => 'get_address',
			'wp_ajax_nopriv_edd_pro_geoip' => 'get_address',
			'edd_built_order'              => 'add_ip_to_data',
			'edd_post_add_to_cart'         => 'fetch',
			'edd_checkout_form_top'        => 'add_ip_to_form',
			'edd_tax_rate'                 => 'maybe_update_tax_rate',
		);
	}

	/**
	 * Enqueues the scripts needed for the geoip request.
	 *
	 * @since 3.1.1
	 * @return void
	 */
	public function enqueue() {
		if ( ! $this->can_enqueue() ) {
			return;
		}
		wp_enqueue_script( 'edd-pro-checkout', edd_get_assets_url( 'pro/js/frontend' ) . 'geolocation.js', array( 'edd-checkout-global' ), EDD_VERSION, true );
		wp_localize_script(
			'edd-pro-checkout',
			'EDDProCheckout',
			array(
				'ajax'  => edd_get_ajax_url(),
				'api'   => $this->endpoint,
				'taxes' => edd_use_taxes(),
				'debug' => edd_doing_script_debug(),
				'nonce' => wp_create_nonce( 'edd-pro-geoip' ),
			)
		);
	}

	/**
	 * Builds the address from the geolocation endpoint.
	 *
	 * @since 3.1.1
	 * @return void
	 */
	public function get_address() {

		if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( sanitize_text_field( $_POST['nonce'] ), 'edd-pro-geoip' ) ) {
			wp_send_json_error( 'nonce failed' );
		}

		$address = array(
			'country' => $this->sanitize_input( 'country_iso' ),
			'city'    => $this->sanitize_input( 'city' ),
			'state'   => $this->get_state(),
			'ip'      => $this->sanitize_input( 'ip' ),
		);

		if ( ! is_user_logged_in() ) {
			wp_send_json_success( $address );
		}
		$user_address = edd_get_customer_address();
		foreach ( $address as $key => $field ) {
			if ( ! empty( $user_address[ $key ] ) ) {
				unset( $address[ $key ] );
			}
		}

		wp_send_json_success( $address );
	}

	/**
	 * If the IP address was added to the form data, add it to the order object.
	 *
	 * @since 3.1.1
	 * @param int $order_id The order ID.
	 * @return void
	 */
	public function add_ip_to_data( $order_id ) {
		$ip = $this->sanitize_input( 'edd_pro_ip' );
		if ( empty( $ip ) ) {
			return;
		}
		edd_update_order(
			$order_id,
			array(
				'ip' => $ip,
			)
		);
	}

	/**
	 * Fetches the IP address from the API.
	 *
	 * @since 3.6.0
	 * @return void
	 */
	public function fetch() {
		if ( EDD()->session->get( 'edd_pro_geoip' ) ) {
			return;
		}

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

		$ip = edd_get_ip();
		if ( empty( $ip ) || '127.0.0.1' === $ip || '::1' === $ip ) {
			return;
		}

		$request = new \EDD\Utils\RemoteRequest(
			"{$this->endpoint}/{$ip}",
			array(
				'timeout' => 5,
			)
		);

		if ( is_wp_error( $request->response ) ) {
			edd_debug_log( $request->response->get_error_message() );
			return;
		}

		if ( 200 !== $request->code ) {
			return;
		}

		$data = json_decode( $request->body, true );
		EDD()->session->set( 'edd_pro_geoip', $data );
	}

	/**
	 * Adds the IP address to the form.
	 *
	 * @since 3.6.0
	 * @return void
	 */
	public function add_ip_to_form() {
		$data = EDD()->session->get( 'edd_pro_geoip' );
		if ( empty( $data['ip'] ) ) {
			return;
		}

		echo '<input type="hidden" name="edd_pro_ip" value="' . esc_attr( $data['ip'] ) . '">';
	}

	/**
	 * Maybe update the tax rate based on the customer's address.
	 *
	 * @since 3.6.1
	 * @param float $rate    The tax rate.
	 * @return float The tax rate.
	 */
	public function maybe_update_tax_rate( $rate ) {

		// Don't update the tax rate if evaluating form data.
		if ( ! empty( $_POST['billing_country'] ) ) {
			return $rate;
		}

		// Don't update the tax rate during admin order requests.
		if ( isset( $_POST['customerId'] ) ) {
			return $rate;
		}

		// Don't override logged-in users with saved addresses.
		if ( is_user_logged_in() ) {
			$user_address = edd_get_customer_address();
			if ( ! empty( $user_address['country'] ) ) {
				return $rate;
			}
		}

		$pro_ip_data = EDD()->session->get( 'edd_pro_geoip' );
		if ( empty( $pro_ip_data ) ) {
			return $rate;
		}

		$tax_rate = edd_get_tax_rate_by_location(
			array(
				'country' => $pro_ip_data['country_iso'],
				'region'  => $this->get_state( $pro_ip_data ),
			)
		);

		$rate = $tax_rate ? $tax_rate->amount : 0.00;

		return $rate / 100;
	}

	/**
	 * Helper function to get the region code or name from the API response.
	 *
	 * @since 3.1.1
	 * @param array $pro_ip_data The IP data.
	 * @return string
	 */
	private function get_state( $pro_ip_data = array() ) {
		if ( ! empty( $pro_ip_data ) ) {
			$country_iso = sanitize_text_field( $pro_ip_data['country_iso'] ?? '' );
			$region_code = sanitize_text_field( $pro_ip_data['region_code'] ?? '' );
			$city        = sanitize_text_field( $pro_ip_data['city'] ?? '' );
			$region_name = sanitize_text_field( $pro_ip_data['region_name'] ?? '' );
		} else {
			$country_iso = $this->sanitize_input( 'country_iso' );
			$region_code = $this->sanitize_input( 'region_code' );
			$city        = $this->sanitize_input( 'city' );
			$region_name = $this->sanitize_input( 'region_name' );
		}
		$states = edd_get_shop_states( $country_iso );

		// If the region code is in the states array, it will work for the address selector.
		if ( ! empty( $region_code ) && array_key_exists( $region_code, $states ) ) {
			return sanitize_text_field( $region_code );
		}

		// Japan region codes in EDD are `JP01`, etc.
		if ( 'JP' === $country_iso && ! empty( $region_code ) && array_key_exists( $country_iso . $region_code, $states ) ) {
			return sanitize_text_field( $country_iso . $region_code );
		}

		// For Great Britain, we can try to get the region code from the city.
		if ( 'GB' === $country_iso && ! empty( $city ) ) {
			return array_search( $city, edd_get_united_kingdom_states_list(), true );
		}

		// Probably this is a state that's not available from a list so return the full region name.
		return $region_name;
	}

	/**
	 * Sanitizes an input string.
	 *
	 * @since 3.1.1
	 * @since 3.3.7 Rename to `sanitize_input`, to avoid conflict with analysis tools.
	 * @param string $key The key to sanitize.
	 * @return false|string
	 */
	private function sanitize_input( $key ) {
		if ( ! isset( $_POST[ $key ] ) ) {
			return false;
		}

		return sanitize_text_field( wp_unslash( $_POST[ $key ] ) );
	}

	/**
	 * Determines if the scripts can be enqueued.
	 *
	 * @since 3.2.8
	 * @return bool
	 */
	private function can_enqueue() {
		if ( ! edd_is_checkout() || EDD()->cart->is_empty() || edd_is_inactive_pro() ) {
			return false;
		}

		if ( \EDD\Blocks\Utility::is_block_editor() ) {
			return false;
		}

		if ( EDD()->session->get( 'edd_pro_geoip' ) ) {
			return false;
		}

		return $this->is_enabled();
	}

	/**
	 * Determines if the GeoIP is enabled.
	 *
	 * @since 3.6.1
	 * @return bool
	 */
	private static function is_enabled() {
		$option = edd_get_option( 'geolocation', 'enabled' );
		if ( 'disabled' === $option || ( 'logged_out' === $option && is_user_logged_in() ) ) {
			return false;
		}

		return true;
	}
}
