<?php
/**
 * Creates a cart with the parameters of the purchase being placed.
 *
 * @package WP_Ultimo
 * @subpackage Order
 * @since 2.0.0
 */

namespace WP_Ultimo\Checkout;

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

use WP_Ultimo\Database\Payments\Payment_Status;
use WP_Ultimo\Models\Product;

/**
 * Creates a cart with the parameters of the purchase being placed.
 *
 * @package WP_Ultimo
 * @subpackage Checkout
 * @since 2.0.0
 */
class Line_Item implements \JsonSerializable {

	/**
	 * The hash to be used as a base to deal with id.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $hash;

	/**
	 * The id of the line item.
	 *
	 * It starts with LN_TYPE.
	 * E.g. LN_FEE_#HASH.
	 * Should not be set manually.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $id;

	/**
	 * The line item type.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $type = 'product'; // discount, fee, credit, proration

	/**
	 * The product id, if any.
	 *
	 * @since 2.0.0
	 * @var null|int
	 */
	protected $product_id;

	/**
	 * The product slug, if any.
	 *
	 * @since 2.0.0
	 * @var null|string
	 */
	protected $product_slug;

	/**
	 * The discount code id, if any.
	 *
	 * @since 2.0.0
	 * @var null|int
	 */
	protected $discount_code_id;

	/**
	 * The title of the line item.
	 *
	 * Usually the name of the product.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $title = '';

	/**
	 * The description of the line item.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $description = '';

	/**
	 * Should we apply discounts to this line item?
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $discountable = false;

	/**
	 * Should we apply taxes to this line item?
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $taxable = false;

	/**
	 * Is this line item recurring?
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $recurring = false;

	/**
	 * The billing cycle duration, if recurring.
	 *
	 * @since 2.0.0
	 * @var integer
	 */
	protected $duration = 1;

	/**
	 * The billing cycle duration unit, if recurring.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $duration_unit = 'month';

	/**
	 * The number of billing cycles, if recurring.
	 *
	 * @since 2.0.0
	 * @var integer
	 */
	protected $billing_cycles = 0;

	/**
	 * Quantity of the given product.
	 *
	 * @since 2.0.0
	 * @var float
	 */
	protected $quantity = 1;

	/**
	 * Unit price of the product.
	 *
	 * @since 2.0.0
	 * @var integer
	 */
	protected $unit_price = 0;

	/**
	 * Value before taxes, discounts, fees and etc.
	 *
	 * @since 2.0.0
	 * @var float
	 */
	protected $subtotal = 0;

	/**
	 * The value of the discount being applied.
	 *
	 * @since 2.0.0
	 * @var float
	 */
	protected $discount_rate = 0;

	/**
	 * The discount type.
	 *
	 * Percentage or absolute (for flat discounts)
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $discount_type = 'percentage';

	/**
	 * Discount Label.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $discount_label = '';

	/**
	 * If we should apply discount to renewals.
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $apply_discount_to_renewals = true;

	/**
	 * The total value in discounts.
	 *
	 * @since 2.0.0
	 * @var integer
	 */
	protected $discount_total = 0;

	/**
	 * The tax category of the line item.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $tax_category = '';

	/**
	 * Label of the tax applied.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $tax_label = '';

	/**
	 * Type of the tax, percentage or absolute.
	 *
	 * @since 2.0.0
	 * @var string
	 */
	protected $tax_type = 'percentage';

	/**
	 * Tax amount, absolute or percentage.
	 *
	 * @since 2.0.0
	 * @var float
	 */
	protected $tax_rate = 0;

	/**
	 * If tax are included in the price or not.
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $tax_inclusive = false;

	/**
	 * If the line item is tax exempt ot not.
	 *
	 * @since 2.0.0
	 * @var boolean
	 */
	protected $tax_exempt = false;

	/**
	 * The amount, in currency, of the tax.
	 *
	 * @since 2.0.0
	 * @var float
	 */
	protected $tax_total = 0;

	/**
	 * The total amount of the line item.
	 *
	 * @since 2.0.0
	 * @var integer
	 */
	protected $total = 0;

	/**
	 * The date the line item was created.
	 *
	 * @since 2.2.0
	 * @var string
	 */
	protected $date_created;

	/**
	 * Instantiate the class.
	 *
	 * @since 2.0.0
	 *
	 * @param array $atts Line item attributes.
	 */
	public function __construct($atts) {

		$this->attributes($atts);

		/*
		* Refresh totals.
		*/
		$this->recalculate_totals();
	}

	/**
	 * Loops through allowed fields and loads them.
	 *
	 * @since 2.0.0
	 *
	 * @param array $data Array of key => values billing address fields.
	 * @return void
	 */
	public function attributes($data): void {
		/*
		* Set type first to allow for overriding the other parameters.
		*/
		$type = wu_get_isset($data, 'type');

		if ($type) {
			$this->set_type($data['type']);
		}

		/*
		* Set product first to allow for overriding the other parameters.
		*/
		$product = wu_get_isset($data, 'product');

		if ($product) {
			$this->set_product($data['product']);

			unset($data['product']);
		}

		$allowed_attributes = array_keys(get_object_vars($this));

		foreach ($data as $key => $value) {
			if (in_array($key, $allowed_attributes, true)) {
				$this->{$key} = $value;
			}
		}

		$this->id = 'LN_' . strtoupper($this->type) . '_' . $this->hash;
	}

	/**
	 * Get the value of id
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_id() {

		return $this->id;
	}

	/**
	 * Get the value of type
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_type() {

		return $this->type;
	}

	/**
	 * Set the value of type.
	 *
	 * Accepted values: product, fee, credit, discount, prorate.
	 *
	 * @since 2.0.0
	 * @param string $type The line item type.
	 * @return void
	 */
	public function set_type($type): void {

		$this->type = $type;
	}

	/**
	 * Get product associated with this line item.
	 *
	 * @since 2.0.0
	 * @return \WP_Ultimo\Models\Product|false
	 */
	public function get_product() {

		$product = wu_get_product($this->product_id);

		if ( ! $product) {
			return false;
		}

		if ($product->is_recurring() && ($product->get_duration_unit() !== $this->duration_unit || $product->get_duration() !== $this->duration)) {
			$product_variation = $product->get_as_variation($this->duration, $this->duration_unit);

			/*
			 * Checks if the variation exists before re-setting the product.
			 */
			if ($product_variation) {
				$product = $product_variation;
			}
		}

		return $product;
	}

	/**
	 * Set product associated with this line item.
	 *
	 * @since 2.0.0
	 * @param Product $product Product associated with this line item.
	 * @return void
	 */
	public function set_product($product): void {

		$this->product_id = $product->get_id();

		$this->hash = $product->get_hash();

		$this->title = $product->get_name();

		$this->description = $product->get_description();

		$this->unit_price = $product->get_amount();

		$this->recurring = $product->is_recurring();

		$this->duration = $product->get_duration();

		$this->duration_unit = $product->get_duration_unit();

		$this->billing_cycles = $product->get_billing_cycles();

		$this->taxable = $product->is_taxable();

		$this->tax_category = $product->get_tax_category();

		$this->discountable = true;
	}

	/**
	 * Calculate the taxes value based on the tax_amount and tax_type.
	 *
	 * @since 2.0.0
	 * @param float $sub_total Value to apply taxes to.
	 * @return float
	 */
	public function calculate_taxes($sub_total) {

		return wu_get_tax_amount($sub_total, $this->get_tax_rate(), $this->get_tax_type(), false, $this->get_tax_inclusive());
	}

	/**
	 * Calculate the discounts value based on the discount_amount and discount_type.
	 *
	 * @since 2.0.0
	 * @param float $sub_total Value to apply discounts to.
	 * @return float
	 */
	public function calculate_discounts($sub_total) {

		return wu_get_tax_amount($sub_total, $this->get_discount_rate(), $this->get_discount_type(), false);
	}

	/**
	 * Recalculate payment totals.
	 *
	 * @since 2.0.0
	 * @return \WP_Ultimo\Checkout\Line_Item
	 */
	public function recalculate_totals() {

		$sub_total = $this->get_quantity() * $this->get_unit_price();

		$discounts = $this->calculate_discounts($sub_total);

		$discounted_subtotal = $sub_total - $discounts;

		if ($sub_total > 0 && $discounted_subtotal < 0) {
			$discounted_subtotal = 0;

			$discounts = $sub_total;
		}

		$taxes = $this->calculate_taxes($discounted_subtotal);

		if ($this->get_tax_inclusive()) {
			$total = $this->is_tax_exempt() ? $discounted_subtotal - $taxes : $discounted_subtotal;
		} else {
			$total = $this->is_tax_exempt() ? $discounted_subtotal : $discounted_subtotal + $taxes; // tax exclusive

		}

		if ($this->is_tax_exempt()) {
			$taxes = 0;
		}

		$totals = [
			'subtotal'       => $sub_total,
			'discount_total' => $discounts,
			'tax_total'      => $taxes,
			'total'          => $total,
		];

		$this->attributes($totals);

		return $this;
	}

	/**
	 * Get quantity of the given product.
	 *
	 * @since 2.0.0
	 * @return float
	 */
	public function get_quantity() {

		return $this->quantity;
	}

	/**
	 * Set quantity of the given product.
	 *
	 * @since 2.0.0
	 * @param float $quantity Quantity of the given product.
	 * @return void
	 */
	public function set_quantity($quantity): void {

		$this->quantity = $quantity;
	}

	/**
	 * Get unit price of the product.
	 *
	 * @since 2.0.0
	 * @return integer
	 */
	public function get_unit_price() {

		return $this->unit_price;
	}

	/**
	 * Set unit price of the product.
	 *
	 * @since 2.0.0
	 * @param integer $unit_price Unit price of the product.
	 * @return void
	 */
	public function set_unit_price($unit_price): void {

		$this->unit_price = $unit_price;
	}

	/**
	 * Get tax amount, absolute or percentage.
	 *
	 * @since 2.0.0
	 * @return float
	 */
	public function get_tax_rate() {

		return $this->tax_rate;
	}

	/**
	 * Set tax amount, absolute or percentage.
	 *
	 * @since 2.0.0
	 * @param float $tax_rate Tax amount, absolute or percentage.
	 * @return void
	 */
	public function set_tax_rate($tax_rate): void {

		$this->tax_rate = $tax_rate;
	}

	/**
	 * Get type of the tax, percentage or absolute.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_tax_type() {

		return $this->tax_type;
	}

	/**
	 * Set type of the tax, percentage or absolute.
	 *
	 * @since 2.0.0
	 * @param string $tax_type Type of the tax, percentage or absolute.
	 * @return void
	 */
	public function set_tax_type($tax_type): void {

		$this->tax_type = $tax_type;
	}

	/**
	 * Get if tax are included in the price or not.
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function get_tax_inclusive() {

		return (bool) $this->tax_inclusive;
	}

	/**
	 * Set if tax are included in the price or not.
	 *
	 * @since 2.0.0
	 * @param boolean $tax_inclusive If tax are included in the price or not.
	 * @return void
	 */
	public function set_tax_inclusive($tax_inclusive): void {

		$this->tax_inclusive = $tax_inclusive;
	}

	/**
	 * Get if the line item is tax exempt ot not.
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function is_tax_exempt() {

		return $this->tax_exempt;
	}

	/**
	 * Set if the line item is tax exempt ot not.
	 *
	 * @since 2.0.0
	 * @param boolean $tax_exempt If the line item is tax exempt ot not.
	 * @return void
	 */
	public function set_tax_exempt($tax_exempt): void {

		$this->tax_exempt = $tax_exempt;
	}

	/**
	 * Get the amount, in currency, of the tax.
	 *
	 * @since 2.0.0
	 * @return float
	 */
	public function get_tax_total() {

		return $this->tax_total;
	}

	/**
	 * Set the amount, in currency, of the tax.
	 *
	 * @since 2.0.0
	 * @param float $tax_total The amount, in currency, of the tax.
	 * @return void
	 */
	public function set_tax_total($tax_total): void {

		$this->tax_total = $tax_total;
	}

	/**
	 * Get the value of total
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_total() {

		return $this->total;
	}

	/**
	 * Set the value of total.
	 *
	 * @since 2.0.0
	 * @param float $total The total value of the line.
	 * @return void
	 */
	public function set_total($total): void {

		$this->total = $total;
	}

	/**
	 * Get the value of recurring.
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function is_recurring() {

		return $this->recurring;
	}

	/**
	 * Set the value of recurring.
	 *
	 * @since 2.0.0
	 * @param boolean $recurring If this item is recurring or not.
	 * @return void
	 */
	public function set_recurring($recurring): void {

		$this->recurring = $recurring;
	}

	/**
	 * Get value before taxes, discounts, fees and etc.
	 *
	 * @since 2.0.0
	 * @return float
	 */
	public function get_subtotal() {

		return $this->subtotal;
	}

	/**
	 * Set value before taxes, discounts, fees and etc.
	 *
	 * @since 2.0.0
	 * @param float $subtotal Value before taxes, discounts, fees and etc.
	 * @return void
	 */
	public function set_subtotal($subtotal): void {

		$this->subtotal = $subtotal;
	}

	/**
	 * Get the value of duration
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_duration() {

		return $this->duration;
	}

	/**
	 * Set the value of duration.
	 *
	 * @since 2.0.0
	 * @param int $duration The billing cycle duration.
	 * @return void
	 */
	public function set_duration($duration): void {

		$this->duration = $duration;
	}

	/**
	 * Get the value of duration_unit.
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_duration_unit() {

		return $this->duration_unit;
	}

	/**
	 * Set the value of duration_unit.
	 *
	 * @since 2.0.0
	 * @param string $duration_unit The duration unit.
	 * @return void
	 */
	public function set_duration_unit($duration_unit): void {

		$this->duration_unit = $duration_unit;
	}

	/**
	 * Get the value of billing_cycles.
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_billing_cycles() {

		return $this->billing_cycles;
	}

	/**
	 * Set the value of billing_cycles.
	 *
	 * @since 2.0.0
	 * @param int $billing_cycles The number of billing cycles.
	 * @return void
	 */
	public function set_billing_cycles($billing_cycles): void {

		$this->billing_cycles = $billing_cycles;
	}

	/**
	 * Get the value of discount_total.
	 *
	 * @since 2.0.0
	 * @return float
	 */
	public function get_discount_total() {

		return $this->discount_total;
	}

	/**
	 * Set the value of discount_total.
	 *
	 * @since 2.0.0
	 * @param float $discount_total The total value of discounts.
	 * @return void
	 */
	public function set_discount_total($discount_total): void {

		$this->discount_total = $discount_total;
	}

	/**
	 * Get the value of tax_category.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_tax_category() {

		return $this->tax_category;
	}

	/**
	 * Set the value of tax_category.
	 *
	 * @since 2.0.0
	 * @param string $tax_category The tax category.
	 * @return void
	 */
	public function set_tax_category($tax_category): void {

		$this->tax_category = $tax_category;
	}

	/**
	 * Get the value of discountable.
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function is_discountable() {

		return $this->discountable;
	}

	/**
	 * Set the value of discountable.
	 *
	 * @since 2.0.0
	 * @param boolean $discountable If the line is discountable.
	 * @return void
	 */
	public function set_discountable($discountable): void {

		$this->discountable = $discountable;
	}

	/**
	 * Get the value of taxable.
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function is_taxable() {

		return $this->taxable;
	}

	/**
	 * Set the value of taxable.
	 *
	 * @since 2.0.0
	 * @param boolean $taxable If the item is taxable or not.
	 * @return void
	 */
	public function set_taxable($taxable): void {

		$this->taxable = $taxable;
	}

	/**
	 * Get the value of discount_rate.
	 *
	 * @since 2.0.0
	 * @return float
	 */
	public function get_discount_rate() {

		return $this->discount_rate;
	}

	/**
	 * Set the value of discount_rate.
	 *
	 * @since 2.0.0
	 * @param float $discount_rate The discount amount (flat or percentage).
	 * @return void
	 */
	public function set_discount_rate($discount_rate): void {

		$this->discount_rate = $discount_rate;
	}

	/**
	 * Get the value of discount_type.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_discount_type() {

		return $this->discount_type;
	}

	/**
	 * Set the value of discount_type.
	 *
	 * @since 2.0.0
	 * @param string $discount_type The type of discount, percentage or absolute.
	 * @return void
	 */
	public function set_discount_type($discount_type): void {

		$this->discount_type = $discount_type;
	}

	/**
	 * Get discount Label.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_discount_label() {

		return $this->discount_label;
	}

	/**
	 * Set discount Label.
	 *
	 * @since 2.0.0
	 * @param string $discount_label Discount Label.
	 * @return void
	 */
	public function set_discount_label($discount_label): void {

		$this->discount_label = $discount_label;
	}

	/**
	 * Get if we should apply discount to renewals.
	 *
	 * @since 2.0.0
	 * @return boolean
	 */
	public function should_apply_discount_to_renewals() {

		return $this->apply_discount_to_renewals;
	}

	/**
	 * Set if we should apply discount to renewals.
	 *
	 * @since 2.0.0
	 * @param boolean $apply_discount_to_renewals If we should apply discount to renewals.
	 * @return void
	 */
	public function set_apply_discount_to_renewals($apply_discount_to_renewals): void {

		$this->apply_discount_to_renewals = $apply_discount_to_renewals;
	}

	/**
	 * Get the value of product_id.
	 *
	 * @since 2.0.0
	 * @return mixed
	 */
	public function get_product_id() {

		return $this->product_id;
	}

	/**
	 * Set the value of product_id.
	 *
	 * @since 2.0.0
	 * @param int $product_id The product id.
	 * @return void
	 */
	public function set_product_id($product_id): void {

		$this->product_id = $product_id;
	}

	/**
	 * Get the value of title
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_title() {

		return $this->title;
	}

	/**
	 * Set the value of title.
	 *
	 * @since 2.0.0
	 * @param string $title The line item title.
	 * @return void
	 */
	public function set_title($title): void {

		$this->title = $title;
	}

	/**
	 * Get the value of description.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_description() {

		return $this->description;
	}

	/**
	 * Set the value of description.
	 *
	 * @since 2.0.0
	 * @param string $description The line item description.
	 * @return void
	 */
	public function set_description($description): void {

		$this->description = $description;
	}

	/**
	 * Get label of the tax applied.
	 *
	 * @since 2.0.0
	 * @return string
	 */
	public function get_tax_label() {

		return $this->tax_label;
	}

	/**
	 * Set label of the tax applied.
	 *
	 * @since 2.0.0
	 * @param string $tax_label Label of the tax applied.
	 * @return void
	 */
	public function set_tax_label($tax_label): void {

		$this->tax_label = $tax_label;
	}

	/**
	 * @return string
	 */
	public function get_date_created(): string {
		return $this->date_created;
	}

	/**
	 * Returns the amount recurring in a human-friendly way.
	 *
	 * @since 2.0.8
	 * @return string
	 */
	public function get_recurring_description() {

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

		$duration      = $this->get_duration();
		$duration_unit = wu_get_translatable_string($duration <= 1 ? $this->get_duration_unit() : $this->get_duration_unit() . 's');

		if ($duration <= 1) {
			$description = $duration_unit;
		} else {
			$description = sprintf(
				// translators: %1$s: the duration number, %2$s: the duration unit (days, weeks, months, etc).
				__('every %1$s %2$s', 'ultimate-multisite'),
				$duration,
				$duration_unit
			);
		}

		return $description;
	}

	/**
	 * Converts the line item to an array.
	 *
	 * @since 2.0.0
	 * @return array
	 */
	public function to_array() {

		$array = get_object_vars($this);

		$array['recurring_description'] = $this->get_recurring_description();

		return $array;
	}

	/**
	 * Implements our on json_decode version of this object. Useful for use in vue.js.
	 *
	 * @since 2.0.0
	 * @return array
	 */
	#[\ReturnTypeWillChange]
	public function jsonSerialize() {

		return $this->to_array();
	}

	/**
	 * Queries the database for Line Items across payments.
	 *
	 * @since 2.0.0
	 *
	 * @param array $query Query arguments.
	 * @return array
	 */
	public static function get_line_items($query = []) {

		global $wpdb;

		$query = wp_parse_args(
			$query,
			[
				'number'         => 100,
				'date_query'     => [],
				'payment_status' => false,
			]
		);
		// TODO: This doesn't seem to work:
		$query['date_query']['column'] = 'p.date_created';

		$date_query = new \WP_Date_Query($query['date_query']);

		$date_query_sql = $date_query->get_sql();

		$taxes_paid_list = [];

		$status_query_sql = '';

		if ($query['payment_status'] && (new Payment_Status())->is_valid($query['payment_status'])) {
			$status_query_sql = "AND p.status = '{$query['payment_status']}'";
		}

		// phpcs:disable;
		$query = $wpdb->prepare( "
			SELECT m.wu_payment_id, m.meta_value as line_items, p.date_created
			FROM {$wpdb->base_prefix}wu_paymentmeta as m
			JOIN {$wpdb->base_prefix}wu_payments as p
			WHERE 
				m.meta_key = %s 
			AND 
				p.id = m.wu_payment_id
			{$status_query_sql}
			{$date_query_sql}
			ORDER BY p.date_created DESC
			LIMIT %d
		", 'wu_line_items', $query['number']);
		// phpcs:enable;

		$results = $wpdb->get_results($query); // phpcs:ignore

		if (! $results) {
			return [];
		}

		foreach ($results as &$ln) {
			$copy = $ln;

			$line_items = $ln->line_items;

			$ln = maybe_unserialize($line_items);

			$ln = array_map(
				function ($_ln) use ($copy) {

					$_ln->date_created = $copy->date_created;

					return $_ln;
				},
				$ln
			);
		}

		return $results;
	}

	/**
	 * Get the product slug, if any.
	 *
	 * @since 2.1.0
	 * @return null|string
	 */
	public function get_product_slug() {

		return $this->product_slug;
	}

	/**
	 * Set the product slug.
	 *
	 * @since 2.1.0
	 * @param null|string $product_slug The product slug.
	 * @return void
	 */
	public function set_product_slug($product_slug): void {

		$this->product_slug = $product_slug;
	}
}
