<?php
/**
 * Server-side translation: queryParams → `get_terms()` args (Phase 4.2).
 *
 * Twin of the editor's `query/normalize.js` terms-entity baseline. Source
 * shape is the same `[ [ 'id' => 'taxonomy', 'value' => 'category' ], ... ]`
 * canonical queryParams — see `src/blocks/content-loop/inspector/query/catalog.js`
 * Terms branch for the full id list.
 *
 * Parallel to `Blocksify_CL_Query` but targets a different result set:
 * a list of `WP_Term` instances rather than `WP_Post`. The class stays
 * additive — no refactor of the posts class.
 *
 * @package Blocksify\Blocks\ContentLoop
 */

defined( 'ABSPATH' ) || exit;

if ( ! class_exists( 'Blocksify_Terms_Query' ) ) :

class Blocksify_Terms_Query {

	/**
	 * `orderby` allowlist for the terms entity. `get_terms()` accepts many
	 * more values (`include_slugs`, `name__like`, …) but the Free catalog
	 * exposes only these four. Pro extends via the
	 * `blocksify.cl.query.catalog` filter when wiring new editors.
	 *
	 * @var array<int,string>
	 */
	private static $orderby_allowlist = array( 'name', 'count', 'term_id', 'slug' );

	/**
	 * Free baseline handler registry for `queryParams` → `get_terms()` args
	 * translation. Each entry is a callable `function ( $val, array $args,
	 * array $flat ): array` that mutates and returns the args bag. Pro /
	 * 3rd-party extends via the `blocksify.cl.query.build_handlers` filter
	 * — registering a new id handler is the canonical way to ship support
	 * for advanced terms params (meta_query, custom term-relationship rows,
	 * …) without touching Free core.
	 *
	 * Conventions mirror the JS twin `query/normalize.js`:
	 *   - `taxonomy` + `number` are seeded directly on the args bag before
	 *     dispatch so the defaults (`category`, `6`) survive when the user
	 *     removes those rows from the Repeater.
	 *   - `hideEmpty` maps to `hide_empty` (REST + WP both use snake_case
	 *     for this arg).
	 *   - Pagination uses `offset` + `number` (computed after dispatch in
	 *     `build_args`) rather than `paged`, because `get_terms()` itself
	 *     doesn't accept `paged`.
	 *
	 * Twin parity with `query/normalize.js` is checked manually — when
	 * extending one, extend the other.
	 *
	 * @return array<string,callable>
	 */
	private static function get_build_handlers() {
		$pass_through = function ( $arg_key ) {
			return function ( $val, $args ) use ( $arg_key ) {
				$args[ $arg_key ] = $val;
				return $args;
			};
		};

		$handlers = array(
			'taxonomy'  => function ( $val, $args ) {
				$args['taxonomy'] = sanitize_key( (string) $val );
				return $args;
			},
			'number'    => function ( $val, $args ) {
				$args['number'] = max( 1, min( 50, (int) $val ) );
				return $args;
			},
			'orderby'   => function ( $val, $args ) {
				$val = sanitize_key( (string) $val );
				if ( in_array( $val, self::$orderby_allowlist, true ) ) {
					$args['orderby'] = $val;
				}
				return $args;
			},
			'order'     => function ( $val, $args ) {
				$args['order'] = strtoupper( (string) $val ) === 'DESC' ? 'DESC' : 'ASC';
				return $args;
			},
			'hideEmpty' => function ( $val, $args ) {
				$args['hide_empty'] = (bool) $val;
				return $args;
			},
			'parent'    => function ( $val, $args ) {
				$args['parent'] = max( 0, (int) $val );
				return $args;
			},
			'include'   => $pass_through( 'include' ),
			'exclude'   => $pass_through( 'exclude' ),
			'search'    => function ( $val, $args ) {
				$args['search'] = (string) $val;
				return $args;
			},
		);

		/**
		 * Filter the handler registry. Pro / 3rd-party can register
		 * handlers for new queryParam ids (e.g. `metaQuery`, custom
		 * term-relationship rows) without touching Free core. The second
		 * `$entity` arg lets Pro target a single entity's handler bag.
		 *
		 * @param array  $handlers Map of `id => callable( $val, $args, $flat ): array`.
		 * @param string $entity   `'terms'` for this call site.
		 */
		return (array) apply_filters(
			'blocksify.cl.query.build_handlers',
			$handlers,
			'terms'
		);
	}

	/**
	 * Build `get_terms()` args from queryParams + current page number.
	 *
	 * @param array $query_params Raw queryParams attribute (object map).
	 * @param int   $current_page Current pagination page (≥ 1).
	 * @return array
	 */
	public static function build_args( $query_params, $current_page = 1 ) {
		$flat = is_array( $query_params ) ? $query_params : array();

		// Seed defaults so removing the row from the Repeater doesn't drop
		// the baseline (`category`, `6`, `hide_empty=true`).
		$args = array(
			'taxonomy'   => 'category',
			'number'     => 6,
			'hide_empty' => true,
			'orderby'    => 'name',
			'order'      => 'ASC',
		);

		// Per-id handler dispatch.
		$handlers = self::get_build_handlers();
		foreach ( $flat as $id => $val ) {
			if ( '' === $val || null === $val ) {
				continue;
			}
			if ( ! isset( $handlers[ $id ] ) || ! is_callable( $handlers[ $id ] ) ) {
				continue;
			}
			$next = call_user_func( $handlers[ $id ], $val, $args, $flat );
			if ( is_array( $next ) ) {
				$args = $next;
			}
		}

		// Sanitize array-of-IDs args.
		foreach ( array( 'include', 'exclude' ) as $k ) {
			if ( isset( $args[ $k ] ) ) {
				$args[ $k ] = array_values( array_filter( array_map( 'absint', (array) $args[ $k ] ) ) );
				if ( empty( $args[ $k ] ) ) {
					unset( $args[ $k ] );
				}
			}
		}

		// Search — trim / drop empty.
		if ( isset( $args['search'] ) ) {
			$args['search'] = trim( (string) $args['search'] );
			if ( '' === $args['search'] ) {
				unset( $args['search'] );
			}
		}

		// Pagination: `get_terms()` uses `offset` + `number` (no `paged`).
		// Compound: user offset isn't a separate Free catalog row (terms
		// pagination is simpler than posts) — page-driven offset only.
		$page   = max( 1, (int) $current_page );
		$per    = (int) $args['number'];
		if ( $page > 1 && $per > 0 ) {
			$args['offset'] = ( $page - 1 ) * $per;
		}

		/**
		 * Filter the final `get_terms()` args before invocation. Last-step
		 * override hook — Pro / user code uses this for runtime context-
		 * aware substitutions (URL params → query args, current-user
		 * resolution, archive context inheritance, …). Shared with
		 * `Blocksify_CL_Query` so the same hook drives both entities;
		 * callbacks branch on `$args['taxonomy']` presence (terms) vs
		 * `$args['post_type']` (posts) to discriminate.
		 *
		 * @param array $args         get_terms() args.
		 * @param array $query_params Raw queryParams source.
		 * @param int   $current_page Current pagination page.
		 */
		return apply_filters( 'blocksify.cl.query_args', $args, $query_params, $current_page );
	}

}

endif;
