<?php

namespace AutomateWoo;

use Automattic\WooCommerce\Internal\DataStores\Orders\OrdersTableDataStore;

defined( 'ABSPATH' ) || exit;

/**
 * @class Customer
 *
 * This class uses direct DB queries to fetch order data for performance reason.
 * The usage of WC_Order_Query is limited and won't return a set of totals.
 *
 * @since 3.0.0
 */
class Customer extends Abstract_Model_With_Meta_Table {

	/**
	 * Meta key storing the workflow that caused the latest opt-out.
	 *
	 * @since 6.5.0
	 *
	 * @var string
	 */
	const UNSUBSCRIBED_WORKFLOW_META_KEY = 'unsubscribed_workflow_id';

	/** @var string */
	public $table_id = 'customers';

	/** @var string  */
	public $object_type = 'customer';

	/**
	 * Customer constructor.
	 *
	 * @param bool|int $id
	 */
	public function __construct( $id = false ) {
		if ( $id ) {
			$this->get_by( 'id', $id );
		}
	}

	/**
	 * Returns the ID of the model's meta table.
	 *
	 * @return string
	 */
	public function get_meta_table_id() {
		return 'customer-meta';
	}

	/**
	 * Get the user ID.
	 *
	 * @return int
	 */
	public function get_user_id() {
		return (int) $this->get_prop( 'user_id' );
	}

	/**
	 * Set the user ID.
	 *
	 * @param mixed $user_id
	 */
	public function set_user_id( $user_id ) {
		$this->set_prop( 'user_id', (int) $user_id );
	}

	/**
	 * Get the guest ID.
	 *
	 * @return int
	 */
	public function get_guest_id() {
		return (int) $this->get_prop( 'guest_id' );
	}

	/**
	 * Set the guest ID.
	 *
	 * @param mixed $guest_id
	 */
	public function set_guest_id( $guest_id ) {
		$this->set_prop( 'guest_id', (int) $guest_id );
	}

	/**
	 * Returns a unique key that can ID the customer. This is added to the customer upon creation.
	 *
	 * @return string
	 */
	public function get_key() {
		return Clean::string( $this->get_prop( 'id_key' ) );
	}

	/**
	 * Set a unique ID key.
	 *
	 * @param string $key
	 */
	public function set_key( $key ) {
		$this->set_prop( 'id_key', Clean::string( $key ) );
	}

	/**
	 * Generates a new key for registered users that don't have one.
	 *
	 * @deprecated tracking keys are replaced with $this->get_key()
	 *
	 * @since 4.0
	 * @return string
	 */
	public function get_tracking_key() {
		wc_deprecated_function( __METHOD__, '5.2.0', 'get_key' );

		return Clean::string( $this->get_linked_prop( 'tracking_key' ) );
	}

	/**
	 * Get the creation date of the customer's last paid order.
	 *
	 * @return bool|DateTime
	 */
	public function get_date_last_purchased() {
		return $this->get_date_column( 'last_purchased' );
	}

	/**
	 * Set the date of the customer's last paid order.
	 *
	 * @param DateTime|string $date
	 */
	public function set_date_last_purchased( $date ) {
		$this->set_date_column( 'last_purchased', $date );
	}

	/**
	 * Get the creation date of the customer's first paid order.
	 *
	 * @since 4.4
	 *
	 * @return DateTime|bool
	 */
	public function get_date_first_purchased() {
		if ( $this->is_registered() ) {
			$first_order = aw_get_customer_first_order( $this->get_user_id() );
		} else {
			$first_order = aw_get_customer_first_order( $this->get_email() );
		}

		if ( $first_order ) {
			return aw_normalize_date( $first_order->get_date_created() );
		}

		return false;
	}

	/**
	 * Takes into account the global optin_mode option.
	 *
	 * If the customer is unsubscribed then all workflows will still run but any emails sending to
	 * this customer will be rejected and marked in the logs.
	 *
	 * @return string
	 */
	public function is_unsubscribed() {
		do_action( 'automatewoo/customer/before_is_unsubscribed', $this );

		if ( Options::optin_enabled() ) {
			$is_unsubscribed = ! $this->get_is_subscribed();
		} else { // opt-out
			$is_unsubscribed = $this->get_is_unsubscribed();
		}

		return apply_filters( 'automatewoo/customer/is_unsubscribed', $is_unsubscribed, $this );
	}

	/**
	 * Check if the customer is opted in.
	 *
	 * @return bool
	 */
	public function is_opted_in() {
		return ! $this->is_unsubscribed();
	}

	/**
	 * Check if the customer is opted out.
	 *
	 * @return string
	 */
	public function is_opted_out() {
		return $this->is_unsubscribed();
	}

	/**
	 * Mark a customer as subscribed
	 */
	public function opt_in() {
		if ( $this->get_is_subscribed() ) {
			return; // already subscribed
		}

		$this->set_is_subscribed( true );
		$this->set_date_subscribed( new DateTime() );
		$this->set_is_unsubscribed( false );
		$this->save();
		$this->delete_meta( self::UNSUBSCRIBED_WORKFLOW_META_KEY );
		do_action( 'automatewoo/customer/opted_in', $this );
	}

	/**
	 * Mark a customer as unsubscribed
	 *
	 * @since 6.5.0 Added the $workflow_id parameter.
	 *
	 * @param int $workflow_id Workflow that caused the opt-out.
	 */
	public function opt_out( $workflow_id = 0 ) {
		if ( $this->get_is_unsubscribed() ) {
			return; // already unsubscribed
		}

		$this->set_is_unsubscribed( true );
		$this->set_date_unsubscribed( new DateTime() );
		$this->set_is_subscribed( false );
		$this->save();

		$workflow_id = Clean::id( $workflow_id );
		if ( $workflow_id ) {
			$this->update_meta( self::UNSUBSCRIBED_WORKFLOW_META_KEY, $workflow_id );
		}

		do_action( 'automatewoo/customer/opted_out', $this, $workflow_id );
	}

	/**
	 * Get the workflow that caused the opt-out.
	 *
	 * @since 6.5.0
	 *
	 * @return int
	 */
	public function get_unsubscribed_workflow_id() {
		return Clean::id( $this->get_meta( self::UNSUBSCRIBED_WORKFLOW_META_KEY ) );
	}

	/**
	 * Set the customer as unsubscribed.
	 *
	 * @param bool $unsubscribed
	 */
	public function set_is_unsubscribed( $unsubscribed ) {
		$this->set_prop( 'unsubscribed', aw_bool_int( $unsubscribed ) );
	}

	/**
	 * Check if the customer is unsubscribed.
	 *
	 * @return bool
	 */
	public function get_is_unsubscribed() {
		return (bool) $this->get_prop( 'unsubscribed' );
	}

	/**
	 * Get the date the customer unsubscribed.
	 *
	 * @return bool|DateTime
	 */
	public function get_date_unsubscribed() {
		return $this->get_date_column( 'unsubscribed_date' );
	}

	/**
	 * Set the date the customer unsubscribed.
	 *
	 * @param DateTime|string $date
	 */
	public function set_date_unsubscribed( $date ) {
		$this->set_date_column( 'unsubscribed_date', $date );
	}

	/**
	 * Set the customer as subscribed.
	 *
	 * @param bool $subscribed
	 */
	public function set_is_subscribed( $subscribed ) {
		$this->set_prop( 'subscribed', aw_bool_int( $subscribed ) );
	}

	/**
	 * Check if the customer is subscribed.
	 *
	 * @return bool
	 */
	public function get_is_subscribed() {
		return (bool) $this->get_prop( 'subscribed' );
	}

	/**
	 * Get the date the customer subscribed.
	 *
	 * @return bool|DateTime
	 */
	public function get_date_subscribed() {
		return $this->get_date_column( 'subscribed_date' );
	}

	/**
	 * Set the date the customer subscribed.
	 *
	 * @param DateTime|string $date
	 */
	public function set_date_subscribed( $date ) {
		$this->set_date_column( 'subscribed_date', $date );
	}

	/**
	 * Get the guest object.
	 *
	 * @return Guest|false
	 */
	public function get_guest() {
		if ( $this->is_registered() ) {
			return false;
		}
		return Guest_Factory::get( $this->get_guest_id() );
	}

	/**
	 * Get the user object.
	 *
	 * @return \WP_User
	 */
	public function get_user() {
		return get_userdata( $this->get_user_id() );
	}

	/**
	 * Get the cart object.
	 *
	 * @return Cart
	 */
	public function get_cart() {
		if ( $this->is_registered() ) {
			return Cart_Factory::get_by_user_id( $this->get_user_id() );
		} else {
			return Cart_Factory::get_by_guest_id( $this->get_guest_id() );
		}
	}

	/**
	 * Deletes the customer's stored cart.
	 *
	 * @since 4.3.0
	 */
	public function delete_cart() {
		$cart = $this->get_cart();
		if ( $cart ) {
			$cart->delete();
		}
	}

	/**
	 * Check if the customer is registered.
	 *
	 * @return bool
	 */
	public function is_registered() {
		return $this->get_user_id() !== 0;
	}

	/**
	 * Get the customer's email.
	 *
	 * @return string
	 */
	public function get_email() {
		return Clean::email( $this->get_linked_prop( 'email' ) );
	}

	/**
	 * Get the customer's first name.
	 *
	 * @return string
	 */
	public function get_first_name() {
		return $this->get_linked_prop( 'first_name' );
	}

	/**
	 * Get the customer's last name.
	 *
	 * @return string
	 */
	public function get_last_name() {
		return $this->get_linked_prop( 'last_name' );
	}

	/**
	 * Get the customer's full name.
	 *
	 * @return string
	 */
	public function get_full_name() {
		/* translators: 1: User First name, 2: User Last name */
		return trim( sprintf( _x( '%1$s %2$s', 'full name', 'automatewoo' ), $this->get_first_name(), $this->get_last_name() ) );
	}

	/**
	 * Get the customer's billing country.
	 *
	 * @return string
	 */
	public function get_billing_country() {
		return $this->get_linked_prop( 'billing_country' );
	}

	/**
	 * Get the customer's billing state.
	 *
	 * @return string
	 */
	public function get_billing_state() {
		return $this->get_linked_prop( 'billing_state' );
	}

	/**
	 * Get the customer's billing phone.
	 *
	 * @return string
	 */
	public function get_billing_phone() {
		return $this->get_linked_prop( 'billing_phone' );
	}

	/**
	 * Get the customer's billing postcode.
	 *
	 * @return string
	 */
	public function get_billing_postcode() {
		return $this->get_linked_prop( 'billing_postcode' );
	}

	/**
	 * Get the customer's billing city.
	 *
	 * @return string
	 */
	public function get_billing_city() {
		return $this->get_linked_prop( 'billing_city' );
	}

	/**
	 * Get the customer's billing address 1.
	 *
	 * @return string
	 */
	public function get_billing_address_1() {
		return $this->get_linked_prop( 'billing_address_1' );
	}

	/**
	 * Get the customer's billing address 2.
	 *
	 * @return string
	 */
	public function get_billing_address_2() {
		return $this->get_linked_prop( 'billing_address_2' );
	}

	/**
	 * Get the customer's billing company.
	 *
	 * @return string
	 */
	public function get_billing_company() {
		return $this->get_linked_prop( 'billing_company' );
	}

	/**
	 * Get the customer's billing address.
	 *
	 * @param bool $include_name
	 * @return array
	 */
	public function get_address( $include_name = true ) {
		$args = [];

		if ( $include_name ) {
			$args['first_name'] = $this->get_first_name();
			$args['last_name']  = $this->get_last_name();
		}

		$args['company']   = $this->get_billing_company();
		$args['address_1'] = $this->get_billing_address_1();
		$args['address_2'] = $this->get_billing_address_2();
		$args['city']      = $this->get_billing_city();
		$args['state']     = $this->get_billing_state();
		$args['postcode']  = $this->get_billing_postcode();
		$args['country']   = $this->get_billing_country();

		return $args;
	}

	/**
	 * Get the customer's formatted billing address.
	 *
	 * @param bool $include_name
	 * @return string
	 */
	public function get_formatted_billing_address( $include_name = true ) {
		return WC()->countries->get_formatted_address( $this->get_address( $include_name ) );
	}

	/**
	 * Get meta value using legacy meta system.
	 *
	 * The legacy meta system stored data in the WP user meta table when the customer was registered
	 * and in the AW guest meta table when the customer was a guest.
	 *
	 * It's worth noting that guest meta does not become user meta when a guest creates an account.
	 *
	 * @param string $key
	 * @return mixed
	 */
	public function get_legacy_meta( $key ) {

		if ( ! $key ) {
			return false;
		}

		if ( $this->is_registered() ) {
			return get_user_meta( $this->get_user_id(), $key, true );
		} else {
			$guest = $this->get_guest();
			if ( $guest ) {
				return $guest->get_meta( $key );
			}
		}

		return false;
	}

	/**
	 * Update meta value using legacy meta system.
	 *
	 * @see \AutomateWoo\Customer::get_legacy_meta()
	 *
	 * @param string $key
	 * @param mixed  $value
	 * @return mixed
	 */
	public function update_legacy_meta( $key, $value ) {

		if ( ! $key ) {
			return false;
		}

		if ( $this->is_registered() ) {
			update_user_meta( $this->get_user_id(), $key, $value );
		} else {
			$guest = $this->get_guest();
			if ( $guest ) {
				$guest->update_meta( $key, $value );
			}
		}
	}

	/**
	 * Get count of customer's orders.
	 *
	 * Includes orders that match user ID OR billing email.
	 *
	 * @return int
	 */
	public function get_order_count() {
		$count = $this->get_meta( 'order_count' );

		if ( '' !== $count ) {
			return (int) $count;
		}

		global $wpdb;

		$statuses = array_map( 'esc_sql', aw_get_counted_order_statuses() );

		if ( HPOS_Helper::is_HPOS_enabled() ) {
			$table = OrdersTableDataStore::get_orders_table_name();
			$query = "
				SELECT COUNT(DISTINCT id)
				FROM $table as orders
				WHERE orders.type = 'shop_order'
				AND orders.status IN ('" . implode( "','", $statuses ) . "')
				AND {$this->get_customer_order_sql()}
			";
		} else {
			$query = "
				SELECT COUNT(DISTINCT ID)
				FROM $wpdb->posts as posts
				LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
				WHERE posts.post_type = 'shop_order'
				AND posts.post_status IN ('" . implode( "','", $statuses ) . "')
				AND {$this->get_customer_order_sql()}
			";
		}

		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$count = (int) $wpdb->get_var( $wpdb->prepare( $query, $this->get_customer_order_sql_args() ) );

		$this->update_meta( 'order_count', $count );

		return $count;
	}

	/**
	 * Get total spent by the customer.
	 *
	 * Includes orders that match user ID OR billing email.
	 *
	 * @return float
	 */
	public function get_total_spent() {
		$total = $this->get_meta( 'total_spent' );

		if ( '' !== $total ) {
			return (float) $total;
		}

		global $wpdb;

		$statuses = array_map( 'aw_add_order_status_prefix', wc_get_is_paid_statuses() );
		$statuses = array_map( 'esc_sql', $statuses );

		if ( HPOS_Helper::is_HPOS_enabled() ) {
			$table = OrdersTableDataStore::get_orders_table_name();
			$query = "
				SELECT SUM(total_amount)
				FROM $table as orders
				WHERE orders.type = 'shop_order'
				AND orders.status IN ('" . implode( "','", $statuses ) . "')
				AND {$this->get_customer_order_sql()}
			";
		} else {
			$query = "
				SELECT SUM(order_total) FROM (
					SELECT posts.ID as order_id, meta2.meta_value as order_total
					FROM $wpdb->posts as posts
					LEFT JOIN {$wpdb->postmeta} AS meta ON posts.ID = meta.post_id
					LEFT JOIN {$wpdb->postmeta} AS meta2 ON posts.ID = meta2.post_id
					WHERE posts.post_type = 'shop_order'
					AND posts.post_status IN ('" . implode( "','", $statuses ) . "')
					AND {$this->get_customer_order_sql()}
					AND meta2.meta_key = '_order_total'
					GROUP BY order_id
				) AS orders_table
			";
		}

		// Use formatting function to round the total
		// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
		$total = (float) Format::decimal( $wpdb->get_var( $wpdb->prepare( $query, $this->get_customer_order_sql_args() ) ) );

		$this->update_meta( 'total_spent', $total );

		return $total;
	}

	/**
	 * Get SQL used for customer order queries.
	 *
	 * Used to get orders that match user ID OR email.
	 *
	 * @since 4.6.0
	 *
	 * @return string
	 */
	protected function get_customer_order_sql() {
		if ( HPOS_Helper::is_HPOS_enabled() ) {
			$sql = '( orders.billing_email = %s )';

			if ( $this->is_registered() ) {
				$sql .= 'OR ( orders.customer_id = %s )';
			}
		} else {
			$sql = "( meta.meta_key = '_billing_email' AND meta.meta_value = %s )";

			if ( $this->is_registered() ) {
				$sql .= "OR ( meta.meta_key = '_customer_user' AND meta.meta_value = %s )";
			}
		}

		if ( $this->is_registered() ) {
			return "(( {$sql} ))";
		}

		return $sql;
	}

	/**
	 * Get SQL query args used for customer order queries.
	 *
	 * @since 4.6.0
	 *
	 * @return array
	 */
	protected function get_customer_order_sql_args() {
		$args = [ $this->get_email() ];

		if ( $this->is_registered() ) {
			$args[] = $this->get_user_id();
		}

		return $args;
	}

	/**
	 * Get the customer's role.
	 *
	 * @return string
	 */
	public function get_role() {
		if ( $this->is_registered() ) {
			$user = $this->get_user();
			if ( $user ) {
				return current( $user->roles );
			}
		}

		return 'guest';
	}

	/**
	 * Get the customer's language if site is multilingual.
	 *
	 * @return string
	 */
	public function get_language() {
		$lang = '';

		if ( Language::is_multilingual() ) {
			if ( $this->is_registered() ) {
				$lang = Language::get_user_language( $this->get_user() );
			} else {
				$lang = Language::get_guest_language( $this->get_guest() );
			}
		}

		return apply_filters( 'automatewoo/customer/get_language', $lang, $this );
	}

	/**
	 * Gets the user registered date, if the user is registered.
	 *
	 * @since 4.4
	 *
	 * @return DateTime|bool
	 */
	public function get_date_registered() {
		$user = $this->get_user();

		if ( $user ) {
			// user_registered is saved in UTC
			return aw_normalize_date( $user->user_registered );
		}

		return false;
	}

	/**
	 * No need to save after using this method
	 *
	 * @param string $language
	 */
	public function update_language( $language ) {

		if ( ! Language::is_multilingual() || ! $language ) {
			return;
		}

		if ( $this->is_registered() ) {
			$user_lang = get_user_meta( $this->get_user_id(), '_aw_persistent_language', true );

			if ( $user_lang !== $language ) {
				Language::set_user_language( $this->get_user_id(), $language );
			}
		} else {
			$guest = $this->get_guest();
			if ( $guest ) {
				if ( $guest->get_language() !== $language ) {
					$guest->set_language( $language );
					$guest->save();
				}
			}
		}
	}

	/**
	 * Get product and variation ids of all the customers purchased products
	 *
	 * @return array
	 */
	public function get_purchased_products() {
		global $wpdb;

		$transient_name = 'aw_cpp_' . md5( $this->get_id() . \WC_Cache_Helper::get_transient_version( 'orders' ) );
		$products       = get_transient( $transient_name );

		if ( $products === false ) {

			$statuses = array_map( 'esc_sql', aw_get_counted_order_statuses( true ) );

			if ( HPOS_Helper::is_HPOS_enabled() ) {
				$orders_table = OrdersTableDataStore::get_orders_table_name();

				$query = "
					SELECT im.meta_value FROM {$orders_table} as orders
					INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON orders.id = i.order_id
					INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id
					WHERE orders.type = 'shop_order'
					AND orders.status IN ('" . implode( "','", $statuses ) . "')
					AND im.meta_key IN ( '_product_id', '_variation_id' )
					AND im.meta_value != 0
					AND {$this->get_customer_order_sql()}
				";
			} else {
				$query = "
					SELECT im.meta_value FROM {$wpdb->posts} AS p
					INNER JOIN {$wpdb->postmeta} AS meta ON p.ID = meta.post_id
					INNER JOIN {$wpdb->prefix}woocommerce_order_items AS i ON p.ID = i.order_id
					INNER JOIN {$wpdb->prefix}woocommerce_order_itemmeta AS im ON i.order_item_id = im.order_item_id
					WHERE p.post_status IN ( '" . implode( "','", $statuses ) . "' )
					AND im.meta_key IN ( '_product_id', '_variation_id' )
					AND im.meta_value != 0
					AND {$this->get_customer_order_sql()}
				";
			}

			// phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
			$result = $wpdb->get_col( $wpdb->prepare( $query, $this->get_customer_order_sql_args() ) );

			$products = array_unique( array_map( 'absint', $result ) );

			set_transient( $transient_name, $products, DAY_IN_SECONDS * 30 );
		}

		return $products;
	}

	/**
	 * Get the customer's linked properties.
	 *
	 * @param string $prop
	 * @return mixed
	 */
	public function get_linked_prop( $prop ) {
		$guest = false;
		$user  = false;

		if ( $this->is_registered() ) {
			$user = $this->get_user();
			if ( ! $user ) {
				return false;
			}
		} else {
			$guest = $this->get_guest();
			if ( ! $guest ) {
				return false;
			}
		}

		switch ( $prop ) {
			case 'email':
				return $this->is_registered() ? $user->user_email : $guest->get_email();
			case 'first_name':
				return $this->is_registered() ? $user->first_name : $guest->get_first_name();
			case 'last_name':
				return $this->is_registered() ? $user->last_name : $guest->get_last_name();
			case 'billing_country':
				return $this->is_registered() ? $user->billing_country : $guest->get_country();
			case 'billing_state':
				return $this->is_registered() ? $user->billing_state : $guest->get_state();
			case 'billing_phone':
				return $this->is_registered() ? $user->billing_phone : $guest->get_phone();
			case 'billing_company':
				return $this->is_registered() ? $user->billing_company : $guest->get_company();
			case 'billing_address_1':
				return $this->is_registered() ? $user->billing_address_1 : $guest->get_address_1();
			case 'billing_address_2':
				return $this->is_registered() ? $user->billing_address_2 : $guest->get_address_2();
			case 'billing_postcode':
				return $this->is_registered() ? $user->billing_postcode : $guest->get_postcode();
			case 'billing_city':
				return $this->is_registered() ? $user->billing_city : $guest->get_city();
			case 'tracking_key':
				if ( $this->is_registered() ) {
					// not every registered user will have a key
					$key = get_user_meta( $this->get_user_id(), 'automatewoo_visitor_key', true );
					if ( ! $key ) {
						$key = aw_generate_key( 32 );
						update_user_meta( $this->get_user_id(), 'automatewoo_visitor_key', $key );
					}
					return $key;
				} else {
					// guests are always created with a key
					return Clean::string( $guest->get_prop( 'tracking_key' ) );
				}
				break;
		}
	}

	/**
	 * Get reviews for our customer.
	 *
	 * @since 4.4
	 *
	 * @param array $args Arguments array.
	 *
	 * @return array|int
	 */
	public function get_reviews( $args = [] ) {
		$query_args = wp_parse_args(
			$args,
			[
				'status' => 'approve',
				'count'  => false,
				'type'   => 'review',
				'parent' => 0,
			]
		);

		if ( $this->is_registered() ) {
			$query_args['user_id'] = $this->get_user_id();
		} else {
			$query_args['author_email'] = $this->get_email();
		}

		return get_comments( $query_args );
	}

	/**
	 * Get the customer's review count.
	 *
	 * NOTE: This excludes multiple reviews of the same product.
	 *
	 * @return int
	 */
	public function get_review_count() {
		$cache_group = 'customer_review_count';

		if ( Cache::exists( $this->get_id(), $cache_group ) ) {
			$count = (int) Cache::get( $this->get_id(), $cache_group );
		} else {
			$count = $this->calculate_unique_product_review_count();
			Cache::set( $this->get_id(), $count, $cache_group );
		}

		return $count;
	}

	/**
	 * Calculate the customer's review count excluding multiple reviews on the same product.
	 *
	 * @since 4.5
	 *
	 * @return int
	 */
	public function calculate_unique_product_review_count() {
		global $wpdb;

		return (int) $wpdb->get_var(
			$wpdb->prepare(
				"SELECT COUNT(DISTINCT comment_post_ID) FROM {$wpdb->comments}
				WHERE comment_parent = 0
				AND comment_approved = 1
				AND comment_type = 'review'
				AND (user_ID = %d OR comment_author_email = %s)",
				$this->get_user_id(),
				$this->get_email()
			)
		);
	}

	/**
	 * Clear customer review count cache.
	 *
	 * @since 4.5
	 */
	public function clear_review_count_cache() {
		Cache::delete( $this->get_id(), 'customer_review_count' );
	}

	/**
	 * Gets the last review date for the user.
	 *
	 * @since 4.4
	 *
	 * @return DateTime|bool
	 */
	public function get_last_review_date() {
		$cache_key         = 'customer_last_review_date';
		$last_comment_date = false;

		if ( Temporary_Data::exists( $cache_key, $this->get_id() ) ) {
			$last_comment_date = Temporary_Data::get( $cache_key, $this->get_id() );
		} else {
			$comments = $this->get_reviews( [ 'number' => 1 ] );

			if ( ! empty( $comments ) ) {
				$last_comment_date = $comments[0]->comment_date_gmt;
				Temporary_Data::set( $cache_key, $this->get_id(), $last_comment_date );
			}
		}

		if ( ! $last_comment_date ) {
			return false;
		}

		return new DateTime( $last_comment_date );
	}

	/**
	 * Get the date that a workflow last run for the customer.
	 *
	 * @since 4.4
	 *
	 * @param int|array|Workflow $workflow Workflow object, ID or array of IDs.
	 *
	 * @return DateTime|bool
	 */
	public function get_workflow_last_run_date( $workflow ) {
		if ( ! $workflow ) {
			return false;
		}

		$query = new Log_Query();
		$query->where_workflow( $workflow );
		$query->where_customer_or_legacy_user( $this, true );
		$query->set_limit( 1 );
		$query->set_ordering( 'date', 'DESC' );
		$results = $query->get_results();

		if ( $results ) {
			return current( $results )->get_date();
		}

		return false;
	}

	/**
	 * Get customer's nth last paid order.
	 *
	 * @param int $n
	 *
	 * @return \WC_Order|bool
	 *
	 * @since 4.8.0
	 */
	public function get_nth_last_paid_order( $n ) {
		$query_args = [
			'type'    => 'shop_order',
			'limit'   => 1,
			'offset'  => $n - 1,
			'status'  => wc_get_is_paid_statuses(),
			'orderby' => 'date',
			'order'   => 'DESC',
		];

		if ( $this->is_registered() ) {
			$query_args['customer'] = [ $this->get_user_id(), $this->get_email() ];
		} else {
			$query_args['customer'] = $this->get_email();
		}

		return current( wc_get_orders( $query_args ) );
	}

	/**
	 * Recalculate the customer's last paid order date.
	 *
	 * @since 6.6.0
	 *
	 * @return DateTime|false
	 */
	public function recache_date_last_purchased() {
		$last_paid_order = $this->get_nth_last_paid_order( 1 );
		$date            = $last_paid_order ? $last_paid_order->get_date_created() : null;

		$this->set_date_last_purchased( $date );
		$this->save();

		return $this->get_date_last_purchased();
	}

	/**
	 * Clear cached customer data.
	 *
	 * @since 6.1.8
	 */
	public function clear_cached_data() {
		// Clear cached dashboard counts.
		Cache::flush_group( 'dashboard' );
	}
}
