<?php

// phpcs:disable Yoast.NamingConventions.NamespaceName.TooLong -- Needed in the folder structure.
namespace Yoast\WP\SEO\Bulk_Editor\User_Interface;

use WP_Error;
use WP_REST_Request;
use WP_REST_Response;
use Yoast\WP\SEO\Bulk_Editor\Application\Content_Types\Content_Type_Access_Checker_Interface;
use Yoast\WP\SEO\Bulk_Editor\Application\Content_Types\Content_Types_Repository;
use Yoast\WP\SEO\Bulk_Editor\Application\Posts\Posts_Collector_Interface;
use Yoast\WP\SEO\Bulk_Editor\Application\Posts\Posts_Repository;
use Yoast\WP\SEO\Bulk_Editor\Domain\Posts\Posts_Query;
use Yoast\WP\SEO\Conditionals\No_Conditionals;
use Yoast\WP\SEO\Helpers\Options_Helper;
use Yoast\WP\SEO\Helpers\Post_Type_Helper;
use Yoast\WP\SEO\Helpers\User_Helper;
use Yoast\WP\SEO\Main;
use Yoast\WP\SEO\Routes\Route_Interface;

/**
 * Registers a route that returns a page of posts with their SEO and social meta.
 */
class Posts_Route implements Route_Interface {

	use No_Conditionals;

	/**
	 * The namespace for this route.
	 *
	 * @var string
	 */
	public const ROUTE_NAMESPACE = Main::API_V1_NAMESPACE;

	/**
	 * The prefix for this route.
	 *
	 * @var string
	 */
	public const ROUTE_PREFIX = '/bulk_editor/posts';

	/**
	 * The default number of posts to return.
	 *
	 * @var int
	 */
	public const DEFAULT_PER_PAGE = 20;

	/**
	 * The maximum number of posts that can be requested in one page.
	 *
	 * @var int
	 */
	public const MAX_PER_PAGE = 100;

	/**
	 * The posts repository.
	 *
	 * @var Posts_Repository
	 */
	private $posts_repository;

	/**
	 * The content types repository.
	 *
	 * @var Content_Types_Repository
	 */
	private $content_types_repository;

	/**
	 * The content type access checker.
	 *
	 * @var Content_Type_Access_Checker_Interface
	 */
	private $content_type_access_checker;

	/**
	 * The user helper.
	 *
	 * @var User_Helper
	 */
	private $user_helper;

	/**
	 * The options helper.
	 *
	 * @var Options_Helper
	 */
	private $options_helper;

	/**
	 * The post type helper.
	 *
	 * @var Post_Type_Helper
	 */
	private $post_type_helper;

	/**
	 * The constructor.
	 *
	 * @param Posts_Repository                      $posts_repository            The posts repository.
	 * @param Content_Types_Repository              $content_types_repository    The content types repository.
	 * @param Content_Type_Access_Checker_Interface $content_type_access_checker The content type access checker.
	 * @param User_Helper                           $user_helper                 The user helper.
	 * @param Options_Helper                        $options_helper              The options helper.
	 * @param Post_Type_Helper                      $post_type_helper            The post type helper.
	 */
	public function __construct(
		Posts_Repository $posts_repository,
		Content_Types_Repository $content_types_repository,
		Content_Type_Access_Checker_Interface $content_type_access_checker,
		User_Helper $user_helper,
		Options_Helper $options_helper,
		Post_Type_Helper $post_type_helper
	) {
		$this->posts_repository            = $posts_repository;
		$this->content_types_repository    = $content_types_repository;
		$this->content_type_access_checker = $content_type_access_checker;
		$this->user_helper                 = $user_helper;
		$this->options_helper              = $options_helper;
		$this->post_type_helper            = $post_type_helper;
	}

	/**
	 * Registers routes with WordPress.
	 *
	 * @return void
	 */
	public function register_routes() {
		\register_rest_route(
			self::ROUTE_NAMESPACE,
			self::ROUTE_PREFIX,
			[
				'methods'             => 'GET',
				'args'                => [
					'content_type'      => [
						'required'          => true,
						'type'              => 'string',
						'description'       => 'The content type to fetch posts for.',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'per_page'          => [
						'required'          => false,
						'type'              => 'integer',
						'default'           => self::DEFAULT_PER_PAGE,
						'minimum'           => 1,
						'maximum'           => self::MAX_PER_PAGE,
						'description'       => 'The number of posts to fetch.',
						'sanitize_callback' => 'absint',
					],
					'page'              => [
						'required'          => false,
						'type'              => 'integer',
						'default'           => 1,
						'minimum'           => 1,
						'description'       => 'The page of posts to fetch.',
						'sanitize_callback' => 'absint',
					],
					'search'            => [
						'required'          => false,
						'type'              => 'string',
						'default'           => '',
						'description'       => 'The term to search posts by.',
						'sanitize_callback' => 'sanitize_text_field',
					],
					'status'            => [
						'required'    => false,
						'type'        => 'array',
						'default'     => Posts_Collector_Interface::STATUSES,
						'items'       => [
							'type' => 'string',
							'enum' => Posts_Collector_Interface::STATUSES,
						],
						'description' => 'The post statuses to include.',
					],
					'needs_improvement' => [
						'required'    => false,
						'type'        => 'array',
						'default'     => [],
						'items'       => [
							'type' => 'string',
							'enum' => Posts_Collector_Interface::NEEDS_IMPROVEMENT_FIELDS,
						],
						'description' => 'The fields to filter posts by; a field matches when it is empty, or (for search fields, while SEO analysis is enabled and the content type shows Yoast\'s controls and assessments) when its score needs improvement.',
					],
					'include'           => [
						'required'    => false,
						'type'        => 'array',
						'default'     => [],
						'maxItems'    => self::MAX_PER_PAGE,
						'items'       => [
							'type'    => 'integer',
							'minimum' => 1,
						],
						'description' => 'Limits the posts to these post IDs, e.g. a selection carried over from the posts overview.',
					],
				],
				'callback'            => [ $this, 'get_posts' ],
				'permission_callback' => [ $this, 'check_permissions' ],
			],
		);
	}

	/**
	 * Returns a page of posts for the requested content type.
	 *
	 * @param WP_REST_Request $request The request object.
	 *
	 * @return WP_REST_Response|WP_Error The posts, or an error when the content type is not editable.
	 */
	public function get_posts( WP_REST_Request $request ) {
		$content_type = $request->get_param( 'content_type' );

		if ( ! $this->is_valid_content_type( $content_type ) ) {
			return new WP_Error(
				'rest_invalid_content_type',
				'The requested content type is not editable.',
				[ 'status' => 400 ],
			);
		}

		// An empty selection (no status filter) means all statuses, never zero results.
		$statuses = (array) $request->get_param( 'status' );
		if ( empty( $statuses ) ) {
			$statuses = Posts_Collector_Interface::STATUSES;
		}

		// Narrow the query to the user's own posts when they cannot edit other authors' posts. This keeps
		// pagination cheap; the exact per-post edit permission is still enforced when collecting the page.
		$author_id = null;
		if ( ! $this->content_type_access_checker->can_edit_others( $content_type ) ) {
			$author_id = $this->user_helper->get_current_user_id();
		}

		// The per-field scores only back the filter while SEO analysis is on globally and the content type
		// shows Yoast's controls and assessments; otherwise they go stale and the filter falls back to the
		// empty-field check.
		$scores_enabled = $this->options_helper->get( 'keyword_analysis_active' ) === true
			&& $this->post_type_helper->has_metabox( $content_type );

		// The schema already coerces the items to positive integers; deduplicate on top of that.
		$include = \array_values( \array_unique( \array_map( 'intval', (array) $request->get_param( 'include' ) ) ) );

		$query = new Posts_Query(
			$content_type,
			(int) $request->get_param( 'page' ),
			(int) $request->get_param( 'per_page' ),
			(string) $request->get_param( 'search' ),
			$statuses,
			$author_id,
			(array) $request->get_param( 'needs_improvement' ),
			$scores_enabled,
			$include,
		);

		// Posts the current user cannot edit are returned locked and without their SEO data; the per-post
		// permission is resolved while collecting the page.
		return new WP_REST_Response( $this->posts_repository->get_posts( $query )->to_array() );
	}

	/**
	 * Checks whether the current user is allowed to use the bulk editor.
	 *
	 * @return bool Whether the current user is allowed to use the bulk editor.
	 */
	public function check_permissions(): bool {
		return \current_user_can( 'wpseo_manage_options' );
	}

	/**
	 * Checks whether the given content type is one the bulk editor can edit.
	 *
	 * @param string $content_type The content type to check.
	 *
	 * @return bool Whether the content type is editable.
	 */
	private function is_valid_content_type( string $content_type ): bool {
		foreach ( $this->content_types_repository->get_content_types() as $editable ) {
			if ( $editable['name'] === $content_type ) {
				return true;
			}
		}

		return false;
	}
}
