<?php

namespace CustomTaxonomyOrder\Includes;

use WP_PowerSuite\Ordering\QueryOrderGuard;

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

/**
 * Applies custom term order to eligible get_terms queries.
 */
class QueryModifier {

	/** @var array<string, mixed> */
	private array $settings;

	/** @var array<string, true> */
	private array $active_taxonomies = [];

	public function __construct( array $settings ) {
		$this->settings = $settings;
	}

	public function registerHooks(): void {
		add_filter( 'get_terms_args', [ $this, 'filterGetTermsArgs' ], 20, 2 );
		add_filter( 'terms_clauses', [ $this, 'filterTermsClauses' ], 20, 3 );
	}

	/**
	 * @param array<string, mixed> $args
	 * @param list<string>         $taxonomies
	 * @return array<string, mixed>
	 */
	public function filterGetTermsArgs( array $args, array $taxonomies ): array {
		if ( QueryOrderGuard::argsSuppressFilters( $args ) ) {
			return $args;
		}

		$taxonomy = $this->resolveTaxonomyFromList( $taxonomies );
		if ( $taxonomy === '' || ! $this->isTaxonomyEnabled( $taxonomy ) ) {
			return $args;
		}

		if ( ! $this->matchesContext( $taxonomy, $args ) ) {
			return $args;
		}

		$respect = ! empty( $this->settings['respect_existing_order'] );
		if ( QueryOrderGuard::termQueryHasExplicitOrderby( $args['orderby'] ?? '', $respect ) ) {
			return $args;
		}

		/**
		 * Whether custom taxonomy order should apply to this get_terms query.
		 *
		 * @param bool                 $apply      Default true when internal checks passed.
		 * @param array<string, mixed> $args       get_terms arguments.
		 * @param string               $taxonomy   Resolved taxonomy slug.
		 * @param array<string, mixed> $settings   Module settings.
		 */
		$apply = apply_filters( 'wp_powersuite_custom_taxonomy_order_apply_to_query', true, $args, $taxonomy, $this->settings );
		if ( ! $apply ) {
			return $args;
		}

		$this->active_taxonomies[ $taxonomy ] = true;
		$args['wpps_custom_taxonomy_order']   = true;

		return $args;
	}

	/**
	 * @param array<string, string> $clauses
	 * @param list<string>          $taxonomies
	 * @param array<string, mixed>  $args
	 * @return array<string, string>
	 */
	public function filterTermsClauses( array $clauses, array $taxonomies, array $args ): array {
		if ( empty( $args['wpps_custom_taxonomy_order'] ) ) {
			return $clauses;
		}

		$taxonomy = $this->resolveTaxonomyFromList( $taxonomies );
		if ( $taxonomy === '' || empty( $this->active_taxonomies[ $taxonomy ] ) ) {
			return $clauses;
		}

		unset( $this->active_taxonomies[ $taxonomy ] );

		// Ordering is meaningless for count queries and would otherwise inject an
		// ORDER BY clause into a `SELECT COUNT(*)` statement.
		if ( isset( $args['fields'] ) && $args['fields'] === 'count' ) {
			return $clauses;
		}

		global $wpdb;
		$meta_key = TaxonomyHelper::getOrderMetaKey();
		$alias    = 'wpps_tax_order_meta';

		if ( strpos( $clauses['join'], $alias ) === false ) {
			$clauses['join'] .= " LEFT JOIN {$wpdb->termmeta} AS {$alias}"
				. $wpdb->prepare( " ON (t.term_id = {$alias}.term_id AND {$alias}.meta_key = %s)", $meta_key );
		}

		$order_sql = "CAST(COALESCE({$alias}.meta_value, 999999) AS SIGNED) ASC, t.name ASC";
		/**
		 * Filter SQL order clause for custom taxonomy order.
		 *
		 * @param string               $order_sql ORDER BY fragment (without the `ORDER BY` keyword).
		 * @param string               $taxonomy  Taxonomy slug.
		 * @param array<string, mixed> $args      get_terms arguments.
		 */
		$order_sql = apply_filters( 'wp_powersuite_custom_taxonomy_order_orderby_args', $order_sql, $taxonomy, $args );

		// WP_Term_Query passes the `orderby` clause already prefixed with `ORDER BY`
		// and appends the separate `order` value after it. We must include the
		// keyword ourselves and clear `order` to avoid a trailing, invalid direction.
		$clauses['orderby'] = 'ORDER BY ' . ltrim( (string) $order_sql );
		$clauses['order']   = '';

		return $clauses;
	}

	/**
	 * @param array<string, mixed> $args
	 */
	private function matchesContext( string $taxonomy, array $args ): bool {
		if ( is_admin() ) {
			if ( ! empty( $this->settings['apply_admin'] ) ) {
				return true;
			}
			if ( ! empty( $this->settings['apply_menus'] ) && $this->isMenuAdminScreen() ) {
				return $taxonomy !== 'nav_menu';
			}
			return false;
		}

		if ( ! empty( $this->settings['apply_widgets'] ) && $this->isWidgetContext() ) {
			return true;
		}

		if ( ! empty( $this->settings['apply_archives'] ) && ( is_category() || is_tag() || is_tax( $taxonomy ) ) ) {
			return true;
		}

		if ( ! empty( $this->settings['apply_frontend'] ) && $this->isFrontendTermListContext( $args ) ) {
			return true;
		}

		return false;
	}

	/**
	 * @param array<string, mixed> $args
	 */
	private function isFrontendTermListContext( array $args ): bool {
		if ( is_admin() ) {
			return false;
		}

		if ( doing_filter( 'widget_categories_args' )
			|| doing_filter( 'widget_categories_dropdown_args' )
			|| doing_filter( 'wp_list_categories' )
			|| doing_filter( 'wp_dropdown_categories' )
		) {
			return true;
		}

		if ( ! empty( $args['wpps_custom_taxonomy_order'] ) ) {
			return true;
		}

		// Avoid overriding unrelated programmatic queries on singular pages.
		if ( is_singular() && empty( $args['fields'] ) ) {
			return false;
		}

		return ! is_feed() && ! wp_doing_cron();
	}

	private function isMenuAdminScreen(): bool {
		global $pagenow;
		return $pagenow === 'nav-menus.php';
	}

	private function isWidgetContext(): bool {
		return doing_filter( 'widget_categories_args' )
			|| doing_filter( 'widget_categories_dropdown_args' )
			|| doing_filter( 'wp_list_categories' )
			|| doing_filter( 'wp_dropdown_categories' )
			|| doing_action( 'widget_archives_args' );
	}

	/**
	 * @param list<string> $taxonomies
	 */
	private function resolveTaxonomyFromList( array $taxonomies ): string {
		if ( $taxonomies === [] ) {
			return '';
		}
		$taxonomy = (string) reset( $taxonomies );
		return sanitize_key( $taxonomy );
	}

	private function isTaxonomyEnabled( string $taxonomy ): bool {
		$taxonomies = $this->settings['taxonomies'] ?? [];
		return is_array( $taxonomies ) && in_array( $taxonomy, $taxonomies, true );
	}
}
