<?php
/**
 * Feed delivery via rewrite rule and token validation.
 *
 * @package Kramar
 * @since   1.0.0
 */

declare( strict_types=1 );

namespace Kramar\Modules\Marketing;

// Prevent direct access.
defined( 'ABSPATH' ) || exit;

/**
 * Registers the /kramar-feed/google-shopping/ rewrite rule and serves the
 * cached feed file after validating the secret token.
 *
 * @since 1.0.0
 */
final class Feed_Delivery {

	/** Rewrite regex for the pretty feed URL. */
	private const REWRITE_REGEX = '^kramar-feed/google-shopping/?$';

	/** Bump when REWRITE_REGEX changes to force a one-time re-flush. */
	private const REWRITE_VERSION = '1';

	/**
	 * Query var written into the rewrite target.
	 *
	 * Deliberately avoids the substring "feed=" so that feed-disabling hardening
	 * (Zamok, Perfmatters, security plugins, etc. — which strip every rewrite
	 * rule whose target contains "feed=") does not remove this rule as collateral
	 * and 404 the pretty feed URL.
	 */
	private const QUERY_VAR = 'kramar_gmc';

	/** Legacy query var, kept so existing ?kramar-feed=… feed URLs keep working. */
	private const LEGACY_QUERY_VAR = 'kramar-feed';

	/**
	 * Constructor.
	 *
	 * @since 1.0.0
	 *
	 * @param Feed_Config $cfg Module configuration.
	 */
	public function __construct( private Feed_Config $cfg ) {}

	/**
	 * Register all delivery hooks.
	 *
	 * @since 1.0.0
	 */
	public function register(): void {
		add_action( 'init', [ $this, 'add_rewrite_rule' ] );
		add_filter( 'query_vars', [ $this, 'add_query_vars' ] );
		add_action( 'template_redirect', [ $this, 'maybe_serve_feed' ] );
		// Self-heal the rewrite rule: flush once whenever our rule is missing
		// from the compiled rules (fresh enable, permalink change, etc.) so the
		// pretty /kramar-feed/google-shopping/ URL resolves instead of 404ing.
		add_action( 'wp_loaded', [ $this, 'maybe_flush_rules' ] );
	}

	/**
	 * Register the rewrite rule for the feed URL.
	 *
	 * @since 1.0.0
	 */
	public function add_rewrite_rule(): void {
		add_rewrite_rule(
			self::REWRITE_REGEX,
			'index.php?' . self::QUERY_VAR . '=google-shopping',
			'top'
		);
	}

	/**
	 * Flush rewrite rules once so the pretty feed URL persists (add_rewrite_rule()
	 * registers it in memory on every 'init' but does not persist it).
	 *
	 * Gated on a tiny autoloaded version flag rather than deserializing the whole
	 * rewrite_rules option on every request. This flushes once on first enable and
	 * again only if REWRITE_VERSION is bumped after a rule change; a permalink
	 * re-save flushes all rules and re-runs our 'init' hook, so that path stays
	 * self-healing without this method.
	 *
	 * @since 1.0.0
	 */
	public function maybe_flush_rules(): void {
		if ( (string) get_option( 'kramar_gmc_rewrite_version' ) === self::REWRITE_VERSION ) {
			return;
		}
		flush_rewrite_rules( false );
		update_option( 'kramar_gmc_rewrite_version', self::REWRITE_VERSION );
	}

	/**
	 * Add the feed query variables.
	 *
	 * @since 1.0.0
	 *
	 * @param array<string> $vars Existing query vars.
	 * @return array<string> Modified query vars.
	 */
	public function add_query_vars( array $vars ): array {
		$vars[] = self::QUERY_VAR;
		$vars[] = self::LEGACY_QUERY_VAR;
		return $vars;
	}

	/**
	 * Serve the feed file if the request matches and the token is valid.
	 *
	 * @since 1.0.0
	 */
	public function maybe_serve_feed(): void {
		$requested = 'google-shopping' === get_query_var( self::QUERY_VAR )
			|| 'google-shopping' === get_query_var( self::LEGACY_QUERY_VAR );
		if ( ! $requested ) {
			return;
		}

		// Validate secret token using timing-safe comparison.
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$submitted = sanitize_key( $_GET['token'] ?? '' );
		$expected  = $this->cfg->get_token();

		if ( '' === $expected || ! hash_equals( $expected, $submitted ) ) {
			status_header( 403 );
			header( 'X-Robots-Tag: noindex' );
			wp_die( esc_html__( 'Forbidden: invalid or missing feed token.', 'kramar' ), '', [ 'response' => 403 ] );
		}

		$file_path = $this->cfg->get_feed_path();

		if ( ! is_file( $file_path ) ) {
			status_header( 404 );
			header( 'X-Robots-Tag: noindex' );
			wp_die( esc_html__( 'Feed not yet generated. Enable the module to schedule generation, or use "Regenerate now".', 'kramar' ), '', [ 'response' => 404 ] );
		}

		// Serve the static XML file.
		status_header( 200 );
		header( 'Content-Type: application/xml; charset=UTF-8' );
		header( 'X-Robots-Tag: noindex' );
		header( 'Cache-Control: public, max-age=3600' );
		header( 'Content-Length: ' . (string) filesize( $file_path ) );

		// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile
		readfile( $file_path );
		exit;
	}
}
