<?php
/**
 * @fs_premium_only
 *
 * Pro — Advanced AJAX search & filter for the vehicle archive.
 *
 * Replaces the free GET search bar with a sticky filter sidebar (dual range
 * sliders for price & mileage, multi-select pill groups for make/year/
 * condition/body) + a live result bar, removable chips, skeleton loading, a
 * mobile drawer, and shareable/bookmarkable URL state. Results update over
 * admin-ajax without a page reload.
 *
 * Integration is hook-based (no mixed-strip code in the free build): this class
 * is loaded only when the /pro/ folder is present AND the license is active,
 * and it swaps the archive body via the `afi_archive_body` filter. The initial
 * page render is still filtered server-side (via pre_get_posts) so filtered
 * views are crawlable.
 *
 * @since 2.6.0
 * @package AutomotiveFeedImport
 */

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

class AFI_Advanced_Filter {

	/** @var AutomotiveFeedImport */
	private $afi;

	/** Results per page / per AJAX batch. */
	const PER_PAGE = 12;

	/**
	 * @param AutomotiveFeedImport $afi Main plugin instance.
	 */
	public function __construct($afi) {
		$this->afi = $afi;
		add_filter('afi_archive_body', array($this, 'render_archive_body'), 10, 2);
		add_action('pre_get_posts', array($this, 'filter_main_query'), 20);
		add_action('wp_enqueue_scripts', array($this, 'enqueue_assets'));
		add_action('wp_ajax_afi_filter_vehicles', array($this, 'ajax_filter'));
		add_action('wp_ajax_nopriv_afi_filter_vehicles', array($this, 'ajax_filter'));
	}

	/* ------------------------------------------------------------------ data */

	/**
	 * Read + normalize the filter params from a source array ($_GET or $_POST).
	 *
	 * @param array $src Raw source.
	 * @return array
	 */
	private function read_filters($src) {
		$arr = static function ($key) use ($src) {
			if (empty($src[$key]) || ! is_array($src[$key])) {
				return array();
			}
			return array_values(array_filter(array_map('sanitize_text_field', wp_unslash($src[$key])), static function ($v) {
				return '' !== $v;
			}));
		};

		return array(
			'kw'         => isset($src['afi_kw']) ? sanitize_text_field(wp_unslash($src['afi_kw'])) : '',
			'makes'      => $arr('afi_makes'),
			'years'      => $arr('afi_years'),
			'conditions' => $arr('afi_conditions'),
			'bodies'     => $arr('afi_bodies'),
			'pmin'       => isset($src['afi_pmin']) ? absint($src['afi_pmin']) : 0,
			'pmax'       => isset($src['afi_pmax']) ? absint($src['afi_pmax']) : 0,
			'mmin'       => isset($src['afi_mmin']) ? absint($src['afi_mmin']) : 0,
			'mmax'       => isset($src['afi_mmax']) ? absint($src['afi_mmax']) : 0,
			'sort'       => isset($src['afi_sort']) ? sanitize_key(wp_unslash($src['afi_sort'])) : '',
			'paged'      => isset($src['paged']) ? max(1, absint($src['paged'])) : 1,
		);
	}

	/**
	 * Whether any filter (not sort/paging) is active.
	 *
	 * @param array $f Normalized filters.
	 * @return bool
	 */
	private function has_active($f) {
		return ('' !== $f['kw'] || $f['makes'] || $f['years'] || $f['conditions'] || $f['bodies']
			|| $f['pmin'] || $f['pmax'] || $f['mmin'] || $f['mmax']);
	}

	/**
	 * Build WP_Query args (meta_query + ordering) for a filter set.
	 *
	 * @param array $f Normalized filters.
	 * @return array
	 */
	private function build_query_args($f) {
		$meta = array('relation' => 'AND');

		// Plain filter clauses (never used as sort targets) stay numeric-indexed.
		if ($f['makes']) {
			$meta[] = array('key' => 'manufacturer', 'value' => $f['makes'], 'compare' => 'IN');
		}
		if ($f['conditions']) {
			$meta[] = array('key' => 'designation', 'value' => $f['conditions'], 'compare' => 'IN');
		}
		if ($f['bodies']) {
			$meta[] = array(
				'relation' => 'OR',
				array('key' => 'body_type', 'value' => $f['bodies'], 'compare' => 'IN'),
				array('key' => 'type', 'value' => $f['bodies'], 'compare' => 'IN'),
			);
		}
		if ('' !== $f['kw']) {
			$meta[] = array(
				'relation' => 'OR',
				array('key' => 'manufacturer', 'value' => $f['kw'], 'compare' => 'LIKE'),
				array('key' => 'model', 'value' => $f['kw'], 'compare' => 'LIKE'),
				array('key' => 'stock_number', 'value' => $f['kw'], 'compare' => 'LIKE'),
			);
		}

		// Sort-target keys use a SINGLE named clause that doubles as both the
		// filter and the orderby reference — a second join on the same key makes
		// WP_Query drop rows.
		if ($f['years']) {
			$meta['year'] = array('key' => 'model_year', 'value' => $f['years'], 'compare' => 'IN', 'type' => 'UNSIGNED');
		}
		if ($f['pmin'] && $f['pmax']) {
			$meta['price'] = array('key' => 'special_web_price', 'value' => array($f['pmin'], $f['pmax']), 'compare' => 'BETWEEN', 'type' => 'DECIMAL(12,2)');
		} elseif ($f['pmin']) {
			$meta['price'] = array('key' => 'special_web_price', 'value' => $f['pmin'], 'compare' => '>=', 'type' => 'DECIMAL(12,2)');
		} elseif ($f['pmax']) {
			$meta['price'] = array('key' => 'special_web_price', 'value' => $f['pmax'], 'compare' => '<=', 'type' => 'DECIMAL(12,2)');
		}
		if ($f['mmin'] && $f['mmax']) {
			$meta['miles'] = array('key' => 'mileage', 'value' => array($f['mmin'], $f['mmax']), 'compare' => 'BETWEEN', 'type' => 'UNSIGNED');
		} elseif ($f['mmin']) {
			$meta['miles'] = array('key' => 'mileage', 'value' => $f['mmin'], 'compare' => '>=', 'type' => 'UNSIGNED');
		} elseif ($f['mmax']) {
			$meta['miles'] = array('key' => 'mileage', 'value' => $f['mmax'], 'compare' => '<=', 'type' => 'UNSIGNED');
		}

		// Ordering references the named clause, adding an EXISTS clause only when
		// that key isn't already being filtered.
		$orderby = array();
		switch ($f['sort']) {
			case 'price_asc':
			case 'price_desc':
				if (! isset($meta['price'])) {
					$meta['price'] = array('key' => 'special_web_price', 'compare' => 'EXISTS', 'type' => 'DECIMAL(12,2)');
				}
				$orderby = array('price' => 'price_asc' === $f['sort'] ? 'ASC' : 'DESC');
				break;
			case 'miles_asc':
				if (! isset($meta['miles'])) {
					$meta['miles'] = array('key' => 'mileage', 'compare' => 'EXISTS', 'type' => 'UNSIGNED');
				}
				$orderby = array('miles' => 'ASC');
				break;
			case 'year_desc':
				if (! isset($meta['year'])) {
					$meta['year'] = array('key' => 'model_year', 'compare' => 'EXISTS', 'type' => 'UNSIGNED');
				}
				$orderby = array('year' => 'DESC');
				break;
		}

		$args = array();
		if (count($meta) > 1) {
			$args['meta_query'] = $meta;
		}
		if ($orderby) {
			$args['orderby'] = $orderby;
		}

		return $args;
	}

	/**
	 * Distinct body styles (from body_type or type) across published vehicles.
	 *
	 * @return string[]
	 */
	private function get_body_options() {
		global $wpdb;
		$vals = $wpdb->get_col(
			"SELECT DISTINCT pm.meta_value
			 FROM {$wpdb->postmeta} pm
			 INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
			 WHERE p.post_type = 'vehicles' AND p.post_status = 'publish'
			   AND pm.meta_key IN ('body_type','type') AND pm.meta_value <> ''
			 ORDER BY pm.meta_value"
		);
		return array_values(array_unique($vals));
	}

	/**
	 * Min/max bounds (rounded to friendly steps) for the price & mileage sliders.
	 *
	 * @return array{price:array,miles:array}
	 */
	private function get_ranges() {
		global $wpdb;

		$price_max = (float) $wpdb->get_var(
			"SELECT MAX(CAST(pm.meta_value AS DECIMAL(12,2)))
			 FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
			 WHERE p.post_type='vehicles' AND p.post_status='publish' AND pm.meta_key='special_web_price'"
		);
		$miles_max = (float) $wpdb->get_var(
			"SELECT MAX(CAST(pm.meta_value AS UNSIGNED))
			 FROM {$wpdb->postmeta} pm INNER JOIN {$wpdb->posts} p ON p.ID = pm.post_id
			 WHERE p.post_type='vehicles' AND p.post_status='publish' AND pm.meta_key='mileage'"
		);

		$price_cap = $price_max > 0 ? (int) (ceil($price_max / 1000) * 1000) : 100000;
		$miles_cap = $miles_max > 0 ? (int) (ceil($miles_max / 5000) * 5000) : 150000;

		return array(
			'price' => array('min' => 0, 'max' => $price_cap, 'step' => 500),
			'miles' => array('min' => 0, 'max' => $miles_cap, 'step' => 1000),
		);
	}

	/**
	 * Localized "N vehicles match" label (with <b> around the count).
	 *
	 * @param int $count Found posts.
	 * @return string HTML.
	 */
	private function count_label($count) {
		$count = (int) $count;
		return sprintf(
			/* translators: %s: number of vehicles. */
			esc_html(_n('%s vehicle match', '%s vehicles match', $count, 'automotive-feed-import')),
			'<b>' . esc_html(number_format_i18n($count)) . '</b>'
		);
	}

	/* --------------------------------------------------------------- queries */

	/**
	 * Filter the main archive query server-side (initial render + SEO), and
	 * normalize the page size so it matches the AJAX batches.
	 *
	 * @param WP_Query $query Query.
	 * @return void
	 */
	public function filter_main_query($query) {
		if (is_admin() || ! $query->is_main_query()) {
			return;
		}
		if (! $query->is_post_type_archive('vehicles') && ! $query->is_tax()) {
			return;
		}

		$query->set('posts_per_page', self::PER_PAGE);

		$f = $this->read_filters($_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if (! $this->has_active($f) && '' === $f['sort']) {
			return;
		}

		$args = $this->build_query_args($f);
		if (! empty($args['meta_query'])) {
			$query->set('meta_query', $args['meta_query']);
		}
		foreach (array('meta_key', 'orderby', 'order') as $k) {
			if (isset($args[$k])) {
				$query->set($k, $args[$k]);
			}
		}
	}

	/**
	 * AJAX: return the filtered card grid + count for the current filter set.
	 *
	 * @return void
	 */
	public function ajax_filter() {
		check_ajax_referer('afi_filter', 'nonce');

		$f    = $this->read_filters($_POST); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- verified above.
		$args = array_merge(
			array(
				'post_type'           => 'vehicles',
				'post_status'         => 'publish',
				'posts_per_page'      => self::PER_PAGE,
				'paged'               => $f['paged'],
				'ignore_sticky_posts' => true,
			),
			$this->build_query_args($f)
		);

		$q = new WP_Query($args);

		ob_start();
		if ($q->have_posts()) {
			while ($q->have_posts()) {
				$q->the_post();
				echo $this->afi->get_vehicle_card_html(get_the_ID()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
			}
		}
		wp_reset_postdata();
		$cards = ob_get_clean();

		wp_send_json_success(array(
			'cards'     => $cards,
			'empty'     => '' === trim($cards) ? $this->empty_html() : '',
			'count'     => (int) $q->found_posts,
			'countHtml' => $this->count_label($q->found_posts),
			'maxPages'  => (int) $q->max_num_pages,
			'page'      => $f['paged'],
		));
	}

	/* ---------------------------------------------------------------- markup */

	/**
	 * Empty-state markup for a no-match result (AJAX + initial render).
	 *
	 * @return string
	 */
	private function empty_html() {
		ob_start();
		?>
		<div class="aii-empty">
			<div class="aii-empty__ic"><svg aria-hidden="true" focusable="false"><use href="#i-search"></use></svg></div>
			<div class="aii-empty__title"><?php esc_html_e('No vehicles match your filters', 'automotive-feed-import'); ?></div>
			<p class="aii-empty__text"><?php esc_html_e('Try widening your price range or removing a filter to see more vehicles.', 'automotive-feed-import'); ?></p>
		</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * One multi-select pill group.
	 *
	 * @param string   $label  Group label.
	 * @param string   $key    Filter key (makes/years/conditions/bodies).
	 * @param array    $values Option values.
	 * @param array    $active Active values.
	 * @param bool     $titlecase Whether to title-case the label display.
	 * @return string
	 */
	private function render_pill_group($label, $key, $values, $active, $titlecase = true) {
		if (empty($values)) {
			return '';
		}
		$active_lower = array_map('strtolower', $active);
		ob_start();
		?>
		<div class="aii-fgroup">
			<span class="aii-fgroup__label"><?php echo esc_html($label); ?></span>
			<div class="aii-fgroup__chips" data-multi data-key="<?php echo esc_attr($key); ?>">
				<?php foreach ($values as $val) :
					$is_active = in_array(strtolower((string) $val), $active_lower, true);
					$display   = $titlecase ? $this->afi->normalize_title_display($val) : $val;
					?>
					<button type="button" class="aii-opt<?php echo $is_active ? ' is-active' : ''; ?>" data-value="<?php echo esc_attr($val); ?>"><?php echo esc_html($display); ?></button>
				<?php endforeach; ?>
			</div>
		</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * One dual-range slider group.
	 *
	 * @param string $label Group label.
	 * @param string $key   'price' or 'miles'.
	 * @param array  $range min/max/step bounds.
	 * @param int    $lo    Current low value.
	 * @param int    $hi    Current high value.
	 * @return string
	 */
	private function render_range_group($label, $key, $range, $lo, $hi) {
		$lo = $lo ? $lo : $range['min'];
		$hi = $hi ? $hi : $range['max'];
		ob_start();
		?>
		<div class="aii-fgroup">
			<span class="aii-fgroup__label"><?php echo esc_html($label); ?></span>
			<div class="aii-range" data-range="<?php echo esc_attr($key); ?>" data-min="<?php echo esc_attr($range['min']); ?>" data-max="<?php echo esc_attr($range['max']); ?>" data-step="<?php echo esc_attr($range['step']); ?>" data-lo="<?php echo esc_attr($lo); ?>" data-hi="<?php echo esc_attr($hi); ?>"<?php echo 'price' === $key ? ' data-money="1"' : ''; ?>>
				<div class="aii-range__track">
					<div class="aii-range__rail"></div>
					<div class="aii-range__fill"></div>
					<input type="range" class="aii-range__lo" aria-label="<?php echo esc_attr(sprintf(/* translators: %s: filter label. */ __('Minimum %s', 'automotive-feed-import'), $label)); ?>">
					<input type="range" class="aii-range__hi" aria-label="<?php echo esc_attr(sprintf(/* translators: %s: filter label. */ __('Maximum %s', 'automotive-feed-import'), $label)); ?>">
				</div>
				<div class="aii-range__inputs">
					<input class="aii-input aii-range__loInput" inputmode="numeric" aria-label="<?php echo esc_attr(sprintf(__('Minimum %s', 'automotive-feed-import'), $label)); ?>">
					<span class="aii-range__sep" aria-hidden="true">&ndash;</span>
					<input class="aii-input aii-range__hiInput" inputmode="numeric" aria-label="<?php echo esc_attr(sprintf(__('Maximum %s', 'automotive-feed-import'), $label)); ?>">
				</div>
			</div>
		</div>
		<?php
		return ob_get_clean();
	}

	/**
	 * The filter group stack (keyword + ranges + pill groups). Shared by the
	 * sidebar and the mobile drawer (the drawer reuses this same DOM, moved in).
	 *
	 * @param array $f       Active filters.
	 * @param array $opts    makes/years/conditions options.
	 * @param array $bodies  body options.
	 * @param array $ranges  price/miles bounds.
	 * @return string
	 */
	private function render_fgroups($f, $opts, $bodies, $ranges) {
		$html  = '';

		// Keyword.
		$html .= '<div class="aii-fgroup"><span class="aii-fgroup__label">' . esc_html__('Keyword', 'automotive-feed-import') . '</span>'
			. '<div class="aii-input-icon"><svg class="aii-input-icon__ic" aria-hidden="true" focusable="false"><use href="#i-search"></use></svg>'
			. '<input class="aii-input" type="search" data-key="kw" value="' . esc_attr($f['kw']) . '" placeholder="' . esc_attr__('Make, model, or stock #', 'automotive-feed-import') . '"></div></div>';

		$html .= $this->render_range_group(__('Price', 'automotive-feed-import'), 'price', $ranges['price'], $f['pmin'], $f['pmax']);
		$html .= $this->render_range_group(__('Mileage', 'automotive-feed-import'), 'miles', $ranges['miles'], $f['mmin'], $f['mmax']);
		$html .= $this->render_pill_group(__('Make', 'automotive-feed-import'), 'makes', $opts['makes'], $f['makes']);
		$html .= $this->render_pill_group(__('Year', 'automotive-feed-import'), 'years', $opts['years'], $f['years'], false);
		$html .= $this->render_pill_group(__('Condition', 'automotive-feed-import'), 'conditions', $opts['conditions'], $f['conditions']);
		$html .= $this->render_pill_group(__('Body style', 'automotive-feed-import'), 'bodies', $bodies, $f['bodies']);

		return $html;
	}

	/**
	 * Sort <select> with the active option preserved.
	 *
	 * @param string $active Active sort key.
	 * @return string
	 */
	private function render_sort($active) {
		$options = array(
			''           => __('Relevance', 'automotive-feed-import'),
			'price_asc'  => __('Price: Low to High', 'automotive-feed-import'),
			'price_desc' => __('Price: High to Low', 'automotive-feed-import'),
			'year_desc'  => __('Newest', 'automotive-feed-import'),
			'miles_asc'  => __('Lowest mileage', 'automotive-feed-import'),
		);
		$html = '<select class="aii-select" id="afi-sort" data-key="sort">';
		foreach ($options as $val => $label) {
			$html .= '<option value="' . esc_attr($val) . '" ' . selected($active, $val, false) . '>' . esc_html($label) . '</option>';
		}
		$html .= '</select>';
		return $html;
	}

	/**
	 * Replace the archive body with the Pro filter layout.
	 *
	 * @param string|null          $html Existing (null = free default).
	 * @param AutomotiveFeedImport $afi  Plugin instance.
	 * @return string
	 */
	public function render_archive_body($html, $afi) {
		global $wp_query;

		$f      = $this->read_filters($_GET); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$opts   = $afi->get_filter_options();
		$bodies = $this->get_body_options();
		$ranges = $this->get_ranges();
		$count  = (int) $wp_query->found_posts;
		$max    = (int) $wp_query->max_num_pages;
		$groups = $this->render_fgroups($f, $opts, $bodies, $ranges);

		ob_start();
		?>
		<div class="aii-archive__layout" id="afi-archive">
			<aside class="aii-archive__sidebar">
				<div class="aii-filters">
					<div class="aii-filters__head">
						<span class="aii-filters__title"><?php esc_html_e('Filters', 'automotive-feed-import'); ?></span>
						<button type="button" class="aii-btn aii-btn--ghost aii-btn--sm" id="afi-reset"><?php esc_html_e('Reset', 'automotive-feed-import'); ?></button>
					</div>
					<div class="aii-filters__body" id="afi-fgroups"><?php echo $groups; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
				</div>
			</aside>

			<div class="aii-archive__results">
				<div class="aii-resultbar">
					<div class="aii-resultbar__count" id="afi-count" data-count="<?php echo esc_attr($count); ?>" aria-live="polite"><?php echo $this->count_label($count); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?></div>
					<div class="aii-resultbar__sort">
						<button type="button" class="aii-btn aii-btn--secondary aii-btn--sm aii-archive__drawerbtn" id="afi-open-drawer"><svg aria-hidden="true" focusable="false"><use href="#i-search"></use></svg> <?php esc_html_e('Filters', 'automotive-feed-import'); ?></button>
						<label class="aii-label" for="afi-sort" style="font-weight:var(--aii-fw-medium);color:var(--aii-color-text-muted);"><?php esc_html_e('Sort', 'automotive-feed-import'); ?></label>
						<?php echo $this->render_sort($f['sort']); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
					</div>
				</div>

				<div class="aii-applied" id="afi-chips" hidden></div>

				<div class="aii-grid" id="afi-results" aria-busy="false">
					<?php
					if (have_posts()) {
						while (have_posts()) {
							the_post();
							echo $afi->get_vehicle_card_html(get_the_ID()); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
						}
					} else {
						echo $this->empty_html(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
					}
					?>
				</div>

				<div class="aii-archive__more" style="text-align:center;margin-top:var(--aii-space-6);"<?php echo $max > 1 ? '' : ' hidden'; ?>>
					<button type="button" class="aii-btn aii-btn--secondary" id="afi-load-more" data-page="1" data-max="<?php echo esc_attr($max); ?>"><?php esc_html_e('Load more vehicles', 'automotive-feed-import'); ?></button>
				</div>
			</div>
		</div>

		<div class="aii-drawer" id="afi-drawer">
			<div class="aii-drawer__scrim" data-close></div>
			<div class="aii-drawer__panel" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e('Filter vehicles', 'automotive-feed-import'); ?>">
				<div class="aii-drawer__head">
					<span class="aii-drawer__title"><?php esc_html_e('Filters', 'automotive-feed-import'); ?></span>
					<button type="button" class="aii-drawer__close" data-close aria-label="<?php esc_attr_e('Close filters', 'automotive-feed-import'); ?>"><svg aria-hidden="true" focusable="false"><use href="#i-x"></use></svg></button>
				</div>
				<div class="aii-drawer__body" id="afi-drawer-body"></div>
				<div class="aii-drawer__foot">
					<button type="button" class="aii-btn aii-btn--secondary" id="afi-drawer-clear"><?php esc_html_e('Clear', 'automotive-feed-import'); ?></button>
					<button type="button" class="aii-btn aii-btn--primary" data-close><span class="aii-drawer__count" id="afi-drawer-count"></span></button>
				</div>
			</div>
		</div>
		<?php
		return ob_get_clean();
	}

	/* --------------------------------------------------------------- assets */

	/**
	 * Enqueue the Pro filter script on the vehicle archive.
	 *
	 * @return void
	 */
	public function enqueue_assets() {
		if (! (is_post_type_archive('vehicles') || is_tax())) {
			return;
		}
		$ver = defined('AFI_VERSION') ? AFI_VERSION : '2.6.0';
		wp_enqueue_script(
			'aii-advanced-filter',
			plugins_url('assets/aii-advanced-filter.js', __FILE__),
			array(),
			$ver,
			true
		);
		wp_localize_script('aii-advanced-filter', 'aiiFilter', array(
			'ajaxUrl' => admin_url('admin-ajax.php'),
			'nonce'   => wp_create_nonce('afi_filter'),
			'i18n'    => array(
				'clearAll' => __('Clear all', 'automotive-feed-import'),
				'active'   => __('Active:', 'automotive-feed-import'),
				'remove'   => __('Remove filter', 'automotive-feed-import'),
				/* translators: %s: result count. */
				'showN'    => __('Show %s results', 'automotive-feed-import'),
			),
		));
	}
}
