<?php
/**
 * 404 Monitor — Frontend Capture
 *
 * Hooks into template_redirect to detect 404 responses
 * and log them to the database.
 *
 * @package SEOvaultSEO
 */

namespace SEOvaultSEO\Monitor404;

use SEOvaultSEO\Core\Options;

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

class Monitor {

	/**
	 * Whether the current request was a 404 at the time `wp` fired.
	 *
	 * Captured early because some themes replace $wp_query on
	 * template_redirect, making is_404() return false by then.
	 *
	 * @var bool
	 */
	private $is_404 = false;

	/**
	 * Initialize hooks.
	 */
	public static function init() {
		$instance = new self();

		// Don't log in admin, REST, or AJAX.
		// Note: is_feed() is intentionally omitted here because it cannot be
		// called before the main query runs. It is checked later in save_404_flag().
		if ( is_admin() || wp_is_json_request() || wp_doing_ajax() ) {
			return;
		}

		// Only log if another SEO plugin is NOT active (avoid duplicate work).
		if ( ! \SEOvaultSEO\Compatibility\Detector::should_output_seo() ) {
			return;
		}

		add_action( 'wp', array( $instance, 'save_404_flag' ) );
		add_action( $instance->get_hook(), array( $instance, 'capture_404' ) );
	}

	/**
	 * Save the 404 status before any theme or plugin can replace $wp_query.
	 */
	public function save_404_flag() {
		// Don't log feeds — safe to call is_feed() here on the 'wp' hook.
		if ( is_feed() ) {
			return;
		}

		$this->is_404 = is_404();
	}

	/**
	 * Log the request details when a 404 is detected.
	 */
	public function capture_404() {
		if ( ( ! $this->is_404 && ! is_404() ) || in_array( http_response_code(), array( 410, 451 ), true ) ) {
			return;
		}

		self::log_request();
	}

	/**
	 * Log the current request using the configured Monitor mode and exclusions.
	 *
	 * This shared writer is also used immediately before a NotFound fallback
	 * redirect exits, when the normal theme capture hook cannot run.
	 *
	 * @return void
	 */
	public static function log_request() {
		// Build the URI relative to home URL.
		$home_url = home_url( '/' );
		$uri      = isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
		$uri      = str_replace( wp_parse_url( $home_url, PHP_URL_PATH ) ?? '', '', $uri );
		$uri      = ltrim( $uri, '/' );

		// Optionally strip query parameters.
		if ( Options::is_monitor404_ignore_query_params() ) {
			$uri = strtok( $uri, '?' );
		}

		$uri = untrailingslashit( $uri );
		if ( '' === $uri ) {
			return;
		}

		// Check exclusions.
		if ( self::is_url_excluded( $uri ) ) {
			return;
		}

		$mode = Options::get_monitor404_mode();

		if ( 'simple' === $mode ) {
			Db::update( array( 'uri' => $uri ) );
			return;
		}

		// Advanced mode — log referrer and user agent too.
		$referer = isset( $_SERVER['HTTP_REFERER'] ) ? esc_url_raw( wp_unslash( $_SERVER['HTTP_REFERER'] ) ) : '';
		$ua      = isset( $_SERVER['HTTP_USER_AGENT'] ) ? sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) : '';

		Db::add(
			array(
				'uri'        => $uri,
				'referer'    => $referer,
				'user_agent' => $ua,
			)
		);
	}

	/**
	 * Check if a URI matches any exclusion pattern.
	 *
	 * @param string $uri The URI to check.
	 * @return bool
	 */
	private static function is_url_excluded( $uri ) {
		$excludes = Options::get_monitor404_excludes();
		if ( ! is_array( $excludes ) || empty( $excludes ) ) {
			return false;
		}

		foreach ( $excludes as $rule ) {
			$pattern    = isset( $rule['pattern'] ) ? trim( $rule['pattern'] ) : '';
			$comparison = isset( $rule['comparison'] ) ? $rule['comparison'] : 'contains';

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

			if ( self::matches( $pattern, $uri, $comparison ) ) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Compare a pattern against a URI using the given comparison type.
	 *
	 * @param string $pattern    Pattern to match.
	 * @param string $uri        URI to check.
	 * @param string $comparison Comparison type: contains, exact, starts, ends, regex.
	 * @return bool
	 */
	private static function matches( $pattern, $uri, $comparison ) {
		switch ( $comparison ) {
			case 'exact':
				return $pattern === $uri;
			case 'starts':
				return 0 === strpos( $uri, $pattern );
			case 'ends':
				return substr( $uri, -strlen( $pattern ) ) === $pattern;
			case 'regex':
				// Add delimiters if missing.
				if ( ! preg_match( '/^([^a-zA-Z0-9\\\\]).*\\1[imsxADSUXJu]*$/', $pattern ) ) {
					$pattern = '/' . str_replace( '/', '\\/', $pattern ) . '/';
				}
				return (bool) @preg_match( $pattern, $uri );
			case 'contains':
			default:
				return false !== stripos( $uri, $pattern );
		}
	}

	/**
	 * Get the appropriate hook for 404 capture depending on the active theme.
	 *
	 * @return string
	 */
	private function get_hook() {
		if ( defined( 'CT_VERSION' ) ) {
			$hook = 'oxygen_enqueue_frontend_scripts';
		} elseif ( function_exists( 'wp_is_block_theme' ) && wp_is_block_theme() ) {
			$hook = 'wp_head';
		} else {
			$hook = 'get_header';
		}

		return apply_filters( 'seovaultseo_monitor404_hook', $hook );
	}
}
