<?php
namespace Pay4All\Pro\Frontend;

use Pay4All\Pro\PostTypes;
use Pay4All\Pro\Domain\Category;
use Pay4All\Pro\Domain\Brand;
use Pay4All\Pro\License\LicenseManager;
use Pay4All\Form\Admin\Settings\Schema;

if ( ! defined( 'ABSPATH' ) ) { exit; }

/**
 * Front-end product filter widget rendered by the free plugin's shortcode
 * template via the `p4all_form_filter_widget` action hook.
 *
 * The widget outputs three flavours of dropdowns above the products step:
 *
 *   1. « Toutes les catégories »   — a flat picker of all leaf categories
 *      (top-level cats with no sub-cats).
 *   2. One dropdown per parent category that has children (e.g. « Boissons »
 *      → « Vin rouge / Vin blanc / … »).
 *   3. Same treatment for brands.
 *   4. A « Tri » dropdown (form order / price ascending / descending).
 *
 * Each product card gets `data-cats`, `data-brands` and `data-price`
 * attributes appended by the sibling `filter_product_card_attrs()` handler
 * so the client-side JS in pay4all-form's form.js can hide / show / re-sort
 * products without a server round-trip.
 *
 * Everything is guarded by:
 *   - `LicenseManager::is_feature_allowed()`  — expired trial stays inert
 *   - `$display['show_filters'] === '1'`      — merchant opted in
 *
 * The heavy CSS + interaction JS still ship with the free plugin (form.css
 * `.p4all-filters` block + form.js `.p4all-filter-select` handler), so the
 * only thing Pro contributes is the server-rendered HTML.
 */
final class FilterWidget {

	public static function register() : void {
		add_action( 'p4all_form_filter_widget',       [ self::class, 'maybe_render' ], 10, 3 );
		add_filter( 'p4all_form_product_card_attrs',  [ self::class, 'card_attrs'    ], 10, 3 );
	}

	/**
	 * @param int             $form_id
	 * @param array<\WP_Post> $products
	 * @param array           $display  Form display settings.
	 */
	public static function maybe_render( int $form_id, array $products, array $display ) : void {
		if ( ! LicenseManager::is_feature_allowed() ) { return; }
		if ( empty( $products ) ) { return; }
		$show_filters = isset( $display['show_filters'] ) && (string) $display['show_filters'] === '1';
		if ( ! $show_filters ) { return; }
		$show_counts = ! isset( $display['show_filter_counts'] ) || (string) $display['show_filter_counts'] !== '0';

		$filter_bg     = (string) Schema::get( 'color_step_active_bg',   '#2271b1' );
		$filter_text   = (string) Schema::get( 'color_step_active_text', '#ffffff' );
		$count_bg      = (string) Schema::get( 'color_field_bg',   '#f5f5f5' );
		$count_bg_text = (string) Schema::get( 'color_field_text', '#333333' );

		$cat_data   = self::filter_term_tree( $products, PostTypes::TAXONOMY );
		$brand_data = self::filter_term_tree( $products, PostTypes::TAXONOMY_BRAND );

		// Nothing to filter on ? Skip the entire bar (the merchant's forms
		// with a « show_filters » toggle enabled but no assigned categories
		// or brands would otherwise render an empty header strip).
		if ( empty( $cat_data['grouped'] ) && empty( $cat_data['leaves'] )
			&& empty( $brand_data['grouped'] ) && empty( $brand_data['leaves'] ) ) {
			return;
		}

		// `!important` on the count-tag palette because host themes (Hello,
		// Elementor, Astra) ship broad `button` / `span` rules that would
		// otherwise repaint everything to the outer bar's dark background.
		$count_style = 'background:' . $count_bg . ' !important;color:' . $count_bg_text . ' !important;';
		$count_tag = static function ( int $count ) use ( $show_counts, $count_style ) : string {
			if ( ! $show_counts || $count <= 0 ) { return ''; }
			return '<span class="p4all-cs-count" style="' . esc_attr( $count_style ) . '">'
				. esc_html( number_format_i18n( $count ) )
				. '</span>';
		};

		?>
		<div class="p4all-filters" role="region" aria-label="<?php esc_attr_e( 'Filtres produits', 'pay4all-pro' ); ?>"
		     style="background:<?php echo esc_attr( $filter_bg ); ?>;color:<?php echo esc_attr( $filter_text ); ?>;">

			<?php if ( ! empty( $cat_data['leaves'] ) ) :
				$dom_id    = 'p4all-filter-cats-flat-' . $form_id;
				$all_label = __( 'Toutes les catégories', 'pay4all-pro' );
				self::emit_dropdown( $dom_id, __( 'Catégories', 'pay4all-pro' ), 'cats', 0,
					$all_label, (int) $cat_data['leaves_all_count'], $cat_data['leaves'],
					$count_bg, $count_bg_text, $count_tag );
			endif;

			foreach ( $cat_data['grouped'] as $parent_id => $node ) :
				$parent_id = (int) $parent_id;
				$dom_id    = 'p4all-filter-cats-' . $parent_id . '-' . $form_id;
				self::emit_dropdown( $dom_id, (string) $node['label'], 'cats', $parent_id,
					(string) $node['all_label'], (int) $node['count'], (array) $node['children'],
					$count_bg, $count_bg_text, $count_tag );
			endforeach;

			if ( ! empty( $brand_data['leaves'] ) ) :
				$dom_id    = 'p4all-filter-brands-flat-' . $form_id;
				$all_label = __( 'Toutes les marques', 'pay4all-pro' );
				self::emit_dropdown( $dom_id, __( 'Marques', 'pay4all-pro' ), 'brands', 0,
					$all_label, (int) $brand_data['leaves_all_count'], $brand_data['leaves'],
					$count_bg, $count_bg_text, $count_tag );
			endif;

			foreach ( $brand_data['grouped'] as $parent_id => $node ) :
				$parent_id = (int) $parent_id;
				$dom_id    = 'p4all-filter-brands-' . $parent_id . '-' . $form_id;
				self::emit_dropdown( $dom_id, (string) $node['label'], 'brands', $parent_id,
					(string) $node['all_label'], (int) $node['count'], (array) $node['children'],
					$count_bg, $count_bg_text, $count_tag );
			endforeach;

			self::emit_sort( $form_id, $count_bg, $count_bg_text );
			?>
		</div>
		<?php
	}

	/**
	 * Renders one filter dropdown : hidden `<select class="p4all-filter-select">`
	 * that owns the state + change events, plus a custom-styled button + list
	 * driven by pay4all-form's form.js. The `data-tax` + `data-parent` attrs
	 * let the JS know which subset of product cards this picker filters.
	 *
	 * @param array<int,array{label:string,count:int}> $options
	 */
	private static function emit_dropdown(
		string $dom_id, string $label, string $tax, int $parent_id,
		string $all_label, int $all_count, array $options,
		string $count_bg, string $count_bg_text, callable $count_tag
	) : void { ?>
		<div class="p4all-filter">
			<label for="<?php echo esc_attr( $dom_id ); ?>-btn"><?php echo esc_html( $label ); ?></label>
			<div class="p4all-cs" data-value="" style="background:<?php echo esc_attr( $count_bg ); ?> !important;color:<?php echo esc_attr( $count_bg_text ); ?> !important;border-color:<?php echo esc_attr( $count_bg ); ?> !important;">
				<button id="<?php echo esc_attr( $dom_id ); ?>-btn" type="button" class="p4all-cs-btn"
				        aria-haspopup="listbox" aria-expanded="false">
					<span class="p4all-cs-current"><?php echo esc_html( $all_label ); ?></span>
					<span class="p4all-cs-arrow" aria-hidden="true">▾</span>
				</button>
				<ul class="p4all-cs-list" role="listbox" hidden>
					<li class="p4all-cs-option is-selected" role="option" data-value="">
						<span class="p4all-cs-option-label"><?php echo esc_html( $all_label ); ?></span>
						<?php echo $count_tag( $all_count ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $count_tag returns HTML already escaped. ?>
					</li>
					<?php foreach ( $options as $opt_value => $opt ) : ?>
						<li class="p4all-cs-option" role="option" data-value="<?php echo (int) $opt_value; ?>">
							<span class="p4all-cs-option-label"><?php echo esc_html( (string) $opt['label'] ); ?></span>
							<?php echo $count_tag( (int) $opt['count'] ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- $count_tag returns HTML already escaped. ?>
						</li>
					<?php endforeach; ?>
				</ul>
				<select id="<?php echo esc_attr( $dom_id ); ?>" class="p4all-filter-select" hidden
				        data-tax="<?php echo esc_attr( $tax ); ?>"
				        data-parent="<?php echo (int) $parent_id; ?>">
					<option value=""><?php echo esc_html( $all_label ); ?></option>
					<?php foreach ( $options as $opt_value => $opt ) : ?>
						<option value="<?php echo (int) $opt_value; ?>"><?php echo esc_html( (string) $opt['label'] ); ?></option>
					<?php endforeach; ?>
				</select>
			</div>
		</div>
	<?php }

	private static function emit_sort( int $form_id, string $count_bg, string $count_bg_text ) : void {
		$dom_id  = 'p4all-filter-sort-' . $form_id;
		$default = __( 'Ordre du formulaire', 'pay4all-pro' );
		$options = [
			''           => $default,
			'price_asc'  => __( 'Prix croissant',   'pay4all-pro' ),
			'price_desc' => __( 'Prix décroissant', 'pay4all-pro' ),
		];
		?>
		<div class="p4all-filter">
			<label for="<?php echo esc_attr( $dom_id ); ?>-btn"><?php esc_html_e( 'Tri', 'pay4all-pro' ); ?></label>
			<div class="p4all-cs" data-value="" style="background:<?php echo esc_attr( $count_bg ); ?> !important;color:<?php echo esc_attr( $count_bg_text ); ?> !important;border-color:<?php echo esc_attr( $count_bg ); ?> !important;">
				<button id="<?php echo esc_attr( $dom_id ); ?>-btn" type="button" class="p4all-cs-btn"
				        aria-haspopup="listbox" aria-expanded="false">
					<span class="p4all-cs-current"><?php echo esc_html( $default ); ?></span>
					<span class="p4all-cs-arrow" aria-hidden="true">▾</span>
				</button>
				<ul class="p4all-cs-list" role="listbox" hidden>
					<?php $first = true; foreach ( $options as $val => $lbl ) : ?>
						<li class="p4all-cs-option<?php echo $first ? ' is-selected' : ''; ?>" role="option" data-value="<?php echo esc_attr( (string) $val ); ?>">
							<span class="p4all-cs-option-label"><?php echo esc_html( (string) $lbl ); ?></span>
						</li>
					<?php $first = false; endforeach; ?>
				</ul>
				<select id="<?php echo esc_attr( $dom_id ); ?>" class="p4all-filter-sort" hidden>
					<?php foreach ( $options as $val => $lbl ) : ?>
						<option value="<?php echo esc_attr( (string) $val ); ?>"><?php echo esc_html( (string) $lbl ); ?></option>
					<?php endforeach; ?>
				</select>
			</div>
		</div>
	<?php }

	/**
	 * Injects `data-cats`, `data-brands` and `data-price` on every product
	 * card so the client-side filter logic in pay4all-form's form.js can
	 * partition + sort them without a server round-trip. Ancestor term ids
	 * are included so filtering by a top-level category still matches
	 * products that are tagged only with a leaf.
	 *
	 * @param array<string,string> $attrs
	 * @param int                  $pid
	 * @param int                  $form_id  (unused; kept for filter signature)
	 * @return array<string,string>
	 */
	public static function card_attrs( array $attrs, int $pid, int $form_id ) : array {
		if ( ! LicenseManager::is_feature_allowed() ) { return $attrs; }

		$cat_ids   = self::term_ids_with_ancestors( $pid, PostTypes::TAXONOMY );
		$brand_ids = self::term_ids_with_ancestors( $pid, PostTypes::TAXONOMY_BRAND );

		if ( ! empty( $cat_ids ) ) {
			$attrs['data-cats']   = implode( ',', $cat_ids );
		}
		if ( ! empty( $brand_ids ) ) {
			$attrs['data-brands'] = implode( ',', $brand_ids );
		}
		// Reuse Product::price() so a sale price wins over the regular one —
		// matches the backup behaviour before the FilterWidget was ripped out.
		if ( class_exists( '\\Pay4All\\Form\\Domain\\Product' ) ) {
			$price = (float) \Pay4All\Form\Domain\Product::price( $pid );
			// The JS only reads data-price when the sort dropdown is set to
			// price_asc / price_desc, so always emit it (empty products
			// sort to the top which is fine).
			$attrs['data-price'] = number_format( $price, 4, '.', '' );
		}
		return $attrs;
	}

	/**
	 * @return int[]
	 */
	private static function term_ids_with_ancestors( int $pid, string $taxonomy ) : array {
		$leaf_ids = wp_get_object_terms( $pid, $taxonomy, [ 'fields' => 'ids' ] );
		if ( is_wp_error( $leaf_ids ) || empty( $leaf_ids ) ) { return []; }
		$all = [];
		foreach ( (array) $leaf_ids as $tid ) {
			$tid = (int) $tid;
			$all[ $tid ] = true;
			foreach ( (array) get_ancestors( $tid, $taxonomy, 'taxonomy' ) as $a ) {
				$all[ (int) $a ] = true;
			}
		}
		return array_keys( $all );
	}

	/**
	 * Builds the two-level tree of terms the widget renders. Ported from the
	 * pre-2.0.0 free plugin's Frontend\Shortcode::filter_term_tree() with the
	 * Category / Brand references updated to the Pro namespace.
	 *
	 * @param \WP_Post[] $products
	 * @return array{
	 *   grouped:array<int,array{label:string,all_label:string,count:int,children:array<int,array{label:string,count:int}>}>,
	 *   leaves:array<int,array{label:string,count:int}>,
	 *   leaves_all_count:int
	 * }
	 */
	private static function filter_term_tree( array $products, string $taxonomy ) : array {
		$empty = [ 'grouped' => [], 'leaves' => [], 'leaves_all_count' => 0 ];
		if ( empty( $products ) ) { return $empty; }

		$pids  = array_map( static function ( $p ) { return (int) $p->ID; }, $products );
		$terms = wp_get_object_terms( $pids, $taxonomy, [ 'orderby' => 'name' ] );
		if ( is_wp_error( $terms ) || empty( $terms ) ) { return $empty; }

		$name_for = static function ( int $term_id ) use ( $taxonomy ) : string {
			if ( $taxonomy === PostTypes::TAXONOMY )       { return Category::display_name( $term_id ); }
			if ( $taxonomy === PostTypes::TAXONOMY_BRAND ) { return Brand::display_name( $term_id ); }
			$t = get_term( $term_id );
			return ( $t && ! is_wp_error( $t ) ) ? (string) $t->name : '';
		};
		$all_label_for = static function ( int $term_id ) use ( $taxonomy ) : string {
			if ( $taxonomy === PostTypes::TAXONOMY )       { return Category::display_all_label( $term_id ); }
			if ( $taxonomy === PostTypes::TAXONOMY_BRAND ) { return Brand::display_all_label( $term_id ); }
			return __( 'Tous', 'pay4all-pro' );
		};
		$order_for = static function ( int $term_id ) use ( $taxonomy ) : int {
			if ( $taxonomy === PostTypes::TAXONOMY )       { return Category::order( $term_id ); }
			if ( $taxonomy === PostTypes::TAXONOMY_BRAND ) { return Brand::order( $term_id ); }
			return 0;
		};

		// Count products per term (including ancestors so a product tagged
		// only with a leaf still counts towards its parent).
		$counts = [];
		foreach ( $products as $p ) {
			$pid  = (int) $p->ID;
			$tids = wp_get_object_terms( $pid, $taxonomy, [ 'fields' => 'ids' ] );
			if ( is_wp_error( $tids ) || empty( $tids ) ) { continue; }
			$expanded = [];
			foreach ( (array) $tids as $tid ) {
				$tid = (int) $tid;
				$expanded[ $tid ] = true;
				foreach ( (array) get_ancestors( $tid, $taxonomy, 'taxonomy' ) as $a ) {
					$expanded[ (int) $a ] = true;
				}
			}
			foreach ( array_keys( $expanded ) as $tid ) {
				$counts[ (int) $tid ] = ( $counts[ (int) $tid ] ?? 0 ) + 1;
			}
		}

		// Index every attached term (and any ancestor we walk to).
		$by_id = [];
		foreach ( $terms as $t ) {
			$by_id[ (int) $t->term_id ] = $t;
		}

		// Group children under their top-level ancestor.
		$tree = [];
		foreach ( $by_id as $term_id => $term ) {
			$top_id = (int) $term_id;
			$cursor = $term;
			while ( (int) $cursor->parent > 0 ) {
				$parent_term = $by_id[ (int) $cursor->parent ] ?? get_term( (int) $cursor->parent, $taxonomy );
				if ( ! $parent_term || is_wp_error( $parent_term ) ) { break; }
				$by_id[ (int) $parent_term->term_id ] = $parent_term;
				$cursor = $parent_term;
				$top_id = (int) $parent_term->term_id;
			}
			if ( ! isset( $tree[ $top_id ] ) ) {
				$tree[ $top_id ] = [
					'label'     => $name_for( $top_id ),
					'all_label' => $all_label_for( $top_id ),
					'count'     => (int) ( $counts[ $top_id ] ?? 0 ),
					'children'  => [],
				];
			}
			if ( $term_id !== $top_id ) {
				$tree[ $top_id ]['children'][ (int) $term_id ] = [
					'label' => $name_for( (int) $term_id ),
					'count' => (int) ( $counts[ (int) $term_id ] ?? 0 ),
				];
			}
		}

		// Split parents-with-children from leaf top-levels.
		$grouped          = [];
		$leaves           = [];
		$leaves_all_count = 0;
		foreach ( $tree as $top_id => $node ) {
			if ( ! empty( $node['children'] ) ) {
				$grouped[ $top_id ] = $node;
			} else {
				$leaves[ $top_id ] = [ 'label' => $node['label'], 'count' => $node['count'] ];
				$leaves_all_count  += $node['count'];
			}
		}

		// Sort by merchant-defined order (drag-and-drop meta) first, then
		// alphabetical fallback for terms with no explicit order.
		$mk_cmp = static function ( callable $order_for ) {
			return static function ( $a, $b ) use ( $order_for ) {
				$oa = $order_for( (int) ( $a['_id'] ?? 0 ) );
				$ob = $order_for( (int) ( $b['_id'] ?? 0 ) );
				if ( $oa !== $ob ) { return $oa - $ob; }
				return strcasecmp( (string) $a['label'], (string) $b['label'] );
			};
		};
		foreach ( $grouped as $tid => &$node ) {
			$node['_id'] = $tid;
			foreach ( $node['children'] as $cid => &$child ) {
				$child['_id'] = $cid;
			}
			unset( $child );
		}
		unset( $node );
		foreach ( $leaves as $tid => &$leaf ) {
			$leaf['_id'] = $tid;
		}
		unset( $leaf );

		$cmp = $mk_cmp( $order_for );
		uasort( $grouped, $cmp );
		foreach ( $grouped as &$node ) {
			uasort( $node['children'], $cmp );
			unset( $node['_id'] );
			foreach ( $node['children'] as &$child ) { unset( $child['_id'] ); }
			unset( $child );
		}
		unset( $node );
		uasort( $leaves, $cmp );
		foreach ( $leaves as &$leaf ) { unset( $leaf['_id'] ); }
		unset( $leaf );

		return [
			'grouped'          => $grouped,
			'leaves'           => $leaves,
			'leaves_all_count' => (int) $leaves_all_count,
		];
	}
}
