<?php
/**
 * PostGate Post Meta Class
 *
 * Handles post-level paywall configuration for both Classic and Block editors.
 * The Block Editor UI is provided via a PluginSidebar and is skipped here.
 *
 * @package PostGate
 * @subpackage Admin
 * @since 1.0.0
 * @author QodeSphere
 * @license GPL-2.0+
 */

defined( 'ABSPATH' ) || exit;

/**
 * PostGate Post Meta Class
 *
 * Manages post-level PostGate settings and meta fields for the Classic Editor.
 * Provides comprehensive configuration options for lock status, lock mode, preview
 * settings, and custom Stripe pricing on a per-post basis.
 *
 * This class handles meta box registration, form rendering, data validation,
 * and saving of post-specific PostGate configuration that can override global
 * and term-level settings.
 *
 * @since 1.0.0
 */
class PostGate_Post_Meta {

	/**
	 * Valid lock override values.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	const LOCK_OVERRIDE_OPTIONS = [ 'inherit', 'lock', 'unlock' ];

	/**
	 * Valid lock mode values.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	const LOCK_MODE_OPTIONS = [ 'inherit', 'paywall', 'login' ];

	/**
	 * Valid preview mode values.
	 *
	 * @since 1.0.0
	 * @var array
	 */
	const PREVIEW_MODE_OPTIONS = [ 'inherit', 'paragraph', 'words' ];

	/**
	 * Initialize the class and hook into WordPress.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function init(): void {
		add_action( 'init', [ __CLASS__, 'register_post_meta' ] );
		add_action( 'add_meta_boxes', [ __CLASS__, 'add_meta_boxes' ], 10, 2 );
		add_action( 'save_post', [ __CLASS__, 'save_meta' ] );
	}

	/**
	 * Register post meta fields used by PostGate.
	 *
	 * These meta fields are shared between the Classic and Block editors.
	 * All fields include REST API support for Gutenberg integration.
	 *
	 * @since 1.0.0
	 * @return void
	 */
	public static function register_post_meta(): void {
		// Lock override setting
		register_post_meta(
			'',
			'_postgate_lock_override',
			[
				'type' => 'string',
				'default' => 'inherit',
				'sanitize_callback' => [ __CLASS__, 'sanitize_lock_override' ],
				'single' => true,
				'show_in_rest' => true,
				'auth_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
			]
		);

		// Lock mode setting
		register_post_meta(
			'',
			'_postgate_lock_mode',
			[
				'type' => 'string',
				'default' => 'inherit',
				'sanitize_callback' => [ __CLASS__, 'sanitize_lock_mode' ],
				'single' => true,
				'show_in_rest' => true,
				'auth_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
			]
		);

		// Preview mode setting
		register_post_meta(
			'',
			'_postgate_preview_mode',
			[
				'type' => 'string',
				'default' => 'inherit',
				'sanitize_callback' => [ __CLASS__, 'sanitize_preview_mode' ],
				'single' => true,
				'show_in_rest' => true,
				'auth_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
			]
		);

		// Preview limit setting
		register_post_meta(
			'',
			'_postgate_preview_limit',
			[
				'type' => 'integer',
				'default' => 0, // 0 == inherit
				'sanitize_callback' => [ __CLASS__, 'sanitize_preview_limit' ],
				'single' => true,
				'show_in_rest' => true,
				'auth_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
			]
		);

		// Product-based meta (array of Stripe product IDs)
		register_post_meta(
			'',
			'_postgate_product_ids',
			[
				'type' => 'array',
				'single' => true,
				'default' => [],
				'show_in_rest' => [
					'schema' => [
						'type' => 'array',
						'items' => [ 'type' => 'string' ],
					],
				],
				'sanitize_callback' => [ __CLASS__, 'sanitize_product_ids' ],
				'auth_callback' => function () {
					return current_user_can( 'edit_posts' );
				},
			]
		);
	}

	/**
	 * Sanitize lock override value.
	 *
	 * @since 1.0.0
	 * @param mixed $value The value to sanitize.
	 * @return string Sanitized lock override value.
	 */
	public static function sanitize_lock_override( $value ): string {
		$value = is_string( $value ) ? $value : '';
		return in_array( $value, self::LOCK_OVERRIDE_OPTIONS, true ) ? $value : 'inherit';
	}

	/**
	 * Sanitize lock mode value.
	 *
	 * @since 1.0.0
	 * @param mixed $value The value to sanitize.
	 * @return string Sanitized lock mode value.
	 */
	public static function sanitize_lock_mode( $value ): string {
		$value = is_string( $value ) ? $value : '';
		return in_array( $value, self::LOCK_MODE_OPTIONS, true ) ? $value : 'inherit';
	}

	/**
	 * Sanitize preview mode value.
	 *
	 * @since 1.0.0
	 * @param mixed $value The value to sanitize.
	 * @return string Sanitized preview mode value.
	 */
	public static function sanitize_preview_mode( $value ): string {
		$value = is_string( $value ) ? $value : '';
		return in_array( $value, self::PREVIEW_MODE_OPTIONS, true ) ? $value : 'inherit';
	}

	/**
	 * Sanitize preview limit value.
	 *
	 * @since 1.0.0
	 * @param mixed $value The value to sanitize.
	 * @return int Sanitized preview limit (0 means inherit).
	 */
	public static function sanitize_preview_limit( $value ): int {
		// Empty/non-numeric => inherit (0)
		if ( '' === $value || null === $value || ! is_numeric( $value ) ) {
			return 0;
		}

		$value = (int) $value;
		return $value > 0 ? $value : 0;
	}

	/**
	 * Sanitize array of Stripe product IDs.
	 *
	 * Ensures the value is an array and all items are valid, non-empty strings.
	 * Removes duplicates and returns a clean indexed array.
	 *
	 * @since 1.0.0
	 * @param mixed $value The value to sanitize.
	 * @return array Sanitized array of product IDs.
	 */
	public static function sanitize_product_ids( $value ): array {
		if ( ! is_array( $value ) ) {
			return [];
		}

		$sanitized = [];
		foreach ( $value as $item ) {
			$item = sanitize_text_field( (string) $item );
			if ( '' !== $item ) {
				$sanitized[] = $item;
			}
		}

		return array_values( array_unique( $sanitized ) );
	}

	/**
	 * Add meta box for Classic Editor.
	 *
	 * Adds the meta box for all public post types when using the Classic Editor.
	 * The meta box is always available regardless of the enabled post types setting,
	 * allowing users to selectively lock individual posts even if the post type
	 * is not locked by default.
	 *
	 * @since 1.0.0
	 * @param string  $post_type The current post type.
	 * @param WP_Post $post      The current post object.
	 * @return void
	 */
	public static function add_meta_boxes( string $post_type, \WP_Post $post ): void {
		// Skip if this post uses the Block Editor
		if ( function_exists( 'use_block_editor_for_post' ) && use_block_editor_for_post( $post ) ) {
			return;
		}

		// Add meta box for all public post types
		$post_type_object = get_post_type_object( $post_type );
		if ( ! $post_type_object || ! $post_type_object->public ) {
			return;
		}

		add_meta_box(
			'postgate_post_overrides',
			__( 'PostGate: Content Protection', 'postgate' ),
			[ __CLASS__, 'render_meta_box' ],
			$post_type,
			'normal',
			'default'
		);
	}

	/**
	 * Render the Classic Editor meta box.
	 *
	 * Generates Select2-ready markup without inline JS/CSS.
	 * All styling and JavaScript should be enqueued separately.
	 *
	 * @since 1.0.0
	 * @param WP_Post $post The current post object.
	 * @return void
	 */
	public static function render_meta_box( \WP_Post $post ): void {
		wp_nonce_field( 'postgate_save_meta', 'postgate_meta_nonce' );

		// Get current meta values with safe defaults
		$current_lock_override = get_post_meta( $post->ID, '_postgate_lock_override', true ) ?: 'inherit';
		$current_lock_mode = get_post_meta( $post->ID, '_postgate_lock_mode', true ) ?: 'inherit';

		$current_preview_mode = get_post_meta( $post->ID, '_postgate_preview_mode', true );
		$current_preview_mode = in_array( $current_preview_mode, self::PREVIEW_MODE_OPTIONS, true ) ? $current_preview_mode : 'inherit';

		$current_preview_limit = get_post_meta( $post->ID, '_postgate_preview_limit', true );
		$current_preview_limit = ( '' === $current_preview_limit || null === $current_preview_limit ) ? '' : (int) $current_preview_limit;

		// Get available Stripe products for the current mode
		$stripe_mode = get_option( 'postgate_stripe_connect_mode', 'sandbox' ) === 'live' ? 'live' : 'sandbox';
		$available_products_map = [];
		$all_stripe_products = get_option( 'postgate_stripe_products' );

		if ( is_array( $all_stripe_products ) && ! empty( $all_stripe_products[ $stripe_mode ]['products'] ) && is_array( $all_stripe_products[ $stripe_mode ]['products'] ) ) {
			foreach ( $all_stripe_products[ $stripe_mode ]['products'] as $product_item ) {
				if ( empty( $product_item['id'] ) ) {
					continue;
				}
				$product_id = (string) $product_item['id'];
				$product_name = isset( $product_item['name'] ) && '' !== $product_item['name'] ? (string) $product_item['name'] : $product_id;
				$available_products_map[ $product_id ] = $product_name;
			}
			asort( $available_products_map, SORT_NATURAL | SORT_FLAG_CASE );
		}

		// Current product selection (array)
		$current_selected_product_ids = get_post_meta( $post->ID, '_postgate_product_ids', true );
		$current_selected_product_ids = is_array( $current_selected_product_ids ) ? array_values( array_filter( array_map( 'strval', $current_selected_product_ids ) ) ) : [];

		?>
		<div class="postgate-field">
			<label for="postgate_lock_override"><strong><?php esc_html_e( 'Lock Status', 'postgate' ); ?></strong></label><br />
			<select id="postgate_lock_override" name="postgate_lock_override" style="width:100%;">
				<option value="inherit" <?php selected( $current_lock_override, 'inherit' ); ?>>
					<?php esc_html_e( 'Inherit (settings/terms)', 'postgate' ); ?>
				</option>
				<option value="lock" <?php selected( $current_lock_override, 'lock' ); ?>>
					<?php esc_html_e( 'Lock', 'postgate' ); ?>
				</option>
				<option value="unlock" <?php selected( $current_lock_override, 'unlock' ); ?>>
					<?php esc_html_e( 'Unlock', 'postgate' ); ?>
				</option>
			</select>
			<p class="postgate-small">
				<?php esc_html_e( 'Control whether this specific post is locked or publicly accessible. "Inherit" follows site-wide settings and category rules. "Lock" always requires payment/login. "Unlock" makes this post always public regardless of other settings.', 'postgate' ); ?>
			</p>
		</div>

		<div class="postgate-field">
			<label for="postgate_lock_mode"><strong><?php esc_html_e( 'Lock Mode', 'postgate' ); ?></strong></label><br />
			<select id="postgate_lock_mode" name="postgate_lock_mode" style="width:100%;">
				<option value="inherit" <?php selected( $current_lock_mode, 'inherit' ); ?>>
					<?php esc_html_e( 'Inherit', 'postgate' ); ?>
				</option>
				<option value="paywall" <?php selected( $current_lock_mode, 'paywall' ); ?>>
					<?php esc_html_e( 'Paywall', 'postgate' ); ?>
				</option>
				<option value="login" <?php selected( $current_lock_mode, 'login' ); ?>>
					<?php esc_html_e( 'Login', 'postgate' ); ?>
				</option>
			</select>
			<p class="postgate-small">
				<?php esc_html_e( 'Choose how users access this post when locked. "Paywall" requires payment/purchase. "Login" only requires an existing account. "Inherit" uses the site-wide default mode.', 'postgate' ); ?>
			</p>
		</div>

		<div class="postgate-field">
			<label
				for="postgate_preview_mode"><strong><?php esc_html_e( 'Content Preview Mode', 'postgate' ); ?></strong></label><br />
			<select id="postgate_preview_mode" name="postgate_preview_mode" style="width:100%;">
				<option value="inherit" <?php selected( $current_preview_mode, 'inherit' ); ?>>
					<?php esc_html_e( 'Inherit', 'postgate' ); ?>
				</option>
				<option value="paragraph" <?php selected( $current_preview_mode, 'paragraph' ); ?>>
					<?php esc_html_e( 'Paragraphs', 'postgate' ); ?>
				</option>
				<option value="words" <?php selected( $current_preview_mode, 'words' ); ?>>
					<?php esc_html_e( 'Words', 'postgate' ); ?>
				</option>
			</select>
			<p class="postgate-small">
				<?php esc_html_e( 'Controls how much free content users see before the paywall. "Paragraphs" works well for articles with natural breaks. "Words" gives precise control for continuous text. "Inherit" uses the site-wide setting.', 'postgate' ); ?>
			</p>
		</div>

		<div class="postgate-field">
			<label
				for="postgate_preview_limit"><strong><?php esc_html_e( 'Preview Limit', 'postgate' ); ?></strong></label><br />
			<input type="number" min="0" step="1" style="width:100%;margin-top:6px" id="postgate_preview_limit"
				name="postgate_preview_limit" value="<?php echo esc_attr( $current_preview_limit ); ?>"
				placeholder="<?php esc_attr_e( 'Amount (N)', 'postgate' ); ?>" />
			<p class="postgate-small">
				<?php esc_html_e( 'Number of paragraphs or words to show for free before the paywall appears. Leave empty to use the site-wide default. Set to 0 to show no preview (paywall appears immediately).', 'postgate' ); ?>
			</p>
		</div>

		<div class="postgate-field">
			<label
				for="postgate_product_ids"><strong><?php esc_html_e( 'Product Assignment', 'postgate' ); ?></strong></label><br />

			<!-- Presence marker so save_post can detect the field was rendered and purge when empty -->
			<input type="hidden" name="postgate_product_ids__present" value="1" />

			<select class="postgate-product-ids" id="postgate_product_ids" name="postgate_product_ids[]" multiple
				style="width:100%;" data-placeholder="<?php echo esc_attr__( 'Search for products...', 'postgate' ); ?>">
				<?php
				foreach ( $available_products_map as $product_id => $product_name ) {
					printf(
						'<option value="%1$s"%2$s>%3$s</option>',
						esc_attr( $product_id ),
						selected( in_array( (string) $product_id, $current_selected_product_ids, true ), true, false ),
						esc_html( $product_name )
					);
				}
				?>
			</select>
			<p class="postgate-small">
				<?php esc_html_e( 'Assign specific products that users must purchase to access this post. Leave empty to use the site-wide default product or category-specific products. Users who purchase any of the selected products will unlock this content.', 'postgate' ); ?>
			</p>
		</div>
		<?php
	}

	/**
	 * Save meta box data for Classic Editor.
	 *
	 * Handles nonce verification, capability checks, and saves all PostGate
	 * meta fields with proper sanitization.
	 *
	 * @since 1.0.0
	 * @param int $post_id The post ID being saved.
	 * @return void
	 */
	public static function save_meta( int $post_id ): void {
		// Verify nonce
		if ( ! isset( $_POST['postgate_meta_nonce'] ) ) {
			return;
		}
		$submitted_nonce = sanitize_text_field( wp_unslash( $_POST['postgate_meta_nonce'] ) );
		if ( ! wp_verify_nonce( $submitted_nonce, 'postgate_save_meta' ) ) {
			// phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Core hook.
			do_action( 'wp_verify_nonce_failed', $submitted_nonce, 'postgate_save_meta', wp_get_current_user(), '' );
			return;
		}

		// Skip autosaves, revisions, and capability checks
		if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) {
			return;
		}
		if ( wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) ) {
			return;
		}
		if ( ! current_user_can( 'edit_post', $post_id ) ) {
			return;
		}

		// Save lock override field
		if ( isset( $_POST['postgate_lock_override'] ) ) {
			$submitted_lock_override = sanitize_text_field( wp_unslash( $_POST['postgate_lock_override'] ) );
			$validated_lock_override = in_array( $submitted_lock_override, self::LOCK_OVERRIDE_OPTIONS, true ) ? $submitted_lock_override : 'inherit';
			update_post_meta( $post_id, '_postgate_lock_override', $validated_lock_override );
		}

		// Save lock mode field
		if ( isset( $_POST['postgate_lock_mode'] ) ) {
			$submitted_lock_mode = sanitize_text_field( wp_unslash( $_POST['postgate_lock_mode'] ) );
			$validated_lock_mode = in_array( $submitted_lock_mode, self::LOCK_MODE_OPTIONS, true ) ? $submitted_lock_mode : 'inherit';
			update_post_meta( $post_id, '_postgate_lock_mode', $validated_lock_mode );
		}

		// Save preview mode field
		if ( isset( $_POST['postgate_preview_mode'] ) ) {
			$submitted_preview_mode = sanitize_text_field( wp_unslash( $_POST['postgate_preview_mode'] ) );
			$validated_preview_mode = in_array( $submitted_preview_mode, self::PREVIEW_MODE_OPTIONS, true ) ? $submitted_preview_mode : 'inherit';
			update_post_meta( $post_id, '_postgate_preview_mode', $validated_preview_mode );
		}

		// Save preview limit field
		if ( array_key_exists( 'postgate_preview_limit', $_POST ) ) {
			$raw_preview_limit = sanitize_text_field( wp_unslash( $_POST['postgate_preview_limit'] ) );
			if ( '' === $raw_preview_limit ) {
				delete_post_meta( $post_id, '_postgate_preview_limit' );
			} else {
				$validated_preview_limit = max( 1, (int) $raw_preview_limit );
				update_post_meta( $post_id, '_postgate_preview_limit', $validated_preview_limit );
			}
		}

		// Save product IDs field
		if ( isset( $_POST['postgate_product_ids__present'] ) ) {
			if ( isset( $_POST['postgate_product_ids'] ) && is_array( $_POST['postgate_product_ids'] ) ) {
				$raw_product_ids = array_map( 'sanitize_text_field', wp_unslash( $_POST['postgate_product_ids'] ) );
				$sanitized_product_ids = self::sanitize_product_ids( $raw_product_ids );
				if ( ! empty( $sanitized_product_ids ) ) {
					update_post_meta( $post_id, '_postgate_product_ids', $sanitized_product_ids );
				} else {
					// Explicitly empty => remove to inherit
					delete_post_meta( $post_id, '_postgate_product_ids' );
				}
			} else {
				// No values submitted => user cleared all; remove to inherit
				delete_post_meta( $post_id, '_postgate_product_ids' );
			}
		}
	}
}