<?php
/**
 * NotFound fallback redirect executor.
 *
 * @package SEOvaultSEO\NotFound
 */

namespace SEOvaultSEO\NotFound;

use SEOvaultSEO\Compatibility\Detector;
use SEOvaultSEO\Core\Options;
use SEOvaultSEO\Monitor404\Monitor as Monitor404;
use SEOvaultSEO\RedirectManager\RedirectDb;

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Performs home / custom fallback redirects.
 */
class FallbackRedirector {

	/**
	 * Run after Frontend\Redirect's priority-zero Redirect Manager lookup.
	 *
	 * @var int
	 */
	const TEMPLATE_REDIRECT_PRIORITY = 1;

	/**
	 * Register fallback redirect handling.
	 */
	public static function init() {
		add_action(
			'template_redirect',
			array( __CLASS__, 'maybe_redirect' ),
			self::TEMPLATE_REDIRECT_PRIORITY
		);
	}

	/**
	 * Resolve and execute the configured fallback redirect.
	 *
	 * @return void
	 */
	public static function maybe_redirect() {
		if ( ! Detector::should_output_seo() ) {
			return;
		}

		$request_uri = self::get_request_uri();
		$is_404      = is_404();
		$options     = array(
			'mode'           => Options::get_notfound_fallback_mode(),
			'status'         => Options::get_notfound_fallback_status(),
			'custom_url'     => Options::get_notfound_fallback_custom_url(),
			'custom_page_id' => Options::get_notfound_fallback_custom_page_id(),
			'request_uri'    => $request_uri,
		);

		// Avoid redirect-table reads when the resolver cannot produce a redirect.
		$rule_matched = $is_404 && 'theme' !== $options['mode']
			? self::has_matching_redirect_rule( $request_uri )
			: false;

		$decision = FallbackResolver::resolve( $is_404, $rule_matched, $options );

		if ( 'none' === $decision['action'] ) {
			return;
		}

		self::send_redirect( $decision['url'], $decision['status'] );
	}

	/**
	 * Send a fallback redirect and terminate after WordPress accepts it.
	 *
	 * Layer A permits only targets accepted by WordPress safe-redirect
	 * validation. Validation is repeated here as defense in depth for callers
	 * that bypass option sanitization.
	 *
	 * @param string $url    Redirect target URL.
	 * @param int    $status Redirect HTTP status.
	 * @return void
	 */
	private static function send_redirect( $url, $status ) {
		$url    = Sanitizer::sanitize_custom_url( $url );
		$status = Sanitizer::sanitize_fallback_status( $status );

		if ( '' === $url ) {
			return;
		}

		if ( Options::is_monitor404_enabled() && Options::is_notfound_log_redirected_fallbacks() ) {
			Monitor404::log_request();
		}

		do_action( 'seovaultseo_notfound_fallback_redirect', $url, $status );

		$redirected = wp_safe_redirect( $url, $status );

		// A false return lets wp_redirect filters cancel redirects in tests/extensions.
		if ( $redirected ) {
			exit;
		}
	}

	/**
	 * Get the current request URI.
	 *
	 * @return string
	 */
	private static function get_request_uri() {
		if ( ! isset( $_SERVER['REQUEST_URI'] ) || ! is_string( $_SERVER['REQUEST_URI'] ) ) {
			return '';
		}

		return esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) );
	}

	/**
	 * Determine whether an existing 404 redirect rule already matched.
	 *
	 * @param string $request_uri Current request URI.
	 * @return bool
	 */
	private static function has_matching_redirect_rule( $request_uri ) {
		$uri = self::get_relative_uri( $request_uri );

		if ( RedirectDb::find_match( $uri ) ) {
			return true;
		}

		$legacy_redirects = get_option( 'seovaultseo_404_redirects', array() );

		return is_array( $legacy_redirects ) && isset( $legacy_redirects[ $uri ] );
	}

	/**
	 * Convert a request URI to the format used by Frontend\Redirect.
	 *
	 * @param string $request_uri Current request URI.
	 * @return string
	 */
	private static function get_relative_uri( $request_uri ) {
		$home_path = wp_parse_url( home_url( '/' ), PHP_URL_PATH );
		$home_path = is_string( $home_path ) ? $home_path : '';
		$uri       = str_replace( $home_path, '', $request_uri );

		return untrailingslashit( ltrim( $uri, '/' ) );
	}
}
