<?php
/**
 * API Route
 *
 * @package WordPressPluginBoilerplate\API
 * @since 1.0.0
 */

namespace FleekDash\Libs\API;

use FleekDash\Libs\API\ApiRouteException;
use FleekDash\Libs\API\RouteAuthMiddleware;

/**
 * Class Route
 *
 * Handles API route registration for the FleekDash plugin.
 *
 * @package FleekDash\Libs\API
 */
class Route
{
	/**
	 * @var string The base namespace for the API
	 */
	private static $namespace = '';

	/**
	 * All registerd routes.
	 *
	 * @var array
	 */
	private static $routes = array();

	/**
	 * Store current route details.
	 *
	 * @var array
	 */
	private $current;

	/**
	 * Get singleton instance
	 *
	 * @return self
	 */
	public static function get_instance()
	{
		static $instance = null;
		if ($instance === null) {
			$instance = new self();
		}
		return $instance;
	}

	/**
	 * Register a GET route
	 *
	 * @param string $endpoint The endpoint path
	 * @param callable|array $callback The callback function
	 * @param bool $auth Whether authentication is required
	 * @param array $args Arguments schema
	 * @return void
	 */
	public static function get($endpoint, $callback, $auth = false, $args = [])
	{
		self::register('GET', $endpoint, $callback, $auth, $args);
	}

	/**
	 * Register a POST route
	 *
	 * @param string $endpoint The endpoint path
	 * @param callable|array $callback The callback function
	 * @param bool $auth Whether authentication is required
	 * @param array $args Arguments schema
	 * @return void
	 */
	public static function post($endpoint, $callback, $auth = false, $args = [])
	{
		self::register('POST', $endpoint, $callback, $auth, $args);
	}

	/**
	 * Register a PUT route
	 *
	 * @param string $endpoint The endpoint path
	 * @param callable|array $callback The callback function
	 * @param bool $auth Whether authentication is required
	 * @param array $args Arguments schema
	 * @return void
	 */
	public static function put($endpoint, $callback, $auth = false, $args = [])
	{
		self::register('PUT', $endpoint, $callback, $auth, $args);
	}

	/**
	 * Register a DELETE route
	 *
	 * @param string $endpoint The endpoint path
	 * @param callable|array $callback The callback function
	 * @param bool $auth Whether authentication is required
	 * @param array $args Arguments schema
	 * @return void
	 */
	public static function delete($endpoint, $callback, $auth = false, $args = [])
	{
		self::register('DELETE', $endpoint, $callback, $auth, $args);
	}

	/**
	 * Group routes under a prefix
	 *
	 * @param string $prefix The prefix for the routes
	 * @param callable $callback The callback containing the routes
	 * @return void
	 */
	public static function prefix($prefix, $callback)
	{
		$previous = self::$namespace;
		self::$namespace = empty(self::$namespace) ? $prefix : trim(self::$namespace . '/' . trim($prefix, '/'), '/');

		call_user_func($callback, new static());

		self::$namespace = $previous;
	}

	/**
	 * Convert HTTP method string to WordPress REST Server constant
	 *
	 * @param string $method HTTP method string (GET, POST, PUT, DELETE, PATCH)
	 * @return string|int WordPress REST Server constant or method string
	 */
	private static function normalize_method($method)
	{
		$method = strtoupper($method);

		switch ($method) {
			case 'GET':
				return \WP_REST_Server::READABLE;
			case 'POST':
				return \WP_REST_Server::CREATABLE;
			case 'PUT':
			case 'PATCH':
				return \WP_REST_Server::EDITABLE;
			case 'DELETE':
				return \WP_REST_Server::DELETABLE;
			default:
				return $method;
		}
	}

	/**
	 * Register a route with WordPress
	 *
	 * @param string $method The HTTP method
	 * @param string $endpoint The endpoint path
	 * @param callable|array $callback The callback function
	 * @param callable $auth Authentication requirement (RouteAuthMiddleware callable)
	 * @param array $args Arguments schema
	 * @return void
	 */
	private static function register($method, $endpoint, $callback, $auth, $args = [])
	{
		$namespace = self::$namespace;
		$endpoint = trim($endpoint, '/');

		// Ensure namespace is set, fallback to FLEEKDASH_API_NAMESPACE if not
		if (empty($namespace)) {
			$namespace = defined('FLEEKDASH_API_NAMESPACE') ? FLEEKDASH_API_NAMESPACE : 'fleekdash/v1';
			self::$namespace = $namespace;
		}

		if (!empty($endpoint)) {
			// Convert {id} to (?P<id>\d+) for numeric params, {type} to (?P<type>[a-zA-Z0-9_-]+) for string params
			$endpoint = preg_replace('/\{(id|ID)\}/', '(?P<$1>\d+)', $endpoint);
			$endpoint = preg_replace('/\{([a-zA-Z0-9_]+)\}/', '(?P<$1>[a-zA-Z0-9_-]+)', $endpoint);
			// WordPress register_rest_route() expects route WITHOUT leading slash
			// It automatically adds the slash between namespace and route
			// So we keep endpoint without leading slash to avoid double slashes
			$route_endpoint = $endpoint;
		} else {
			// WordPress requires a non-empty route; use root '/'
			$route_endpoint = '/';
		}

		// Determine permission callback
		$permission_callback = self::resolve_permission_callback($auth);

		// Normalize HTTP method to WordPress REST Server constant
		$wp_method = self::normalize_method($method);

		// DEBUG: Log route registration in debug mode
		// Disabled to reduce verbosity in debug.log
		// if (defined('WP_DEBUG') && WP_DEBUG) {
		// 	error_log(sprintf('FleekDash: Registering route %s %s/%s', $method, $namespace, $route_endpoint));
		// }

		register_rest_route($namespace, $route_endpoint, [
			'methods' => $wp_method,
			'callback' => self::resolve_callback($callback),
			'permission_callback' => $permission_callback,
			'args' => $args
		]);
	}

	/**
	 * Resolve permission callback from auth parameter
	 *
	 * @param callable $auth Authentication requirement (RouteAuthMiddleware callable)
	 * @return callable Permission callback
	 */
	private static function resolve_permission_callback($auth)
	{
		// RouteAuthMiddleware callable
		if (is_callable($auth)) {
			return $auth;
		}

		// Default: public route
		return RouteAuthMiddleware::public();
	}

	/**
	 * Resolve the callback
	 *
	 * @param callable|array $callback The callback
	 * @return callable
	 */
	private static function resolve_callback($callback)
	{
		if (is_callable($callback)) {
			return $callback;
		}

		if (is_array($callback) && count($callback) === 2) {
			list($class, $method) = $callback;
			if (is_string($class)) {
				$instance = new $class();
				return [$instance, $method];
			}
		}

		if (is_string($callback) && strpos($callback, '@') !== false) {
			list($class, $method) = explode('@', $callback);
			$instance = new $class();
			return [$instance, $method];
		}

		return $callback;
	}

	/**
	 * Statically called method handle
	 *
	 * @since 1.0.0
	 *
	 * @param string $name function name.
	 * @param array  $arguments arguments.
	 *
	 * @return self
	 */
	public static function __callStatic($name, $arguments)
	{
		$self = new self();
		$count = count($arguments);

		if ('get' === $name && in_array($count, array(3, 4), true)) {
			$self->get_with_namespace(...$arguments);
		}

		if ('post' === $name && in_array($count, array(3, 4), true)) {
			$self->post_with_namespace(...$arguments);
		}

		if ('prefix' === $name && 1 === $count) {
			return $self->prefix_one(...$arguments);
		}

		if ('prefix' === $name && 2 === $count) {
			return $self->prefix_two(...$arguments);
		}
	}

	/**
	 * Set default namespace for class resolve.
	 *
	 * @since 1.0.0
	 *
	 * @param string $namespace namespace.
	 * @return void
	 */
	public static function set_class_namespace($namespace)
	{
		self::$namespace = $namespace;
	}

	/**
	 * Get all registered routes.
	 *
	 * @since 1.0.0
	 *
	 * @return array
	 */
	public static function get_routes()
	{
		return self::$routes;
	}

	/**
	 * Prepare route data.
	 *
	 * @since 1.0.0
	 *
	 * @param string               $method route method GET, POST.
	 * @param string               $prefix route prefix.
	 * @param string               $endpoint route endpoint.
	 * @param string               $callback callback.
	 * @param bool|string|callable $auth auth callback.
	 *
	 * @return array
	 */
	private function prepare_route_data($method, $prefix, $endpoint, $callback, $auth = false)
	{
		$data = array(
			'prefix' => $prefix,
			'method' => $method,
			'endpoint' => $endpoint,
			'callback' => $callback,
			'auth' => $auth,
		);

		if (isset($this->current['group'])) {
			$data['group'] = $this->current['group'];
		}

		return $data;
	}

	/**
	 * Get route register.
	 *
	 * @since 1.0.0
	 *
	 * @param string $endpoint api endpint.
	 * @param mixed  $callback callback function.
	 * @param mixed  $auth auth check logic.
	 *
	 * @return void
	 */
	private function get_normal($endpoint, $callback, $auth = false)
	{
		$prefix = $this->current['prefix'];
		$route = $this->prepare_route_data('GET', $prefix, $endpoint, $callback, $auth);

		self::$routes[$prefix][] = $route;
	}

	/**
	 * Get route with namespace.
	 *
	 * @since 1.0.0
	 *
	 * @param string $prefix prefix.
	 * @param string $endpoint api endpint.
	 * @param mixed  $callback callback function.
	 * @param mixed  $auth auth check logic.
	 *
	 * @return void
	 */
	private function get_with_namespace($prefix, $endpoint, $callback, $auth = false)
	{
		self::$routes[$prefix][] = $this->prepare_route_data('GET', $prefix, $endpoint, $callback, $auth);
	}

	/**
	 * Post route register.
	 *
	 * @since 1.0.0
	 *
	 * @param string $endpoint api endpint.
	 * @param mixed  $callback callback function.
	 * @param mixed  $auth auth check logic.
	 *
	 * @return void
	 */
	public function post_normal($endpoint, $callback, $auth = false)
	{
		$prefix = $this->current['prefix'];
		$route = $this->prepare_route_data('POST', $prefix, $endpoint, $callback, $auth);

		self::$routes[$prefix][] = $route;
	}

	/**
	 * Post route with namespace.
	 *
	 * @since 1.0.0
	 *
	 * @param string $prefix prefix.
	 * @param string $endpoint api endpint.
	 * @param mixed  $callback callback function.
	 * @param mixed  $auth auth check logic.
	 *
	 * @return void
	 */
	private function post_with_namespace($prefix, $endpoint, $callback, $auth = false)
	{
		self::$routes[$prefix][] = $this->prepare_route_data('POST', $prefix, $endpoint, $callback, $auth);
	}

	/**
	 * Set prefix for a route.
	 *
	 * @since 1.0.0
	 *
	 * @param string $name prefix name.
	 *
	 * @return self
	 */
	private function prefix_one($name)
	{
		$this->current['prefix'] = $name;
		return $this;
	}

	/**
	 * Register routes in a prefix.
	 *
	 * @since 1.0.0
	 *
	 * @param string   $name prefix name.
	 * @param callable $callback callback.
	 *
	 * @return self
	 *
	 * @throws ApiRouteException If invalid callback provided.
	 */
	private function prefix_two($name, $callback)
	{
		if (!is_callable($callback)) {
			throw new ApiRouteException('Invalid callback for prefix');
		}

		$this->current['prefix'] = $name;
		$rand = function_exists('wp_rand') ? \call_user_func('wp_rand', 0, 999) : rand(0, 999);
		$this->current['group'] = (int) (microtime(true) * 1000) + (int) $rand;

		/**
		 * The invokation of $callback must be under prefix set.
		 * Callback has one or more routes.
		 */
		$callback($this);

		return $this;
	}

	/**
	 * Set auth callback to all route under a prefix.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $callback callback.
	 *
	 * @return void
	 */
	public function auth($callback)
	{
		$target_prefix = $this->current['prefix'];
		foreach (self::$routes as &$prefix) {
			foreach ($prefix as &$route) {
				/**
				 * The authorization callback is overridden
				 * only when no previous auth callback has been registered.
				 */
				if (
					isset($this->current['group'])
					&& $this->current['group'] === $route['group']
					&& $target_prefix === $route['prefix']
					&& empty($route['auth'])
				) {
					$route['auth'] = $callback;
				}
			}
		}

		if (isset($this->current['group'])) {
			unset($this->current['group']);
		}
	}

	/**
	 * Check string as specific substring.
	 *
	 * @since 1.0.0
	 *
	 * @param string $str target string.
	 * @param string $find find string.
	 *
	 * @return bool
	 */
	private static function str_has($str, $find)
	{
		return strpos($str, $find) !== false;
	}

	/**
	 * Prepare callback
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $callback callback.
	 *
	 * @return mixed
	 *
	 * @throws ApiRouteException If invalid callback.
	 */
	private static function prepare_callback($callback)
	{
		if (false === $callback) {
			return '__return_false';
		}

		if (true === $callback) {
			return '__return_false';
		}

		if (!is_array($callback) && is_callable($callback)) {
			return $callback;
		}

		if (is_array($callback)) {
			$class = $callback[0];
			$method = $callback[1];

			return array(
				new $class(),
				$method,
			);
		}

		if (is_string($callback) && self::str_has($callback, '@')) {
			$arr = explode('@', $callback, 2);
			$class = $arr[0];
			$method = $arr[1];

			if (!empty(self::$namespace) && !self::str_has($class, '\\')) {
				$class = self::$namespace . '\\' . $class;
			}

			return array(
				new $class(),
				$method,
			);
		}

		throw new ApiRouteException('Invalid callback');
	}

	/**
	 * Dispatch all registered routes.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 *
	 * @throws ApiRouteException If route does not have a prefix.
	 */
	public static function dispatch()
	{
		foreach (self::$routes as $prefix) {
			foreach ($prefix as $route) {

				$route = (object) $route;
				$callback = self::prepare_callback($route->callback);
				$auth = self::prepare_callback($route->auth);

				$args = array(
					'methods' => $route->method,
					'permission_callback' => $auth,
					'callback' => $callback,
				);

				if (false === $route->auth) {
					$args['permission_callback'] = '__return_true';
				}

				if (!isset($route->prefix)) {
					throw new ApiRouteException("{$route->endpoint} must have a prefix");
				}

				$endpoint = self::convert_to_regex($route->endpoint);

				register_rest_route($route->prefix, $endpoint, $args);
			}
		}
	}

	/**
	 * Convert URL schema to regex pattern.
	 *
	 * @since 1.0.0
	 */
	private static function convert_to_regex($url)
	{
		// Replace placeholders with regex patterns.
		$regex = preg_replace_callback(
			'/\{(\w+)\}/',
			function ($matches) {
				return '(?P<' . $matches[1] . '>\d+)'; // Capture numeric values.
			},
			$url,
		);

		return $regex;
	}
}
