<?php
/**
 * Frontend runtime for Maintenance Center.
 *
 * @package Polanger_Maintenance_Center
 */

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

class Polanger_Maintenance_Runtime {

	const PREVIEW_QUERY_ARG           = 'maintenance-preview';
	const PREVIEW_EXPIRES_QUERY_ARG   = 'maintenance-preview-expires';
	const PREVIEW_SIGNATURE_QUERY_ARG = 'maintenance-preview-signature';

	/**
	 * Singleton instance.
	 *
	 * @var Polanger_Maintenance_Runtime|null
	 */
	private static $instance = null;

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

		return self::$instance;
	}

	/**
	 * Register runtime hooks.
	 *
	 * @return void
	 */
	public function register_hooks() {
		add_action( 'template_redirect', array( $this, 'maybe_grant_preview_access' ), 0 );
		add_action( 'template_redirect', array( $this, 'guard_frontend_request' ), 1 );
		add_action( 'admin_init', array( $this, 'guard_ajax_request' ), 0 );
		add_filter( 'rest_authentication_errors', array( $this, 'filter_rest_authentication_errors' ), 5 );
		add_filter( 'comments_open', array( $this, 'filter_comments_open' ), 10, 2 );
		add_filter( 'pre_option_users_can_register', array( $this, 'filter_users_can_register' ) );
		add_filter( 'pre_option_woocommerce_enable_myaccount_registration', array( $this, 'filter_woocommerce_registration_option' ) );
		add_filter( 'pre_option_woocommerce_enable_signup_and_login_from_checkout', array( $this, 'filter_woocommerce_registration_option' ) );
		add_filter( 'woocommerce_checkout_registration_enabled', array( $this, 'filter_woocommerce_checkout_registration' ) );
		add_filter( 'woocommerce_checkout_registration_required', array( $this, 'filter_woocommerce_checkout_registration' ) );
		add_filter( 'woocommerce_is_purchasable', array( $this, 'filter_woocommerce_is_purchasable' ), 10, 2 );
		add_filter( 'woocommerce_variation_is_purchasable', array( $this, 'filter_woocommerce_is_purchasable' ), 10, 2 );
	}

	/**
	 * Build signed preview URL.
	 *
	 * @param array       $settings Settings payload.
	 * @param string|null $base_url Optional base URL.
	 * @return string
	 */
	public static function build_preview_url( $settings, $base_url = null ) {
		$settings = is_array( $settings ) ? $settings : Polanger_Maintenance_Settings::instance()->get_settings();
		$base_url = $base_url ? $base_url : home_url( '/' );
		$expires  = time() + ( max( 1, absint( $settings['preview_token_ttl'] ?? 24 ) ) * HOUR_IN_SECONDS );

		return add_query_arg(
			array(
				self::PREVIEW_QUERY_ARG           => (string) $settings['preview_token'],
				self::PREVIEW_EXPIRES_QUERY_ARG   => $expires,
				self::PREVIEW_SIGNATURE_QUERY_ARG => self::build_preview_link_signature( (string) $settings['preview_token'], $expires ),
			),
			$base_url
		);
	}

	/**
	 * Backward alias.
	 *
	 * @param array       $settings Settings payload.
	 * @param string|null $base_url Optional base URL.
	 * @return string
	 */
	public static function generate_preview_url( $settings, $base_url = null ) {
		return self::build_preview_url( $settings, $base_url );
	}

	/**
	 * Resolve current runtime state.
	 *
	 * @param array|null $settings Optional settings payload.
	 * @return array
	 */
	public static function get_runtime_state( $settings = null ) {
		$settings = is_array( $settings ) ? $settings : Polanger_Maintenance_Settings::instance()->get_settings();

		if ( empty( $settings['enabled'] ) ) {
			return array(
				'active'      => false,
				'badge_class' => 'idle',
				'label'       => __( 'Disabled', 'polanger-admin-menu-manager' ),
				'description' => __( 'The maintenance layer is turned off, so the public site remains fully open.', 'polanger-admin-menu-manager' ),
			);
		}

		if ( empty( $settings['schedule_enabled'] ) ) {
			return array(
				'active'      => true,
				'badge_class' => 'live',
				'label'       => __( 'Live Manually', 'polanger-admin-menu-manager' ),
				'description' => __( 'Maintenance Center is active now and will stay up until you manually disable it.', 'polanger-admin-menu-manager' ),
			);
		}

		$timezone = self::resolve_timezone( $settings['timezone'] ?? 'UTC' );
		$end      = self::parse_local_datetime( $settings['schedule_end'] ?? '', $timezone );
		$now      = new DateTimeImmutable( 'now', $timezone );

		if ( ! $end ) {
			return array(
				'active'      => false,
				'badge_class' => 'warning',
				'label'       => __( 'End Time Invalid', 'polanger-admin-menu-manager' ),
				'description' => __( 'The automatic end time could not be parsed, so timed maintenance cannot run correctly.', 'polanger-admin-menu-manager' ),
			);
		}

		if ( $now > $end ) {
			return array(
				'active'      => false,
				'badge_class' => 'idle',
				'label'       => __( 'Completed', 'polanger-admin-menu-manager' ),
				'description' => __( 'The scheduled window has already ended, so the public site is open again.', 'polanger-admin-menu-manager' ),
			);
		}

		return array(
			'active'      => true,
			'badge_class' => 'live',
			'label'       => __( 'Live Until End Time', 'polanger-admin-menu-manager' ),
			'description' => sprintf(
				/* translators: %s: end date. */
				__( 'Maintenance Center is active and will reopen the site automatically at %s.', 'polanger-admin-menu-manager' ),
				wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $end->getTimestamp(), $timezone )
			),
		);
	}

	/**
	 * Resolve active HTTP response code.
	 *
	 * @param array $settings Settings payload.
	 * @return int
	 */
	public static function get_effective_response_code( $settings ) {
		$settings = is_array( $settings ) ? $settings : Polanger_Maintenance_Settings::instance()->get_settings();

		if ( '503' === (string) $settings['response_code'] ) {
			return 503;
		}

		if ( '200' === (string) $settings['response_code'] ) {
			return 200;
		}

		return 'coming-soon' === ( $settings['mode'] ?? 'maintenance' ) ? 200 : 503;
	}

	/**
	 * Grant preview access.
	 *
	 * @return void
	 */
	public function maybe_grant_preview_access() {
		if ( $this->should_skip_runtime() ) {
			return;
		}

		if ( ! isset( $_GET[ self::PREVIEW_QUERY_ARG ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}

		$settings  = Polanger_Maintenance_Settings::instance()->get_settings();
		$token     = sanitize_text_field( wp_unslash( $_GET[ self::PREVIEW_QUERY_ARG ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$expires   = isset( $_GET[ self::PREVIEW_EXPIRES_QUERY_ARG ] ) ? absint( wp_unslash( $_GET[ self::PREVIEW_EXPIRES_QUERY_ARG ] ) ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$signature = isset( $_GET[ self::PREVIEW_SIGNATURE_QUERY_ARG ] ) ? sanitize_text_field( wp_unslash( $_GET[ self::PREVIEW_SIGNATURE_QUERY_ARG ] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		if ( ! hash_equals( (string) $settings['preview_token'], (string) $token ) ) {
			return;
		}

		if ( $expires <= time() || '' === $signature ) {
			return;
		}

		$expected_signature = self::build_preview_link_signature( (string) $settings['preview_token'], $expires );
		if ( ! hash_equals( $expected_signature, $signature ) ) {
			return;
		}

		$this->render_maintenance_screen( $settings, true );
	}

	/**
	 * Guard public frontend requests.
	 *
	 * @return void
	 */
	public function guard_frontend_request() {
		if ( $this->should_skip_runtime() ) {
			return;
		}

		$settings = Polanger_Maintenance_Settings::instance()->get_settings();
		$state    = self::get_runtime_state( $settings );

		if ( empty( $state['active'] ) ) {
			return;
		}

		if ( $this->request_is_sitemap() && ! empty( $settings['allow_sitemap'] ) ) {
			return;
		}

		if ( $this->request_is_allowed( $settings ) ) {
			return;
		}

		$this->render_maintenance_screen( $settings );
	}

	/**
	 * Guard public admin-ajax requests.
	 *
	 * @return void
	 */
	public function guard_ajax_request() {
		if ( ! wp_doing_ajax() || $this->request_originates_from_admin() ) {
			return;
		}

		$settings = Polanger_Maintenance_Settings::instance()->get_settings();
		$state    = self::get_runtime_state( $settings );

		if ( empty( $state['active'] ) || $this->request_is_allowed( $settings ) ) {
			return;
		}

		$status_code = self::get_effective_response_code( $settings );
		$this->send_maintenance_headers( $settings, $status_code );

		wp_send_json_error(
			array(
				'code'    => 'maintenance_active',
				'message' => __( 'This request is temporarily unavailable while maintenance mode is active.', 'polanger-admin-menu-manager' ),
			),
			$status_code
		);
	}

	/**
	 * Guard public REST requests.
	 *
	 * @param mixed $result Existing result.
	 * @return mixed
	 */
	public function filter_rest_authentication_errors( $result ) {
		if ( ! ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || $this->request_originates_from_admin() ) {
			return $result;
		}

		if ( ! empty( $result ) ) {
			return $result;
		}

		$settings = Polanger_Maintenance_Settings::instance()->get_settings();
		$state    = self::get_runtime_state( $settings );

		if ( empty( $state['active'] ) || $this->request_is_allowed( $settings ) ) {
			return $result;
		}

		$status_code = self::get_effective_response_code( $settings );
		$this->send_maintenance_headers( $settings, $status_code );

		return new WP_Error(
			'pmc_rest_locked',
			__( 'This endpoint is temporarily unavailable while maintenance mode is active.', 'polanger-admin-menu-manager' ),
			array(
				'status' => $status_code,
			)
		);
	}

	/**
	 * Disable comments during active deployment.
	 *
	 * @param bool $open    Current open state.
	 * @param int  $post_id Post ID.
	 * @return bool
	 */
	public function filter_comments_open( $open, $post_id ) {
		unset( $post_id );

		if ( ! $this->is_deployment_lock_enabled( 'deployment_disable_comments' ) ) {
			return $open;
		}

		return false;
	}

	/**
	 * Disable registration during active deployment.
	 *
	 * @param mixed $value Existing value.
	 * @return int|mixed
	 */
	public function filter_users_can_register( $value ) {
		if ( ! $this->is_deployment_lock_enabled( 'deployment_disable_registration' ) ) {
			return $value;
		}

		return 0;
	}

	/**
	 * Disable WooCommerce registration options during deployment.
	 *
	 * @param mixed $value Existing value.
	 * @return mixed
	 */
	public function filter_woocommerce_registration_option( $value ) {
		if ( ! $this->is_deployment_lock_enabled( 'deployment_disable_registration' ) ) {
			return $value;
		}

		return 'no';
	}

	/**
	 * Disable WooCommerce checkout registration during deployment.
	 *
	 * @param bool $enabled Existing state.
	 * @return bool
	 */
	public function filter_woocommerce_checkout_registration( $enabled ) {
		if ( ! $this->is_deployment_lock_enabled( 'deployment_disable_registration' ) ) {
			return $enabled;
		}

		return false;
	}

	/**
	 * Disable product purchasing during deployment.
	 *
	 * @param bool $purchasable Existing state.
	 * @param mixed $product Product object.
	 * @return bool
	 */
	public function filter_woocommerce_is_purchasable( $purchasable, $product ) {
		unset( $product );

		if ( ! $this->is_deployment_lock_enabled( 'deployment_disable_purchases' ) ) {
			return $purchasable;
		}

		if ( current_user_can( 'manage_options' ) ) {
			return $purchasable;
		}

		return false;
	}

	/**
	 * Render the branded maintenance screen.
	 *
	 * @param array $settings   Settings payload.
	 * @param bool  $is_preview Whether this is a signed preview request.
	 * @return void
	 */
	private function render_maintenance_screen( $settings, $is_preview = false ) {
		$status_code = $is_preview ? 200 : self::get_effective_response_code( $settings );
		$this->send_maintenance_headers( $settings, $status_code );

		wp_enqueue_style(
			'polanger-maintenance-center-screen',
			POLANGER_MAINTENANCE_CENTER_URL . 'assets/css/maintenance-screen.css',
			array(),
			POLANGER_MAINTENANCE_CENTER_VERSION
		);

		wp_enqueue_script(
			'polanger-maintenance-center-screen',
			POLANGER_MAINTENANCE_CENTER_URL . 'assets/js/maintenance-screen.js',
			array(),
			POLANGER_MAINTENANCE_CENTER_VERSION,
			true
		);

		$pmc_context = $this->build_template_context( $settings, $status_code );

		include POLANGER_MAINTENANCE_CENTER_DIR . 'templates/maintenance-screen.php';
		exit;
	}

	/**
	 * Build template context.
	 *
	 * @param array $settings    Settings payload.
	 * @param int   $status_code Status code.
	 * @return array
	 */
	private function build_template_context( $settings, $status_code ) {
		$site_title = wp_strip_all_tags( get_bloginfo( 'name' ), true );
		$countdown  = $this->build_countdown_context( $settings );
		$logo_url   = ! empty( $settings['logo_url'] ) ? $settings['logo_url'] : '';

		if ( empty( $logo_url ) && ! empty( $settings['logo_id'] ) ) {
			$resolved_logo = wp_get_attachment_image_url( absint( $settings['logo_id'] ), 'full' );
			$logo_url      = $resolved_logo ? $resolved_logo : '';
		}

		return array(
			'page_title'         => $this->get_document_title( $site_title ),
			'site_title'         => $site_title,
			'headline'           => (string) $settings['headline'],
			'headline_alignment' => $this->sanitize_alignment( $settings['headline_alignment'] ?? 'center' ),
			'message'            => $this->format_rich_text( $settings['message'] ?? '' ),
			'message_alignment'  => $this->sanitize_alignment( $settings['message_alignment'] ?? 'center' ),
			'status_note'        => (string) $settings['status_note'],
			'countdown_label'    => (string) $settings['countdown_label'],
			'button_label'       => (string) $settings['button_label'],
			'button_url'         => ! empty( $settings['button_url'] ) ? $settings['button_url'] : home_url( '/' ),
			'logo_url'           => $logo_url,
			'logo_alignment'     => $this->sanitize_alignment( $settings['logo_alignment'] ?? 'center' ),
			'footer_content'     => $this->format_rich_text( $settings['footer_content'] ?? '' ),
			'footer_alignment'   => $this->sanitize_alignment( $settings['message_alignment'] ?? 'center' ),
			'status_code'        => $status_code,
			'background_style'   => $this->build_background_style( $settings ),
			'robots_content'     => $this->get_robots_meta_content( $settings, $status_code ),
			'countdown'          => $countdown,
			'custom_css'         => $this->sanitize_code_block( $settings['custom_css'] ?? '' ),
			'custom_js'          => $this->sanitize_code_block( $settings['custom_js'] ?? '' ),
		);
	}

	/**
	 * Let WordPress resolve the browser title for the intercepted request.
	 *
	 * @param string $fallback Fallback site title.
	 * @return string
	 */
	private function get_document_title( $fallback ) {
		$title = function_exists( 'wp_get_document_title' ) ? wp_get_document_title() : '';
		$title = wp_strip_all_tags( (string) $title, true );

		return '' !== $title ? $title : (string) $fallback;
	}

	/**
	 * Build background CSS variables.
	 *
	 * @param array $settings Settings payload.
	 * @return string
	 */
	private function build_background_style( $settings ) {
		$primary   = ! empty( $settings['background_primary'] ) ? $settings['background_primary'] : '#0f172a';
		$secondary = ! empty( $settings['background_secondary'] ) ? $settings['background_secondary'] : '#312e81';
		$image     = ! empty( $settings['background_image_url'] ) ? esc_url_raw( $settings['background_image_url'] ) : '';
		$type      = $settings['background_type'] ?? 'gradient';

		if ( 'image' === $type && '' !== $image ) {
			return sprintf(
				'--pmc-shell-background: linear-gradient(135deg, %1$s, %2$s), url("%3$s") center/cover no-repeat fixed; --pmc-accent-primary: %4$s; --pmc-accent-secondary: %5$s;',
				$this->hex_to_rgba( $primary, 0.8 ),
				$this->hex_to_rgba( $secondary, 0.6 ),
				$image
				,
				$primary,
				$secondary
			);
		}

		if ( 'color' === $type ) {
			return sprintf(
				'--pmc-shell-background: %1$s; --pmc-accent-primary: %1$s; --pmc-accent-secondary: %2$s;',
				$primary,
				$secondary
			);
		}

		return sprintf(
			'--pmc-shell-background: linear-gradient(135deg, %1$s 0%%, %2$s 100%%); --pmc-accent-primary: %1$s; --pmc-accent-secondary: %2$s;',
			$primary,
			$secondary
		);
	}

	/**
	 * Convert hex color to rgba string.
	 *
	 * @param string $hex   Hex color.
	 * @param float  $alpha Alpha channel.
	 * @return string
	 */
	private function hex_to_rgba( $hex, $alpha ) {
		$hex = ltrim( (string) $hex, '#' );

		if ( 3 === strlen( $hex ) ) {
			$hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2];
		}

		if ( 6 !== strlen( $hex ) ) {
			return 'rgba(15, 23, 42, ' . (float) $alpha . ')';
		}

		$red   = hexdec( substr( $hex, 0, 2 ) );
		$green = hexdec( substr( $hex, 2, 2 ) );
		$blue  = hexdec( substr( $hex, 4, 2 ) );

		return sprintf( 'rgba(%d, %d, %d, %.3f)', $red, $green, $blue, (float) $alpha );
	}

	/**
	 * Build countdown context.
	 *
	 * @param array $settings Settings payload.
	 * @return array
	 */
	private function build_countdown_context( $settings ) {
		if ( empty( $settings['countdown_enabled'] ) || empty( $settings['schedule_enabled'] ) || empty( $settings['schedule_end'] ) ) {
			return array(
				'enabled' => false,
			);
		}

		$target_value = (string) $settings['schedule_end'];
		if ( '' === $target_value ) {
			return array(
				'enabled' => false,
			);
		}

		$timezone = self::resolve_timezone( $settings['timezone'] ?? 'UTC' );
		$target   = self::parse_local_datetime( $target_value, $timezone );

		if ( ! $target ) {
			return array(
				'enabled' => false,
			);
		}

		$labels = array(
			'years'   => __( 'Years', 'polanger-admin-menu-manager' ),
			'days'    => __( 'Days', 'polanger-admin-menu-manager' ),
			'hours'   => __( 'Hours', 'polanger-admin-menu-manager' ),
			'minutes' => __( 'Minutes', 'polanger-admin-menu-manager' ),
			'seconds' => __( 'Seconds', 'polanger-admin-menu-manager' ),
		);

		$units = array();
		foreach ( array( 'years', 'days', 'hours', 'minutes', 'seconds' ) as $unit ) {
			$key = 'countdown_show_' . $unit;
			if ( empty( $settings[ $key ] ) ) {
				continue;
			}

			$units[] = array(
				'key'   => $unit,
				'label' => $labels[ $unit ],
			);
		}

		if ( empty( $units ) ) {
			return array(
				'enabled' => false,
			);
		}

		return array(
			'enabled'      => true,
			'target_iso'   => $target->format( DATE_ATOM ),
			'target_label' => wp_date( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), $target->getTimestamp(), $timezone ),
			'units'        => $units,
		);
	}

	/**
	 * Format rich content for frontend output.
	 *
	 * @param string $content Raw content.
	 * @return string
	 */
	private function format_rich_text( $content ) {
		$content = trim( (string) $content );

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

		$content = do_shortcode( $content );
		$content = wp_kses_post( $content );

		if ( $content === wp_strip_all_tags( $content ) ) {
			$content = wpautop( $content );
		}

		return $content;
	}

	/**
	 * Sanitize inline code content.
	 *
	 * @param string $content Raw content.
	 * @return string
	 */
	private function sanitize_code_block( $content ) {
		$content = (string) $content;
		$content = str_replace( "\0", '', $content );
		$content = str_replace( array( "\r\n", "\r" ), "\n", $content );

		return trim( $content );
	}

	/**
	 * Sanitize alignment key.
	 *
	 * @param string $alignment Raw alignment.
	 * @return string
	 */
	private function sanitize_alignment( $alignment ) {
		$alignment = sanitize_key( (string) $alignment );

		return in_array( $alignment, array( 'left', 'center', 'right' ), true ) ? $alignment : 'center';
	}

	/**
	 * Check whether current request should bypass the maintenance screen.
	 *
	 * @param array $settings Settings payload.
	 * @return bool
	 */
	private function request_is_allowed( $settings ) {
		if ( current_user_can( 'manage_options' ) ) {
			return true;
		}

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

		if ( is_user_logged_in() && ! empty( $settings['allow_logged_in'] ) ) {
			return true;
		}

		if ( is_user_logged_in() && ! empty( $settings['bypass_roles'] ) ) {
			$user = wp_get_current_user();

			if ( $user instanceof WP_User && array_intersect( (array) $user->roles, (array) $settings['bypass_roles'] ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check whether current visitor IP is allowed.
	 *
	 * @param array $settings Settings payload.
	 * @return bool
	 */
	private function current_ip_is_allowed( $settings ) {
		if ( empty( $settings['bypass_ips'] ) ) {
			return false;
		}

		$current_ip = $this->current_request_ip();
		if ( '' === $current_ip ) {
			return false;
		}

		return in_array( $current_ip, $this->parse_ip_list( $settings['bypass_ips'] ), true );
	}

	/**
	 * Get request IP.
	 *
	 * @return string
	 */
	private function current_request_ip() {
		$candidates = array(
			$_SERVER['HTTP_CF_CONNECTING_IP'] ?? '',
			$_SERVER['HTTP_X_FORWARDED_FOR'] ?? '',
			$_SERVER['REMOTE_ADDR'] ?? '',
		);

		foreach ( $candidates as $candidate ) {
			$candidate = sanitize_text_field( wp_unslash( (string) $candidate ) );

			if ( '' === $candidate ) {
				continue;
			}

			if ( false !== strpos( $candidate, ',' ) ) {
				$parts     = explode( ',', $candidate );
				$candidate = trim( (string) reset( $parts ) );
			}

			if ( false !== filter_var( $candidate, FILTER_VALIDATE_IP ) ) {
				return $candidate;
			}
		}

		return '';
	}

	/**
	 * Parse IP list from settings.
	 *
	 * @param string $raw Raw list.
	 * @return array
	 */
	private function parse_ip_list( $raw ) {
		$items = preg_split( '/[\r\n,]+/', (string) $raw );
		$valid = array();

		foreach ( (array) $items as $item ) {
			$item = trim( $item );

			if ( '' === $item ) {
				continue;
			}

			if ( false !== filter_var( $item, FILTER_VALIDATE_IP ) ) {
				$valid[] = $item;
			}
		}

		return array_values( array_unique( $valid ) );
	}

	/**
	 * Check if current request is checkout.
	 *
	 * @return bool
	 */
	private function request_is_checkout() {
		return function_exists( 'is_checkout' ) && is_checkout();
	}

	/**
	 * Detect if request originates from wp-admin.
	 *
	 * @return bool
	 */
	private function request_originates_from_admin() {
		$referer = function_exists( 'wp_get_raw_referer' ) ? wp_get_raw_referer() : '';

		if ( empty( $referer ) ) {
			return false;
		}

		return 0 === strpos( (string) $referer, admin_url() );
	}

	/**
	 * Determine if runtime should be skipped entirely.
	 *
	 * @return bool
	 */
	private function should_skip_runtime() {
		if ( is_admin() || wp_doing_ajax() ) {
			return true;
		}

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

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

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

		if ( function_exists( 'is_customize_preview' ) && is_customize_preview() ) {
			return true;
		}

		if ( function_exists( 'wp_installing' ) && wp_installing() ) {
			return true;
		}

		$path = $this->get_request_path();

		return in_array( $path, array( '/robots.txt', '/favicon.ico' ), true );
	}

	/**
	 * Check if current request is sitemap.
	 *
	 * @return bool
	 */
	private function request_is_sitemap() {
		$path = $this->get_request_path();

		return (bool) preg_match( '#/(wp-sitemap\.xml|sitemap(_index)?\.xml|.+-sitemap\d*\.xml)$#i', $path );
	}

	/**
	 * Get request path.
	 *
	 * @return string
	 */
	private function get_request_path() {
		$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '/';
		$parts       = wp_parse_url( rawurldecode( $request_uri ) );
		$path        = isset( $parts['path'] ) ? $parts['path'] : '/';

		return '/' . ltrim( (string) $path, '/' );
	}

	/**
	 * Check whether a deployment lock should apply.
	 *
	 * @param string $setting_key Deployment setting key.
	 * @return bool
	 */
	private function is_deployment_lock_enabled( $setting_key ) {
		$settings = Polanger_Maintenance_Settings::instance()->get_settings();
		$state    = self::get_runtime_state( $settings );

		if ( empty( $state['active'] ) || 'deployment' !== ( $settings['mode'] ?? '' ) ) {
			return false;
		}

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

		return ! empty( $settings[ $setting_key ] );
	}

	/**
	 * Resolve timezone safely.
	 *
	 * @param string $timezone Timezone identifier.
	 * @return DateTimeZone
	 */
	private static function resolve_timezone( $timezone ) {
		try {
			return new DateTimeZone( (string) $timezone );
		} catch ( Exception $exception ) {
			unset( $exception );
			return new DateTimeZone( 'UTC' );
		}
	}

	/**
	 * Parse datetime-local string.
	 *
	 * @param string       $value    Datetime string.
	 * @param DateTimeZone $timezone Timezone.
	 * @return DateTimeImmutable|null
	 */
	private static function parse_local_datetime( $value, $timezone ) {
		$value = trim( (string) $value );

		if ( '' === $value ) {
			return null;
		}

		$date = DateTimeImmutable::createFromFormat( 'Y-m-d\TH:i', $value, $timezone );

		return $date instanceof DateTimeImmutable ? $date : null;
	}

	/**
	 * Send maintenance headers.
	 *
	 * @param array $settings    Settings payload.
	 * @param int   $status_code Response status.
	 * @return void
	 */
	private function send_maintenance_headers( $settings, $status_code ) {
		status_header( $status_code );
		nocache_headers();

		if ( 503 === (int) $status_code ) {
			$retry_after = $this->get_retry_after_seconds( $settings );

			if ( $retry_after > 0 ) {
				header( 'Retry-After: ' . (string) $retry_after, true );
			}
		}

		$robots = $this->get_robots_meta_content( $settings, $status_code );
		if ( '' !== $robots ) {
			header( 'X-Robots-Tag: ' . $robots, true );
		}
	}

	/**
	 * Resolve retry-after seconds.
	 *
	 * @param array $settings Settings payload.
	 * @return int
	 */
	private function get_retry_after_seconds( $settings ) {
		if ( empty( $settings['schedule_enabled'] ) || empty( $settings['schedule_end'] ) ) {
			return 0;
		}

		$timezone = self::resolve_timezone( $settings['timezone'] ?? 'UTC' );
		$end      = self::parse_local_datetime( $settings['schedule_end'], $timezone );

		if ( ! $end ) {
			return 0;
		}

		$seconds = $end->getTimestamp() - time();

		return $seconds > 0 ? $seconds : 0;
	}

	/**
	 * Resolve robots meta/header content.
	 *
	 * @param array $settings    Settings payload.
	 * @param int   $status_code Status code.
	 * @return string
	 */
	private function get_robots_meta_content( $settings, $status_code ) {
		if ( empty( $settings['block_index'] ) ) {
			return '';
		}

		return 503 === (int) $status_code ? 'noindex,nofollow' : 'noindex,follow';
	}

	/**
	 * Build preview URL signature.
	 *
	 * @param string $token   Preview token.
	 * @param int    $expires Expiry timestamp.
	 * @return string
	 */
	private static function build_preview_link_signature( $token, $expires ) {
		return hash_hmac( 'sha256', 'preview-link|' . $token . '|' . absint( $expires ), wp_salt( 'auth' ) );
	}
}
