<?php // phpcs:ignore WordPress.Files.FileName.NotHyphenatedLowercase, WordPress.Files.FileName.InvalidClassFileName
/**
 * Wholesale notice lifecycle manager.
 *
 * @package Autoship\Modules\WholesalePricing\Services
 * @since   2.13.0
 */

namespace Autoship\Modules\WholesalePricing\Services;

use Autoship\Domain\WholesalePricing\WholesaleValidationResult;

/**
 * Owns the entire WC notice lifecycle for wholesale pricing.
 *
 * Builds notice messages from validation results, collects pending notices
 * during per-item processing, and flushes them once per cart recalculation.
 *
 * @package Autoship\Modules\WholesalePricing\Services
 * @since   2.13.0
 */
class WholesaleNoticeManager {

	/**
	 * Whether render_notice_block() has already fired this request.
	 *
	 * @var bool
	 */
	private static bool $rendered_this_request = false;


	/**
	 * Pending notices collected during cart item processing.
	 *
	 * @var array<int, array{type: string, message: string}>
	 */
	private array $pending_notices = array();

	/**
	 * Current notices to render, populated by flush().
	 *
	 * @var array<int, array{type: string, message: string}>
	 */
	private array $current_notices = array();

	/**
	 * The pricing service for price adjustments in notice messages.
	 *
	 * @var WholesalePricingServiceInterface
	 */
	private WholesalePricingServiceInterface $pricing;

	/**
	 * Constructor.
	 *
	 * @param WholesalePricingServiceInterface $pricing The pricing service.
	 */
	public function __construct( WholesalePricingServiceInterface $pricing ) {
		$this->pricing = $pricing;
	}

	/**
	 * Build a notice from a validation result.
	 *
	 * @param WholesaleValidationResult $result              The validation result.
	 * @param int                       $product_id          The product ID.
	 * @param array                     $user_wholesale_role The user's wholesale role.
	 *
	 * @return array { @type string $type, @type string $message } or empty array.
	 */
	public function build_notice( WholesaleValidationResult $result, int $product_id, array $user_wholesale_role ): array {
		if ( $result->passed() ) {
			return array();
		}

		global $wc_wholesale_prices_premium;

		$product             = wc_get_product( $product_id );
		$active_currency     = get_woocommerce_currency();
		$notice              = array();
		$failure_type        = $result->get_failure_type();
		$requirements        = $result->get_requirements();
		$minimum_order_items = $requirements->get_minimum_order_items();
		$minimum_order_price = $requirements->get_minimum_order_price();
		$item_total          = $result->get_actual_qty();
		$items_total_price   = $result->get_actual_total();

		if ( 'min_qty' === $failure_type ) {
			$wholesale_price           = $this->pricing->adjust_price( $product_id );
			$formatted_wholesale_price = $wc_wholesale_prices_premium->wwpp_wholesale_prices->get_product_shop_price_with_taxing_applied( $product, $wholesale_price, array( 'currency' => $active_currency ), $user_wholesale_role );

			$message = sprintf(
				// translators: %1$s and %5$s are HTML wrapper tags, %2$s is the minimum order quantity, %3$s is the product title, %4$s is the wholesale price.
				__( '%1$sYou did not meet the minimum order quantity %2$s of the product %3$s to activate wholesale pricing %4$s. Please increase quantities to the cart to activate adjusted pricing.%5$s', 'autoship' ),
				'<span class="wwpp-notice">',
				'<b>(' . (int) $minimum_order_items . ' items)</b>',
				'<b>' . $product->get_title() . '</b>',
				'<b>(' . $formatted_wholesale_price . ')</b>',
				'</span>'
			);

			$notice = apply_filters(
				'autoship_filter_wholesale_price_min_quantity_requirement_failure_notice',
				array(
					'type'    => 'notice',
					'message' => $message,
				),
				$item_total,
				$minimum_order_items,
				$items_total_price,
				$minimum_order_price,
				array()
			);
		} elseif ( 'min_price' === $failure_type ) {
			$message = sprintf(
				// translators: %1$s and %4$s are HTML tags, %2$s is the minimum order subtotal, %3$s is the cart subtotal.
				__( '%1$sYou have not met the minimum order subtotal of %2$s to activate adjusted pricing. Retail prices will be shown below until the minimum order threshold is met. The cart subtotal calculated with wholesale prices is %3$s%4$s', 'woocommerce-wholesale-prices-premium' ),
				'<span class="wwpp-notice">',
				'<b>(' . wc_price( $minimum_order_price ) . ')</b>',
				'<b>(' . wc_price( $items_total_price ) . ')</b>',
				'</span>'
			);

			$notice = apply_filters(
				'autoship_filter_wholesale_price_min_subtotel_requirement_failure_notice',
				array(
					'type'    => 'notice',
					'message' => $message,
				),
				$item_total,
				$minimum_order_items,
				$items_total_price,
				$minimum_order_price,
				array()
			);
		} elseif ( 'min_qty_and_price' === $failure_type ) {
			$logic = $requirements->get_conditional_logic();

			if ( strcasecmp( $logic, 'and' ) === 0 ) {
				// translators: %1$s and %5$s are HTML tags, %2$s is the minimum order quantity, %3$s is the minimum order subtotal, %4$s is the cart subtotal.
				$format = __( ' %1$sYou have not met the minimum order quantity of %2$s and minimum order subtotal of %3$s to activate adjusted pricing. Retail prices will be shown below until the minimum order threshold is met. The cart subtotal calculated with wholesale prices is %4$s%5$s', 'woocommerce-wholesale-prices-premium' );
			} else {
				// translators: %1$s and %5$s are HTML tags, %2$s is the minimum order quantity, %3$s is the minimum order subtotal, %4$s is the cart subtotal.
				$format = __( ' %1$sYou have not met the minimum order quantity of %2$s or minimum order subtotal of %3$s to activate adjusted pricing. Retail prices will be shown below until the minimum order threshold is met. The cart subtotal calculated with wholesale prices is %4$s%5$s', 'woocommerce-wholesale-prices-premium' );
			}

			$message = sprintf(
				$format,
				'<span class="wwpp-notice">',
				'<b>(' . (int) $minimum_order_items . ')</b>',
				'<b>(' . wc_price( $minimum_order_price ) . ')</b>',
				'<b>(' . wc_price( $items_total_price ) . ')</b>',
				'</span>'
			);

			$notice = apply_filters(
				'autoship_filter_wholesale_price_min_quantity_and_min_subtotel_requirement_failure_notice',
				array(
					'type'    => 'notice',
					'message' => $message,
				),
				$item_total,
				$minimum_order_items,
				$items_total_price,
				$minimum_order_price,
				array()
			);
		}

		return $notice;
	}

	/**
	 * Collect a notice for deferred display.
	 *
	 * @param int   $product_id The product ID.
	 * @param array $notice     The notice array with 'type' and 'message'.
	 *
	 * @return void
	 */
	public function collect( int $product_id, array $notice ): void {
		if ( ! empty( $notice ) ) {
			$this->pending_notices[ $product_id ] = $notice;
		}
	}

	/**
	 * Dismiss a pending notice for a product.
	 *
	 * Called when validation passes for a product that may have had a
	 * stale pending notice from an earlier filter invocation (e.g. the
	 * woocommerce_cart_loaded_from_session run which uses pre-update
	 * quantities).
	 *
	 * @param int $product_id The product ID.
	 *
	 * @return void
	 */
	public function dismiss( int $product_id ): void {
		unset( $this->pending_notices[ $product_id ] );
	}

	/**
	 * Flush all collected notices.
	 *
	 * Called once per cart recalculation via woocommerce_after_calculate_totals.
	 * Moves pending notices to current_notices for rendering by
	 * render_notice_block().
	 *
	 * Does NOT scrub the WC session here — the legacy
	 * flush_wholesale_notices() in src/wholesale-pricing.php manages
	 * session notices via wc_add_notice().  Scrubbing here would remove
	 * the notice the legacy flush just added, preventing it from ever
	 * reaching woocommerce_output_all_notices() on classic pages.
	 *
	 * Session cleanup is handled by the early_scrub_and_recalculate()
	 * hook on woocommerce_before_cart / woocommerce_before_checkout_form.
	 *
	 * @return void
	 */
	public function flush(): void {
		$this->current_notices = $this->pending_notices;
		$this->pending_notices = array();
	}

	/**
	 * Get the current notices to render.
	 *
	 * @return array<int, array{type: string, message: string}>
	 */
	public function get_current_notices(): array {
		return $this->current_notices;
	}

	/**
	 * Clear current notices.
	 *
	 * Called by early_scrub_and_recalculate() after forcing calculate_totals().
	 * The session notice (from the legacy flush) will be rendered by
	 * woocommerce_output_all_notices().  Clearing current_notices prevents
	 * prepend_notices_to_block() and render_notice_block() from injecting a
	 * duplicate copy.
	 *
	 * @return void
	 */
	public function clear_current(): void {
		$this->current_notices = array();
	}

	/**
	 * Filter the store-notices block output.
	 *
	 * Hooked to render_block_woocommerce/store-notices.  Strips any
	 * wholesale notices that leaked into the block via the WC session,
	 * then appends our own from $current_notices.  A static flag
	 * ensures we render at most once per request.
	 *
	 * On classic shortcode pages the session-based path
	 * (woocommerce_output_all_notices) handles display, so this
	 * method only strips — it does not inject.
	 *
	 * @param string $block_content The rendered block HTML.
	 *
	 * @return string Modified block HTML.
	 */
	public function render_notice_block( string $block_content ): string {
		// Always strip session-sourced wholesale notices to prevent duplication.
		$block_content = $this->strip_wholesale_notices_from_html( $block_content );

		if ( self::$rendered_this_request || empty( $this->current_notices ) ) {
			return $block_content;
		}

		self::$rendered_this_request = true;

		$notice_html = '';
		foreach ( $this->current_notices as $notice ) {
			if ( ! empty( $notice['message'] ) ) {
				$notice_html .= '<div class="woocommerce-info">' . wp_kses_post( $notice['message'] ) . '</div>';
			}
		}

		if ( '' === $notice_html ) {
			return $block_content;
		}

		$wrapped = '<div class="woocommerce-notices-wrapper">' . $notice_html . '</div>';

		if ( '' !== trim( $block_content ) ) {
			return $block_content . $wrapped;
		}

		return '<div class="wc-block-store-notices woocommerce">' . $wrapped . '</div>';
	}

	/**
	 * Prepend wholesale notices into a block's rendered HTML.
	 *
	 * Hooked to render_block_woocommerce/cart and render_block_woocommerce/checkout.
	 * On block-based pages the woocommerce/store-notices block may not exist,
	 * so render_notice_block() never fires.  This method injects the notice
	 * HTML directly before the cart/checkout block content.
	 *
	 * Skipped after the first render (rendered_this_request flag) or when
	 * current_notices was cleared by early_scrub_and_recalculate() (which
	 * means the classic session path handles rendering).
	 *
	 * @param string $block_content The rendered block HTML.
	 *
	 * @return string Modified block HTML.
	 */
	public function prepend_notices_to_block( string $block_content ): string {
		// Strip any wholesale notices already in the block HTML (e.g. from
		// session notices rendered by wc_print_notices inside the block)
		// to prevent duplication with the notices we prepend below.
		$block_content = $this->strip_wholesale_notices_from_html( $block_content );

		if ( self::$rendered_this_request || empty( $this->current_notices ) ) {
			return $block_content;
		}

		self::$rendered_this_request = true;

		$notice_html = '';
		foreach ( $this->current_notices as $notice ) {
			if ( ! empty( $notice['message'] ) ) {
				$notice_html .= '<div class="woocommerce-info">' . wp_kses_post( $notice['message'] ) . '</div>';
			}
		}

		if ( '' === $notice_html ) {
			return $block_content;
		}

		return '<div class="woocommerce-notices-wrapper">' . $notice_html . '</div>' . $block_content;
	}

	/**
	 * Strip wholesale notice divs from HTML.
	 *
	 * Removes any <div class="woocommerce-info"> that contains the
	 * wwpp-notice CSS class marker.  The WC notice template never
	 * nests <div> inside a notice div, so matching to the next
	 * </div> is safe.
	 *
	 * @param string $html The HTML to filter.
	 *
	 * @return string Filtered HTML.
	 */
	private function strip_wholesale_notices_from_html( string $html ): string {
		if ( '' === $html || false === strpos( $html, 'wwpp-notice' ) ) {
			return $html;
		}

		$result = preg_replace(
			'/<div\b[^>]*\bwoocommerce-info\b[^>]*>(?:(?!<\/div>).)*?wwpp-notice(?:(?!<\/div>).)*?<\/div>/s',
			'',
			$html
		);

		return ( null !== $result ) ? $result : $html;
	}

	/**
	 * Remove any existing wholesale pricing notices from WC's notice store.
	 *
	 * Wholesale notices are identified by the 'wwpp-notice' CSS class marker
	 * present in all notice messages built by build_notice().
	 *
	 * @return void
	 */
	public function remove_stale_wholesale_notices(): void {
		if ( ! function_exists( 'wc_get_notices' ) || ! function_exists( 'wc_set_notices' ) ) {
			return;
		}

		$all_notices = wc_get_notices();
		$changed     = false;

		foreach ( $all_notices as $type => $notices ) {
			$filtered = array();
			foreach ( $notices as $notice ) {
				$message = is_array( $notice ) ? ( $notice['notice'] ?? '' ) : $notice;
				if ( false !== strpos( $message, 'wwpp-notice' ) ) {
					$changed = true;
					continue;
				}
				$filtered[] = $notice;
			}
			$all_notices[ $type ] = $filtered;
		}

		if ( $changed ) {
			wc_set_notices( $all_notices );
		}
	}

	/**
	 * Check if there are pending notices.
	 *
	 * @return bool
	 */
	public function has_pending(): bool {
		return ! empty( $this->pending_notices );
	}
}
