<?php
/**
 * The Pattern library class file.
 *
 * @package GenerateBlocks\Pattern_Library
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit; // Exit if accessed directly.
}

/**
 * Main class for the Pattern Library Rest functions.
 *
 * @since 1.9
 */
class GenerateBlocks_Pattern_Library_Rest extends GenerateBlocks_Singleton {

	/**
	 * Initialize all hooks.
	 *
	 * @return void
	 */
	public function init() {
		add_action( 'rest_api_init', array( $this, 'register_routes' ) );
	}

	/**
	 * Register the REST routes.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		$namespace = 'generateblocks/v1';

		register_rest_route(
			$namespace,
			'/pattern-library/libraries',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'list_libraries' ),
				'permission_callback' => function() {
					return apply_filters(
						'generateblocks_can_view_pattern_library',
						$this->edit_posts_permission()
					);
				},
			)
		);

		register_rest_route(
			$namespace,
			'/pattern-library/libraries/save',
			array(
				'methods'             => WP_REST_Server::CREATABLE,
				'callback'            => array( $this, 'save_libraries' ),
				'permission_callback' => array( $this, 'manage_options_permission' ),
			)
		);

		register_rest_route(
			$namespace,
			'/pattern-library/categories',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'list_categories' ),
				'permission_callback' => function() {
					return apply_filters(
						'generateblocks_can_view_pattern_library',
						$this->edit_posts_permission()
					);
				},
			)
		);

		register_rest_route(
			$namespace,
			'/pattern-library/patterns',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'list_patterns' ),
				'permission_callback' => function() {
					return apply_filters(
						'generateblocks_can_view_pattern_library',
						$this->edit_posts_permission()
					);
				},
			)
		);

		register_rest_route(
			$namespace,
			'/pattern-library/get-cache-data',
			array(
				'methods'             => WP_REST_Server::READABLE,
				'callback'            => array( $this, 'get_cache_data' ),
				'permission_callback' => function() {
					return apply_filters(
						'generateblocks_can_view_pattern_library',
						$this->edit_posts_permission()
					);
				},
			)
		);

		register_rest_route(
			$namespace,
			'/pattern-library/clear-cache',
			array(
				'methods'             => WP_REST_Server::EDITABLE,
				'callback'            => array( $this, 'clear_cache' ),
				'permission_callback' => array( $this, 'edit_posts_permission' ),
			)
		);
	}

	/**
	 * Manage options permission callback.
	 *
	 * @return bool
	 */
	public function manage_options_permission(): bool {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Manage options permission callback.
	 *
	 * @return bool
	 */
	public function edit_posts_permission(): bool {
		return current_user_can( 'edit_posts' );
	}

	/**
	 * Returns a list of registered libraries.
	 *
	 * @param WP_REST_Request $request The request.
	 *
	 * @return WP_REST_Response
	 */
	public function list_libraries( WP_REST_Request $request ): WP_REST_Response {
		$is_enabled = $request->get_param( 'is_enabled' );
		$libraries = GenerateBlocks_Libraries::get_instance();
		$data = $libraries->get_all( rest_sanitize_boolean( $is_enabled ) );
		$data = array_values( $data ); // Fix indexes.

		return new WP_REST_Response(
			array(
				'error'  => false,
				'data' => $data,
			),
			200
		);
	}

	/**
	 * Saves the list of libraries.
	 *
	 * @param WP_REST_Request $request The request.
	 *
	 * @return WP_REST_Response
	 */
	public function save_libraries( WP_REST_Request $request ): WP_REST_Response {
		$data = $request->get_param( 'data' );

		if ( ! is_array( $data ) ) {
			return $this->error( 400, __( 'Invalid library data.', 'generateblocks' ) );
		}

		$libraries = array_values(
			array_filter(
				array_map(
					[ $this, 'sanitize_library_data' ],
					$data
				)
			)
		);

		update_option( 'generateblocks_pattern_libraries', $libraries );
		return $this->success( $libraries );
	}

	/**
	 * Returns a list of categories.
	 *
	 * @param WP_REST_Request $request The request.
	 *
	 * @return WP_REST_Response
	 */
	public function list_categories( WP_REST_Request $request ): WP_REST_Response {
		$library_id = $this->sanitize_request_string( $request->get_param( 'libraryId' ) );

		if ( ! $library_id ) {
			return $this->error( 404, "Library of id \"$library_id\" was not found." );
		}

		$libraries = GenerateBlocks_Libraries::get_instance();
		$library = $libraries->get_one( $library_id );

		if ( ! is_null( $library ) ) {
			return self::remote_fetch( $library, 'categories' );
		}

		return $this->error( 404, "Library of id \"$library_id\" was not found." );
	}

	/**
	 * Returns a list of patterns.
	 *
	 * @param WP_REST_Request $request The request.
	 *
	 * @return WP_REST_Response
	 */
	public function list_patterns( WP_REST_Request $request ): WP_REST_Response {
		$library_id = $this->sanitize_request_string( $request->get_param( 'libraryId' ) );
		$search = $this->sanitize_request_string( $request->get_param( 'search' ) );
		$category_id = $this->sanitize_request_string( $request->get_param( 'categoryId' ) );

		if ( ! $library_id ) {
			return $this->error( 404, "Library of id \"$library_id\" was not found." );
		}

		$libraries = GenerateBlocks_Libraries::get_instance();
		$library = $libraries->get_one( $library_id );

		if ( ! is_null( $library ) ) {
			return self::remote_fetch(
				$library,
				'patterns',
				array(
					'search' => $search,
					'categoryId' => $category_id,
				)
			);
		}

		return $this->error( 404, "Library of id \"$library_id\" was not found." );
	}

	/**
	 * Fetch pattern library remotely.
	 *
	 * @param GenerateBlocks_Library_DTO $library The library to fetch data.
	 * @param string                     $collection The collection type. Either 'categories' or 'patterns'.
	 * @param array                      $query_args The extra query arguments.
	 *
	 * @return WP_REST_Response
	 */
	private function remote_fetch(
		GenerateBlocks_Library_DTO $library,
		string $collection,
		array $query_args = array()
	): WP_REST_Response {
		if ( ! in_array( $collection, array( 'categories', 'patterns' ), true ) ) {
			return $this->error( 400, __( 'Invalid library collection.', 'generateblocks' ) );
		}

		$domain = esc_url_raw( $library->domain );

		if ( ! $domain ) {
			return $this->error( 400, __( 'Invalid library domain.', 'generateblocks' ) );
		}

		$endpoint = trailingslashit( $domain ) . "wp-json/generateblocks-pro/v1/pattern-library/$collection";
		$url = add_query_arg( $query_args, $endpoint );
		$cache_key = $library->id . '-' . $collection;
		$cache = GenerateBlocks_Libraries::get_cached_data( $cache_key, $query_args, $collection );

		if ( false !== $cache ) {
			return $this->success( $cache );
		}

		$request = wp_remote_get(
			$url,
			array(
				'headers' => array(
					'X-GB-Public-Key' => $library->public_key,
				),
				'timeout' => 15,
			)
		);

		if ( is_wp_error( $request ) ) {
			return $this->error( 500, "Unable to request from $endpoint" );
		}

		$response_code = (int) wp_remote_retrieve_response_code( $request );

		if ( 200 !== $response_code ) {
			return $this->error( $response_code ? $response_code : 500, "Unable to request from $endpoint" );
		}

		$body = wp_remote_retrieve_body( $request );
		$body = json_decode( $body, true );

		if ( ! is_array( $body ) ) {
			return $this->error( 500, __( 'Invalid library response.', 'generateblocks' ) );
		}

		$data = $body['response']['data'] ?? [];

		if ( ! is_array( $data ) ) {
			return $this->error( 500, __( 'Invalid library response data.', 'generateblocks' ) );
		}

		// Cache our data.
		GenerateBlocks_Libraries::set_cached_data( $data, $cache_key, $collection );

		return $this->success( $data );
	}

	/**
	 * Get the expiry time of a cache.
	 *
	 * @param WP_REST_Request $request The request.
	 *
	 * @return WP_REST_Response
	 */
	public function get_cache_data( WP_REST_Request $request ): WP_REST_Response {
		$id = $this->sanitize_request_string( $request->get_param( 'id' ) );

		if ( ! $id ) {
			return $this->error( 400, __( 'Invalid library id.', 'generateblocks' ) );
		}

		if ( is_null( GenerateBlocks_Libraries::get_instance()->get_one( $id ) ) ) {
			return $this->error( 404, __( 'Library not found.', 'generateblocks' ) );
		}

		$expiration_time = get_option( '_transient_timeout_' . $id . '-patterns_0' );

		if ( ! $expiration_time ) {
			return $this->failed( 'no_cache' );
		}

		$current_time = time();
		$cache_made_time = $expiration_time - GenerateBlocks_Libraries::get_cache_expiry();
		$can_clear_cache = $current_time > ( $cache_made_time + 300 );

		return $this->success(
			[
				'expiry_time_raw' => $expiration_time,
				'expiry_time' => gmdate( 'Y-m-d H:i:s', $expiration_time ),
				'can_clear' => $can_clear_cache,
			]
		);
	}

	/**
	 * Clear caches for a specific collection.
	 *
	 * @param WP_REST_Request $request The request.
	 *
	 * @return WP_REST_Response
	 */
	public function clear_cache( WP_REST_Request $request ): WP_REST_Response {
		$id = $this->sanitize_request_string( $request->get_param( 'id' ) );

		if ( ! $id ) {
			return $this->error( 400, __( 'Invalid library id.', 'generateblocks' ) );
		}

		if ( is_null( GenerateBlocks_Libraries::get_instance()->get_one( $id ) ) ) {
			return $this->error( 404, __( 'Library not found.', 'generateblocks' ) );
		}

		GenerateBlocks_Libraries::delete_cached_data( $id . '-categories' );
		GenerateBlocks_Libraries::delete_cached_data( $id . '-patterns', 'patterns' );
		GenerateBlocks_Libraries::delete_cached_data( $id . '_global-style-data' );

		return $this->success( [] );
	}

	/**
	 * Sanitize scalar REST request values.
	 *
	 * @param mixed $value The value to sanitize.
	 * @return string
	 */
	private function sanitize_request_string( $value ): string {
		if ( ! is_scalar( $value ) ) {
			return '';
		}

		return sanitize_text_field( (string) $value );
	}

	/**
	 * Sanitize scalar REST request URLs.
	 *
	 * @param mixed $value The value to sanitize.
	 * @return string
	 */
	private function sanitize_request_url( $value ): string {
		if ( ! is_string( $value ) ) {
			return '';
		}

		return esc_url_raw( $value, array( 'http', 'https' ) );
	}

	/**
	 * Sanitize a saved library row.
	 *
	 * @param mixed $library The library data.
	 * @return array
	 */
	private function sanitize_library_data( $library ): array {
		if ( ! is_array( $library ) ) {
			return [];
		}

		$id = $this->sanitize_request_string( $library['id'] ?? '' );

		if ( ! $id ) {
			return [];
		}

		$is_local = rest_sanitize_boolean( $library['isLocal'] ?? false );
		$is_default = rest_sanitize_boolean( $library['isDefault'] ?? false );
		$is_enabled = rest_sanitize_boolean( $library['isEnabled'] ?? false );

		if ( ! $is_local && ! $is_default ) {
			$domain = $this->sanitize_request_url( $library['domain'] ?? '' );

			if ( ! $domain ) {
				return [];
			}

			// Save all data if this is a remote library.
			return [
				'id' => $id,
				'name' => $this->sanitize_request_string( $library['name'] ?? '' ),
				'domain' => $domain,
				'publicKey' => $this->sanitize_request_string( $library['publicKey'] ?? '' ),
				'isEnabled' => $is_enabled,
				'isDefault' => $is_default,
				'isLocal' => $is_local,
			];
		}

		// Only save the ID and status for local and default libraries.
		// The rest of the data will be supplied via the PHP filter.
		return [
			'id' => $id,
			'isEnabled' => $is_enabled,
		];
	}

	/**
	 * Returns a success response.
	 *
	 * @param array $data The data.
	 *
	 * @return WP_REST_Response
	 */
	private function success( array $data ): WP_REST_Response {
		return new WP_REST_Response(
			array(
				'success'  => true,
				'response' => array(
					'data' => $data,
				),
			),
			200
		);
	}

	/**
	 * Returns a success response.
	 *
	 * @param string $message The error message.
	 *
	 * @return WP_REST_Response
	 */
	private function failed( string $message ): WP_REST_Response {
		return new WP_REST_Response(
			array(
				'success'  => false,
				'response' => $message,
			),
			200
		);
	}

	/**
	 * Returns a error response.
	 *
	 * @param int    $code Error code.
	 * @param string $message Error message.
	 *
	 * @return WP_REST_Response
	 */
	private function error( int $code, string $message ): WP_REST_Response {
		return new WP_REST_Response(
			array(
				'error'      => true,
				'success'    => false,
				'error_code' => $code,
				'response'   => $message,
			),
			$code
		);
	}
}

GenerateBlocks_Pattern_Library_Rest::get_instance()->init();
