<?php
/**
 * Archive filter handler.
 *
 * Reads the filter query string on the vehicle archive and restricts the
 * main query to listings whose specs row matches. Per ADR-03, this routes
 * through VehicleQuery so the archive page and the REST API return the
 * same result set for the same inputs.
 *
 * @package ThemeREX\AutoDealer
 * @version 1.0.0
 */

declare(strict_types=1);

namespace ThemeREX\AutoDealer\Frontend;

defined( 'ABSPATH' ) || exit;
use ThemeREX\AutoDealer\PostTypes\VehiclePostType;
use ThemeREX\AutoDealer\Search\FilterParams;
use ThemeREX\AutoDealer\Search\VehicleQuery;

/**
 * Bridges the public-facing archive query with the centralised search engine.
 */
class ArchiveFilters {

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register(): void {
		add_action( 'pre_get_posts', array( $this, 'applyFilters' ) );
	}

	/**
	 * On the vehicle archive, intersect the WP query with VehicleQuery results.
	 *
	 * @param \WP_Query $query The query being prepared.
	 * @return void
	 */
	public function applyFilters( \WP_Query $query ): void {
		if ( is_admin() || ! $query->is_main_query() ) {
			return;
		}

		if ( ! $query->is_post_type_archive( VehiclePostType::POST_TYPE ) ) {
			return;
		}

		$input = wp_unslash( $_GET ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		if ( ! is_array( $input ) ) {
			$input = array();
		}

		// Apply the admin-configured default sort when the visitor hasn't
		// chosen one. Friendly tokens (newest / price_asc / price_desc /
		// year_desc) are mapped to canonical pairs inside FilterParams.
		if ( ! isset( $input['orderby'] ) || '' === (string) $input['orderby'] ) {
			$default_sort = (string) \ThemeREX\AutoDealer\getSettings( 'default_sort', 'newest' );
			if ( '' !== $default_sort ) {
				$input['orderby'] = $default_sort;
			}
		}

		$params = FilterParams::fromArray( $input );

		// Even when no filters are active, respect the admin "Listings per
		// page" setting and the default sort order — both should apply to the
		// unfiltered archive. For sort orders that hit specs columns (price /
		// year / mileage) we need to route through VehicleQuery just like the
		// filtered path; otherwise WP's main query can't see those columns.
		if ( ! self::hasAnyFilter( $input ) ) {
			$query->set( 'posts_per_page', FilterParams::configuredPerPage() );
			$orderby = $params->getOrderby();
			if ( 'date' === $orderby ) {
				$query->set( 'order', $params->getOrder() );
				return;
			}
			// Composite or specs-backed sort — fall through to the VehicleQuery
			// path below so the order matches /api/search exactly.
		}

		// Hand WP_Query the FULL ordered match set as `post__in` and let it
		// own the LIMIT/offset. VehicleQuery must NOT pre-paginate here: if it
		// returned only the current page's IDs, WP_Query would then re-apply
		// its own `paged` offset on top of that single page and slice every
		// page after the first down to nothing (the "pagination dies past
		// page 1" bug). With the full set in post__in, WP computes found_posts
		// + max_num_pages correctly and the_posts_pagination() links work.
		$ids = ( new VehicleQuery() )->fetchIds( $params );

		// `post__in => []` would be ignored by WP_Query; substitute a dead ID.
		if ( empty( $ids ) ) {
			$ids = array( 0 );
		}

		$query->set( 'post__in', $ids );
		$query->set( 'orderby', 'post__in' );
		$query->set( 'posts_per_page', $params->getPerPage() );
		$query->set( 'ignore_sticky_posts', true );
	}

	/**
	 * Whether the input array contains any recognised filter key.
	 *
	 * @param array<string,mixed> $input Input.
	 * @return bool
	 */
	private static function hasAnyFilter( array $input ): bool {
		$keys = array_merge(
			array_keys( FilterParams::TEXT_FILTERS ),
			array_keys( FilterParams::BOOL_FILTERS ),
			array_keys( FilterParams::RANGE_FILTERS ),
			array( 'dealer_id', 'search', 'orderby', 'order', 'per_page' )
		);
		foreach ( $keys as $key ) {
			if ( isset( $input[ $key ] ) && '' !== $input[ $key ] ) {
				return true;
			}
		}
		return false;
	}
}
