<?php

namespace WeDevs\AppBridge\Integrations\Dokan\Notifications;

use Throwable;
use WC_Order;
use WC_Product;
use WP_Comment;
use WP_Post;
use WeDevs\AppBridge\Core\Contracts\Hookable;
use WeDevs\AppBridge\Core\Notifications\EventDispatcher;

/**
 * Bridges Dokan Lite + Pro hooks into the vendor-app event catalog.
 *
 * Each callback resolves the matching `vendor.*` event from
 * {@see VendorEventCatalog}, fills the title/body templates with context,
 * and ships it through {@see EventDispatcher}, targeting the vendor's
 * external user ID.
 */
class VendorEventDispatcher implements Hookable {

	/**
	 * Statuses that should NOT trigger the generic `order_status_changed`
	 * event — either because they have a dedicated event (cancelled) or
	 * because they're covered by a separate hook (pending/new from
	 * dokan_checkout_update_order_meta).
	 *
	 * @var string[]
	 */
	private const STATUS_CHANGE_SKIP = [ 'cancelled', 'pending', 'auto-draft', 'checkout-draft' ];

	private EventDispatcher $dispatcher;

	public function __construct( ?EventDispatcher $dispatcher = null ) {
		$this->dispatcher = $dispatcher ?? new EventDispatcher( null, 'app_bridge_vendor_settings' );
	}

	public function register_hooks(): void {
		// ── Orders ─────────────────────────────────────────────────────
		add_action( 'dokan_checkout_update_order_meta', [ $this, 'on_new_vendor_order' ], 10, 2 );
		add_action( 'woocommerce_order_status_changed', [ $this, 'on_order_status_changed' ], 10, 4 );
		add_action( 'dokan_rma_save_warranty_request', [ $this, 'on_warranty_request_saved' ], 10, 1 );

		// ── Withdrawals ────────────────────────────────────────────────
		add_action( 'dokan_after_withdraw_request', [ $this, 'on_withdraw_requested' ], 10, 4 );
		add_action( 'dokan_withdraw_status_updated', [ $this, 'on_withdraw_status_updated' ], 10, 3 );

		// ── Products ───────────────────────────────────────────────────
		add_action( 'dokan_new_product_added', [ $this, 'on_new_product' ], 10, 2 );
		add_action( 'transition_post_status', [ $this, 'on_product_status_transition' ], 10, 3 );
		add_action( 'woocommerce_low_stock_notification', [ $this, 'on_low_stock' ] );
		add_action( 'woocommerce_no_stock_notification', [ $this, 'on_no_stock' ] );

		// ── Reviews ────────────────────────────────────────────────────
		add_action( 'comment_post', [ $this, 'on_comment_posted' ], 10, 3 );
		add_action( 'dokan_store_review_saved', [ $this, 'on_store_review_saved' ], 10, 3 );

		// ── Support tickets (Dokan Pro: store-support module) ──────────
		add_action( 'dss_new_ticket_created', [ $this, 'on_support_ticket_opened' ], 10, 2 );
		add_action( 'wp_insert_comment', [ $this, 'on_support_message' ], 20, 2 );

		// ── Store moderation ───────────────────────────────────────────
		add_action( 'dokan_vendor_enabled', [ $this, 'on_store_approved' ], 10, 1 );
		add_action( 'dokan_vendor_disabled', [ $this, 'on_store_suspended' ], 10, 1 );

		// ── Account ────────────────────────────────────────────────────
		add_action( 'dokan_new_seller_created', [ $this, 'on_vendor_registered' ], 10, 1 );
		add_action( 'profile_update', [ $this, 'on_profile_update' ], 10, 2 );
	}

	// ── Orders ─────────────────────────────────────────────────────────────

	/**
	 * `dokan_checkout_update_order_meta` fires once per Dokan sub-order with
	 * the seller_id attached. The parent / non-vendor orders never see this.
	 *
	 * @param int $order_id
	 * @param int $seller_id
	 */
	public function on_new_vendor_order( $order_id, $seller_id ): void {
		$order = wc_get_order( (int) $order_id );
		if ( ! $order instanceof WC_Order || (int) $seller_id <= 0 ) {
			return;
		}

		$this->dispatch_for_order( 'order_new', $order, (int) $seller_id );
	}

	/**
	 * @param int       $order_id
	 * @param string    $old_status
	 * @param string    $new_status
	 * @param \WC_Order $order
	 */
	public function on_order_status_changed( $order_id, $old_status, $new_status, $order ): void {
		if ( ! $order instanceof WC_Order ) {
			$order = wc_get_order( (int) $order_id );
		}
		if ( ! $order instanceof WC_Order ) {
			return;
		}

		$vendor_id = $this->vendor_id_for_order( $order );
		if ( $vendor_id <= 0 ) {
			return;
		}

		// Don't self-notify the vendor when they're the one driving the change
		// from the vendor dashboard / REST API.
		if ( get_current_user_id() === $vendor_id ) {
			return;
		}

		// Cancellations split by who triggered them.
		if ( 'cancelled' === $new_status ) {
			$event_id = $this->cancellation_actor_is_admin( $order )
				? 'order_cancelled_by_admin'
				: 'order_cancelled_by_customer';
			$this->dispatch_for_order( $event_id, $order, $vendor_id );
			return;
		}

		// Skip statuses with dedicated events / non-events.
		if ( in_array( $new_status, self::STATUS_CHANGE_SKIP, true ) ) {
			return;
		}

		$this->dispatch_for_order(
			'order_status_changed',
			$order,
			$vendor_id,
			[ 'status' => wc_get_order_status_name( $new_status ) ]
		);
	}

	/**
	 * Customer-initiated refund request via the Dokan Pro RMA module.
	 *
	 * The actual user-facing "Request a Refund" button on My-Account → Orders
	 * fires `dokan_save_warranty_request()` (works for the frontend form *and*
	 * the warranty REST controller). It does NOT fire `dokan_refund_request_created`
	 * — that one fires later, when the vendor approves with a refund.
	 *
	 * `woocommerce_order_refunded` is also wrong: that fires only after a
	 * refund is actually processed, never for a pending request.
	 *
	 * @param array<string, mixed> $data Warranty request payload — see
	 *                                   `dokan_save_warranty_request()` for shape.
	 */
	/**
	 * Map of RMA `type` → catalog event id. Customer warranty form lets the
	 * shopper pick one of three: Replace, Refund, Refund-as-store-credit (coupon).
	 * Each routes to its own event so vendors can opt in/out per type.
	 */
	private const RMA_TYPE_EVENT_MAP = [
		'refund'  => 'order_refund_requested',
		'replace' => 'order_replacement_requested',
		'coupon'  => 'order_store_credit_requested',
	];

	public function on_warranty_request_saved( $data ): void {
		if ( ! is_array( $data ) || empty( $data['order_id'] ) ) {
			return;
		}

		$type     = (string) ( $data['type'] ?? '' );
		$event_id = self::RMA_TYPE_EVENT_MAP[ $type ] ?? '';
		if ( '' === $event_id ) {
			return;
		}

		$order = wc_get_order( (int) $data['order_id'] );
		if ( ! $order instanceof WC_Order ) {
			return;
		}

		$vendor_id = isset( $data['vendor_id'] ) ? (int) $data['vendor_id'] : $this->vendor_id_for_order( $order );
		if ( $vendor_id <= 0 ) {
			return;
		}

		// Vendor submitting on customer's behalf — don't self-notify.
		if ( get_current_user_id() === $vendor_id ) {
			return;
		}

		$this->dispatch_for_order( $event_id, $order, $vendor_id );
	}

	// ── Withdrawals ────────────────────────────────────────────────────────

	/**
	 * @param int    $user_id
	 * @param mixed  $amount
	 * @param string $method
	 * @param int    $withdraw_id
	 */
	public function on_withdraw_requested( $user_id, $amount, $method, $withdraw_id = 0 ): void {
		$this->dispatch_for_vendor(
			'withdraw_request_submitted',
			(int) $user_id,
			[
				'amount'         => $this->format_amount( $amount ),
				'method'         => (string) $method,
				'action_type_id' => (string) $withdraw_id,
			]
		);
	}

	/**
	 * @param string $status
	 * @param int    $user_id
	 * @param int    $withdraw_id
	 */
	public function on_withdraw_status_updated( $status, $user_id, $withdraw_id ): void {
		$status_map = [
			'approved'  => 'withdraw_approved',
			'cancelled' => 'withdraw_rejected',
		];
		$event_id   = $status_map[ $status ] ?? null;
		if ( ! $event_id ) {
			return;
		}

		$this->dispatch_for_vendor(
			$event_id,
			(int) $user_id,
			[
				'amount'         => $this->withdraw_amount( (int) $withdraw_id ),
				'action_type_id' => (string) $withdraw_id,
			]
		);
	}

	// ── Products ───────────────────────────────────────────────────────────

	/**
	 * Vendor created a product. Only notify when it landed in `pending` (admin
	 * approval needed); auto-published products skip the "Under review" alert.
	 *
	 * @param int   $product_id
	 * @param array<string, mixed> $data
	 */
	public function on_new_product( $product_id, $data = [] ): void {
		$product = wc_get_product( (int) $product_id );
		if ( ! $product instanceof WC_Product || 'pending' !== $product->get_status() ) {
			return;
		}

		$vendor_id = (int) get_post_field( 'post_author', $product_id );
		if ( $vendor_id <= 0 ) {
			return;
		}

		$this->dispatch_for_vendor(
			'product_pending_review',
			$vendor_id,
			[
				'product_name'   => $product->get_name(),
				'action_type_id' => (string) $product_id,
			]
		);
	}

	/**
	 * `pending → publish` = approved; `* → trash` = rejected.
	 *
	 * @param string  $new_status
	 * @param string  $old_status
	 * @param WP_Post $post
	 */
	public function on_product_status_transition( $new_status, $old_status, $post ): void {
		if ( ! $post instanceof WP_Post || 'product' !== $post->post_type ) {
			return;
		}
		if ( $new_status === $old_status ) {
			return;
		}

		$vendor_id = (int) $post->post_author;
		if ( $vendor_id <= 0 ) {
			return;
		}

		if ( 'pending' === $old_status && 'publish' === $new_status ) {
			$this->dispatch_for_vendor(
				'product_approved',
				$vendor_id,
				[
					'product_name'   => $post->post_title,
					'action_type_id' => (string) $post->ID,
				]
			);
			return;
		}

		if ( 'trash' === $new_status && 'trash' !== $old_status ) {
			$this->dispatch_for_vendor(
				'product_rejected',
				$vendor_id,
				[
					'product_name'   => $post->post_title,
					'action_type_id' => (string) $post->ID,
				]
			);
		}
	}

	/** @param WC_Product $product */
	public function on_low_stock( $product ): void {
		if ( ! $product instanceof WC_Product ) {
			return;
		}
		$this->dispatch_for_stock( 'product_low_stock', $product );
	}

	/** @param WC_Product $product */
	public function on_no_stock( $product ): void {
		if ( ! $product instanceof WC_Product ) {
			return;
		}
		$this->dispatch_for_stock( 'product_out_of_stock', $product );
	}

	private function dispatch_for_stock( string $event_id, WC_Product $product ): void {
		$vendor_id = (int) get_post_field( 'post_author', $product->get_id() );
		if ( $vendor_id <= 0 ) {
			return;
		}

		$this->dispatch_for_vendor(
			$event_id,
			$vendor_id,
			[
				'product_name'   => $product->get_name(),
				'stock_count'    => (string) ( $product->get_stock_quantity() ?? 0 ),
				'action_type_id' => (string) $product->get_id(),
			]
		);
	}

	// ── Reviews ────────────────────────────────────────────────────────────

	/**
	 * Approved comments on vendor-owned products surface as `review_new`;
	 * pending ones as `review_pending_approval`.
	 *
	 * @param int    $comment_id
	 * @param int|string $approved 1 = approved, 0 = pending
	 * @param array<string, mixed> $commentdata
	 */
	public function on_comment_posted( $comment_id, $approved, $commentdata ): void {
		$comment = get_comment( $comment_id );
		if ( ! $comment instanceof WP_Comment || 'review' !== $comment->comment_type ) {
			return;
		}

		$post = get_post( $comment->comment_post_ID );
		if ( ! $post instanceof WP_Post || 'product' !== $post->post_type ) {
			return;
		}

		$vendor_id = (int) $post->post_author;
		if ( $vendor_id <= 0 ) {
			return;
		}

		$rating = (int) get_comment_meta( $comment_id, 'rating', true );
		$event_id = ( '1' === (string) $approved || 1 === $approved )
			? 'review_new'
			: 'review_pending_approval';

		$this->dispatch_for_vendor(
			$event_id,
			$vendor_id,
			[
				'product_name'   => $post->post_title,
				'rating'         => (string) $rating,
				'action_type_id' => (string) $post->ID,
			]
		);
	}

	/**
	 * New store review submitted via the store-reviews module. Fires for both
	 * inserts and edits — we filter out edits so the vendor isn't pinged each
	 * time the customer tweaks their wording.
	 *
	 * @param int                  $post_id  `dokan_store_reviews` CPT id.
	 * @param array<string, mixed> $postdata Form payload (`store_id`, etc.).
	 * @param int                  $rating   0–5.
	 */
	public function on_store_review_saved( $post_id, $postdata, $rating ): void {
		$post_id = (int) $post_id;
		if ( $post_id <= 0 ) {
			return;
		}

		// Edits route through the same hook with a `post_id` in the payload.
		if ( is_array( $postdata ) && ! empty( $postdata['post_id'] ) && (int) $postdata['post_id'] > 0 ) {
			return;
		}

		$vendor_id = is_array( $postdata ) && ! empty( $postdata['store_id'] )
			? (int) $postdata['store_id']
			: (int) get_post_meta( $post_id, 'store_id', true );

		if ( $vendor_id <= 0 || get_current_user_id() === $vendor_id ) {
			return;
		}

		$this->dispatch_for_vendor(
			'store_review_new',
			$vendor_id,
			[
				'rating'         => (string) (int) $rating,
				'action_type_id' => (string) $post_id,
			]
		);
	}

	// ── Support tickets ────────────────────────────────────────────────────

	/**
	 * Customer opened a support ticket against a store. Fires from both the
	 * legacy AJAX form and the REST controller in the store-support module.
	 *
	 * @param int $post_id  `dokan_store_support` CPT id.
	 * @param int $store_id Vendor (store) user id.
	 */
	public function on_support_ticket_opened( $post_id, $store_id ): void {
		$post_id   = (int) $post_id;
		$vendor_id = (int) $store_id;
		if ( $post_id <= 0 || $vendor_id <= 0 ) {
			return;
		}

		$this->dispatch_for_vendor(
			'support_ticket_opened',
			$vendor_id,
			[
				'order_number'   => $this->support_order_number( $post_id ),
				'action_type_id' => (string) $post_id,
			]
		);
	}

	/**
	 * Reply posted on a `dokan_store_support` ticket. Pushed to the vendor
	 * unless the vendor themselves authored the reply.
	 *
	 * @param int        $comment_id
	 * @param WP_Comment $comment
	 */
	public function on_support_message( $comment_id, $comment ): void {
		if ( ! $comment instanceof WP_Comment ) {
			$comment = get_comment( $comment_id );
		}
		if ( ! $comment instanceof WP_Comment ) {
			return;
		}

		$post = get_post( (int) $comment->comment_post_ID );
		if ( ! $post instanceof WP_Post || 'dokan_store_support' !== $post->post_type ) {
			return;
		}

		$vendor_id = (int) get_post_meta( $post->ID, 'store_id', true );
		if ( $vendor_id <= 0 ) {
			return;
		}

		// Skip the vendor's own replies.
		$author_id = (int) $comment->user_id;
		if ( $author_id > 0 && $author_id === $vendor_id ) {
			return;
		}

		$this->dispatch_for_vendor(
			'support_new_message',
			$vendor_id,
			[
				'order_number'   => $this->support_order_number( $post->ID ),
				'action_type_id' => (string) $post->ID,
			]
		);
	}

	/**
	 * Resolve the order_number for a ticket; falls back to the ticket id when
	 * the ticket isn't tied to an order (general inquiries).
	 */
	private function support_order_number( int $ticket_id ): string {
		$order_id = (int) get_post_meta( $ticket_id, 'order_id', true );
		if ( $order_id > 0 ) {
			$order = wc_get_order( $order_id );
			if ( $order instanceof WC_Order ) {
				return (string) $order->get_order_number();
			}
		}

		return (string) $ticket_id;
	}

	// ── Store moderation ───────────────────────────────────────────────────

	/**
	 * Vendor was enabled (post-registration approval, or re-enable after a
	 * suspension). Fires from `Vendor::make_active()` with the vendor user id.
	 *
	 * @param int $vendor_id
	 */
	public function on_store_approved( $vendor_id ): void {
		$vendor_id = (int) $vendor_id;
		if ( $vendor_id <= 0 ) {
			return;
		}

		$this->dispatch_for_vendor(
			'store_approved',
			$vendor_id,
			[ 'store_name' => $this->store_name_for( $vendor_id ) ]
		);
	}

	/**
	 * Vendor was disabled by the admin.
	 *
	 * @param int $vendor_id
	 */
	public function on_store_suspended( $vendor_id ): void {
		$vendor_id = (int) $vendor_id;
		if ( $vendor_id <= 0 ) {
			return;
		}

		$this->dispatch_for_vendor(
			'store_suspended',
			$vendor_id,
			[ 'store_name' => $this->store_name_for( $vendor_id ) ]
		);
	}

	private function store_name_for( int $vendor_id ): string {
		if ( function_exists( 'dokan' ) ) {
			try {
				$vendor = dokan()->vendor->get( $vendor_id );
				if ( $vendor && method_exists( $vendor, 'get_shop_name' ) ) {
					$name = (string) $vendor->get_shop_name();
					if ( '' !== $name ) {
						return $name;
					}
				}
			} catch ( Throwable $e ) { // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedCatch
			}
		}

		$user = get_userdata( $vendor_id );
		return $user instanceof \WP_User ? (string) $user->display_name : '';
	}

	// ── Account ────────────────────────────────────────────────────────────

	public function on_vendor_registered( $user_id ): void {
		$this->dispatch_for_vendor( 'account_registered', (int) $user_id );
	}

	/**
	 * `profile_update` is the canonical "user data changed" hook in WP. It
	 * fires for every user save, so we narrow to: (1) the password actually
	 * changed, and (2) the user is a Dokan vendor.
	 *
	 * @param int      $user_id
	 * @param \WP_User $old_user_data
	 */
	public function on_profile_update( $user_id, $old_user_data ): void {
		$user_id = (int) $user_id;
		if ( $user_id <= 0 || ! $old_user_data instanceof \WP_User ) {
			return;
		}

		if ( ! function_exists( 'dokan_is_user_seller' ) || ! dokan_is_user_seller( $user_id ) ) {
			return;
		}

		$current = get_userdata( $user_id );
		if ( ! $current instanceof \WP_User || $current->user_pass === $old_user_data->user_pass ) {
			return;
		}

		$this->dispatch_for_vendor( 'account_password_changed', $user_id );
	}

	// ── Helpers ────────────────────────────────────────────────────────────

	private function dispatch_for_order( string $event_id, WC_Order $order, int $vendor_id, array $extra = [] ): void {
		$context = array_merge(
			[
				'order_number'   => $order->get_order_number(),
				'amount'         => self::clean_price( $order->get_formatted_order_total() ),
				'action_type_id' => (string) $order->get_id(),
			],
			$extra
		);

		$this->dispatch_for_vendor( $event_id, $vendor_id, $context );
	}

	/**
	 * Strip HTML and decode entities from a `wc_price()`-style string so the
	 * push notification shows `$49.99` instead of `&#36;49.99`.
	 */
	private static function clean_price( string $html ): string {
		return html_entity_decode( wp_strip_all_tags( $html ), ENT_QUOTES, 'UTF-8' );
	}

	private function dispatch_for_vendor( string $event_id, int $vendor_id, array $context = [] ): void {
		if ( $vendor_id <= 0 ) {
			return;
		}

		$audience = [
			'external_user_ids' => [ (string) $vendor_id ],
		];

		try {
			$this->dispatcher->dispatch( 'vendor', $event_id, $context, $audience );
		} catch ( Throwable $e ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log(
				sprintf(
					'[AppBridge vendor event "%s" for user #%d] %s',
					$event_id,
					$vendor_id,
					$e->getMessage()
				)
			);
		}
	}

	/**
	 * Resolve the vendor for a (sub-)order. Returns 0 if the order doesn't
	 * belong to a Dokan vendor (parent orders, non-Dokan installs).
	 */
	private function vendor_id_for_order( WC_Order $order ): int {
		if ( function_exists( 'dokan_get_seller_id_by_order' ) ) {
			$id = (int) dokan_get_seller_id_by_order( $order->get_id() );
			if ( $id > 0 ) {
				return $id;
			}
		}

		$meta = (int) $order->get_meta( '_dokan_vendor_id' );
		return $meta > 0 ? $meta : 0;
	}

	/**
	 * Heuristic: an admin-side cancellation is one issued by a user who can
	 * `manage_woocommerce` and isn't the order's customer. Anything else
	 * (frontend My-Account cancel, automated, guest) is treated as the
	 * customer's action.
	 */
	private function cancellation_actor_is_admin( WC_Order $order ): bool {
		$current_id = get_current_user_id();

		if ( $current_id > 0 && (int) $order->get_customer_id() === $current_id ) {
			return false;
		}

		return current_user_can( 'manage_woocommerce' );
	}

	/**
	 * @param mixed $amount
	 */
	private function format_amount( $amount ): string {
		if ( $amount instanceof \WC_Order_Item_Product ) {
			return '';
		}
		if ( ! is_numeric( $amount ) ) {
			return (string) $amount;
		}
		return self::clean_price( wc_price( (float) $amount ) );
	}

	private function withdraw_amount( int $withdraw_id ): string {
		if ( $withdraw_id <= 0 || ! function_exists( 'dokan' ) ) {
			return '';
		}

		try {
			$withdraw = dokan()->withdraw->get( $withdraw_id );
			if ( $withdraw && method_exists( $withdraw, 'get_amount' ) ) {
				return $this->format_amount( $withdraw->get_amount() );
			}
		} catch ( Throwable $e ) {
			// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
			error_log(
				sprintf(
					'[AppBridge withdraw amount for withdrawal #%d] %s',
					$withdraw_id,
					$e->getMessage()
				)
			);
		}

		return '';
	}
}
