<?php

namespace WeDevs\AppBridge\Integrations\WooCommerce\Notifications;

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

/**
 * Bridges WooCommerce + WordPress core hooks into the customer-app event
 * catalog.
 *
 * Each callback resolves the matching `customer.*` event from
 * {@see CustomerEventCatalog}, fills the title/body templates with
 * context, and ships it through {@see EventDispatcher} — which honors
 * the per-event toggle / title / body the admin set in
 * Push Notifications → Events.
 *
 * Catalog events without a WC-native source (`refund_requested`,
 * `refund_approved`, `refund_rejected`, `support_ticket_closed`) are
 * intentionally not wired — they remain in the catalog so the admin UI
 * still surfaces them, but no internal dispatch fires them.
 */
class CustomerEventDispatcher implements Hookable {

	/**
	 * WC order status (after change) → customer event ID.
	 *
	 * `pending` is the initial state and is covered by woocommerce_new_order,
	 * so it's intentionally absent here to avoid double-firing.
	 *
	 * @var array<string, string>
	 */
	private const STATUS_EVENT_MAP = [
		'processing' => 'order_processing',
		'completed'  => 'order_delivered',
		'cancelled'  => 'order_cancelled',
		'failed'     => 'order_failed',
		'on-hold'    => 'order_on_hold',
	];

	/**
	 * Pre-failure statuses that imply a payment attempt was in flight. When
	 * an order falls from one of these into `failed`, fire `payment_failed`
	 * alongside `order_failed` so the customer is nudged to update their
	 * payment method (the events route to different screens).
	 *
	 * @var string[]
	 */
	private const PAYMENT_PENDING_STATUSES = [ 'pending', 'on-hold', 'checkout-draft' ];

	private EventDispatcher $dispatcher;

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

	public function register_hooks(): void {
		// ── Orders ─────────────────────────────────────────────────────
		add_action( 'woocommerce_new_order', [ $this, 'on_new_order' ], 10, 2 );
		add_action( 'woocommerce_order_status_changed', [ $this, 'on_status_changed' ], 10, 4 );
		add_action( 'woocommerce_payment_complete', [ $this, 'on_payment_complete' ], 10 );
		add_action( 'woocommerce_order_refunded', [ $this, 'on_order_refunded' ], 10, 2 );

		// ── Support (customer note on order) ───────────────────────────
		add_action( 'woocommerce_new_customer_note', [ $this, 'on_customer_note_added' ], 10, 1 );

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

		// ── Account ────────────────────────────────────────────────────
		add_action( 'woocommerce_created_customer', [ $this, 'on_customer_created' ], 10, 1 );
		add_action( 'profile_update', [ $this, 'on_profile_update' ], 10, 2 );
		add_action( 'password_reset', [ $this, 'on_password_reset_completed' ], 10, 2 );
		add_action( 'retrieve_password', [ $this, 'on_password_reset_requested' ], 10, 1 );
	}

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

	/**
	 * @param int            $order_id
	 * @param \WC_Order|null $order
	 */
	public function on_new_order( $order_id, $order = null ): void {
		$order = $this->resolve_order( $order, $order_id );
		if ( ! $order ) {
			return;
		}

		$this->dispatch_for_order( 'order_placed', $order );
	}

	/**
	 * @param int       $order_id
	 * @param string    $old_status
	 * @param string    $new_status
	 * @param \WC_Order $order
	 */
	public function on_status_changed( $order_id, $old_status, $new_status, $order ): void {
		$order = $this->resolve_order( $order, $order_id );
		if ( ! $order ) {
			return;
		}

		// Payment confirmed by gateway — treat the first move to `processing` as
		// the customer-facing "Order Confirmed" milestone.
		if ( 'pending' === $old_status && 'processing' === $new_status ) {
			$this->dispatch_for_order( 'order_confirmed', $order );
		}

		// Failure originating from a payment-pending state = payment failed.
		if ( 'failed' === $new_status && in_array( $old_status, self::PAYMENT_PENDING_STATUSES, true ) ) {
			$this->dispatch_for_order( 'payment_failed', $order );
		}

		$event_id = self::STATUS_EVENT_MAP[ $new_status ] ?? null;
		if ( ! $event_id ) {
			return;
		}

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

	public function on_payment_complete( $order_id ): void {
		$order = $this->resolve_order( null, $order_id );
		if ( ! $order ) {
			return;
		}

		$this->dispatch_for_order( 'payment_success', $order );
	}

	/**
	 * @param int $order_id
	 * @param int $refund_id
	 */
	public function on_order_refunded( $order_id, $refund_id ): void {
		$order = $this->resolve_order( null, $order_id );
		if ( ! $order ) {
			return;
		}

		$refund = wc_get_order( $refund_id );
		$amount = $refund instanceof \WC_Order_Refund
			? self::clean_price( wc_price( $refund->get_amount(), [ 'currency' => $order->get_currency() ] ) )
			: self::clean_price( $order->get_formatted_order_total() );

		$this->dispatch_for_order( 'refund_completed', $order, [ 'amount' => $amount ] );
	}

	// ── Support (customer note) ────────────────────────────────────────────

	/**
	 * `woocommerce_new_customer_note` fires when a shop admin adds a customer-
	 * visible note to an order. This is the closest WC-native equivalent to
	 * a support reply addressed to the customer.
	 *
	 * @param array{order_id?:int, customer_note?:string} $args
	 */
	public function on_customer_note_added( $args ): void {
		if ( ! is_array( $args ) || empty( $args['order_id'] ) ) {
			return;
		}

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

		$this->dispatch_for_order( 'support_message_received', $order );
	}

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

	/**
	 * Reply on a product review — push to the original review author when
	 * the reply isn't theirs.
	 *
	 * @param int        $comment_id
	 * @param int|string $approved
	 * @param array<string, mixed> $commentdata
	 */
	public function on_review_reply( $comment_id, $approved, $commentdata ): void {
		$comment = get_comment( $comment_id );
		if ( ! $comment instanceof WP_Comment ) {
			return;
		}

		$parent_id = (int) $comment->comment_parent;
		if ( $parent_id <= 0 ) {
			return;
		}

		$parent = get_comment( $parent_id );
		if ( ! $parent instanceof WP_Comment ) {
			return;
		}

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

		$customer_id = (int) $parent->user_id;
		if ( $customer_id <= 0 ) {
			return;
		}

		// Don't notify the customer about their own follow-up reply.
		if ( (int) $comment->user_id === $customer_id ) {
			return;
		}

		$this->dispatch_for_user(
			'review_reply_received',
			$customer_id,
			[
				'product_name'   => (string) $post->post_title,
				'action_type_id' => (string) $post->ID,
			]
		);
	}

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

	public function on_customer_created( $user_id ): void {
		$user_id = (int) $user_id;
		if ( $user_id <= 0 ) {
			return;
		}

		$this->dispatch_for_user( 'account_registered', $user_id );
	}

	/**
	 * `profile_update` is the canonical "user data changed" hook. Narrow to
	 * password-actually-changed for customer-role users.
	 *
	 * @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 ( ! $this->is_customer( $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_user( 'account_password_changed', $user_id );
	}

	/**
	 * `password_reset` fires after the password is set via the reset flow.
	 * `profile_update` does NOT fire from `reset_password()`, so we hook this
	 * separately to cover the lost-password path.
	 *
	 * @param WP_User $user
	 * @param string  $new_pass
	 */
	public function on_password_reset_completed( $user, $new_pass ): void {
		if ( ! $user instanceof WP_User ) {
			return;
		}

		if ( ! $this->is_customer( (int) $user->ID ) ) {
			return;
		}

		$this->dispatch_for_user( 'account_password_changed', (int) $user->ID );
	}

	/**
	 * Lost-password form submitted — fire the "reset requested" event.
	 *
	 * @param string $user_login
	 */
	public function on_password_reset_requested( $user_login ): void {
		if ( ! is_string( $user_login ) || '' === $user_login ) {
			return;
		}

		$user = get_user_by( 'login', $user_login );
		if ( ! $user instanceof WP_User ) {
			$user = get_user_by( 'email', $user_login );
		}

		if ( ! $user instanceof WP_User || ! $this->is_customer( (int) $user->ID ) ) {
			return;
		}

		$this->dispatch_for_user( 'account_password_reset', (int) $user->ID );
	}

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

	/**
	 * Build order context + audience and dispatch.
	 *
	 * @param array<string, mixed> $extra Extra context keys merged with order defaults.
	 */
	private function dispatch_for_order( string $event_id, WC_Order $order, array $extra = [] ): void {
		$customer_id = (int) $order->get_customer_id();

		// Guest checkouts have no WP user — nothing to push to until we wire
		// email/device-token routing. Skip silently.
		if ( $customer_id <= 0 ) {
			return;
		}

		$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->ship( $event_id, $customer_id, $context, $order->get_id() );
	}

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

		$this->ship( $event_id, $customer_id, $context, 0 );
	}

	private function ship( string $event_id, int $customer_id, array $context, int $order_id ): void {
		$audience = [
			'external_user_ids' => [ (string) $customer_id ],
		];

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

	/**
	 * @param mixed     $maybe_order
	 * @param int|mixed $order_id
	 */
	private function resolve_order( $maybe_order, $order_id ): ?WC_Order {
		if ( $maybe_order instanceof WC_Order ) {
			return $maybe_order;
		}

		$order = wc_get_order( (int) $order_id );

		return $order instanceof WC_Order ? $order : null;
	}

	/**
	 * Treat as a customer when the user has the `customer` role.
	 */
	private function is_customer( int $user_id ): bool {
		$user = get_userdata( $user_id );
		return $user instanceof WP_User && in_array( 'customer', (array) $user->roles, true );
	}

	/**
	 * 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' );
	}
}
