<?php
namespace FortAwesome;

require_once trailingslashit( FONTAWESOME_DIR_PATH ) . 'includes/class-fontawesome-exception.php';
require_once trailingslashit( FONTAWESOME_DIR_PATH ) . 'includes/class-fontawesome-rest-response.php';

use WP_REST_Controller, WP_Error, Error, Exception;

/**
 * REST Controller for managing data on the FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY.
 *
 * Internal use only, not part of this plugin's public API.
 *
 * @ignore
 * @internal
 */
class FontAwesome_Conflict_Detection_Controller extends WP_REST_Controller {
	// phpcs:ignore Generic.Commenting.DocComment.MissingShort
	/**
	 * @ignore
	 */
	private $plugin_slug = null;

	// phpcs:ignore Generic.Commenting.DocComment.MissingShort
	/**
	 * @ignore
	 */
	protected $namespace = null;

	// phpcs:ignore Generic.Commenting.DocComment.MissingShort
	/**
	 * @ignore
	 */
	protected $valid_attrs = array( 'type', 'technology', 'href', 'src', 'innerText', 'tagName' );

	// phpcs:ignore Generic.Commenting.DocComment.MissingShort
	/**
	 * @ignore
	 */
	public function __construct( $plugin_slug, $rest_namespace ) {
		$this->plugin_slug = $plugin_slug;
		$this->namespace   = $rest_namespace;
	}

	// phpcs:ignore Generic.Commenting.DocComment.MissingShort
	/**
	 * @ignore
	 */
	public function register_routes() {
		$route_base = 'conflict-detection';

		register_rest_route(
			$this->namespace,
			'/' . $route_base . '/until',
			array(
				array(
					'methods'             => 'POST',
					'callback'            => array( $this, 'update_detect_conflicts_until' ),
					'permission_callback' => function () {
						return current_user_can( 'manage_options' ); },
					'args'                => array(),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $route_base . '/conflicts',
			array(
				array(
					'methods'             => 'POST',
					'callback'            => array( $this, 'report_conflicts' ),
					'permission_callback' => function () {
						return current_user_can( 'manage_options' ); },
					'args'                => array(),
				),
			)
		);

		register_rest_route(
			$this->namespace,
			'/' . $route_base . '/conflicts',
			array(
				array(
					'methods'             => 'DELETE',
					'callback'            => array( $this, 'delete_conflicts' ),
					'permission_callback' => function () {
						return current_user_can( 'manage_options' ); },
					'args'                => array(),
				),
			)
		);

		/**
		 * The given blocklist, an array of md5s, will become the new blocklist,
		 * for each md5 in the given array that already exists as a key in
		 * the unregisteredClients array.
		 */
		register_rest_route(
			$this->namespace,
			'/' . $route_base . '/conflicts/blocklist',
			array(
				array(
					'methods'             => 'POST',
					'callback'            => array( $this, 'update_blocklist' ),
					'permission_callback' => function () {
						return current_user_can( 'manage_options' ); },
					'args'                => array(),
				),
			)
		);
	}

	/**
	 * Report conflicts. Adds and/or updates unregisteredClients
	 *
	 * The response will have an HTTP 204 status if the request results in no changes.
	 * If changes are made, the response will have an HTTP 200 status, and
	 * the response body will include just the new status of the
	 * unregisteredClients (not the entire conflict-detection option data).
	 *
	 * If the plugin is not currently in conflict detection mode, this
	 * returns an HTTP 404 status.
	 *
	 * @param WP_REST_Request $request the request.
	 * @return FontAwesome_REST_Response
	 */
	// phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing
	public function report_conflicts( $request ) {
		try {
			if ( ! fa()->detecting_conflicts() ) {
				return new FontAwesome_REST_Response( null, 404 );
			}

			$item = $this->prepare_unregistered_clients_for_database( $request );

			$prev_option = get_option(
				FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY,
				FontAwesome::DEFAULT_CONFLICT_DETECTION_OPTIONS
			);

			$prev_option_unregistered_clients = (
				isset( $prev_option['unregisteredClients'] )
				&& is_array( $prev_option['unregisteredClients'] )
			)
				? $prev_option['unregisteredClients']
				: array();

			$new_option_unregistered_clients = array_merge(
				$prev_option_unregistered_clients,
				$item
			);

			if ( $this->unregistered_clients_array_has_changes( $prev_option_unregistered_clients, $new_option_unregistered_clients ) ) {
				// Update only the unregisteredClients key, leaving any other keys unchanged.
				$new_option_value = array_merge(
					$prev_option,
					array(
						'unregisteredClients' => $new_option_unregistered_clients,
					)
				);

				if ( update_option( FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY, $new_option_value ) ) {
					return new FontAwesome_REST_Response( $new_option_unregistered_clients, 200 );
				} else {
					throw new ConflictDetectionStorageException();
				}
			} else {
				// No change.
				return new FontAwesome_REST_Response( null, 204 );
			}
		} catch ( FontAwesome_ServerException $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_server_exception( $e ), 500 );
		} catch ( FontAwesome_Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_client_exception( $e ), 400 );
		} catch ( Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		} catch ( Error $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		}
	}

	/**
	 * Remove (forget) some specified previously detected conflicts.
	 *
	 * The request body should contain an array of md5 values to be
	 * deleted. Any unrecognized md5s are ignored.
	 *
	 * The response will have an HTTP 204 status if the request results in no changes,
	 * otherwise it will include the new set of unregistered clients.
	 *
	 * If the plugin is not currently in conflict detection mode, this
	 * returns an HTTP 404 status.
	 *
	 * @param WP_REST_Request $request the request.
	 * @return FontAwesome_REST_Response
	 */
	// phpcs:ignore Squiz.Commenting.FunctionCommentThrowTag.Missing
	public function delete_conflicts( $request ) {
		try {
			$body = $request->get_json_params();

			if ( ! $this->is_array_of_md5( $body ) ) {
				throw new ConflictDetectionSchemaException();
			}

			$prev_option = get_option(
				FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY,
				FontAwesome::DEFAULT_CONFLICT_DETECTION_OPTIONS
			);

			$prev_option_unregistered_clients = (
				isset( $prev_option['unregisteredClients'] )
				&& is_array( $prev_option['unregisteredClients'] )
			)
				? $prev_option['unregisteredClients']
				: array();

			// Make a copy.
			$new_option_unregistered_clients = array_merge(
				array(),
				$prev_option_unregistered_clients
			);

			foreach ( $body as $md5 ) {
				unset( $new_option_unregistered_clients[ $md5 ] );
			}

			if ( $this->unregistered_clients_array_has_changes( $prev_option_unregistered_clients, $new_option_unregistered_clients ) ) {
				// Update only the unregisteredClients key, leaving any other keys unchanged.
				$new_option_value = array_merge(
					$prev_option,
					array(
						'unregisteredClients' => $new_option_unregistered_clients,
					)
				);

				if ( update_option( FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY, $new_option_value ) ) {
					return new FontAwesome_REST_Response( $new_option_unregistered_clients, 200 );
				} else {
					throw new ConflictDetectionStorageException();
				}
			} else {
				// No change.
				return new FontAwesome_REST_Response( null, 204 );
			}
		} catch ( FontAwesome_ServerException $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_server_exception( $e ), 500 );
		} catch ( FontAwesome_Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_client_exception( $e ), 400 );
		} catch ( Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		} catch ( Error $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		}
	}

	/**
	 * Update the value of detectConflictsUntil to start/stop conflict detection.
	 *
	 * @throws ConflictDetectionSchemaException
	 * @throws ConflictDetectionStorageException
	 * @param WP_REST_Request $request the request.
	 * @return FontAwesome_REST_Response
	 */
	public function update_detect_conflicts_until( $request ) {
		try {
			$body = $request->get_body();

			$new_value = intval( $body );

			if ( 0 === $new_value && '0' !== $body ) {
				throw new ConflictDetectionSchemaException();
			}

			$prev_option = get_option(
				FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY,
				FontAwesome::DEFAULT_CONFLICT_DETECTION_OPTIONS
			);

			$prev_option_detect_conflicts_until = (
				isset( $prev_option['detectConflictsUntil'] ) &&
				is_integer( $prev_option['detectConflictsUntil'] )
			)
				? $prev_option['detectConflictsUntil']
				: null;

			if ( $prev_option_detect_conflicts_until !== $new_value ) {
				// Update only the detectConflictsUntil key, leaving any other keys unchanged.
				$new_option_value = array_merge(
					$prev_option,
					array( 'detectConflictsUntil' => $new_value )
				);

				if ( update_option( FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY, $new_option_value ) ) {
					return new FontAwesome_REST_Response( array( 'detectConflictsUntil' => $new_value ), 200 );
				} else {
					throw new ConflictDetectionStorageException();
				}
			} else {
				// No change.
				return new FontAwesome_REST_Response( null, 204 );
			}
		} catch ( FontAwesome_ServerException $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_server_exception( $e ), 500 );
		} catch ( FontAwesome_Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_client_exception( $e ), 400 );
		} catch ( Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		} catch ( Error $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		}
	}

	/**
	 * Updates which unregistered will be blocked.
	 *
	 * The body of the request is an array of md5 values identifying those
	 * unregistered clients that will be blocked as a result of this request.
	 * Any unrecognized md5s will be ignored.
	 *
	 * Internal use only, not part of this plugin's public API.
	 *
	 * @throws ConflictDetectionSchemaException
	 * @throws ConflictDetectionStorageException
	 * @ignore
	 * @internal
	 */
	public function update_blocklist( $request ) {
		try {
			$body = $request->get_json_params();

			if ( ! $this->is_array_of_md5( $body ) ) {
				throw new ConflictDetectionSchemaException();
			}

			$prev_option = get_option(
				FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY,
				FontAwesome::DEFAULT_CONFLICT_DETECTION_OPTIONS
			);

			$prev_option_unregistered_clients = (
				isset( $prev_option['unregisteredClients'] )
				&& is_array( $prev_option['unregisteredClients'] )
			)
				? $prev_option['unregisteredClients']
				: array();

			// Make a copy.
			$new_option_unregistered_clients = array_merge(
				array(),
				$prev_option_unregistered_clients
			);

			foreach ( array_keys( $new_option_unregistered_clients ) as $md5 ) {
				if ( in_array( $md5, $body, true ) ) {
					$new_option_unregistered_clients[ $md5 ]['blocked'] = true;
				} else {
					$new_option_unregistered_clients[ $md5 ]['blocked'] = false;
				}
			}

			if ( $this->unregistered_clients_array_has_changes( $prev_option_unregistered_clients, $new_option_unregistered_clients ) ) {
				// Update only the unregisteredClients key, leaving any other keys unchanged.
				$new_option_value = array_merge(
					$prev_option,
					array(
						'unregisteredClients' => $new_option_unregistered_clients,
					)
				);

				if ( update_option( FontAwesome::CONFLICT_DETECTION_OPTIONS_KEY, $new_option_value ) ) {
					return new FontAwesome_REST_Response( fa()->blocklist(), 200 );
				} else {
					throw new ConflictDetectionStorageException();
				}
			} else {
				// No change.
				return new FontAwesome_REST_Response( null, 204 );
			}
		} catch ( FontAwesome_ServerException $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_server_exception( $e ), 500 );
		} catch ( FontAwesome_Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_client_exception( $e ), 400 );
		} catch ( Exception $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		} catch ( Error $e ) {
			return new FontAwesome_REST_Response( wpe_fontawesome_unknown_error( $e ), 500 );
		}
	}

	/**
	 * Reads a json body of the given Request, validates it, and turns it
	 * into a valid value for the unregisteredClients key of the conflict detection
	 * option.
	 *
	 * @internal
	 * @ignore
	 * @throws ConflictDetectionSchemaException
	 * @return array
	 */
	protected function prepare_unregistered_clients_for_database( $request ) {
		$body = $request->get_json_params();

		if ( ! \is_array( $body ) || count( $body ) === 0 ) {
			throw new ConflictDetectionSchemaException();
		}

		$validated = array();

		foreach ( $body as $md5 => $attrs ) {
			if ( ! is_string( $md5 ) || ! strlen( $md5 ) === 32 ) {
				throw new ConflictDetectionSchemaException();
			}

			if ( ! is_array( $attrs ) ) {
				throw new ConflictDetectionSchemaException();
			}

			$validated[ $md5 ] = array();

			foreach ( $attrs as $key => $value ) {
				if ( in_array( $key, $this->valid_attrs, true ) ) {
					$validated[ $md5 ][ $key ] = $value;
				}
			}
		}

		return $validated;
	}

	protected function unregistered_clients_array_has_changes( $old_value, $new_value ) {
		if ( ! is_array( $old_value ) ) {
			return true;
		}

		if ( count( array_diff_key( $old_value, $new_value ) ) > 0 || count( array_diff_key( $new_value, $old_value ) ) > 0 ) {
			return true;
		} else {
			foreach ( $old_value as $key => $value ) {
				if ( count( array_diff_assoc( $old_value[ $key ], $new_value[ $key ] ) ) > 0 ) {
					return true;
				}
			}
			foreach ( $new_value as $key => $value ) {
				if ( count( array_diff_assoc( $new_value[ $key ], $old_value[ $key ] ) ) > 0 ) {
					return true;
				}
			}
			return false;
		}
	}

	protected function is_array_of_md5( $data ) {
		return \is_array( $data ) &&
			(
			count( $data ) === 0 ||
			(
				0 === count(
					array_filter(
						$data,
						function ( $md5 ) {
							return ! is_string( $md5 ) || strlen( $md5 ) !== 32;
						}
					)
				)
					)
			);
	}
}
