<?php

namespace WeDevs\Dokan\Order;

use Exception;
use WC_Coupon;
use WC_Discounts;
use WC_Order;
use WC_Product;

// don't call the file directly
if ( ! defined( 'ABSPATH' ) ) {
    exit;
}

/**
 * Admin Hooks
 *
 * @since   3.0.0
 *
 * @package dokan
 *
 * @author  weDevs
 */
class Hooks {

    /**
     * Load automatically when class initiate
     *
     * @since 3.0.0
     *
     * @return void
     */
    public function __construct() {
        // on order status change
        add_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_change' ], 10, 4 );
        add_action( 'woocommerce_order_status_changed', [ $this, 'manage_refunded_for_order' ], 15, 4 );
        add_action( 'woocommerce_order_status_changed', [ $this, 'on_sub_order_change' ], 99, 4 );

        // create sub-orders
        add_action( 'woocommerce_checkout_update_order_meta', [ $this, 'split_vendor_orders' ] );
        add_action( 'woocommerce_store_api_checkout_order_processed', [ $this, 'split_vendor_orders' ] );

        // order table synced for WooCommerce update order meta
        add_action( 'woocommerce_checkout_update_order_meta', 'dokan_sync_insert_order', 20 );
        add_action( 'woocommerce_store_api_checkout_order_processed', 'dokan_sync_insert_order', 20 );

        // order table synced for dokan update order meta
        add_action( 'dokan_checkout_update_order_meta', 'dokan_sync_insert_order' );

        // prevent non-vendor coupons from being added
        add_filter( 'woocommerce_coupon_is_valid', [ $this, 'ensure_coupon_is_valid' ], 10, 3 );

        if ( is_admin() ) {
            add_action( 'woocommerce_process_shop_order_meta', 'dokan_sync_insert_order', 60 );
        }

        // prevent stock reduction for parent orders
        add_filter( 'woocommerce_can_reduce_order_stock', [ $this, 'prevent_stock_reduction_for_parent_order' ], 10, 2 );

        add_action( 'woocommerce_reduce_order_item_stock', [ $this, 'sync_parent_order_item_stock' ], 10, 3 );

        // TODO: This is for backward compatibility. It renames dokan_commission_meta key to _dokan_commission_meta. Remove this after few versions.
        add_filter( 'woocommerce_order_item_get__dokan_commission_meta', [ $this, 'get_dokan_commission_meta' ], 10, 2 );
    }

    /**
     * Retrieve commission meta using old dokan_commission_meta.
     *
     * @return mixed
     */
    public function get_dokan_commission_meta( $value, $line_item ) {
        if ( '' === $value ) {
            $value = $line_item->get_meta( 'dokan_commission_meta', true );
            if ( '' !== $value ) {
                $line_item->delete_meta_data( 'dokan_commission_meta' );
                $line_item->update_meta_data( '_dokan_commission_meta', $value );
                $line_item->save();
            }
        }

        return $value;
    }

    /**
     * Update the child order status when a parent order status is changed
     *
     * @param int      $order_id
     * @param string   $old_status
     * @param string   $new_status
     * @param WC_Order $order
     *
     * @return void
     */
    public function on_order_status_change( $order_id, $old_status, $new_status, $order ) {
        global $wpdb;

        // Split order if the order doesn't have parent and sub orders,
        // and the order is created from dashboard.
        if ( empty( $order->get_parent_id() ) && empty( $order->get_meta( 'has_sub_order' ) ) && is_admin() ) {
            // Remove the hook to prevent recursive callas.
            remove_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_change' ], 10 );

            // Split the order.
            dokan()->order->maybe_split_orders( $order_id );

            // Add the hook back.
            add_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_change' ], 10, 4 );
        }

        // make sure order status contains "wc-" prefix
        if ( stripos( $new_status, 'wc-' ) === false ) {
            $new_status = 'wc-' . $new_status;
        }

        // insert on dokan sync table
        $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->dokan_orders,
            [ 'order_status' => $new_status ],
            [ 'order_id' => $order_id ],
            [ '%s' ],
            [ '%d' ]
        );

        // Update sub-order statuses
        $sub_orders = dokan()->order->get_child_orders( $order_id );
        if ( $sub_orders ) {
            foreach ( $sub_orders as $sub_order ) {
                if ( is_callable( [ $sub_order, 'update_status' ] ) ) {
                    $current_status = $sub_order->get_status();
                    if ( $this->is_status_change_allowed( $current_status, $new_status ) ) {
                        $sub_order->update_status( $new_status );
                    } else {
                        $this->log_skipped_status_update( $sub_order->get_id(), $current_status, $new_status );
                    }
                }
            }
        }

        /**
         * If `exclude_cod_payment` is enabled, don't include the fund in vendor's withdrawal balance.
         *
         * @since 3.8.3
         */
        $exclude_cod_payment = 'on' === dokan_get_option( 'exclude_cod_payment', 'dokan_withdraw', 'off' );

        $should_exclude_cod_payment = $exclude_cod_payment && 'cod' === $order->get_payment_method();

        /**
         * Filter whether the order should be excluded from vendor withdrawal balance.
         *
         * @since 4.2.9
         *
         * @param bool     $should_exclude_cod_payment Whether to exclude COD payment from balance.
         * @param WC_Order $order                      Order object.
         * @param int      $order_id                   Order ID.
         * @param string   $new_status                 New order status.
         * @param bool     $exclude_cod_payment        Whether exclude COD option is enabled.
         */
        $should_exclude_cod_payment = apply_filters(
            'dokan_order_should_exclude_from_vendor_balance',
            $should_exclude_cod_payment,
            $order,
            $order_id,
            $new_status,
            $exclude_cod_payment
        );

        if ( $should_exclude_cod_payment ) {
            return;
        }

        // update on vendor-balance table
        $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->dokan_vendor_balance,
            [ 'status' => $new_status ],
            [
                'trn_id'   => $order_id,
                'trn_type' => 'dokan_orders',
            ],
            [ '%s' ],
            [ '%d', '%s' ]
        );
    }

    /**
     * Check if a status change is allowed for a sub-order.
     *
     * This method determines whether a sub-order can transition from its current status
     * to a new status, based on a configurable whitelist of allowed transitions.
     *
     * @since 3.12.2
     *
     * @param string $current_status The current status of the sub-order (should include 'wc-' prefix).
     * @param string $new_status     The new status to check (should include 'wc-' prefix).
     *
     * @return bool True if the status change is allowed, false otherwise.
     */
    private function is_status_change_allowed( string $current_status, string $new_status ): bool {
        // Ensure both statuses have 'wc-' prefix
        $current_status = $this->maybe_add_wc_prefix( $current_status );
        $new_status     = $this->maybe_add_wc_prefix( $new_status );

        // Define the default whitelist of allowed status transitions
        $default_whitelist = [
            'wc-pending'    => [ 'any' ],
            'wc-on-hold'    => [ 'wc-pending', 'wc-on-hold', 'wc-processing', 'wc-completed', 'wc-failed' ],
            'wc-processing' => [ 'wc-completed', 'wc-failed', 'wc-cancelled', 'wc-refunded' ],
            'wc-completed'  => [ 'wc-refunded' ],
            'wc-failed'     => [ 'wc-pending', 'wc-on-hold', 'wc-processing', 'wc-failed', 'wc-cancelled' ],
            'wc-cancelled'  => [],
            'wc-refunded'   => [],
        ];

        /**
         * Filter the whitelist of allowed status transitions for sub-orders.
         *
         * This filter allows developers to customize the whitelist that determines
         * which status transitions are allowed for sub-orders when the main order
         * status is updated. By modifying this whitelist, you can control how
         * sub-order statuses are updated in relation to the main order.
         *
         * @since 3.12.2
         *
         * @param array $whitelist An associative array where keys are current statuses
         *                         and values are arrays of allowed new statuses.
         *                         The special value 'any' allows transition to any status.
         *
         * @return array Modified whitelist of allowed status transitions.
         */
        $whitelist = apply_filters( 'dokan_sub_order_status_update_whitelist', $default_whitelist );

        // Allow any status change if the current status is not in the whitelist or the new status is not allowed
        if ( ! array_key_exists( $current_status, $whitelist ) || ! array_key_exists( $new_status, $whitelist ) ) {
            return true;
        }

        // If 'any' is allowed for the current status, all transitions are allowed
        if ( in_array( 'any', $whitelist[ $current_status ], true ) ) {
            return true;
        }

        // Check if the new status is in the list of allowed transitions
        return in_array( $new_status, $whitelist[ $current_status ], true );
    }

    /**
     * Ensure a status string has the 'wc-' prefix.
     *
     * @since 3.12.2
     *
     * @param string $status The status string to check.
     *
     * @return string The status string with 'wc-' prefix added if it was missing.
     */
    private function maybe_add_wc_prefix( string $status ): string {
        return strpos( $status, 'wc-' ) === 0 ? $status : 'wc-' . $status;
    }

    /**
     * Log a skipped status update for a sub-order.
     *
     * This method logs a message to the error log when a status update for a sub-order
     * is skipped because the status change is not allowed.
     *
     * @since 3.12.2
     *
     * @param int    $order_id      The ID of the sub-order.
     * @param string $current_status The current status of the sub-order.
     * @param string $new_status     The new status that was not allowed.
     *
     * @return void
     */
    private function log_skipped_status_update( int $order_id, string $current_status, string $new_status ) {
        dokan_log( sprintf( 'Dokan: Skipped status update for sub-order %d from %s to %s', $order_id, $current_status, $new_status ) );
    }

    /**
     * If order status is set to refunded from vendor dashboard, enter remaining balance into vendor balance table.
     *
     * @since 3.8.0 Created this method from on_order_status_change()
     *
     * @param int      $order_id
     * @param string   $old_status
     * @param string   $new_status
     * @param WC_Order $order
     *
     * @return void
     */
    public function manage_refunded_for_order( $order_id, $old_status, $new_status, $order ) {
        global $wpdb;

        // verify nonce
        if ( ! isset( $_POST['_wpnonce'], $_POST['post_type'] ) || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['_wpnonce'] ) ), 'dokan_change_status' ) ) {
            return;
        }

        $exclude_cod_payment = 'on' === dokan_get_option( 'exclude_cod_payment', 'dokan_withdraw', 'off' );
        if ( $exclude_cod_payment && 'cod' === $order->get_payment_method() ) {
            return;
        }

        if ( $new_status !== 'wc-refunded' ) {
            return;
        }

        if ( $_POST['post_type'] !== 'shop_order' ) {
            return;
        }

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
        $balance_data = $wpdb->get_var(
            $wpdb->prepare(
                "SELECT 1 FROM $wpdb->dokan_vendor_balance WHERE trn_id = %d AND trn_type = %s AND status = 'approved'",
                [ $order_id, 'dokan_refund' ]
            )
        );

        if ( ! empty( $balance_data ) ) {
            return;
        }

        $seller_id  = dokan_get_seller_id_by_order( $order_id );
        $net_amount = dokan()->commission->get_earning_by_order( $order );

        // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
        $wpdb->insert(
            $wpdb->dokan_vendor_balance,
            [
                'vendor_id'    => $seller_id,
                'trn_id'       => $order_id,
                'trn_type'     => 'dokan_refund',
                'debit'        => 0,
                'credit'       => $net_amount,
                'status'       => 'approved',
                'trn_date'     => current_time( 'mysql' ),
                'balance_date' => current_time( 'mysql' ),
            ],
            [
                '%d',
                '%d',
                '%s',
                '%f',
                '%f',
                '%s',
                '%s',
                '%s',
            ]
        );

        // update the order table with new refund amount
        $order_data = $wpdb->get_row( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
            $wpdb->prepare(
                "select * from $wpdb->dokan_orders where order_id = %d",
                $order_id
            )
        );

        if ( isset( $order_data->order_total, $order_data->net_amount ) ) {
            // insert on dokan sync table
            $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
                $wpdb->dokan_orders,
                [
                    'order_total' => 0,
                    'net_amount'  => 0,
                ],
                [
                    'order_id' => $order_id,
                ],
                [
                    '%f',
                    '%f',
                ],
                [
                    '%d',
                ]
            );
        }
    }

    /**
     * Mark the parent order as complete when all the child order are completed
     *
     * @param integer  $order_id
     * @param string   $old_status
     * @param string   $new_status
     * @param WC_Order $order
     *
     * @return void
     */
    public function on_sub_order_change( $order_id, $old_status, $new_status, $order ) {
        // we are monitoring only child orders
        if ( $order->get_parent_id() === 0 ) {
            return;
        }

        // get all the child orders and monitor the status
        $parent_order_id = $order->get_parent_id();
        $sub_orders      = dokan()->order->get_child_orders( $parent_order_id );

        if ( ! $sub_orders ) {
            return;
        }

        // return if any child order is not completed
        $all_complete = true;

        // Exclude manual gateways from auto complete order status update for digital products.
        $excluded_gateways = apply_filters( 'dokan_excluded_payment_gateways_on_order_status_update', array( 'bacs', 'cheque', 'cod' ) );

        foreach ( $sub_orders as $sub_order ) {
            // if the order is a downloadable and virtual product, then we need to set the status to complete
            if ( 'processing' === $sub_order->get_status() && $order->is_paid() && ! in_array( $order->get_payment_method(), $excluded_gateways, true ) && ! $sub_order->needs_processing() ) {
                $sub_order->set_status( 'completed', __( 'Marked as completed because it contains digital products only.', 'dokan-lite' ) );
                $sub_order->save();
            }

            // if any child order is not completed, break the loop
            if ( $sub_order->get_status() !== 'completed' ) {
                $all_complete = false;
            }
        }

        // seems like all the child orders are completed
        // mark the parent order as complete
        if ( $all_complete ) {
            $parent_order = wc_get_order( $parent_order_id );
            $parent_order->update_status( 'wc-completed', __( 'Mark parent order completed when all child orders are completed.', 'dokan-lite' ) );
        }
    }

    /**
     * Split order for vendor
     *
     * @since 3.0.0
     *
     * @param $parent_order_id
     *
     * @return void
     */
    public function split_vendor_orders( $parent_order_id ) {
        dokan()->order->maybe_split_orders( $parent_order_id );
    }

    /**
     * Ensure vendor coupon
     *
     * For consistency, restrict coupons in cart if only
     * products from that vendor exists in the cart. Also, a coupon
     * should be restricted with a product.
     *
     * For example: When entering a coupon created by admin is applied, make
     * sure a product of the admin is in the cart. Otherwise it wouldn't be
     * possible to distribute the coupon in sub orders.
     *
     * @since 4.0.0 Refactored to make it more flexible, and added filter
     *
     * @param boolean      $valid      Whether the coupon is currently considered valid.
     * @param WC_Coupon    $coupon     The coupon object being validated.
     * @param WC_Discounts $discounts  The discount object containing cart/order items being validated.
     *
     * @return boolean True if the coupon is valid, false otherwise
     * @throws Exception When the coupon is invalid for multiple vendors
     */
    public function ensure_coupon_is_valid( bool $valid, WC_Coupon $coupon, WC_Discounts $discounts ): bool {
        $available_vendors = [];

	    foreach ( $discounts->get_items() as $item ) {
		    if ( ! isset( $item->product ) || ! $item->product instanceof WC_Product ) {
			    continue;
		    }

		    $available_vendors[] = (int) dokan_get_vendor_by_product( $item->product->get_id(), true );
	    }

        $available_vendors = array_unique( $available_vendors );

        if ( $coupon->is_type( 'fixed_cart' ) && count( $available_vendors ) > 1 ) {
            throw new Exception( esc_html__( 'This coupon is invalid for multiple vendors.', 'dokan-lite' ) );
        }

        /**
         * Filter the validity of a coupon.
         *
         * @since 4.0.0
         *
         * @param boolean      $valid     The validity of the coupon.
         * @param WC_Coupon    $coupon    The coupon object.
         * @param WC_Discounts $discounts The discount object, which contains the order details.
         */
        return apply_filters( 'dokan_coupon_is_valid', $valid, $coupon, $discounts );
    }

    /**
     * Prevent stock reduction for parent orders
     *
     * Parent orders should not have their stock reduced. Only sub-orders
     * should manage stock reductions.
     *
     * @param bool     $can_reduce Whether stock can be reduced.
     * @param WC_Order $order      The order object.
     *
     * @return bool False if this is a parent order, true otherwise.
     */
    public function prevent_stock_reduction_for_parent_order( $can_reduce, $order ) {
        // If this is a parent order (has sub-orders), prevent stock reduction
        if ( $order->get_meta( 'has_sub_order' ) ) {
            return false;
        }

        return $can_reduce;
    }

    /**
     * Sync parent order item stock metadata when sub-order item stock is reduced
     *
     * When a sub-order item has its stock reduced, also update the parent order item's
     * _reduced_stock metadata to keep them in sync.
     * @param WC_Order_Item_Product $item The sub-order item.
     * @param array                 $change Change details (product, from, to).
     * @param WC_Order              $order The sub-order.
     *
     * @return void
     */
    public function sync_parent_order_item_stock( $item, $change, $order ) {
        // Only process sub-orders (those with a parent)
        if ( ! $order->get_parent_id() ) {
            return;
        }

        // Get the parent order item ID from the sub-order item meta
        $parent_item_id = $item->get_meta( '_dokan_parent_order_item_id', true );

        if ( ! $parent_item_id ) {
            return;
        }

        $reduced_qty = $item->get_meta( '_reduced_stock', true );

        // Update the parent item directly in the database
        if ( $reduced_qty ) {
            wc_update_order_item_meta( $parent_item_id, '_reduced_stock', $reduced_qty );
        }
	}
}
