<?php
/**
 * Native WordPress recording workspace access guard.
 *
 * @package ScheduledChanges
 */

namespace ScheduledChanges\WordPressContent\Recording;

use ScheduledChanges\Capabilities\CapabilityManager;
use ScheduledChanges\Recording\RecordingWorkspaceMetadata;
use ScheduledChanges\Repository\RecordingSessionRepository;

/**
 * Keeps editable recording copies out of public and ordinary admin collections.
 */
final class NativeWordPressWorkspaceAccessGuard {
	private const NONCE_ACTION  = 'scheduled_changes_native_recording_status';
	private const NONCE_NAME    = 'scheduled_changes_native_recording_status_nonce';
	private const ADMIN_ACTION  = 'scheduled_changes_admin_action';
	private const AFTER_SAVE    = 'scheduled_changes_recording_after_save';
	private const SCRIPT_HANDLE = 'scheduled-changes-native-recording';
	private RecordingSessionRepository $recordings;

	/**
	 * Creates the native editor guard with recording state access.
	 */
	public function __construct( RecordingSessionRepository $recordings ) {
		$this->recordings = $recordings;
	}

	/**
	 * Registers privacy and editor integration hooks.
	 */
	public function registerHooks(): void {
		add_action( 'pre_get_posts', array( $this, 'excludeFromAdminLists' ), 20 );
		add_filter( 'rest_post_query', array( $this, 'excludeFromRestCollections' ) );
		add_filter( 'rest_page_query', array( $this, 'excludeFromRestCollections' ) );
		add_filter( 'wp_sitemaps_posts_query_args', array( $this, 'excludeFromSitemaps' ), 10, 2 );
		add_filter( 'display_post_states', array( $this, 'displayPostStates' ), 10, 2 );
		add_filter( 'wp_insert_post_data', array( $this, 'forceNonPublicStatus' ), 20, 2 );
		add_action( 'add_meta_boxes', array( $this, 'addRecordingMetaBox' ), 20, 2 );
		add_action( 'save_post', array( $this, 'saveDesiredStatus' ), 20, 2 );
		add_action( 'admin_notices', array( $this, 'renderEditorNotice' ) );
		add_action( 'admin_enqueue_scripts', array( $this, 'enqueueClassicEditorAssets' ) );
		add_action( 'enqueue_block_editor_assets', array( $this, 'enqueueBlockEditorAssets' ) );
		add_filter( 'redirect_post_location', array( $this, 'redirectAfterWorkspaceSave' ), 10, 2 );
	}

	/**
	 * Detects the stable native workspace marker.
	 */
	public static function isWorkspace( int $post_id ): bool {
		return '1' === (string) get_post_meta( $post_id, RecordingWorkspaceMetadata::NATIVE_MARKER, true );
	}

	/**
	 * Excludes workspaces from ordinary Posts and Pages list tables.
	 */
	public function excludeFromAdminLists( \WP_Query $query ): void {
		if ( ! is_admin() || ! $query->is_main_query() ) {
			return;
		}

		$post_type = $query->get( 'post_type' );

		if ( ! in_array( $post_type, array( 'post', 'page' ), true ) ) {
			return;
		}

		$query->set( 'meta_query', $this->excludingMetaQuery( $query->get( 'meta_query' ) ) );
	}

	/**
	 * @param array<string,mixed> $args REST collection query arguments.
	 * @return array<string,mixed>
	 */
	public function excludeFromRestCollections( array $args ): array {
		// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required collection privacy boundary.
		$args['meta_query'] = $this->excludingMetaQuery( $args['meta_query'] ?? array() );

		return $args;
	}

	/**
	 * @param array<string,mixed> $args Sitemap query arguments.
	 * @return array<string,mixed>
	 */
	public function excludeFromSitemaps( array $args, string $post_type ): array {
		if ( in_array( $post_type, array( 'post', 'page' ), true ) ) {
			// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- Required sitemap privacy boundary.
			$args['meta_query'] = $this->excludingMetaQuery( $args['meta_query'] ?? array() );
		}

		return $args;
	}

	/**
	 * @param string[] $states Existing post state labels.
	 * @return string[]
	 */
	public function displayPostStates( array $states, \WP_Post $post ): array {
		if ( self::isWorkspace( $post->ID ) ) {
			$states['scheduled-changes-recording-workspace'] = __( 'JJ\'s Post Scheduler recording workspace', 'jjs-post-scheduler' );
		}

		return $states;
	}

	/**
	 * Forces every workspace save to remain a non-public draft.
	 *
	 * @param array<string,mixed> $data Sanitized post data.
	 * @param array<string,mixed> $postarr Raw post data.
	 * @return array<string,mixed>
	 */
	public function forceNonPublicStatus( array $data, array $postarr ): array {
		$post_id = (int) ( $postarr['ID'] ?? 0 );

		if ( $post_id > 0 && self::isWorkspace( $post_id ) ) {
			$data['post_status'] = 'draft';
		}

		return $data;
	}

	/**
	 * Adds Record Changes actions and active recording state to native editors.
	 */
	public function addRecordingMetaBox( string $post_type, \WP_Post $post ): void {
		if (
			! in_array( $post_type, array( 'post', 'page' ), true )
			|| ! current_user_can( 'edit_post', $post->ID )
			|| $this->usesBlockEditor( $post )
			|| ( ! self::isWorkspace( $post->ID ) && ! current_user_can( CapabilityManager::CREATE_SCHEDULES ) )
		) {
			return;
		}

		add_meta_box(
			'scheduled-changes-native-recording',
			__( 'JJ\'s Post Scheduler recording', 'jjs-post-scheduler' ),
			array( $this, 'renderRecordingMetaBox' ),
			$post_type,
			'side',
			'high'
		);
	}

	/**
	 * Renders the desired launch status without exposing the workspace itself.
	 */
	public function renderRecordingMetaBox( \WP_Post $post ): void {
		if ( ! self::isWorkspace( $post->ID ) ) {
			$this->renderSourceRecordingMetaBox( $post );
			return;
		}

		$status        = (string) get_post_meta( $post->ID, RecordingWorkspaceMetadata::DESIRED_STATUS, true );
		$uuid          = (string) get_post_meta( $post->ID, RecordingWorkspaceMetadata::SESSION_UUID, true );
		$source        = absint( get_post_meta( $post->ID, RecordingWorkspaceMetadata::SOURCE_ID, true ) );
		$source_url    = $source > 0 && current_user_can( 'edit_post', $source )
			? get_edit_post_link( $source, 'raw' )
			: '';
		$dashboard_url = admin_url( 'admin.php?page=jjs-scheduler-list' );
		wp_nonce_field( self::NONCE_ACTION, self::NONCE_NAME );
		?>
		<p><strong><?php esc_html_e( 'You are editing a scheduled-change copy. The live item is not being changed.', 'jjs-post-scheduler' ); ?></strong></p>
		<p><?php esc_html_e( 'Save this copy when you are ready, then review what changed and choose when it should go live.', 'jjs-post-scheduler' ); ?></p>
		<p class="scheduled-changes-native-recording-actions">
			<button type="submit" form="post" class="button button-primary" name="<?php echo esc_attr( self::AFTER_SAVE ); ?>" value="review">
				<?php esc_html_e( 'Save and review changes', 'jjs-post-scheduler' ); ?>
			</button>
			<button type="submit" form="post" class="button" name="<?php echo esc_attr( self::AFTER_SAVE ); ?>" value="draft">
				<?php esc_html_e( 'Save draft copy', 'jjs-post-scheduler' ); ?>
			</button>
		</p>
		<?php if ( '' !== $uuid ) : ?>
			<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" class="scheduled-changes-native-recording-cancel">
				<input type="hidden" name="action" value="scheduled_changes_cancel_recording">
				<input type="hidden" name="recording_uuid" value="<?php echo esc_attr( $uuid ); ?>">
				<?php wp_nonce_field( self::ADMIN_ACTION ); ?>
				<button type="submit" class="button">
					<?php esc_html_e( 'Cancel recording', 'jjs-post-scheduler' ); ?>
				</button>
			</form>
		<?php endif; ?>
		<p>
			<?php if ( is_string( $source_url ) && '' !== $source_url ) : ?>
				<a href="<?php echo esc_url( $source_url ); ?>"><?php esc_html_e( 'View source item', 'jjs-post-scheduler' ); ?></a>
				<span aria-hidden="true"> · </span>
			<?php endif; ?>
			<a href="<?php echo esc_url( $dashboard_url ); ?>"><?php esc_html_e( 'Open scheduled changes', 'jjs-post-scheduler' ); ?></a>
		</p>
		<label for="scheduled-changes-recording-desired-status"><?php esc_html_e( 'When this goes live, set the item status to', 'jjs-post-scheduler' ); ?></label>
		<select id="scheduled-changes-recording-desired-status" name="scheduled_changes_recording_desired_status" class="widefat">
			<?php foreach ( array( 'draft', 'pending', 'private', 'publish', 'future' ) as $option ) : ?>
				<option value="<?php echo esc_attr( $option ); ?>" <?php selected( $status, $option ); ?>><?php echo esc_html( $this->statusOptionLabel( $option ) ); ?></option>
			<?php endforeach; ?>
		</select>
		<?php
	}

	/**
	 * Adds the Record/Review action beside the native Add New title action.
	 */
	public function enqueueClassicEditorAssets(): void {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;

		if ( null === $screen || ! in_array( (string) $screen->post_type, array( 'post', 'page' ), true ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only editor integration.
		$post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;

		if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$post = get_post( $post_id );

		if ( ! $post instanceof \WP_Post || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
			return;
		}

		if ( $this->usesBlockEditor( $post ) ) {
			return;
		}

		$is_workspace = self::isWorkspace( $post_id );

		if ( ! $is_workspace && ! current_user_can( CapabilityManager::CREATE_SCHEDULES ) ) {
			return;
		}

		$action = $this->titleActionData( $post, $is_workspace );

		$this->enqueueNativeRecordingAsset(
			array(
				'mode'               => 'classicTitleAction',
				'buttonLabel'        => $action['label'],
				'recordUrl'          => $action['href'],
				'postType'           => $post->post_type,
				'submitReview'       => $action['submitReview'],
				'afterSaveFieldName' => self::AFTER_SAVE,
				'savingLabel'        => __( 'Saving safe copy', 'jjs-post-scheduler' ),
			)
		);
	}

	/**
	 * Renders the source editor start/resume action for posts and pages.
	 */
	private function renderSourceRecordingMetaBox( \WP_Post $post ): void {
		$reference = $post->post_type . ':' . $post->ID;
		$session   = $this->recordings->findActiveBySourceIdentity( 'wordpress-core', $post->post_type, $reference );

		if ( null !== $session && null !== $session->workspaceId() ) {
			$workspace_url = get_edit_post_link( $session->workspaceId(), 'raw' );
			?>
			<p><strong><?php esc_html_e( 'Safe copy in progress', 'jjs-post-scheduler' ); ?></strong></p>
			<p><?php esc_html_e( 'Your live item is not changing while you work on this copy.', 'jjs-post-scheduler' ); ?></p>
			<p>
				<?php if ( is_string( $workspace_url ) && '' !== $workspace_url ) : ?>
					<a class="button button-primary" href="<?php echo esc_url( $workspace_url ); ?>"><?php esc_html_e( 'Continue safe copy', 'jjs-post-scheduler' ); ?></a>
				<?php endif; ?>
				<a class="button" href="<?php echo esc_url( admin_url( 'admin.php?page=jjs-scheduler-list' ) ); ?>"><?php esc_html_e( 'Open scheduled changes', 'jjs-post-scheduler' ); ?></a>
			</p>
			<?php
			return;
		}

		$start_url = $this->startRecordingUrl( $post->ID );
		?>
		<p><?php esc_html_e( 'Create a safe copy, edit it, then choose when the changes should go live.', 'jjs-post-scheduler' ); ?></p>
		<p><a class="button button-primary" href="<?php echo esc_url( $start_url ); ?>"><?php esc_html_e( 'Record a safe copy', 'jjs-post-scheduler' ); ?></a></p>
		<?php
	}

	/**
	 * @return array{href:string,label:string,submitReview:bool}
	 */
	private function titleActionData( \WP_Post $post, bool $is_workspace ): array {
		if ( $is_workspace ) {
			return array(
				'href'         => '#scheduled-changes-review',
				'label'        => __( 'Review and schedule', 'jjs-post-scheduler' ),
				'submitReview' => true,
			);
		}

		$reference = $post->post_type . ':' . $post->ID;
		$session   = $this->recordings->findActiveBySourceIdentity( 'wordpress-core', $post->post_type, $reference );

		if ( null !== $session && null !== $session->workspaceId() ) {
			$workspace_url = get_edit_post_link( $session->workspaceId(), 'raw' );

			if ( is_string( $workspace_url ) && '' !== $workspace_url ) {
				return array(
					'href'         => $workspace_url,
					'label'        => __( 'Continue recording', 'jjs-post-scheduler' ),
					'submitReview' => false,
				);
			}
		}

		return array(
			'href'         => $this->startRecordingUrl( $post->ID ),
			'label'        => __( 'Record', 'jjs-post-scheduler' ),
			'submitReview' => false,
		);
	}

	/**
	 * Builds the authorized native recording start URL for a source object.
	 */
	private function startRecordingUrl( int $source_id ): string {
		return add_query_arg(
			'_wpnonce',
			wp_create_nonce( self::ADMIN_ACTION ),
			add_query_arg(
				array(
					'action'    => 'scheduled_changes_start_dashboard_recording',
					'source_id' => $source_id,
				),
				admin_url( 'admin-post.php' )
			)
		);
	}

	/**
	 * Persists the separately staged status after an authorized editor save.
	 */
	public function saveDesiredStatus( int $post_id, \WP_Post $post ): void {
		if ( ! self::isWorkspace( $post_id ) || ! in_array( $post->post_type, array( 'post', 'page' ), true ) ) {
			return;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing,WordPress.Security.NonceVerification.Recommended -- Nonce is read and verified immediately below.
		$nonce  = sanitize_text_field( wp_unslash( $_POST[ self::NONCE_NAME ] ?? '' ) );
		$status = sanitize_key( wp_unslash( $_POST['scheduled_changes_recording_desired_status'] ?? '' ) );
		// phpcs:enable

		if ( '' === $status && $this->isRestRequest() ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- REST post save already passed through WordPress REST authentication.
			$status = sanitize_key( wp_unslash( $_REQUEST['status'] ?? '' ) );
		}

		if (
			! ( wp_verify_nonce( $nonce, self::NONCE_ACTION ) || $this->isRestRequest() )
			|| ! current_user_can( 'edit_post', $post_id )
			|| ! in_array( $status, array( 'draft', 'pending', 'private', 'publish', 'future' ), true )
		) {
			return;
		}

		update_post_meta( $post_id, RecordingWorkspaceMetadata::DESIRED_STATUS, $status );
	}

	/**
	 * Redirects native safe-copy save actions to the matching next step.
	 */
	public function redirectAfterWorkspaceSave( string $location, int $post_id ): string {
		if ( ! self::isWorkspace( $post_id ) ) {
			return $location;
		}

		// phpcs:disable WordPress.Security.NonceVerification.Missing -- Nonce is verified before honoring the requested save action.
		$nonce  = sanitize_text_field( wp_unslash( $_POST[ self::NONCE_NAME ] ?? '' ) );
		$action = sanitize_key( wp_unslash( $_POST[ self::AFTER_SAVE ] ?? '' ) );
		// phpcs:enable

		if (
			! in_array( $action, array( 'review', 'draft' ), true )
			|| ! wp_verify_nonce( $nonce, self::NONCE_ACTION )
			|| ! current_user_can( 'edit_post', $post_id )
		) {
			return $location;
		}

		if ( 'review' === $action ) {
			$this->queueAdminNotice(
				'success',
				__( 'Safe copy saved. Review the changes and choose when they should go live.', 'jjs-post-scheduler' )
			);

			return admin_url( 'admin.php?page=jjs-scheduler-list' );
		}

		return add_query_arg( 'scheduled_changes_safe_copy_saved', '1', $location );
	}

	/**
	 * Shows an unambiguous recording banner in the post/page editor.
	 */
	public function renderEditorNotice(): void {
		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only editor notice.
		$post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;

		if ( $post_id <= 0 || ! self::isWorkspace( $post_id ) ) {
			return;
		}

		$post = get_post( $post_id );

		if ( $post instanceof \WP_Post && $this->usesBlockEditor( $post ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only editor notice.
		$was_saved = '1' === sanitize_text_field( wp_unslash( $_GET['scheduled_changes_safe_copy_saved'] ?? '' ) );
		$message   = $was_saved
			? __( 'Safe copy saved. The live item was not changed.', 'jjs-post-scheduler' )
			: __( 'You are editing a scheduled-change copy. The live item is not being changed.', 'jjs-post-scheduler' );

		echo '<div class="notice notice-info scheduled-changes-native-recording-notice"><p><strong>'
			. esc_html( $message )
			. '</strong></p><p>'
			. esc_html__( 'Save this copy when you are ready, then review what changed and choose when it should go live.', 'jjs-post-scheduler' )
			. '</p></div>';
	}

	/**
	 * Returns friendly labels for staged WordPress statuses.
	 */
	private function statusOptionLabel( string $status ): string {
		$labels = array(
			'draft'   => __( 'Draft', 'jjs-post-scheduler' ),
			'pending' => __( 'Pending review', 'jjs-post-scheduler' ),
			'private' => __( 'Private', 'jjs-post-scheduler' ),
			'publish' => __( 'Published', 'jjs-post-scheduler' ),
			'future'  => __( 'Scheduled', 'jjs-post-scheduler' ),
		);

		return $labels[ $status ] ?? ucfirst( $status );
	}

	/**
	 * Queues a dashboard notice for the existing admin app notice reader.
	 */
	private function queueAdminNotice( string $type, string $message ): void {
		set_transient(
			'scheduled_changes_admin_notice_' . get_current_user_id(),
			array(
				'type'    => $type,
				'message' => $message,
			),
			60
		);
	}

	/**
	 * Enqueues the block-editor recording button, modal, and bottom rail.
	 */
	public function enqueueBlockEditorAssets(): void {
		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;

		if ( null === $screen || ! in_array( (string) $screen->post_type, array( 'post', 'page' ), true ) ) {
			return;
		}

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only editor asset selection.
		$post_id = isset( $_GET['post'] ) ? absint( wp_unslash( $_GET['post'] ) ) : 0;

		if ( $post_id <= 0 || ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		$post = get_post( $post_id );

		if ( ! $post instanceof \WP_Post || ! $this->usesBlockEditor( $post ) ) {
			return;
		}

		$data = $this->blockEditorRecordingData( $post );

		if ( null === $data ) {
			return;
		}

		$this->enqueueNativeRecordingAsset( $data );
	}

	/**
	 * Enqueues native editor recording assets and exposes bounded config data.
	 *
	 * @param array<string,mixed> $data Client-side recording data.
	 */
	private function enqueueNativeRecordingAsset( array $data ): void {
		$asset_file = SCHEDULED_CHANGES_PLUGIN_DIR . 'assets/build/native-recording.asset.php';
		$asset      = is_readable( $asset_file )
			? include $asset_file
			: array(
				'dependencies' => array( 'wp-i18n' ),
				'version'      => SCHEDULED_CHANGES_VERSION,
			);
		$version    = (string) ( $asset['version'] ?? SCHEDULED_CHANGES_VERSION );

		wp_enqueue_script(
			self::SCRIPT_HANDLE,
			SCHEDULED_CHANGES_PLUGIN_URL . 'assets/build/native-recording.js',
			is_array( $asset['dependencies'] ?? null ) ? $asset['dependencies'] : array( 'wp-i18n' ),
			$version,
			true
		);
		wp_set_script_translations(
			self::SCRIPT_HANDLE,
			'jjs-post-scheduler',
			SCHEDULED_CHANGES_PLUGIN_DIR . 'languages'
		);
		wp_enqueue_style(
			self::SCRIPT_HANDLE,
			SCHEDULED_CHANGES_PLUGIN_URL . 'assets/build/native-recording.css',
			array(),
			$version
		);
		wp_add_inline_script(
			self::SCRIPT_HANDLE,
			'window.scheduledChangesNativeRecording = ' . wp_json_encode( $data ) . ';',
			'before'
		);
	}

	/**
	 * @return array<string,mixed>|null
	 */
	private function blockEditorRecordingData( \WP_Post $post ): ?array {
		$is_workspace = self::isWorkspace( $post->ID );

		if ( ! $is_workspace && ! current_user_can( CapabilityManager::CREATE_SCHEDULES ) ) {
			return null;
		}

		if ( $is_workspace ) {
			$uuid       = (string) get_post_meta( $post->ID, RecordingWorkspaceMetadata::SESSION_UUID, true );
			$source_id  = absint( get_post_meta( $post->ID, RecordingWorkspaceMetadata::SOURCE_ID, true ) );
			$source_url = $source_id > 0 && current_user_can( 'edit_post', $source_id )
				? get_edit_post_link( $source_id, 'raw' )
				: '';

			if ( '' === $uuid || ! is_string( $source_url ) || '' === $source_url ) {
				return null;
			}

			$default_time = $this->defaultScheduleTime();

			return array(
				'mode'          => 'workspace',
				'sessionUuid'   => $uuid,
				'postId'        => $post->ID,
				'postType'      => $post->post_type,
				'restUrl'       => rest_url( 'scheduled-changes/v1/recordings/' . rawurlencode( $uuid ) ),
				'restNonce'     => wp_create_nonce( 'wp_rest' ),
				'sourceTitle'   => get_the_title( $source_id ),
				'sourceEditUrl' => $source_url,
				'launchesUrl'   => admin_url( 'admin.php?page=jjs-scheduler-list' ),
				'defaultDate'   => $default_time->format( 'Y-m-d' ),
				'defaultTime'   => $default_time->format( 'H:i' ),
				'timezone'      => $this->siteTimezoneName(),
			);
		}

		$action = $this->titleActionData( $post, false );

		return array(
			'mode'        => 'source',
			'buttonLabel' => $action['label'],
			'recordUrl'   => $action['href'],
		);
	}

	/**
	 * Checks if the post is using the block editor.
	 */
	private function usesBlockEditor( \WP_Post $post ): bool {
		return function_exists( 'use_block_editor_for_post' )
			&& use_block_editor_for_post( $post );
	}

	/**
	 * Detects a REST request already authenticated by WordPress.
	 */
	private function isRestRequest(): bool {
		return defined( 'REST_REQUEST' ) && REST_REQUEST;
	}

	/**
	 * Returns the default recording schedule time.
	 */
	private function defaultScheduleTime(): \DateTimeImmutable {
		return new \DateTimeImmutable( '+1 hour', new \DateTimeZone( $this->siteTimezoneName() ) );
	}

	/**
	 * Returns the site's timezone with a UTC fallback.
	 */
	private function siteTimezoneName(): string {
		$timezone = wp_timezone_string();

		return is_string( $timezone ) && '' !== $timezone
			? $timezone
			: 'UTC';
	}

	/**
	 * @param mixed $existing Existing meta query.
	 * @return array<int|string,mixed>
	 */
	private function excludingMetaQuery( $existing ): array {
		$meta_query   = is_array( $existing ) ? $existing : array();
		$meta_query[] = array(
			'key'     => RecordingWorkspaceMetadata::NATIVE_MARKER,
			'compare' => 'NOT EXISTS',
		);

		return $meta_query;
	}
}
