<?php

namespace WeDevs\DokanPro\Modules\Germanized;

use Vendidero\OrderWithdrawalButton\WithdrawalOrder;
use WC_Order;

if ( ! defined( 'ABSPATH' ) ) {
    exit; // Exit if accessed directly
}

/**
 * Keeps a parent order's withdrawal request and its per-vendor sub-order requests in sync when
 * either side is resolved, so no request is ever left dangling.
 *
 * {@see WithdrawalCascade} fans a customer's parent request out into one request per vendor
 * sub-order. Those requests are then resolved independently — the admin can confirm/reject the
 * parent, and each vendor can confirm/reject their own sub-order. Without coordination the two
 * sides drift apart: confirming the parent leaves sub-order requests open, and resolving every
 * sub-order leaves the parent request open. This class closes both gaps:
 *
 *  - Down: when the parent request is confirmed/rejected, the same outcome is applied to every
 *    still-open sub-order request.
 *  - Up: when a sub-order request is resolved, the parent request is resolved too once all of the
 *    order's cascaded sub-order requests are done — confirmed only if every vendor confirmed,
 *    otherwise rejected.
 *  - Delete: when the customer deletes the parent request, the derived sub-order requests are
 *    deleted with it.
 *
 * The customer is notified of the real per-vendor outcome: eu-owb's "confirmed"/"rejected" customer
 * email is kept for each sub-order (and for standalone orders), and suppressed only for the umbrella
 * parent — whose single roll-up outcome would misrepresent a mixed result (one vendor confirmed,
 * another rejected).
 *
 * @since 5.0.12
 */
class WithdrawalSync {

    /**
     * Order meta recording how a sub-order's withdrawal request was resolved ('confirmed' or
     * 'rejected'); read when rolling the outcome up to the parent.
     *
     * Named "revocation" rather than "withdrawal": in Dokan's domain language a Withdraw is a
     * vendor's payout request (see dokan-lite CONTEXT.md), so persisted keys avoid the clash.
     */
    const OUTCOME_META = '_dokan_gzd_revocation_outcome';

    /**
     * True while this class is itself resolving requests, so the confirm/reject it triggers do not
     * recurse back through the same handlers.
     *
     * @var bool
     */
    protected $syncing = false;

    /**
     * Constructor.
     *
     * @since 5.0.12
     */
    public function __construct() {
        // Clear any stale outcome from a previous cycle when a fresh request is created on an order.
        add_action( 'eu_owb_woocommerce_withdrawal_request_created', [ $this, 'clear_outcome' ], 10, 1 );

        add_action( 'eu_owb_woocommerce_withdrawal_request_confirmed', [ $this, 'on_confirmed' ], 10, 1 );
        add_action( 'eu_owb_woocommerce_withdrawal_request_rejected', [ $this, 'on_rejected' ], 10, 3 );
        add_action( 'eu_owb_woocommerce_withdrawal_request_deleted', [ $this, 'on_deleted' ], 10, 1 );

        // Notify the customer of the real per-vendor outcome: keep each sub-order's confirmed/rejected
        // email, and suppress only the umbrella parent's (its single roll-up outcome would misstate a
        // mixed result).
        add_filter( 'woocommerce_email_recipient_customer_withdrawal_request_confirmed', [ $this, 'suppress_parent_customer_email' ], 999, 2 );
        add_filter( 'woocommerce_email_recipient_customer_withdrawal_request_rejected', [ $this, 'suppress_parent_customer_email' ], 999, 2 );
    }

    /**
     * Forget a previous resolution outcome when a new request is created on the order.
     *
     * When the fresh request is created on a parent, every one of its sub-orders is reset too — not
     * just the ones re-cascaded this cycle. Otherwise a sub-order the customer does not re-select
     * keeps last cycle's outcome, and roll_up() (which reads every sibling) would count that stale
     * result and resolve the new request with the wrong outcome.
     *
     * The full reset is also correct for a mid-cycle *edit* of an open parent request: a vendor
     * who already resolved and whose items remain selected is re-asked by the cascade (their
     * request re-created), so their outcome must reset; a vendor whose items were dropped is no
     * longer part of the request, so their old outcome must not weigh on the new roll-up either.
     *
     * @since 5.0.12
     *
     * @param WC_Order|false $order The order the request was created for.
     *
     * @return void
     */
    public function clear_outcome( $order ) {
        if ( ! $order instanceof WC_Order ) {
            return;
        }

        $this->forget_outcome( $order );

        if ( ! wc_string_to_bool( $order->get_meta( 'has_sub_order' ) ) ) {
            return;
        }

        foreach ( (array) dokan_get_suborder_ids_by( $order->get_id() ) as $sub_order_id ) {
            $sub_order = wc_get_order( $sub_order_id );
            if ( $sub_order instanceof WC_Order ) {
                $this->forget_outcome( $sub_order );
            }
        }
    }

    /**
     * Delete the recorded outcome meta from an order, if present.
     *
     * @since 5.0.12
     *
     * @param WC_Order $order The order.
     *
     * @return void
     */
    protected function forget_outcome( WC_Order $order ) {
        if ( '' !== (string) $order->get_meta( self::OUTCOME_META ) ) {
            $order->delete_meta_data( self::OUTCOME_META );
            $order->save();
        }
    }

    /**
     * Handle a confirmed withdrawal request.
     *
     * @since 5.0.12
     *
     * @param WC_Order|false $order The order whose request was confirmed.
     *
     * @return void
     */
    public function on_confirmed( $order ) {
        $this->on_resolved( $order, 'confirmed', '' );
    }

    /**
     * Handle a rejected withdrawal request.
     *
     * @since 5.0.12
     *
     * @param WC_Order|false $order  The order whose request was rejected.
     * @param mixed          $request The rejected request (unused).
     * @param string         $reason  The rejection reason.
     *
     * @return void
     */
    public function on_rejected( $order, $request = null, $reason = '' ) {
        $this->on_resolved( $order, 'rejected', (string) $reason );
    }

    /**
     * Route a resolved request down to sub-orders (if it is a parent) or up to the parent (if it is
     * a sub-order).
     *
     * @since 5.0.12
     *
     * @param mixed  $order   The resolved order.
     * @param string $outcome 'confirmed' or 'rejected'.
     * @param string $reason  Rejection reason, when rejecting.
     *
     * @return void
     */
    protected function on_resolved( $order, $outcome, $reason ) {
        if ( $this->syncing || ! $order instanceof WC_Order || ! function_exists( 'eu_owb_get_withdrawal_request' ) ) {
            return;
        }

        if ( wc_string_to_bool( $order->get_meta( 'has_sub_order' ) ) ) {
            $this->push_down( $order, $outcome, $reason );
        } elseif ( dokan_is_sub_order( $order ) ) {
            $this->record_outcome( $order, $outcome );
            $this->roll_up( $order );
        }
    }

    /**
     * Apply a parent's outcome to each of its still-open sub-order requests.
     *
     * @since 5.0.12
     *
     * @param WC_Order $parent_order The parent order.
     * @param string   $outcome      'confirmed' or 'rejected'.
     * @param string   $reason       Rejection reason, when rejecting.
     *
     * @return void
     */
    protected function push_down( WC_Order $parent_order, $outcome, $reason ) {
        if ( ! function_exists( 'eu_owb_order_confirm_withdrawal_request' ) || ! function_exists( 'eu_owb_order_reject_withdrawal_request' ) ) {
            return;
        }

        $this->syncing = true;

        try {
            foreach ( (array) dokan_get_suborder_ids_by( $parent_order->get_id() ) as $sub_order_id ) {
                $sub_order = wc_get_order( $sub_order_id );
                if ( ! $sub_order instanceof WC_Order || ! eu_owb_get_withdrawal_request( $sub_order ) ) {
                    continue;
                }

                $this->record_outcome( $sub_order, $outcome );

                if ( 'confirmed' === $outcome ) {
                    eu_owb_order_confirm_withdrawal_request( $sub_order );
                } else {
                    eu_owb_order_reject_withdrawal_request( $sub_order, $reason );
                }
            }
        } finally {
            $this->syncing = false;
        }
    }

    /**
     * Resolve the parent request once every cascaded sub-order request is resolved.
     *
     * Confirms the parent only when every resolved sub-order was confirmed; if any vendor rejected,
     * the parent is rejected (the confirmed sub-orders still stand on their own).
     *
     * @since 5.0.12
     *
     * @param WC_Order $sub_order The sub-order that was just resolved.
     *
     * @return void
     */
    protected function roll_up( WC_Order $sub_order ) {
        if ( ! function_exists( 'eu_owb_order_confirm_withdrawal_request' ) || ! function_exists( 'eu_owb_order_reject_withdrawal_request' ) ) {
            return;
        }

        $parent_id    = $sub_order->get_parent_id();
        $parent_order = $parent_id ? wc_get_order( $parent_id ) : false;

        // No parent, or the parent request is already resolved (e.g. the admin drove this) — nothing
        // to roll up.
        if ( ! $parent_order instanceof WC_Order || ! eu_owb_get_withdrawal_request( $parent_order ) ) {
            return;
        }

        $outcomes = [];

        foreach ( (array) dokan_get_suborder_ids_by( $parent_id ) as $sibling_id ) {
            $sibling = wc_get_order( $sibling_id );
            if ( ! $sibling instanceof WC_Order ) {
                continue;
            }

            // A sibling still awaiting its vendor — the parent is not ready to resolve.
            if ( eu_owb_get_withdrawal_request( $sibling ) ) {
                return;
            }

            $recorded = (string) $sibling->get_meta( self::OUTCOME_META );
            if ( '' !== $recorded ) {
                $outcomes[] = $recorded;
            }
        }

        // Nothing was ever cascaded to a sub-order.
        if ( empty( $outcomes ) ) {
            return;
        }

        $this->syncing = true;

        try {
            if ( in_array( 'rejected', $outcomes, true ) ) {
                eu_owb_order_reject_withdrawal_request( $parent_order, __( 'A vendor rejected their part of this request.', 'dokan' ) );
            } else {
                eu_owb_order_confirm_withdrawal_request( $parent_order );
            }
        } finally {
            $this->syncing = false;
        }
    }

    /**
     * Delete the derived sub-order requests when the customer deletes the parent request.
     *
     * @since 5.0.12
     *
     * @param mixed $request The deleted request.
     *
     * @return void
     */
    public function on_deleted( $request ) {
        if ( $this->syncing || ! function_exists( 'eu_owb_get_withdrawal_request' ) || ! function_exists( 'eu_owb_order_delete_withdrawal_request' ) ) {
            return;
        }

        $order = ( is_object( $request ) && method_exists( $request, 'get_parent' ) ) ? $request->get_parent() : null;
        if ( ! $order instanceof WC_Order || ! wc_string_to_bool( $order->get_meta( 'has_sub_order' ) ) ) {
            return;
        }

        $this->syncing = true;

        try {
            foreach ( (array) dokan_get_suborder_ids_by( $order->get_id() ) as $sub_order_id ) {
                $sub_order = wc_get_order( $sub_order_id );
                if ( $sub_order instanceof WC_Order && eu_owb_get_withdrawal_request( $sub_order ) ) {
                    // Always delete the derived sub-order requests as an internal ($by_customer =
                    // false) operation: the customer's deletion already sent the admin one "deleted"
                    // notification for the parent, and eu-owb sends that admin email on every
                    // by-customer delete — so forwarding $by_customer here would send one per
                    // sub-order (N+1 for one customer action).
                    eu_owb_order_delete_withdrawal_request( $sub_order, false );
                }
            }
        } finally {
            $this->syncing = false;
        }
    }

    /**
     * Suppress eu-owb's "confirmed"/"rejected" customer email for the umbrella parent order only.
     *
     * The customer is notified per sub-order instead, so each vendor's real outcome is reported
     * accurately; the parent's single roll-up outcome would misstate a mixed result (one vendor
     * confirmed, another rejected). Sub-orders and standalone orders keep their email.
     *
     * @since 5.0.12
     *
     * @param string $recipient    The customer recipient.
     * @param mixed  $email_object The email object — a WithdrawalOrder for these emails.
     *
     * @return string
     */
    public function suppress_parent_customer_email( $recipient, $email_object ) {
        if ( ! $email_object instanceof WithdrawalOrder ) {
            return $recipient;
        }

        $order = $email_object->get_parent();

        return ( $order instanceof WC_Order && wc_string_to_bool( $order->get_meta( 'has_sub_order' ) ) ) ? '' : $recipient;
    }

    /**
     * Record how an order's request was resolved.
     *
     * @since 5.0.12
     *
     * @param WC_Order $order   The order.
     * @param string   $outcome 'confirmed' or 'rejected'.
     *
     * @return void
     */
    protected function record_outcome( WC_Order $order, $outcome ) {
        $order->update_meta_data( self::OUTCOME_META, $outcome );
        $order->save();
    }
}
