<?php
/**
 * Formidable data REST endpoints.
 *
 * @package EntryFoundry
 */

namespace EntryFoundry\Core\Controllers\Rest;

use EntryFoundry\Core\Services\Security\RestPermissionService;
use EntryFoundry\Formidable\FormidableAccessPolicy;
use EntryFoundry\Formidable\FormidableData;
use RuntimeException;

/**
 * Exposes neutral Formidable data to the builder.
 */
final class FormidableRestController {

	/**
	 * REST API namespace.
	 */
	private const REST_NAMESPACE = 'entryfoundry/v1';

	/**
	 * Formidable data model.
	 *
	 * @var FormidableData
	 */
	private $data;

	/**
	 * REST authorization service.
	 *
	 * @var RestPermissionService
	 */
	private $permissions;

	/**
	 * Formidable resource access policy.
	 *
	 * @var FormidableAccessPolicy
	 */
	private $access_policy;

	/**
	 * Constructor.
	 *
	 * @param FormidableData              $data          Formidable data model.
	 * @param RestPermissionService       $permissions   REST authorization service.
	 * @param FormidableAccessPolicy|null $access_policy Formidable resource access policy.
	 */
	public function __construct( FormidableData $data, RestPermissionService $permissions, ?FormidableAccessPolicy $access_policy = null ) {
		$this->data          = $data;
		$this->permissions   = $permissions;
		$this->access_policy = $access_policy ?? new FormidableAccessPolicy();
	}

	/**
	 * Register Phase 1 builder routes.
	 *
	 * @return void
	 */
	public function register_routes(): void {
		register_rest_route(
			self::REST_NAMESPACE,
			'/sources',
			$this->get_route_args( 'get_sources', 'forms' )
		);
		register_rest_route(
			self::REST_NAMESPACE,
			'/sources/(?P<source_id>[\d]+)/fields',
			$this->get_route_args( 'get_fields', 'forms', true )
		);
		register_rest_route(
			self::REST_NAMESPACE,
			'/sources/(?P<source_id>[\d]+)/entries',
			$this->get_route_args( 'get_entries', 'entries', true )
		);
		register_rest_route(
			self::REST_NAMESPACE,
			'/sources/(?P<source_id>[\d]+)/entries/(?P<entry_id>[\d]+)',
			$this->get_route_args( 'get_entry_values', 'entries', true, true )
		);
		register_rest_route(
			self::REST_NAMESPACE,
			'/sources/(?P<source_id>[\d]+)/entries/(?P<entry_id>[\d]+)/display-values',
			$this->get_route_args( 'get_entry_display_values', 'entries', true, true )
		);
		register_rest_route(
			self::REST_NAMESPACE,
			'/sources/(?P<source_id>[\d]+)/preview',
			$this->get_route_args( 'get_preview_entry', 'entries', true )
		);
	}

	/**
	 * Authorize a route request.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return true|\WP_Error
	 */
	public function authorize_forms( \WP_REST_Request $request ) {
		$authentication = $this->permissions->authorize( $request, 'frm_view_forms' );

		return is_wp_error( $authentication )
			? $authentication
			: $this->access_policy->authorize_form_request( $request );
	}

	/**
	 * Authorize a route that exposes Formidable entries.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return true|\WP_Error
	 */
	public function authorize_entries( \WP_REST_Request $request ) {
		$authentication = $this->permissions->authorize( $request, 'frm_view_entries' );

		return is_wp_error( $authentication )
			? $authentication
			: $this->access_policy->authorize_entry_request( $request );
	}

	/**
	 * Return all Formidable sources.
	 *
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_sources() {
		return $this->respond(
			function () {
				return $this->data->get_sources();
			}
		);
	}

	/**
	 * Return fields for a source.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_fields( \WP_REST_Request $request ) {
		return $this->respond(
			function () use ( $request ) {
				return $this->data->get_fields( (int) $request['source_id'] );
			}
		);
	}

	/**
	 * Return values for one source entry.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_entry_values( \WP_REST_Request $request ) {
		return $this->respond(
			function () use ( $request ) {
				return $this->data->get_entry_values(
					(int) $request['source_id'],
					(int) $request['entry_id']
				);
			}
		);
	}

	/**
	 * Return Formidable-formatted values for one source entry.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_entry_display_values( \WP_REST_Request $request ) {
		return $this->respond(
			function () use ( $request ) {
				return $this->data->get_entry_display_values(
					(int) $request['source_id'],
					(int) $request['entry_id']
				);
			}
		);
	}

	/**
	 * Return recent entries for a source.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_entries( \WP_REST_Request $request ) {
		return $this->respond(
			function () use ( $request ) {
				return $this->data->get_entries( (int) $request['source_id'] );
			}
		);
	}

	/**
	 * Return the most recent preview entry for a source.
	 *
	 * @param \WP_REST_Request $request REST request.
	 * @return \WP_REST_Response|\WP_Error
	 */
	public function get_preview_entry( \WP_REST_Request $request ) {
		return $this->respond(
			function () use ( $request ) {
				return $this->data->get_preview_entry( (int) $request['source_id'] );
			}
		);
	}

	/**
	 * Build common REST route arguments.
	 *
	 * @param string $callback        Controller callback method.
	 * @param string $access_scope    Either forms or entries.
	 * @param bool   $has_source_id   Whether the route has a source ID.
	 * @param bool   $has_entry_id    Whether the route has an entry ID.
	 * @return array<string, mixed>
	 */
	private function get_route_args( string $callback, string $access_scope, bool $has_source_id = false, bool $has_entry_id = false ): array {
		$args = array(
			'methods'             => \WP_REST_Server::READABLE,
			'callback'            => array( $this, $callback ),
			'permission_callback' => array( $this, 'entries' === $access_scope ? 'authorize_entries' : 'authorize_forms' ),
		);

		if ( $has_source_id ) {
			$args['args']['source_id'] = $this->get_id_schema();
		}

		if ( $has_entry_id ) {
			$args['args']['entry_id'] = $this->get_id_schema();
		}

		return $args;
	}

	/**
	 * Get the route schema shared by numeric IDs.
	 *
	 * @return array<string, mixed>
	 */
	private function get_id_schema(): array {
		return array(
			'required'          => true,
			'sanitize_callback' => 'absint',
			'validate_callback' => static function ( $value ) {
				return is_numeric( $value ) && (int) $value > 0;
			},
		);
	}

	/**
	 * Convert model failures into a stable REST error.
	 *
	 * @param callable $callback Data callback.
	 * @return \WP_REST_Response|\WP_Error
	 */
	private function respond( callable $callback ) {
		try {
			return rest_ensure_response( $callback() );
		} catch ( RuntimeException $exception ) {
			return new \WP_Error(
				'entryfoundry_formidable_unavailable',
				__( 'Formidable Forms is unavailable, so EntryFoundry cannot load form data.', 'entryfoundry-pdf-builder-for-formidable-forms' ),
				array( 'status' => 503 )
			);
		}
	}
}
