<?php

namespace WeDevs\Dokan\Vendor;

use WC_Coupon;
use WC_Discounts;
use WC_Order;
use WC_Cart;
use WC_Order_Item_Product;
use WC_Order_Item_Coupon;
use WC_Data;
use Exception;

class Coupon {

    public const DOKAN_COUPON_META_KEY = '_dokan_coupon_info';

    public function __construct() {
        add_action( 'woocommerce_checkout_create_order_line_item', [ $this, 'add_coupon_info_to_order_item' ], 10, 4 );
        add_action( 'woocommerce_removed_coupon', [ $this, 'remove_coupon_info_from_cart_item' ] );
        add_filter( 'woocommerce_coupon_get_apply_quantity', [ $this, 'intercept_wc_coupon' ], 15, 4 );
        add_action( 'dokan_wc_coupon_applied', [ $this, 'save_item_coupon_discount' ], 10, 3 );
        add_action( 'woocommerce_before_delete_order_item', [ $this, 'remove_coupon_info_from_order_item' ] );
    }

    /**
     * Removes coupon information from an order item when a coupon is deleted.
     *
     * This function:
     * - Retrieves the coupon code from the order item.
     * - Removes the coupon discount from all applicable items in the order.
     * - If the order has sub-orders, it ensures the coupon is also removed from all child orders.
     * - If it's a sub-order, it removes the coupon from relevant items in the parent order.
     *
     * @param int $item_id The ID of the order item representing the coupon.
     *
     * @return void
     *
     * @throws Exception If there is an issue initializing the WC_Order_Item_Coupon object.
     */
    public function remove_coupon_info_from_order_item( $item_id ) {
        $data = new WC_Order_Item_Coupon( $item_id );
        if ( ! $data->get_order_id() ) {
            return;
        }

        $removed_coupon = $data->get_code();
        $order = wc_get_order( $data->get_order_id() );
        $order_items = $order->get_items();

        $product_ids = $this->remove_coupon_discount_from_items( $removed_coupon, $order_items );
        // Remove coupon with child orders
		if ( $order->get_meta( 'has_sub_order' ) ) {
			$this->remove_coupon_from_child_orders( $order, $removed_coupon );
		} else {
			// Remove coupon from child order items
			$parent_order = wc_get_order( $order->get_parent_id() );
			if ( $parent_order ) {
                $parent_items = array_filter(
                    $parent_order->get_items(),
                    function ( $order_item ) use ( $product_ids ) {
                        return in_array( $order_item->get_product_id(), $product_ids, true );
                    }
                );
                $this->remove_coupon_discount_from_items( $removed_coupon, $parent_items );
			}
		}
    }

    /**
     * Get Product IDs from order items and remove coupon discount from items.
     *
     * @param $removed_coupon
     * @param $order_items
     *
     * @return array
     *
     * @throws Exception
     */
    private function remove_coupon_discount_from_items( $removed_coupon, $order_items ): array {
        $product_ids = [];
		foreach ( $order_items as $order_item ) {
			$item = $order_item->get_meta( self::DOKAN_COUPON_META_KEY, true );

			if ( isset( $item[ $removed_coupon ] ) ) {
				unset( $item[ $removed_coupon ] );
				wc_update_order_item_meta( $order_item->get_id(), self::DOKAN_COUPON_META_KEY, $item );
				$product_ids[] = $order_item->get_product_id();
			}
		}
		return $product_ids;
    }

    /**
     * Intercepts coupon application to handle line-item coupon discounts.
     * WooCommerce removes the coupon from the order and recalculates totals. For reference, see:
     *
     * @see https://github.com/woocommerce/woocommerce/blob/8abd6e97ca598381cb07287a2e7b735799cb55d5/plugins/woocommerce/includes/abstracts/abstract-wc-order.php#L1339
     * WooCommerce does not provide a direct hook to retrieve coupon amounts per line items from the WC_Discounts object.
     * However, the `get_discounts` method of the `WC_Discounts` class allows access to this information.
     * This implementation utilizes the following steps to calculate line-item discounts:
     * 1. Remove the interfering WC hook used by Dokan hook.
     * 2. Reapply the coupon to the order or cart.
     * 3. Trigger the Dokan action to apply the coupon to the order or cart.
     * 4. Reattach the interfering WC hook used by Dokan hook.
     *
     * @param int $apply_quantity The number of items to which the coupon applies.
     * @param object $item The cart or order item object.
     * @param WC_Coupon $coupon The coupon being applied.
     * @param WC_Discounts $discounts The discount object managing the coupon.
     *
     * @return int
     */
    public function intercept_wc_coupon( int $apply_quantity, $item, WC_Coupon $coupon, WC_Discounts $discounts ): int {
        remove_filter( 'woocommerce_coupon_get_apply_quantity', [ $this, 'intercept_wc_coupon' ], 15 );

        // Clone the discount object to avoid side effects.
        $discounts_clone        = clone $discounts;
        $should_validate_coupon = apply_filters( 'dokan_coupon_should_validate', true, $coupon, $item, $discounts_clone );

        // Reapply the coupon to the order or cart.
        $discounts_clone->apply_coupon( $coupon, $should_validate_coupon );

        do_action( 'dokan_wc_coupon_applied', $coupon, $discounts_clone, $item, $apply_quantity, $this );

        add_filter( 'woocommerce_coupon_get_apply_quantity', [ $this, 'intercept_wc_coupon' ], 15, 4 );

        return $apply_quantity;
    }

    /**
     * Save coupon discount data for an item.
     *
     * @param WC_Coupon $coupon The coupon being applied.
     * @param WC_Discounts $discounts Discount object.
     * @param object $item The cart or order item object.
     *
     * @return void
     * @throws Exception
     */
    public function save_item_coupon_discount( WC_Coupon $coupon, WC_Discounts $discounts, $item ): void {
        $object_to_apply_coupon = $discounts->get_object();

        if ( $object_to_apply_coupon instanceof WC_Cart ) {
            $this->save_coupon_data_to_cart_item( $coupon, $discounts, $item );
        } elseif ( $object_to_apply_coupon instanceof WC_Order ) {
            $order = wc_get_order( $object_to_apply_coupon->get_id() );
            $this->save_coupon_data_to_order_item( $coupon, $discounts, $item, $order );
        }
    }

    /**
     * Save coupon data to a cart item.
     *
     * @param WC_Coupon $coupon The coupon being applied.
     * @param WC_Discounts $discounts Discount object.
     * @param object $item The cart item object.
     *
     * @return void
     */
    protected function save_coupon_data_to_cart_item( WC_Coupon $coupon, WC_Discounts $discounts, $item ): void {
        $cart = WC()->cart;
        $item_key = $item->object['key'];
        $item_object = $cart->cart_contents[ $item_key ];
        $coupon_info = $item_object[ self::DOKAN_COUPON_META_KEY ] ?? [];

        $item_wise_discounts = $discounts->get_discounts();

        if ( isset( $item_wise_discounts[ $coupon->get_code() ][ $item_key ] ) ) {
            $discount_amount = $item_wise_discounts[ $coupon->get_code() ][ $item_key ];

            $coupon_info[ $coupon->get_code() ] = [
                'discount'                         => $discount_amount,
                'coupon_code'                      => $coupon->get_code(),
                'per_qty_amount'                   => $item_object['quantity'] > 0 ? ( $discount_amount / $item_object['quantity'] ) : 0,
                'quantity'                         => $item_object['quantity'],
                'admin_coupons_enabled_for_vendor' => $coupon->get_meta( 'admin_coupons_enabled_for_vendor', true ),
                'coupon_commissions_type'          => $coupon->get_meta( 'coupon_commissions_type', true ),
                'admin_shared_coupon_type'         => $coupon->get_meta( 'admin_shared_coupon_type', true ),
                'admin_shared_coupon_amount'       => $coupon->get_meta( 'admin_shared_coupon_amount', true ),
            ];
        }

        $item_object[ self::DOKAN_COUPON_META_KEY ] = $coupon_info;
        $cart->cart_contents[ $item_object['key'] ] = $item_object;
    }

    /**
     * Save coupon data to an order item.
     *
     * @param WC_Coupon $coupon The coupon being applied.
     * @param WC_Discounts $discounts Discount object.
     * @param object $item The order item object.
     * @param WC_Order $order The order object.
     *
     * @return void
     * @throws Exception
     */
    protected function save_coupon_data_to_order_item( WC_Coupon $coupon, WC_Discounts $discounts, $item, WC_Order $order ): void {
        $coupon_codes = $order->get_coupon_codes();

        /** @var WC_Order_Item_Product $order_item */
        $order_item = $item->object;
        $item_key = $order_item->get_id();

        $coupon_info = $order_item->get_meta( self::DOKAN_COUPON_META_KEY );

        if ( ! is_array( $coupon_info ) ) {
            $coupon_info = [];
        }

        $coupon_info = array_filter(
            $coupon_info,
            function ( $coupon_temp ) use ( $coupon_codes ) {
                return in_array( $coupon_temp['coupon_code'], $coupon_codes, true );
            }
        );

        $item_wise_discounts = $discounts->get_discounts();

        if ( isset( $item_wise_discounts[ $coupon->get_code() ][ $item_key ] ) ) {
            $discount_amount = $item_wise_discounts[ $coupon->get_code() ][ $item_key ];

            $coupon_info[ $coupon->get_code() ] = [
                'discount'                         => $discount_amount,
                'coupon_code'                      => $coupon->get_code(),
                'per_qty_amount'                   => $order_item->get_quantity() > 0 ? $discount_amount / $order_item->get_quantity() : 0,
                'quantity'                         => $order_item->get_quantity(),
                'admin_coupons_enabled_for_vendor' => $coupon->get_meta( 'admin_coupons_enabled_for_vendor', true ),
                'coupon_commissions_type'          => $coupon->get_meta( 'coupon_commissions_type', true ),
                'admin_shared_coupon_type'         => $coupon->get_meta( 'admin_shared_coupon_type', true ),
                'admin_shared_coupon_amount'       => $coupon->get_meta( 'admin_shared_coupon_amount', true ),
            ];
        }

        wc_update_order_item_meta( $order_item->get_id(), self::DOKAN_COUPON_META_KEY, $coupon_info );
        do_action( 'dokan_after_coupon_info_data_updated_on_order_item_meta', $order_item, $coupon_info );

        // apply coupon sub order
        if ( $order->get_meta( 'has_sub_order' ) ) {
            $this->apply_coupon_to_child_orders( $order, $coupon );
        }
    }

    /**
    * Process coupon for child orders.
    *
    * @param WC_Order $order
    * @param WC_Coupon $coupon
    *
    * @return void
    * @throws Exception
    */
    public function apply_coupon_to_child_orders( WC_Order $order, WC_Coupon $coupon ): void {
        $sub_orders = dokan()->order->get_child_orders( $order->get_id() );
        foreach ( $sub_orders as $sub_order ) {
            // Check if the coupon is already applied
            $used_coupons = $sub_order->get_coupon_codes();
            $coupon_code = $coupon->get_code();
            if ( in_array( $coupon_code, $used_coupons, true ) ) {
                continue;
            }
            // Add the coupon to the order
            $sub_order->apply_coupon( $coupon_code );
            $sub_order->save();
        }
    }

    /**
     * @param WC_Order $order
     * @param string $removed_coupon
     * @return void
     */
    private function remove_coupon_from_child_orders( WC_Order $order, string $removed_coupon ) {
        $sub_orders = dokan()->order->get_child_orders( $order->get_id() );
        foreach ( $sub_orders as $sub_order ) {
            $used_coupons = $sub_order->get_coupon_codes();
            if ( in_array( $removed_coupon, $used_coupons, true ) ) {
                $sub_order->remove_coupon( $removed_coupon );
                $sub_order->save();
            }
        }
    }

    /**
     * Add coupon info to an order item during checkout.
     *
     * @param WC_Order_Item_Product $item The order item object.
     * @param string $cart_item_key The cart item key.
     * @param array $values Cart item values.
     */
    public function add_coupon_info_to_order_item( $item, $cart_item_key, $values ): void {
        if ( ! empty( $values[ self::DOKAN_COUPON_META_KEY ] ) ) {
            $total_discount = 0;
            $limit_reached = false;
            $coupon_info = $values[ self::DOKAN_COUPON_META_KEY ];

            foreach ( $coupon_info as $key => $coupon ) {
                $total_discount += $coupon['discount'];
                $product = wc_get_product( $item->get_product_id() );
                $total_product_price = $product->get_price() * $values['quantity'];

                if ( $limit_reached ) {
                    $coupon_info[ $key ]['discount'] = 0;
                }

                if ( $total_discount > $total_product_price && $limit_reached === false ) {
                    $remain_discount = $total_discount - $total_product_price;
                    $adjusted_discount = max( $coupon['discount'] - $remain_discount, 0 );
                    $coupon_info[ $key ]['discount'] = $adjusted_discount;
                    $limit_reached = true;
                }
            }

            $item->add_meta_data( self::DOKAN_COUPON_META_KEY, $coupon_info, true );
        }
    }

    /**
     * Remove coupon info from a cart item when a coupon is removed.
     *
     * @param string $coupon_code The coupon code being removed.
     */
    public function remove_coupon_info_from_cart_item( string $coupon_code ): void {
        $cart = WC()->cart;
        $cart_contents = $cart->cart_contents;

        foreach ( $cart_contents as $cart_item_key => $cart_item ) {
            $coupon_info = $cart_item[ self::DOKAN_COUPON_META_KEY ] ?? [];

            if ( isset( $coupon_info[ $coupon_code ] ) ) {
                unset( $coupon_info[ $coupon_code ] );
                $cart_contents[ $cart_item_key ][ self::DOKAN_COUPON_META_KEY ] = $coupon_info;
            }
        }

        $cart->set_cart_contents( $cart_contents );
    }
}
