<?php
/**
 * Frontend maintenance gate.
 *
 * @package MacaHold
 */

defined( 'ABSPATH' ) || exit;

/**
 * Blocks public requests during maintenance.
 */
class Maca_Hold_Maintenance_Gate {

	/**
	 * Template renderer.
	 *
	 * @var Maca_Hold_Template_Renderer
	 */
	private $renderer;

	/**
	 * Constructor.
	 *
	 * @param Maca_Hold_Template_Renderer $renderer Template renderer.
	 */
	public function __construct( Maca_Hold_Template_Renderer $renderer ) {
		$this->renderer = $renderer;
	}

	/**
	 * Register hooks.
	 *
	 * @return void
	 */
	public function register() {
		add_action( 'template_redirect', array( $this, 'maybe_block_request' ), 1 );
		add_filter( 'rest_pre_dispatch', array( $this, 'maybe_block_rest' ), 10, 3 );
	}

	/**
	 * Block frontend requests when maintenance is active.
	 *
	 * @return void
	 */
	public function maybe_block_request() {
		if ( $this->should_skip_gate() ) {
			return;
		}

		$is_preview = $this->is_preview_request();

		/**
		 * Force the maintenance page on the current request even when global maintenance is off.
		 *
		 * @param bool $force Whether to force maintenance mode.
		 */
		$force_maintenance = (bool) apply_filters( 'maca_hold_force_maintenance', false );

		if ( ! Maca_Hold_Settings::is_active() && ! $force_maintenance && ! $is_preview ) {
			return;
		}

		if ( ! $is_preview && $this->should_bypass( $force_maintenance ) ) {
			return;
		}

		$settings = Maca_Hold_Settings::get();
		$this->renderer->render( $settings );
	}

	/**
	 * Block anonymous REST API access during maintenance.
	 *
	 * @param mixed            $result  Response to replace.
	 * @param WP_REST_Server   $server  Server instance.
	 * @param WP_REST_Request  $request Request object.
	 * @return mixed
	 */
	public function maybe_block_rest( $result, $server, $request ) {
		unset( $server, $request );

		if ( ! Maca_Hold_Settings::is_active() ) {
			return $result;
		}

		if ( $this->should_bypass() ) {
			return $result;
		}

		return new WP_Error(
			'maca_hold_maintenance',
			__( 'Site is under maintenance.', 'maca-hold' ),
			array( 'status' => 503 )
		);
	}

	/**
	 * Determine if the current request should bypass maintenance.
	 *
	 * @param bool $force_maintenance Whether maintenance is forced for this request.
	 * @return bool
	 */
	private function should_bypass( $force_maintenance = false ) {
		$bypass = false;

		/**
		 * Filter whether the current request bypasses maintenance mode.
		 *
		 * @param bool $bypass            Current bypass state.
		 * @param bool $force_maintenance Whether maintenance is forced for this request.
		 */
		$bypass = apply_filters( 'maca_hold_should_bypass', $bypass, $force_maintenance );

		if ( $bypass ) {
			return true;
		}

		if ( $force_maintenance ) {
			return false;
		}

		if ( $this->user_has_bypass_role() ) {
			return true;
		}

		if ( $this->is_whitelisted_ip() ) {
			return true;
		}

		if ( $this->has_valid_secret_access() ) {
			return true;
		}

		return false;
	}

	/**
	 * Skip gate for admin, cron, CLI, and similar contexts.
	 *
	 * @return bool
	 */
	private function should_skip_gate() {
		if ( is_admin() && ! wp_doing_ajax() ) {
			return true;
		}

		if ( wp_doing_cron() ) {
			return true;
		}

		if ( defined( 'WP_CLI' ) && WP_CLI ) {
			return true;
		}

		if ( defined( 'REST_REQUEST' ) && REST_REQUEST && is_user_logged_in() && $this->user_has_bypass_role() ) {
			return true;
		}

		return false;
	}

	/**
	 * Check if logged-in user has a bypass role.
	 *
	 * @return bool
	 */
	private function user_has_bypass_role() {
		if ( ! is_user_logged_in() ) {
			return false;
		}

		$settings = Maca_Hold_Settings::get();
		$user     = wp_get_current_user();
		$roles    = (array) ( $settings['bypass_roles'] ?? array( 'administrator' ) );

		return (bool) array_intersect( $roles, (array) $user->roles );
	}

	/**
	 * Check if request IP is whitelisted.
	 *
	 * @return bool
	 */
	private function is_whitelisted_ip() {
		$settings = Maca_Hold_Settings::get();
		$list     = trim( (string) ( $settings['ip_whitelist'] ?? '' ) );

		if ( '' === $list ) {
			return false;
		}

		$remote_ip = $this->get_client_ip();
		if ( '' === $remote_ip ) {
			return false;
		}

		$ips = preg_split( '/[\r\n]+/', $list );
		return in_array( $remote_ip, array_map( 'trim', $ips ), true );
	}

	/**
	 * Validate secret URL token or bypass cookie.
	 *
	 * @return bool
	 */
	private function has_valid_secret_access() {
		$settings = Maca_Hold_Settings::get();
		$token    = (string) ( $settings['secret_token'] ?? '' );

		if ( '' === $token ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Public secret bypass URL query parameter.
		$query_token = isset( $_GET['maca_hold'] ) ? sanitize_text_field( wp_unslash( $_GET['maca_hold'] ) ) : '';

		if ( '' !== $query_token && hash_equals( $token, $query_token ) ) {
			$this->set_bypass_cookie( $token );
			return true;
		}

		$cookie_token = isset( $_COOKIE[ MACA_HOLD_BYPASS_COOKIE ] )
			? sanitize_text_field( wp_unslash( $_COOKIE[ MACA_HOLD_BYPASS_COOKIE ] ) )
			: '';

		return '' !== $cookie_token && hash_equals( $token, $cookie_token );
	}

	/**
	 * Store bypass cookie after successful secret URL visit.
	 *
	 * @param string $token Secret token.
	 * @return void
	 */
	private function set_bypass_cookie( $token ) {
		if ( headers_sent() ) {
			return;
		}

		$secure   = is_ssl();
		$expires  = time() + MONTH_IN_SECONDS;
		$path     = COOKIEPATH ? COOKIEPATH : '/';
		$domain   = COOKIE_DOMAIN;
		$httponly = true;

		setcookie( MACA_HOLD_BYPASS_COOKIE, $token, $expires, $path, $domain, $secure, $httponly );

		if ( COOKIEPATH !== SITECOOKIEPATH ) {
			setcookie( MACA_HOLD_BYPASS_COOKIE, $token, $expires, SITECOOKIEPATH, $domain, $secure, $httponly );
		}
	}

	/**
	 * Admin preview mode.
	 *
	 * @return bool
	 */
	private function is_preview_request() {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified below for authorized preview.
		if ( empty( $_GET['maca_hold_preview'] ) ) {
			return false;
		}

		if ( ! current_user_can( 'manage_options' ) ) {
			return false;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Verified with wp_verify_nonce below.
		$nonce = isset( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
		return (bool) wp_verify_nonce( $nonce, 'maca_hold_preview' );
	}

	/**
	 * Get the visitor IP address.
	 *
	 * @return string
	 */
	private function get_client_ip() {
		$ip = '';

		if ( ! empty( $_SERVER['REMOTE_ADDR'] ) ) {
			$ip = sanitize_text_field( wp_unslash( $_SERVER['REMOTE_ADDR'] ) );
		}

		return filter_var( $ip, FILTER_VALIDATE_IP ) ? $ip : '';
	}
}
