<?php
/**
 * Coupon model.
 *
 * @since 2.5.12
 *
 * @package Masteriyo\Addons\Coupons
 */

namespace Masteriyo\Addons\Coupons\Models;

use Masteriyo\Addons\Coupons\Enums\AppliesToType;
use Masteriyo\Addons\Coupons\Enums\CouponDiscountType;
use Masteriyo\Addons\Coupons\Enums\CouponStatus;
use Masteriyo\Addons\Coupons\Enums\DeductionMethodType;
use Masteriyo\Database\Model;
use Masteriyo\Addons\Coupons\Repository\CouponRepository;
use Masteriyo\Helper\Utils;
use Masteriyo\PostType\PostType;

defined( 'ABSPATH' ) || exit;

/**
 * Coupon model (post type).
 *
 * @since 2.5.12
 */
class Coupon extends Model {

	/**
	 * This is the name of this object type.
	 *
	 * @since 2.5.12
	 *
	 * @var string
	 */
	protected $object_type = 'coupon';

	/**
	 * Post type.
	 *
	 * @since 2.5.12
	 *
	 * @var string
	 */
	protected $post_type = PostType::COUPON;

	/**
	 * Cache group.
	 *
	 * @since 2.5.12
	 *
	 * @var string
	 */
	protected $cache_group = 'coupons';

	/**
	 * Stores coupon data.
	 *
	 * @since 2.5.12
	 *
	 * @var array
	 */
	protected $data = array(
		'code'                   => '',
		'description'            => '',
		'author_id'              => 0,
		'created_at'             => null,
		'modified_at'            => null,
		'status'                 => CouponStatus::DRAFT,
		'discount_type'          => CouponDiscountType::FIXED_CART,
		'discount_amount'        => 0,
		'usage_limit_per_user'   => 0,
		'usage_limit_per_coupon' => 0,
		'usage_count'            => 0,
		'start_at'               => null,
		'expire_at'              => null,
		'stackable'              => 'no',

		/**
		 * Newly added.
		 *
		 * @since 2.18.3
		 */
		'method'                 => DeductionMethodType::CODE,
		'applies_to'             => array(
			'type'   => AppliesToType::ALL,
			'target' => array(),
		),
	);

	/**
	 * Get the coupon if ID
	 *
	 * @since 2.5.12
	 *
	 * @param CouponRepository $coupon_repository Coupon Repository.
	 */
	public function __construct( CouponRepository $coupon_repository ) {
		$this->repository = $coupon_repository;
	}

	/*
	|--------------------------------------------------------------------------
	| Non-CRUD Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get the object type.
	 *
	 * @since 2.5.12
	 *
	 * @return string
	 */
	public function get_object_type() {
		return $this->object_type;
	}

	/**
	 * Get the post type.
	 *
	 * @since 2.5.12
	 *
	 * @return string
	 */
	public function get_post_type() {
		return $this->post_type;
	}

	/**
	 * Check if the coupon is valid.
	 *
	 * @since 2.5.12
	 *
	 * @param \Masteriyo\Cart\Cart|\Masteriyo\Models\Order\Order|null $context Related cart or order object.
	 *
	 * @return boolean|\WP_Error
	 */
	public function is_valid( $context = null ) {
		$error = new \WP_Error();

		if ( ! $this->exists() ) {
			$error->add( 'coupon_does_not_exist', __( 'Coupon does not exist.', 'learning-management-system' ) );
			return $error;
		}
		if ( ! $this->is_active() ) {
			$error->add( 'coupon_cannot_be_used', __( 'The coupon cannot be used.', 'learning-management-system' ) );
			return $error;
		}
		if ( $this->is_usage_limit_per_coupon_reached() || $this->is_usage_limit_per_user_reached( 0, $context ) ) {
			$error->add( 'coupon_does_not_exist', __( 'Coupon usage limit has been reached.', 'learning-management-system' ) );
			return $error;
		}
		if ( ! $this->is_started() ) {
			$error->add( 'coupon_cannot_be_used', __( 'The coupon cannot be used yet.', 'learning-management-system' ) );
			return $error;
		}
		if ( $this->is_expired() ) {
			$error->add( 'coupon_cannot_be_used', __( 'This coupon has expired.', 'learning-management-system' ) );
			return $error;
		}

		$item = masteriyo_get_item_from_cart( $context );

		if ( $item ) {
			$is_valid = $this->validate_applicability( $item );

			if ( ! $is_valid ) {
				$error->add( 'invalid_coupon_applicability', __( 'Coupon does not apply to this item.', 'learning-management-system' ) );
				return $error;
			}
		}

		/**
		 * Filters validity of a coupon.
		 *
		 * @since 2.5.12
		 *
		 * @param \WP_Error $error Errors container.
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $coupon Coupon model object.
		 * @param \Masteriyo\Cart\Cart|\Masteriyo\Models\Order\Order|null $context Related cart or order object.
		 */
		$error = apply_filters( 'masteriyo_coupon_validate', $error, $this, $context );

		if ( $error->has_errors() ) {
			return $error;
		}

		return true;
	}

	/**
	 * Check if the coupon exists.
	 *
	 * A coupon is also considered to no longer exist if it has been placed in the trash, even if the trash has not yet
	 * been emptied.
	 *
	 * @since 2.5.12
	 *
	 * @return boolean
	 */
	public function exists() {
		$exists = false;

		if ( $this->get_id() && CouponStatus::TRASH !== $this->get_status() ) {
			$exists = true;
		}

		/**
		 * Filter coupon exists.
		 *
		 * @since 2.5.12
		 *
		 * @param boolean $exists Coupon exist.
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $this Coupon object.
		 */
		return apply_filters( 'masteriyo_coupon_exists', $exists, $this );
	}

	/**
	 * Check if the usage limit per coupon has reached.
	 *
	 * @since 2.5.12
	 *
	 * @return boolean
	 */
	public function is_usage_limit_per_coupon_reached() {
		$limit_reached = false;

		if ( $this->get_usage_limit_per_coupon() ) {
			$usage_count   = $this->get_usage_count();
			$limit         = $this->get_usage_limit_per_coupon();
			$limit_reached = $usage_count >= $limit ? true : false;
		}

		/**
		 * Filter coupon usage limit per coupon reached.
		 *
		 * @since 2.5.12
		 *
		 * @param boolean $limit_reached
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $this Coupon object.
		 */
		return apply_filters( 'masteriyo_coupon_is_usage_limit_per_coupon_reached', $limit_reached, $this );
	}

	/**
	 * Check if the usage limit has reached for the given user or the current user.
	 *
	 * @since 2.5.12
	 *
	 * @param integer $user_id User ID.
	 * @param \Masteriyo\Cart\Cart|\Masteriyo\Models\Order\Order|null $context Related cart or order object.
	 *
	 * @return boolean
	 */
	public function is_usage_limit_per_user_reached( $user_id = 0, $context = null ) {
		$limit_reached = false;

		if ( $this->get_usage_limit_per_user() ) {
			if ( empty( $user_id ) ) {
				$user_id = is_a( $context, '\Masteriyo\Models\Order\Order' ) ? $context->get_customer_id() : get_current_user_id();
			}

			if ( $user_id ) {
				$data_store    = masteriyo_create_coupon_store();
				$usage_count   = $data_store->get_usage_count_by_user( $this, $user_id );
				$limit_reached = $usage_count >= $this->get_usage_limit_per_user() ? true : false;
			}
		}

		/**
		 * Filter whether coupon usage limit per user is reached or not.
		 *
		 * @since 2.5.12
		 *
		 * @param boolean $limit_reached
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $this Coupon object.
		 */
		return apply_filters( 'masteriyo_coupon_is_usage_limit_per_user_reached', $limit_reached, $this );
	}

	/**
	 * Check if the start date has been reached.
	 *
	 * @since 2.5.12
	 *
	 * @return boolean
	 */
	public function is_started() {
		$started_at = $this->get_start_at();
		$is_started = true;

		if ( $started_at ) {
			$is_started = time() >= $started_at->getTimestamp();
		}

		/**
		 * Filters boolean: True if the coupon start date has been reached, otherwise false.
		 *
		 * @since 2.5.12
		 *
		 * @param boolean $is_started True if the coupon start date has been reached, otherwise false.
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $coupon
		 */
		$is_started = apply_filters( 'masteriyo_coupon_is_started', $is_started, $this );

		return masteriyo_string_to_bool( $is_started );
	}

	/**
	 * Checks if the coupon has expired.
	 *
	 * @since 2.5.12
	 *
	 * @return boolean
	 */
	public function is_expired() {
		$expiry_date = $this->get_expire_at();
		$expired     = false;

		if ( ! empty( $expiry_date ) ) {
			$expired = time() > $expiry_date->getTimestamp();
		}

		/**
		 * Filters boolean: True if the coupon has expired, otherwise false.
		 *
		 * @since 2.5.12
		 *
		 * @param boolean $expired True if the coupon has expired, otherwise false.
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $coupon
		 */
		$expired = apply_filters( 'masteriyo_coupon_is_expired', $expired, $this );

		return masteriyo_string_to_bool( $expired );
	}

	/**
	 * Check if the coupon's static status is 'active'.
	 *
	 * @since 2.5.12
	 *
	 * @return boolean
	 */
	public function is_active() {
		return $this->get_status() === CouponStatus::ACTIVE;
	}

	/**
	 * Check if the coupon can be used.
	 *
	 * @since 2.5.12
	 *
	 * @return boolean
	 */
	public function is_usable() {
		$is_usable = $this->is_active() && $this->is_started() && ! $this->is_expired();

		/**
		 * Filters whether the coupon can be used or not.
		 *
		 * @since 2.5.12
		 *
		 * @param boolean $is_usable
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $this Coupon object.
		 */
		return apply_filters( 'masteriyo_coupon_is_usable', $is_usable, $this );
	}

	/**
	 * Alias for 'is_usable' method. Check if the coupon can be used.
	 *
	 * @since 2.5.12
	 *
	 * @return boolean
	 */
	public function can_be_used() {
		return $this->is_usable();
	}

	/**
	 * Get dynamic status of the coupon.
	 *
	 * @since 2.5.12
	 *
	 * @return string
	 */
	public function get_dynamic_status() {
		$status = $this->get_status();

		if ( CouponStatus::DRAFT === $status || CouponStatus::TRASH === $status ) {
			$dynamic_status = $status;
		} elseif ( $this->is_expired() ) {
			$dynamic_status = CouponStatus::EXPIRED;
		} elseif ( ! $this->is_started() ) {
			$dynamic_status = CouponStatus::SCHEDULED;
		} else {
			$dynamic_status = $status;
		}

		/**
		 * Filters coupon dynamic status.
		 *
		 * @since 2.5.12
		 *
		 * @param string $dynamic_status Dynamic status.
		 * @param string $status Coupon status
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $this Coupon object.
		 */
		return apply_filters( 'masteriyo_coupon_dynamic_status', $dynamic_status, $status, $this );
	}

	/**
	 * Check if the coupon applies to the given product, bundle, or category.
	 *
	 * @since 2.18.3
	 *
	 * @param \Masteriyo\Models\Course|\Masteriyo\Addons\CourseBundle\Models\CourseBundle $item Item object.
	 *
	 * @return boolean
	 */
	public function validate_applicability( $item ) {
		$coupon_to_be_applied_item_type = '';

		if ( is_a( $item, 'Masteriyo\Models\Course' ) ) {
			$coupon_to_be_applied_item_type = 'course';
		}

		if ( is_a( $item, 'Masteriyo\Addons\CourseBundle\Models\CourseBundle' ) ) {
			$coupon_to_be_applied_item_type = 'bundle';
		}

		if ( empty( $coupon_to_be_applied_item_type ) ) {
			return false;
		}

		$applies_to = $this->get_applies_to();

		// If 'type' is not set or invalid, assume it's applicable.
		if ( ! isset( $applies_to['type'] ) || ! in_array( $applies_to['type'], AppliesToType::all(), true ) ) {
			return true;
		}

		$applies_to_type = $applies_to['type'];

		// If the coupon applies to all items, return true immediately.
		if ( AppliesToType::ALL === $applies_to_type ) {
			return true;
		}

		if ( AppliesToType::ALL_COURSES === $applies_to_type ) {
			return 'course' === $coupon_to_be_applied_item_type;
		}

		if ( AppliesToType::ALL_BUNDLES === $applies_to_type ) {
			return 'bundle' === $coupon_to_be_applied_item_type;
		}

		// If no specific target are defined, assume applicability.
		if ( ! isset( $applies_to['target'] ) || empty( $applies_to['target'] ) ) {
			return true;
		}

		$target_ids = array_filter(
			array_map(
				function ( $target ) {
					return absint( $target['id'] );
				},
				$applies_to['target']
			)
		);

		$item_id = $item->get_id();

		// Validate applicable targets.
		switch ( $applies_to_type ) {
			case AppliesToType::SPECIFIC_COURSES:
				// If saved value is empty, return true (can't validate).
				if ( empty( $target_ids ) ) {
					return true;
				}

				return 'course' === $coupon_to_be_applied_item_type && in_array( $item_id, $target_ids, true );

			case AppliesToType::SPECIFIC_BUNDLES:
				// If saved value is empty, return true (can't validate).
				if ( empty( $target_ids ) ) {
					return true;
				}

				return 'bundle' === $coupon_to_be_applied_item_type && in_array( $item_id, $target_ids, true );

			case AppliesToType::SPECIFIC_CATEGORIES:
				// If saved value is empty, return true (can't validate).
				if ( empty( $target_ids ) ) {
					return true;
				}

				if ( 'course' === $coupon_to_be_applied_item_type ) {
					$category_ids = array_map(
						fn( $term ) => $term->term_id,
						Utils::get_object_terms( $item_id, 'course_cat' )
					);

					// Check if any category ID overlaps with the specific categories
					return ! empty( array_intersect( $category_ids, $target_ids ) );
				}
				break;

			default:
				return false;
		}

		return false;
	}


	/*
	|--------------------------------------------------------------------------
	| Getters
	|--------------------------------------------------------------------------
	*/

	/**
	 * Get coupon code.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_code( $context = 'view' ) {
		/**
		 * Filters coupon code.
		 *
		 * @since 2.5.12
		 *
		 * @param string $code Coupon code.
		 * @param \Masteriyo\Addons\Coupons\Models\Coupon $coupon Coupon object.
		 */
		return apply_filters( 'masteriyo_coupon_code', $this->get_prop( 'code', $context ), $this );
	}

	/**
	 * Get coupon created date.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_created_at( $context = 'view' ) {
		return $this->get_prop( 'created_at', $context );
	}

	/**
	 * Get coupon modified date.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_modified_at( $context = 'view' ) {
		return $this->get_prop( 'modified_at', $context );
	}

	/**
	 * Get coupon description.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_description( $context = 'view' ) {
		return $this->get_prop( 'description', $context );
	}

	/**
	 * Returns the coupon's author id.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_author_id( $context = 'view' ) {
		return $this->get_prop( 'author_id', $context );
	}

	/**
	 * Get coupon status.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_status( $context = 'view' ) {
		return $this->get_prop( 'status', $context );
	}

	/**
	 * Get coupon discount type.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string
	 */
	public function get_discount_type( $context = 'view' ) {
		return $this->get_prop( 'discount_type', $context );
	}

	/**
	 * Get coupon discount amount.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int
	 */
	public function get_discount_amount( $context = 'view' ) {
		return $this->get_prop( 'discount_amount', $context );
	}

	/**
	 * Get coupon usage per limit user.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int
	 */
	public function get_usage_limit_per_user( $context = 'view' ) {
		return $this->get_prop( 'usage_limit_per_user', $context );
	}

	/**
	 * Get coupon usage per limit coupon.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return int
	 */
	public function get_usage_limit_per_coupon( $context = 'view' ) {
		return $this->get_prop( 'usage_limit_per_coupon', $context );
	}

	/**
	 * Get coupon usage count.
	 *
	 * @since 2.5.12
	 *
	 * @param string $context What the value is for. Valid values are view and edit.
	 *
	 * @return integer
	 */
	public function get_usage_count( $context = 'view' ) {
		return $this->get_prop( 'usage_count', $context );
	}

	/**
	 * Get coupon start/active date.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return DateTime|NULL|int object if the date is set or null if there is no date.
	 */
	public function get_start_at( $context = 'view' ) {
		$date = $this->get_prop( 'start_at', $context );

		return ( $date && 'edit' === $context ) ? $date->getTimestamp() : $date;
	}

	/**
	 * Get coupon expire/active date.
	 *
	 * @since  2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return DateTime|NULL object if the date is set or null if there is no date.
	 */
	public function get_expire_at( $context = 'view' ) {
		$date = $this->get_prop( 'expire_at', $context );

		return ( $date && 'edit' === $context ) ? $date->getTimestamp() : $date;
	}

	/**
	 * Get the applies_to value.
	 *
	 * @since 2.18.3
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return array Array of applies_to value.
	 */
	public function get_applies_to( $context = 'view' ) {
		return $this->get_prop( 'applies_to', array(), $context );
	}

	/**
	 * Get the deduction method value.
	 *
	 * @since 2.5.12
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string Deduction method value.
	 */
	public function get_method( $context = 'view' ) {
		return $this->get_prop( 'method', $context );
	}

	/**
	 * Get the stackable value.
	 *
	 * @since 2.18.3
	 *
	 * @param  string $context What the value is for. Valid values are view and edit.
	 *
	 * @return string Stackable value (yes/no).
	 */
	public function get_stackable( $context = 'view' ) {
		return $this->get_prop( 'stackable', $context );
	}

	/*
	|--------------------------------------------------------------------------
	| Setters
	|--------------------------------------------------------------------------
	|
	*/

	/**
	 * Set coupon code.
	 *
	 * @since 2.5.12
	 *
	 * @param string $code coupon code.
	 */
	public function set_code( $code ) {
		$this->set_prop( 'code', $code );
	}

	/**
	 * Set coupon created date.
	 *
	 * @since 2.5.12
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_created_at( $date = null ) {
		$this->set_date_prop( 'created_at', $date );
	}

	/**
	 * Set coupon modified date.
	 *
	 * @since 2.5.12
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_modified_at( $date = null ) {
		$this->set_date_prop( 'modified_at', $date );
	}

	/**
	 * Set coupon description.
	 *
	 * @since 2.5.12
	 *
	 * @param string $description Coupon description.
	 */
	public function set_description( $description ) {
		$this->set_prop( 'description', $description );
	}

	/**
	 * Set the coupon's author id.
	 *
	 * @since 2.5.12
	 *
	 * @param int $author_id author id.
	 */
	public function set_author_id( $author_id ) {
		$this->set_prop( 'author_id', absint( $author_id ) );
	}

	/**
	 * Set coupon status.
	 *
	 * @since 2.5.12
	 *
	 * @param string $status Coupon status.
	 */
	public function set_status( $status ) {
		$this->set_prop( 'status', $status );
	}

	/**
	 * Set coupon discount type.
	 *
	 * @since 2.5.12
	 *
	 * @param string $discount_type Coupon discount type.
	 */
	public function set_discount_type( $discount_type ) {
		$this->set_prop( 'discount_type', $discount_type );
	}

	/**
	 * Set coupon discount amount.
	 *
	 * @since 2.5.12
	 *
	 * @param float $discount_amount Coupon discount amount.
	 */
	public function set_discount_amount( $discount_amount ) {
		$this->set_prop( 'discount_amount', floatval( $discount_amount ) );
	}

	/**
	 * Set coupon usage limit per user.
	 *
	 * @since 2.5.12
	 *
	 * @param string $usage_limit_per_user Coupon usage limit per user.
	 */
	public function set_usage_limit_per_user( $usage_limit_per_user ) {
		$this->set_prop( 'usage_limit_per_user', absint( $usage_limit_per_user ) );
	}

	/**
	 * Set coupon usage limit per coupon.
	 *
	 * @since 2.5.12
	 *
	 * @param string $usage_limit_per_coupon Coupon usage limit per coupon.
	 */
	public function set_usage_limit_per_coupon( $usage_limit_per_coupon ) {
		$this->set_prop( 'usage_limit_per_coupon', absint( $usage_limit_per_coupon ) );
	}

	/**
	 * Set coupon usage count.
	 *
	 * @since 2.5.12
	 *
	 * @param string $usage_count Coupon usage count.
	 */
	public function set_usage_count( $usage_count ) {
		$this->set_prop( 'usage_count', absint( $usage_count ) );
	}

	/**
	 * Set coupon start/active date.
	 *
	 * @since 2.5.12
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_start_at( $date = null ) {
		$this->set_date_prop( 'start_at', $date );
	}

	/**
	 * Set coupon expire/active date.
	 *
	 * @since 2.5.12
	 *
	 * @param string|integer|null $date UTC timestamp, or ISO 8601 DateTime. If the DateTime string has no timezone or offset, WordPress site timezone will be assumed. Null if their is no date.
	 */
	public function set_expire_at( $date = null ) {
		$this->set_date_prop( 'expire_at', $date );
	}

	/**
	 * Set the applies_to value.
	 *
	 * @since 2.18.3
	 *
	 * @param array $applies_to
	 */
	public function set_applies_to( $applies_to ) {
		$this->set_prop( 'applies_to', $applies_to );
	}

	/**
	 * Set the deduction method.
	 *
	 * @since 2.18.3
	 *
	 * @param int $amount Amount to deduct from usage count.
	 */
	public function set_method( $method ) {
		$this->set_prop( 'method', $method );
	}

	/**
	 * Set the stackable property.
	 *
	 * @since 2.18.3
	 *
	 * @param string $stackable Indicates if the coupon is stackable with other coupons. (yes/no)
	 */
	public function set_stackable( $stackable ) {
		$this->set_prop( 'stackable', $stackable );
	}
}
