<?php
/**
 * Product editor recording request validation.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChangesWooCommerce\CoreCompat\Admin;

use ScheduledChanges\Domain\Recording\RecordingSession;
use ScheduledChanges\Domain\Shared\Uuid;

/**
 * Owns the product-editor recording nonce and request-context policy.
 */
final class ProductEditorRecordingRequestGuard {
	private const NONCE_ACTION = 'scheduled_changes_product_editor_recording';

	/**
	 * Verifies the source-scoped AJAX nonce for starting a recording.
	 */
	public function checkStartAjaxNonce(): int {
		$source_product_id = $this->rawPostedInt( 'source_product_id' );

		check_ajax_referer( $this->sourceNonceAction( 'start', $source_product_id ), 'nonce' );

		return $source_product_id;
	}

	/**
	 * Verifies a session/action-scoped AJAX nonce and returns the submitted UUID.
	 *
	 * @param string $action Recording action.
	 */
	public function checkSessionAjaxNonce( string $action ): Uuid {
		$uuid_value = $this->rawPostedString( 'session_uuid' );

		check_ajax_referer( $this->sessionNonceActionValue( $action, $uuid_value ), 'nonce' );

		return new Uuid( $uuid_value );
	}

	/**
	 * Builds a source-scoped nonce for starting a recording.
	 *
	 * @param int $source_product_id Source product ID.
	 */
	public function sourceStartNonce( int $source_product_id ): string {
		return wp_create_nonce( $this->sourceNonceAction( 'start', $source_product_id ) );
	}

	/**
	 * Builds all action nonces used by the shadow product editor.
	 *
	 * @param RecordingSession $session Recording session.
	 * @return array<string,string>
	 */
	public function shadowActionNonces( RecordingSession $session ): array {
		$actions = array( 'save', 'cancel', 'finalize', 'schedule', 'pending_term' );
		$nonces  = array();

		foreach ( $actions as $action ) {
			$nonces[ $action ] = wp_create_nonce( $this->sessionNonceAction( $action, $session->uuid() ) );
		}

		return $nonces;
	}

	/**
	 * Rejects product-editor context that points outside the active source/shadow pair.
	 *
	 * @param RecordingSession $session Recording session.
	 * @param int              $shadow_id Shadow product ID.
	 */
	public function assertShadowSaveContext( RecordingSession $session, int $shadow_id ): void {
		$allowed_ids = array_values(
			array_filter(
				array( $session->sourceProductId(), $shadow_id ),
				static fn ( int $post_id ): bool => $post_id > 0
			)
		);

		foreach ( array( 'post_ID', 'ID', 'post' ) as $key ) {
			if ( ! $this->postedKeyExists( $key ) ) {
				continue;
			}

			$submitted_id = $this->postedInt( $key );

			if ( $submitted_id > 0 && ! in_array( $submitted_id, $allowed_ids, true ) ) {
				throw new \RuntimeException( 'Recording save payload does not match this session.' );
			}
		}

		$referer_id = $this->postIdFromReferer( $this->postedRawString( '_wp_http_referer' ) );

		if ( $referer_id > 0 && ! in_array( $referer_id, $allowed_ids, true ) ) {
			throw new \RuntimeException( 'Recording save referer does not match this session.' );
		}
	}

	/**
	 * Builds a source-scoped nonce action name.
	 *
	 * @param string $action Recording action.
	 * @param int    $source_product_id Source product ID.
	 */
	public function sourceNonceAction( string $action, int $source_product_id ): string {
		return self::NONCE_ACTION . ':' . sanitize_key( $action ) . ':' . max( 0, $source_product_id );
	}

	/**
	 * Builds a session-scoped nonce action name.
	 *
	 * @param string $action Recording action.
	 * @param Uuid   $uuid Recording session UUID.
	 */
	public function sessionNonceAction( string $action, Uuid $uuid ): string {
		return $this->sessionNonceActionValue( $action, $uuid->value() );
	}

	/**
	 * Builds a session-scoped nonce action name from a raw UUID value.
	 *
	 * @param string $action Recording action.
	 * @param string $uuid_value Recording session UUID value.
	 */
	public function sessionNonceActionValue( string $action, string $uuid_value ): string {
		return self::NONCE_ACTION . ':' . sanitize_key( $action ) . ':' . sanitize_text_field( $uuid_value );
	}

	/**
	 * Returns the edited post ID from an admin referer URL.
	 *
	 * @param string $referer Admin referer URL.
	 */
	public function postIdFromReferer( string $referer ): int {
		if ( '' === $referer ) {
			return 0;
		}

		$query = wp_parse_url( $referer, PHP_URL_QUERY );

		if ( ! is_string( $query ) || '' === $query ) {
			return 0;
		}

		parse_str( $query, $params );

		return absint( $params['post'] ?? 0 );
	}

	/**
	 * Reads an integer from POST data before deciding whether to accept the request.
	 *
	 * @param string $key POST key.
	 */
	private function rawPostedInt( string $key ): int {
		// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only routing data used before nonce verification; value is unslashed and cast below.
		$value = $_POST[ $key ] ?? 0;
		// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		return absint( wp_unslash( $value ) );
	}

	/**
	 * Reads a sanitized string from POST data before deciding whether to accept the request.
	 *
	 * @param string $key POST key.
	 */
	private function rawPostedString( string $key ): string {
		// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Read-only routing data used before nonce verification; value is unslashed and sanitized below.
		$value = $_POST[ $key ] ?? '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		return sanitize_text_field( wp_unslash( $value ) );
	}

	/**
	 * Reads an integer from POST data after AJAX nonce verification.
	 *
	 * @param string $key POST key.
	 */
	private function postedInt( string $key ): int {
		// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- AJAX nonce is verified before this helper is used; value is unslashed and sanitized below.
		$value = $_POST[ $key ] ?? 0;
		// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		return absint( wp_unslash( $value ) );
	}

	/**
	 * Reads an unslashed raw string from POST data for validation.
	 *
	 * @param string $key POST key.
	 */
	private function postedRawString( string $key ): string {
		// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- AJAX nonce is verified before this helper is used; callers validate without rewriting.
		$value = $_POST[ $key ] ?? '';
		// phpcs:enable WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized

		if ( ! is_string( $value ) ) {
			return '';
		}

		return wp_unslash( $value );
	}

	/**
	 * Checks whether a POST key exists after AJAX nonce verification.
	 *
	 * @param string $key POST key.
	 */
	private function postedKeyExists( string $key ): bool {
		// phpcs:disable WordPress.Security.NonceVerification.Missing -- AJAX nonce is verified before this helper is used.
		$exists = array_key_exists( $key, $_POST );
		// phpcs:enable WordPress.Security.NonceVerification.Missing

		return $exists;
	}
}
