<?php
/**
 * Author:          Andrei Baicus <andrei@themeisle.com>
 * Created on:      31/08/2018
 *
 * @package Neve\Views\Pluggable
 */

namespace Neve\Views\Pluggable;

use Neve\Views\Base_View;

/**
 * Class Pagination
 *
 * @package Neve\Views\Pluggable
 */
class Pagination extends Base_View {
	/**
	 * Function that is run after instantiation.
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'rest_api_init', array( $this, 'register_endpoints' ) );
		add_filter( 'neve_filter_main_script_localization', array( $this, 'filter_localization' ) );
		add_action( 'neve_do_pagination', array( $this, 'render_pagination' ) );
		add_action( 'neve_post_navigation', array( $this, 'render_post_navigation' ) );
	}

	/**
	 * Register Rest API endpoint for posts retrieval.
	 */
	public function register_endpoints() {
		register_rest_route(
			'nv/v1/posts',
			'/page/(?P<page_number>\d+)(?:/(?P<lang>[a-zA-Z0-9-_]+))?',
			array(
				'methods'             => \WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'get_posts' ),
				'permission_callback' => '__return_true',
			)
		);
	}

	/**
	 * Get paginated posts.
	 *
	 * @param \WP_REST_Request $request the rest request.
	 *
	 * @return \WP_REST_Response
	 */
	public function get_posts( \WP_REST_Request $request ) {
		if ( empty( $request['page_number'] ) ) {
			return new \WP_REST_Response( '' );
		}

		$page_number = absint( $request['page_number'] );
		$query_args  = $request->get_body();
		$args        = json_decode( $query_args, true );
		$per_page    = get_option( 'posts_per_page' );
		if ( $per_page > 100 ) {
			$per_page = 100;
		}
		$args = $this->sanitize_infinite_scroll_query_args( is_array( $args ) ? $args : array() );

		/**
		 * If homepage is set to 'A static page', there will be a parameter inside the query named 'pagename'.
		 * That parameter is the title of the blog page and because of it the WP_Query will return 0 posts.
		 * Here, we unset it. We can't reset the query because we can lose search parameters for example.
		 */
		if ( array_key_exists( 'pagename', $args ) ) {
			unset( $args['pagename'] );
		}

		$args['posts_per_page'] = $per_page;
		$args['paged']          = $page_number;

		if ( ! empty( $request['lang'] ) ) {
			if ( defined( 'POLYLANG_VERSION' ) ) {
				$args['lang'] = sanitize_text_field( $request['lang'] );
			}

			if ( defined( 'ICL_SITEPRESS_VERSION' ) ) {
				global $sitepress;
				if ( gettype( $sitepress ) === 'object' && method_exists( $sitepress, 'switch_lang' ) ) {
					$sitepress->switch_lang( sanitize_text_field( $request['lang'] ) );
				}
			}
		}

		$output = '';

		$query = new \WP_Query( $args );
		if ( $query->have_posts() ) {
			ob_start();
			while ( $query->have_posts() ) { // @phpstan-ignore-line impure WP function
				$query->the_post();

				/**
				 * Executes actions before rendering the post content.
				 *
				 * @since 2.11 in the /index.php
				 */
				do_action( 'neve_loop_entry_before' );

				get_template_part( 'template-parts/content' );

				/**
				 * Executes actions after rendering the post content.
				 *
				 * @since 2.11 in the /index.php
				 */
				do_action( 'neve_loop_entry_after' );
			} // @phpstan-ignore-next-line code is reachable
			wp_reset_postdata();
			$output = ob_get_contents();
			ob_end_clean();
		}

		return new \WP_REST_Response( $output );
	}

	/**
	 * Filter localization to add infinite scroll to the main script.
	 *
	 * @param array $data localization array.
	 *
	 * @return array
	 */
	public function filter_localization( $data ) {
		if ( ! $this->has_infinite_scroll() ) {
			return $data;
		}
		global $wp_query;
		$max_pages = $wp_query->max_num_pages;

		$query = $wp_query->query;

		if ( is_tax() ) {
			$post_type = get_post_type();
			if ( $post_type ) {
				$query['post_type'] = $post_type;
			}
		}
		$data['infScroll'] = 'enabled';
		$data['maxPages']  = $max_pages;
		$data['endpoint']  = rest_url( 'nv/v1/posts/page/' );
		$data['query']     = wp_json_encode( $query );
		$data['lang']      = get_locale();

		// WPML language parameter
		$current_lang = apply_filters( 'wpml_current_language', null );
		if ( ! empty( $current_lang ) ) {
			$data['lang'] = $current_lang;
		}

		return $data;
	}

	/**
	 * Render the pagination.
	 *
	 * @param string $context Pagination location context.
	 */
	public function render_pagination( $context ) {
		if ( $context === 'single' ) {
			$this->render_single_pagination();

			return;
		}

		$paginate_args = array( 'type' => 'list' );
		if ( $this->has_jump_to() ) {
			$paginate_args['format'] = '?paged=%#%';
		}
		$links = paginate_links( $paginate_args );

		if ( empty( $links ) ) {
			return;
		}

		if ( ! $this->has_infinite_scroll() ) {
			/**
			 * Executes actions before pagination.
			 *
			 * @since 2.3.8
			 */
			do_action( 'neve_before_pagination' );
		}

		$links = str_replace(
			array( '<a class="prev', '<a class="next' ),
			array(
				'<a rel="prev" class="prev',
				'<a rel="next" class="next',
			),
			$links
		);

		$allowed_tags = 'post';
		if ( $this->has_jump_to() ) {

			parse_str( wp_parse_url( get_pagenum_link(), PHP_URL_QUERY ), $url_query_args );

			$current_page      = ( ! empty( get_query_var( 'paged' ) ) ) ? get_query_var( 'paged' ) : 1;
			$additional_fields = '';
			foreach ( $url_query_args as $key => $value ) {
				$additional_fields .= '<input type="hidden" name="' . esc_attr( $key ) . '" value="' . esc_attr( $value ) . '" />';
			}

			$allowed_tags = array(
				'ul'    => array(
					'class' => array(),
				),
				'li'    => array(
					'class' => array(),
				),
				'a'     => array(
					'class' => array(),
					'href'  => array(),
				),
				'span'  => array(
					'class' => array(),
				),
				'form'  => array(
					'class'        => array(),
					'action'       => array( esc_url( get_pagenum_link() ) ),
					'method'       => array( 'get' ),
					'autocomplete' => array( 'off' ),
				),
				'input' => array(
					'class'       => array(),
					'type'        => array( 'number', 'hidden' ),
					'name'        => array( 'paged', 'page_id', 's' ),
					'value'       => array(),
					'min'         => array(),
					'step'        => array(),
					'placeholder' => array(),
					'size'        => array(),
				),
			);

			$jump_to_form = '<form class="nv-page-nav-form" action="' . esc_url( get_pagenum_link() ) . '" method="get" autocomplete="off">
				<input class="page-input" type="number" min="1" step="1" value="' . absint( $current_page ) . '" placeholder="1" size="3" name="paged" />
				' . wp_kses(
					$additional_fields,
					$allowed_tags
				) . '
				<input value="»" type="submit" >
			</form>';

			$links = str_replace(
				'</ul>',
				'<li>' . $jump_to_form . '</li></ul>',
				$links
			);
		}

		echo $this->has_infinite_scroll() ? '<div style="display: none;">' : '';
		echo wp_kses( $links, $allowed_tags );
		echo $this->has_infinite_scroll() ? '</div>' : '';

		if ( $this->has_infinite_scroll() ) {
			echo wp_kses_post( '<div class="load-more-posts"><span class="nv-loader" style="display: none;"></span><span class="infinite-scroll-trigger"></span></div>' );
		}
	}

	/**
	 * Render single post / page pagination.
	 */
	private function render_single_pagination() {
		wp_link_pages(
			array(
				'before'      => '<div class="post-pages-links"><span>' . apply_filters( 'neve_page_link_before', esc_html__( 'Pages:', 'neve' ) ) . '</span>',
				'after'       => '</div>',
				'link_before' => '<span>',
				'link_after'  => '</span>',
			)
		);
	}

	/**
	 * Render single post navigation links
	 */
	public function render_post_navigation() {
		$prev_format = '<div class="previous">%link</div>';
		$next_format = '<div class="next">%link</div>';

		$prev_link = sprintf(
			'<span class="nav-direction">%1$s</span><span>%2$s</span>',
			esc_html__( 'previous', 'neve' ),
			'%title'
		);
		$next_link = sprintf(
			'<span class="nav-direction">%1$s</span><span>%2$s</span>',
			esc_html__( 'next', 'neve' ),
			'%title'
		);

		echo '<div class="' . esc_attr( apply_filters( 'neve_post_navigation_class', 'nv-post-navigation' ) ) . '">';
		previous_post_link( $prev_format, $prev_link );
		next_post_link( $next_format, $next_link );
		echo '</div>';
	}

	/**
	 * Sanitize query arguments for infinite scroll to prevent query manipulation.
	 *
	 * This method implements a strict allowlist approach to prevent:
	 * - Expensive database queries (DoS risk via meta_query, fields etc.)
	 * - Exposure of unintended content types
	 * - Manipulation of query parameters by anonymous users
	 *
	 * @param array<string, mixed> $args Raw query arguments from client request.
	 *
	 * @return array<string, mixed> Sanitized query arguments safe for WP_Query.
	 */
	private function sanitize_infinite_scroll_query_args( $args ) {
		// Define allowlist of safe query parameters for public infinite scroll.
		$allowed_keys = array(
			'category_name',
			'tag',
			's',
			'order',
			'orderby',
			'author',
			'author_name',
			'year',
			'monthnum',
			'day',
		);

		$sanitized = array();
		foreach ( $allowed_keys as $key ) {
			if ( isset( $args[ $key ] ) ) {
				$sanitized[ $key ] = $args[ $key ];
			}
		}

		if ( isset( $sanitized['category_name'] ) ) {
			$sanitized['category_name'] = sanitize_text_field( $sanitized['category_name'] );
		}
		if ( isset( $sanitized['tag'] ) ) {
			$sanitized['tag'] = sanitize_text_field( $sanitized['tag'] );
		}
		if ( isset( $sanitized['s'] ) ) {
			$sanitized['s'] = sanitize_text_field( $sanitized['s'] );
		}
		if ( isset( $sanitized['order'] ) ) {
			$order_upper        = is_string( $sanitized['order'] ) ? strtoupper( $sanitized['order'] ) : '';
			$sanitized['order'] = in_array( $order_upper, array( 'ASC', 'DESC' ), true ) ? $order_upper : 'DESC';
		}
		if ( isset( $sanitized['orderby'] ) ) {
			$safe_orderby         = array( 'date', 'title', 'author', 'modified', 'comment_count' );
			$sanitized['orderby'] = in_array( $sanitized['orderby'], $safe_orderby, true ) ? $sanitized['orderby'] : 'date';
		}
		if ( isset( $sanitized['author'] ) ) {
			$sanitized['author'] = absint( $sanitized['author'] );
		}
		if ( isset( $sanitized['author_name'] ) ) {
			$sanitized['author_name'] = sanitize_user( $sanitized['author_name'] );
		}
		if ( isset( $sanitized['year'] ) ) {
			$sanitized['year'] = absint( $sanitized['year'] );
		}
		if ( isset( $sanitized['monthnum'] ) ) {
			$sanitized['monthnum'] = absint( $sanitized['monthnum'] );
		}
		if ( isset( $sanitized['day'] ) ) {
			$sanitized['day'] = absint( $sanitized['day'] );
		}

		$post_type     = ( ! empty( $args['post_type'] ) && is_string( $args['post_type'] ) ) ? sanitize_key( $args['post_type'] ) : 'post';
		$post_type_obj = get_post_type_object( $post_type );

		// Only allow if post type exists and is publicly queryable.
		if ( $post_type_obj && $post_type_obj->publicly_queryable ) {
			$sanitized['post_type'] = $post_type;
		} else {
			$sanitized['post_type'] = 'post';
			$post_type              = 'post';
		}

		// Build an allowlist of taxonomies that are both publicly queryable and registered to the resolved post type.
		$allowed_taxonomies     = array_filter(
			get_object_taxonomies( $post_type, 'objects' ),
			function ( $tax_obj ) {
				return ! empty( $tax_obj->publicly_queryable );
			}
		);
		$allowed_taxonomy_names = array_keys( $allowed_taxonomies );

		$tax_queries = array();
		foreach ( $args as $key => $value ) {
			if ( ! in_array( $key, $allowed_taxonomy_names, true ) ) {
				continue;
			}

			$terms = array();
			if ( is_array( $value ) ) {
				foreach ( $value as $maybe_term ) {
					if ( is_scalar( $maybe_term ) ) {
						$sanitized_term = sanitize_title( (string) $maybe_term );
						if ( $sanitized_term !== '' ) {
							$terms[] = $sanitized_term;
						}
					}
				}
			} elseif ( is_scalar( $value ) ) {
				$sanitized_term = sanitize_title( (string) $value );
				if ( $sanitized_term !== '' ) {
					$terms[] = $sanitized_term;
				}
			}

			if ( empty( $terms ) ) {
				continue;
			}

			$tax_queries[] = array(
				'taxonomy' => $key,
				'field'    => 'slug',
				'terms'    => $terms,
			);
		}

		if ( ! empty( $tax_queries ) ) {
			$sanitized['tax_query'] = array_merge( // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
				array(
					'relation' => 'AND',
				),
				$tax_queries
			);
		}

		// Explicitly unset dangerous query args that could be smuggled in.
		$dangerous_keys = array_flip(
			array(
				'meta_query',
				'meta_key',
				'meta_value',
				'meta_value_num',
				'meta_compare',
				'fields',
				'post__in',
				'post__not_in',
				'post_parent',
				'post_parent__in',
				'post_parent__not_in',
			)
		);
		$sanitized      = array_diff_key( $sanitized, $dangerous_keys );

		// Force safe defaults for core query behavior.
		$sanitized['post_status']         = 'publish';
		$sanitized['ignore_sticky_posts'] = 1;

		return $sanitized;
	}

	/**
	 * Go to page option is enabled
	 *
	 * @return bool
	 */
	private function has_jump_to() {
		return get_theme_mod( 'neve_pagination_type', 'number' ) === 'jump-to';
	}

	/**
	 * Has infinite scroll.
	 *
	 * @return bool
	 */
	private function has_infinite_scroll() {
		if ( neve_is_amp() ) {
			return false;
		}
		if ( is_search() ) {
			return false;
		}

		$pagination_type = get_theme_mod( 'neve_pagination_type', 'number' );
		if ( $pagination_type === 'infinite' ) {
			return true;
		}

		return false;
	}
}
