<?php
/**
 * Campaign functions.
 *
 * @package flo-sync
 */

namespace NMFS\Sync\Items\Campaigns;

use Automattic\WooCommerce\Utilities\NumberUtil;
use DateTime;
use DateTimeZone;
use NMFS\Admin\Settings_Fields;
use NMFS\Shared;
use NMFS\Sync\Items\Campaigns\Models\Discounter_Interface;
use NMFS\Sync\Items\Campaigns\Models\Product_Model;
use Northmill_Online_Shared_Sync\Northmill_Online_Shared\Modules\Loggers\Log_Level_Enum;
use WC_Cache_Helper;
use WC_Cart;
use WC_Coupon;
use WC_Product;
use WC_Tax;

defined( 'ABSPATH' ) || exit;

/**
 * Class Campaigns
 */
class Campaigns {
	/**
	 * Campaigns constructor.
	 */
	public static function setup(): void {
		if ( ! self::is_enabled() ) {
			return;
		}

		add_filter( 'woocommerce_coupon_discount_types', array( __CLASS__, 'coupon_discount_types' ) );
		add_filter( 'woocommerce_cart_coupon_types', array( __CLASS__, 'cart_coupon_types' ) );
		add_filter( 'woocommerce_coupon_get_discount_amount', array( __CLASS__, 'get_discount_amount' ), 10, 5 );
		add_action( 'woocommerce_before_cart', array( __CLASS__, 'update_cart_discounts' ) );
		add_action( 'woocommerce_before_checkout_form', array( __CLASS__, 'update_cart_discounts' ) );
		add_action( 'woocommerce_applied_coupon', array( __CLASS__, 'remove_wrongly_applied_discounts' ) );
		add_filter( 'woocommerce_add_message', array( __CLASS__, 'filter_woocommerce_add_message' ) );
		add_filter( 'woocommerce_cart_totals_coupon_label', array( __CLASS__, 'coupon_label' ), 20, 2 );

		add_action( 'moreflo_sync_update_cart_discounts', array( __CLASS__, 'update_cart_discounts' ) );
	}

	/**
	 * Checks if campaign sync is enabled.
	 */
	public static function is_enabled(): bool {
		return Shared::instance()->admin->settings->is_setting_enabled( Settings_Fields::SYNC_CAMPAIGNS );
	}

	/**
	 * Hooks into woocommerce_coupon_discount_types.
	 * Adds Apps coupon types to WooCommerce
	 *
	 * @param array $discount_types Discount types.
	 *
	 * @return array
	 */
	public static function coupon_discount_types( array $discount_types ): array {
		$discount_types['mf_type_0'] = __( 'Buy X amount and get Y amount', 'flo-sync' );
		$discount_types['mf_type_1'] = __( 'Buy X amount and get Y discount', 'flo-sync' );
		$discount_types['mf_type_2'] = __( 'Buy X amount and pay Y', 'flo-sync' );
		$discount_types['mf_type_3'] = __( 'Percent Discount', 'flo-sync' );
		$discount_types['mf_type_4'] = __( 'Special Offer', 'flo-sync' );
		$discount_types['mf_type_5'] = __( 'Price Reduction', 'flo-sync' );

		$discount_types['mf_type_set'] = __( 'Article Set', 'flo-sync' );

		return $discount_types;
	}

	/**
	 * Hooks into woocommerce_cart_coupon_types.
	 * Adds Apps coupon types to WooCommerce cart
	 *
	 * @param array $coupon_types Coupon types.
	 *
	 * @return array
	 */
	public static function cart_coupon_types( array $coupon_types ): array {
		$coupon_types[] = 'mf_type_0';
		$coupon_types[] = 'mf_type_1';
		$coupon_types[] = 'mf_type_2';
		$coupon_types[] = 'mf_type_3';
		$coupon_types[] = 'mf_type_4';
		$coupon_types[] = 'mf_type_5';

		$coupon_types[] = 'mf_type_set';

		return $coupon_types;
	}

	/**
	 * Hooks into woocommerce_coupon_get_discount_amount.
	 *
	 * Gets the discounted amount for a specific cart item.
	 *
	 * @param float      $discount           Current discounted amount.
	 * @param float      $discounting_amount Amount the coupon is being applied to.
	 * @param array|null $cart_item          Cart item being discounted if applicable.
	 * @param boolean    $single             True if discounting a single qty item, false if it's the line.
	 * @param WC_Coupon  $coupon             The current coupon being applied.
	 *
	 * @return float New discounted amount.
	 * @noinspection PhpUnusedParameterInspection WP action/filter function.
	 */
	public static function get_discount_amount( float $discount, float $discounting_amount, ?array $cart_item, bool $single, WC_Coupon $coupon ): float {
		// We cannot provide a discount when no cart item is specified.
		if ( is_null( $cart_item ) ) {
			return $discount;
		}

		$products = self::get_products_from_cart( WC()->cart );

		// If there are no products in the cart, abort.
		if ( empty( $products ) ) {
			return $discount;
		}

		$discounter = Campaign_Factory::create_discounter( $coupon );

		// Not an Apps coupon.
		if ( is_null( $discounter ) ) {
			return $discount;
		}

		// Calculate discount.
		/**
		 * Magic method okay for WC().
		 *
		 * @noinspection PhpUndefinedFieldInspection
		 */
		$discount = self::calc_discount( $discounter, $cart_item, $products, WC()->cart->subtotal );

		return NumberUtil::round( $discount, wc_get_rounding_precision() );
	}

	/**
	 * Updates coupons in cart based on its contents.
	 */
	public static function update_cart_discounts(): void {
		$discounters = self::get_cart_discounters();

		self::toggle_wc_notices( false );

		// Adds coupons to cart if it's not already added.
		if ( ! empty( $discounters ) ) {
			foreach ( $discounters as $discounter ) {
				$coupon_code = $discounter->get_coupon()->get_code();

				// Do not apply the same coupon twice.
				if ( WC()->cart->has_discount( $coupon_code ) ) {
					// Cart already contains discounter.
					continue;
				}

				WC()->cart->apply_coupon( $coupon_code );
			}
		}

		self::toggle_wc_notices( true );

		// Remove discounts not applicable anymore (keep applicable discounts).
		self::clear_cart_of_discounts( $discounters );
	}

	/**
	 * Hooked into woocommerce_applied_coupon.
	 *
	 * @return void
	 */
	public static function remove_wrongly_applied_discounts(): void {
		$discounters = self::get_cart_discounters();

		// Remove discounts not applicable anymore (keep applicable discounts).
		self::clear_cart_of_discounts( $discounters );
	}

	/**
	 * Filters 'filter_woocommerce_add_message'.
	 *
	 * @param string $message Message.
	 *
	 * @return string
	 */
	public static function filter_woocommerce_add_message( string $message ): string {
		return self::$wc_notices
			? $message
			: '';
	}

	/**
	 * Filters 'woocommerce_cart_totals_coupon_label'.
	 *
	 * @param string    $label Coupon label.
	 * @param WC_Coupon $coupon Coupon object.
	 */
	public static function coupon_label( string $label, WC_Coupon $coupon ): string {
		$mf_coupon_types = self::cart_coupon_types( array() );

		if ( in_array( $coupon->get_discount_type(), $mf_coupon_types, true ) ) {
			// translators: %s: coupon code.
			$label = sprintf( esc_html__( 'Campaign: %s', 'flo-sync' ), $coupon->get_code() );
		}

		return $label;
	}

	/**
	 * Toggles adding of WooCommerce messages on or off
	 *
	 * @param bool $on True to enable, false to disable.
	 */
	private static function toggle_wc_notices( bool $on ): void {
		self::$wc_notices = $on;
	}

	/**
	 * Whether to show WooCommerce messages.
	 *
	 * @var bool
	 */
	private static bool $wc_notices = true;

	/**
	 * Get applicable discounts for cart.
	 *
	 * @return Discounter_Interface[] Array of applicable discounts.
	 */
	private static function get_cart_discounters(): array {
		// We need a cart to push coupons onto.
		if ( is_null( WC()->cart ) ) {
			return array();
		}

		// Get all products from cart.
		$products = self::get_products_from_cart( WC()->cart );

		// Get possible discounts.
		$possible_discounts = array_filter(
			self::get_available_discounts_for_cart( $products ),
			array( __CLASS__, 'filter_active_now' )
		);

		// Keep a list of applicable discounts, so we can remove everything else.
		$applicable_discounts = array();

		// Adds coupons to cart if it's not already added.
		if ( ! empty( $possible_discounts ) ) {
			foreach ( $possible_discounts as $possible_discount ) {
				// Check if coupon is applicable for the cart.
				/**
				 * Magic method okay for WC().
				 *
				 * @noinspection PhpUndefinedFieldInspection
				 */
				if ( ! $possible_discount->is_applicable( WC()->cart->subtotal, $products ) ) {
					continue;
				}

				// Get the WooCommerce coupon.
				$coupon_code = $possible_discount->get_coupon()->get_code();
				if ( ! $coupon_code ) {
					return array();
				}

				// Keep a list of applicable discounts.
				$applicable_discounts[] = $possible_discount;
			}
		}

		return $applicable_discounts;
	}

	/**
	 * Removes all discounts except the applicable ones from the cart.
	 *
	 * @param Discounter_Interface[] $applicable_discounts Array of applicable discounts.
	 */
	private static function clear_cart_of_discounts( array $applicable_discounts ): void {
		// We need a cart.
		if ( is_null( WC()->cart ) ) {
			return;
		}

		$cart_coupons = WC()->cart->get_coupons();

		// Cart already cleared of coupons.
		if ( empty( $cart_coupons ) ) {
			return;
		}

		$site_coupons = self::get_possible_site_apps_coupons();
		// No coupons to remove.
		if ( empty( $site_coupons ) ) {
			return;
		}

		$site_coupon_codes = array_map(
			function ( $coupon ) {
				return $coupon->get_code();
			},
			$site_coupons
		);

		$applicable_discounts_codes = array_map(
			function ( $discount ) {
				return $discount->get_coupon()->get_code();
			},
			$applicable_discounts
		);

		self::toggle_wc_notices( false );

		// Loop through site coupons and remove them from the cart if they have been applied.
		foreach ( $cart_coupons as $cart_coupon ) {
			// Do not remove applicable discounts.
			if ( in_array( $cart_coupon->get_code(), $applicable_discounts_codes, true ) ) {
				continue;
			}

			// Do not remove discounts not found in cart.
			if ( ! in_array( $cart_coupon->get_code(), $site_coupon_codes, true ) ) {
				continue;
			}

			WC()->cart->remove_coupon( $cart_coupon->get_code() );
		}

		self::toggle_wc_notices( true );
	}

	/**
	 * Gets available discounts for given products.
	 *
	 * @param Product_Model[] $products Products to get discounts for.
	 *
	 * @return Discounter_Interface[]
	 */
	private static function get_available_discounts_for_cart( array $products ): array {
		if ( empty( $products ) ) {
			return array();
		}

		$coupons = self::get_possible_site_apps_coupons();

		$skus = array_unique(
			array_map(
				function ( $product ) {
					return $product->get_sku();
				},
				$products
			)
		);

		$coupons = array_filter(
			$coupons,
			function ( $coupon ) use ( $skus ) {
				$coupon_skus = json_decode( get_post_meta( $coupon->get_id(), 'mf_parts_sku', true ) );
				if ( is_null( $coupon_skus ) ) {
					// Article sets lacks parts sku.
					return true;
				}

				foreach ( $skus as $sku ) {
					if ( in_array( $sku, $coupon_skus, true ) ) {
						return true;
					}
				}

				return false;
			}
		);

		if ( empty( $coupons ) ) {
			return array();
		}

		$discounters = array_filter(
			array_map(
				function ( $coupon ) {
					return Campaign_Factory::create_discounter( $coupon );
				},
				$coupons
			)
		);

		usort( $discounters, array( __CLASS__, 'sort_by_priority' ) );

		return $discounters;
	}

	/**
	 * Gets all site coupons that originated from Apps.
	 *
	 * @return WC_Coupon[]
	 */
	private static function get_possible_site_apps_coupons(): array {
		$posts = get_posts(
			array(
				'post_type'           => 'shop_coupon',
				'post_status'         => 'publish',
				'ignore_sticky_posts' => true,
				'nopaging'            => true,
				'meta_query'          => array(
					array(
						'key'     => 'mf_id',
						'compare' => 'EXISTS',
					),
					array(
						'key'     => 'mf_end_date',
						'compare' => '>=',
						'value'   => ( new DateTime() )->format( 'Y-m-d' ),
					),
				),
			)
		);

		return array_map(
			function ( $post ) {
				return new WC_Coupon( $post->post_title );
			},
			$posts
		);
	}

	/**
	 * Calculate cart item per quantity discount with discounter.
	 *
	 * @param Discounter_Interface $discounter Discounter to use.
	 * @param array                $cart_item Cart item.
	 * @param Product_Model[]      $products Products in cart.
	 * @param float                $cart_total Total price of cart, inc VAT.
	 *
	 * @return float|int
	 */
	private static function calc_discount(
		Discounter_Interface $discounter,
		array $cart_item,
		array $products,
		float $cart_total
	): float|int {
		$discounted_products = $discounter->get_discounted_products( $products, $cart_total );

		if ( empty( $discounted_products ) ) {
			return 0;
		}

		$total_discount = 0;
		foreach ( $discounted_products as $discounted_product ) {
			if ( $cart_item['data']->get_sku() !== $discounted_product->get_sku() ) {
				continue;
			}

			$total_discount += $discounted_product->get_discount( ! wc_prices_include_tax() );
		}

		return $total_discount / $cart_item['quantity'];
	}

	/**
	 * Get products from the cart.
	 *
	 * @param WC_Cart $cart Cart object.
	 *
	 * @return Product_Model[] Products ordered by price ASC. All prices are inc VAT.
	 */
	private static function get_products_from_cart( WC_Cart $cart ): array {
		if ( ! is_a( $cart, 'WC_Cart' ) ) {
			return array();
		}

		$products = array();

		// Transform cart items to products.
		foreach ( $cart->get_cart_contents() as $cart_item ) {
			$cart_item_products = Campaign_Factory::create_products_from_cart_item( $cart_item, $cart );

			// We get a product per quantity of item.
			foreach ( $cart_item_products as $cart_item_product ) {
				$products[] = $cart_item_product;
			}
		}

		usort( $products, array( __CLASS__, 'sort_by_price' ) );

		return $products;
	}

	/**
	 * Gets tax percentage for product in cart.
	 *
	 * @param WC_Product $product Product to get tax percentage for.
	 * @param WC_Cart    $cart    Cart that the product is in.
	 *
	 * @return int
	 */
	public static function get_cart_product_tax_rate( WC_Product $product, WC_Cart $cart ): int {
		$tax_percentage = 0;
		$wc_tax         = new WC_Tax();

		$tax_class = $product->get_tax_class();

		$tax_data = $wc_tax->get_rates( $tax_class, $cart->get_customer() );

		if ( ! empty( $tax_data ) ) {
			foreach ( $tax_data as $tax_rate ) {
				$tax_percentage = $tax_rate['rate'];
			}
		}

		return $tax_percentage;
	}

	/**
	 * Sort by price ascending.
	 *
	 * @param Product_Model $a First product to compare.
	 * @param Product_Model $b Second product to compare.
	 *
	 * @return int
	 */
	private static function sort_by_price( Product_Model $a, Product_Model $b ): int {
		if ( $a->get_price() === $b->get_price() ) {
			return 0;
		}

		return ( $a->get_price() < $b->get_price() ) ? - 1 : 1;
	}

	/**
	 * Sort by priority ascending.
	 *
	 * @param Discounter_Interface $a First Discounter to compare.
	 * @param Discounter_Interface $b Second Discounter to compare.
	 *
	 * @return int
	 */
	private static function sort_by_priority( Discounter_Interface $a, Discounter_Interface $b ): int {
		if ( $a->get_priority() === $b->get_priority() ) {
			return 0;
		}

		return ( $a->get_priority() < $b->get_priority() ) ? - 1 : 1;
	}

	/**
	 * Filters discounters so that those within start and end times are left.
	 *
	 * @param Discounter_Interface $discounter Discounter to filter.
	 *
	 * @return bool
	 */
	private static function filter_active_now( Discounter_Interface $discounter ): bool {
		$now        = new DateTime();
		$start_time = DateTime::createFromFormat( 'Y-m-d\\TH:i:s', get_post_meta( $discounter->get_coupon()->get_id(), 'mf_start_time', true ), new DateTimeZone( 'Europe/Stockholm' ) );
		$end_time   = DateTime::createFromFormat( 'Y-m-d\\TH:i:s', get_post_meta( $discounter->get_coupon()->get_id(), 'mf_end_time', true ), new DateTimeZone( 'Europe/Stockholm' ) );

		// Remove passed discounts.
		if ( $end_time && $now > $end_time ) {
			return false;
		}

		// Remove future discounts.
		if ( $start_time && $now < $start_time ) {
			return false;
		}

		return true;
	}

	/**
	 * Creates or updates post that has unique mf id
	 *
	 * @param string $mf_id     Unique mf id for this post.
	 * @param string $name      Title of the post.
	 * @param string $post_type Post type.
	 * @param string $mf_type   Mf Campaign Type.
	 * @param string $content   Post Content.
	 *
	 * @return int|bool Post ID on success, else false
	 */
	public static function update_post( string $mf_id, string $name, string $post_type, string $mf_type, string $content = '' ): bool|int {
		$post_id = self::get_post_id( $mf_id, $post_type, $mf_type );

		if ( $post_id ) {
			$post    = get_post( $post_id );
			$updates = array();
			if ( $name !== $post->post_title ) {
				$updates['post_title'] = $name;
			}
			if ( $content !== $post->post_content ) {
				$updates['post_content'] = $content;
			}
			if ( ! empty( $updates ) ) {
				$updates['ID'] = $post_id;
				wp_update_post( $updates );

				Shared::instance()->logger->log(
					'sync',
					'Campaign updated in WP',
					array(
						'hub_id'    => $mf_id,
						'post_type' => $post_type,
					),
					Log_Level_Enum::DEBUG
				);
			}
		} else {
			$post_id = wp_insert_post(
				array(
					'post_type'    => $post_type,
					'post_author'  => 1,
					'post_title'   => $name,
					'post_status'  => 'publish',
					'post_content' => $content,
				)
			);

			if ( empty( $post_id ) || is_wp_error( $post_id ) ) {
				Shared::instance()->logger->log(
					'sync',
					'Error creating new campaign in WP',
					array(
						'hub_id' => $mf_id,
						'error'  => is_wp_error( $post_id ) ?
							$post_id->get_error_message() :
							'wp_insert_post returned 0',
					),
					Log_Level_Enum::ERROR
				);

				return false;
			}

			update_post_meta( $post_id, 'coupon_amount', '' );
			update_post_meta( $post_id, 'individual_use', 'no' );
			update_post_meta( $post_id, 'product_ids', '' );
			update_post_meta( $post_id, 'exclude_product_ids', '' );
			update_post_meta( $post_id, 'usage_limit', '' );
			update_post_meta( $post_id, 'expiry_date', '' );
			update_post_meta( $post_id, 'apply_before_tax', 'yes' );
			update_post_meta( $post_id, 'free_shipping', 'no' );

			update_post_meta( $post_id, 'mf_id', $mf_id );
			update_post_meta( $post_id, 'mf_type', $mf_type );

			Shared::instance()->logger->log(
				'sync',
				'Campaign created in WP',
				array(
					'hub_id'    => $mf_id,
					'name'      => $name,
					'post_type' => $post_type,
				),
				Log_Level_Enum::DEBUG
			);
		}

		return $post_id;
	}

	/**
	 * Gets post id that has unique mf id.
	 *
	 * @param string $mf_id     Unique mf id.
	 * @param string $post_type Post Type.
	 * @param string $mf_type   Mf Campaign Type.
	 *
	 * @return int|bool Post id on success, else false
	 */
	public static function get_post_id( string $mf_id, string $post_type, string $mf_type ): bool|int {
		$posts = get_posts(
			array(
				'post_type'           => $post_type,
				'status'              => 'all',
				'posts_per_page'      => 1,
				'ignore_sticky_posts' => true,
				'meta_query'          => array(
					array(
						'key'   => 'mf_id',
						'value' => $mf_id,
					),
					array(
						'key'   => 'mf_type',
						'value' => $mf_type,
					),
				),
			)
		);

		if ( ! empty( $posts ) ) {
			return $posts[0]->ID;
		}

		return false;
	}

	/**
	 * Deletes a post that has unique mf id. If it's not found then it does nothing
	 *
	 * @param string $mf_id     Unique mf id.
	 * @param string $post_type Post type.
	 * @param string $mf_type   Mf Campaign Type.
	 *
	 * @return bool True on deleted, false on not found
	 */
	public static function delete_post( string $mf_id, string $post_type, string $mf_type ): bool {
		$post_id = self::get_post_id( $mf_id, $post_type, $mf_type );

		if ( $post_id ) {
			$title = get_the_title( $post_id );
			wp_delete_post( $post_id );
			wp_cache_delete( WC_Cache_Helper::get_cache_prefix( 'coupons' ) . 'coupon_id_from_code_' . $title, 'coupons' );

			Shared::instance()->logger->log(
				'sync',
				'Campaign deleted in WP',
				array(
					'hub_id'    => $mf_id,
					'title'     => $title,
					'post_type' => $post_type,
				),
				Log_Level_Enum::DEBUG
			);

			return true;
		}

		return false;
	}
}
