<?php
/**
 * PostGate Settings Class
 *
 * Handles WordPress settings registration and REST API endpoints for plugin configuration.
 * Manages all PostGate settings including general options, payment/Stripe configuration,
 * appearance customization, and REST endpoints for roles, currencies, and Stripe prices.
 *
 * @package PostGate
 * @subpackage Admin
 * @since 1.0.0
 * @author QodeSphere
 * @license GPL-2.0+
 */

defined( 'ABSPATH' ) || exit;

/**
 * PostGate Settings Class
 *
 * Manages plugin settings registration and REST API endpoints for configuration.
 * This class handles all aspects of PostGate configuration including:
 *
 * - General settings: site-wide lock, enabled post types, lock mode, bypass roles
 * - Payment settings: Stripe connection mode, webhook secrets, price management
 * - Appearance settings: preview mode/limit, paywall content customization
 * - REST endpoints: roles, currencies, Stripe price fetching and synchronization
 * - Sanitization: comprehensive sanitizers for all setting types
 * - Stripe integration: price fetching, validation, and storage
 *
 * Settings are organized into groups:
 * - postgate_general_settings: Core functionality and access control
 * - postgate_payment_settings: Stripe integration and payment processing
 * - postgate_appearance_settings: UI customization and preview behavior
 *
 * REST API endpoints provide:
 * - GET /postgate/v1/roles: Available WordPress roles
 * - GET /postgate/v1/currencies: Supported Stripe currencies
 * - POST /postgate/v1/stripe/prices: Fetch and sync prices from Stripe API
 *
 * @since 1.0.0
 */
class PostGate_Settings {
	/**
	 * Initialize the settings class.
	 *
	 * Registers WordPress hooks for REST API routes and settings.
	 * Sets up all plugin configuration endpoints and option registration.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function init() {
		add_action( 'rest_api_init', [ __CLASS__, 'register_routes' ] );
		add_action( 'init', [ __CLASS__, 'register_settings' ] );
	}

	/**
	 * Register REST API routes for settings.
	 *
	 * Registers REST endpoints:
	 * - GET /postgate/v1/roles: Retrieve available WordPress roles
	 * - GET /postgate/v1/currencies: Retrieve supported Stripe currencies
	 * - POST /postgate/v1/stripe/products: Fetch and sync products from Stripe API
	 *
	 * All endpoints require 'manage_options' capability (administrator access).
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function register_routes() {
		// Roles
		register_rest_route(
			'postgate/v1',
			'/roles',
			[
				'methods' => 'GET',
				'callback' => [ __CLASS__, 'rest_get_roles' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
			]
		);

		// Supported currencies
		register_rest_route(
			'postgate/v1',
			'/currencies',
			[
				'methods' => 'GET',
				'callback' => [ __CLASS__, 'rest_get_currencies' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
			]
		);

		// Stripe products: fetch and persist per mode (product-based architecture)
		register_rest_route(
			'postgate/v1',
			'/stripe/products',
			[
				'methods' => 'POST',
				'callback' => [ __CLASS__, 'rest_fetch_products' ],
				'permission_callback' => [ __CLASS__, 'check_permission' ],
				'args' => [
					'mode' => [
						'type' => 'string',
						'required' => false,
						'sanitize_callback' => [ __CLASS__, 'sanitize_stripe_connect_mode' ],
						'default' => get_option( 'postgate_stripe_connect_mode', 'sandbox' ),
					],
					'limit' => [
						'type' => 'integer',
						'required' => false,
						'default' => 100,
					],
				],
			]
		);
	}

	/**
	 * Check if current user has permission to access settings endpoints.
	 *
	 * Verifies that the current user has the 'manage_options' capability,
	 * which is required for all settings-related REST API endpoints.
	 *
	 * @since 1.0.0
	 *
	 * @return bool True if user has manage_options capability, false otherwise.
	 */
	public static function check_permission(): bool {
		return current_user_can( 'manage_options' );
	}

	/**
	 * Register all plugin settings with WordPress.
	 *
	 * Registers settings across six groups:
	 *
	 * General Settings:
	 * - postgate_lock_entire_site: Enable site-wide content locking
	 * - postgate_enabled_post_types: Post types locked by default (empty = none locked by default)
	 * - postgate_lock_mode: Default lock mode (paywall/login)
	 * - postgate_bypass_roles: User roles that bypass locks
	 * - postgate_after_purchase_behavior: Post-purchase redirect behavior
	 * - postgate_after_purchase_page: Target page for redirects
	 *
	 * Payment Settings:
	 * - postgate_stripe_connect_mode: Stripe mode (live/sandbox)
	 * - postgate_display_currency: Display currency for prices
	 * - postgate_webhook_secret_live: Live mode webhook secret
	 * - postgate_webhook_secret_sandbox: Sandbox mode webhook secret
	 * - postgate_stripe_products: Stored Stripe products per mode
	 *
	 * Appearance Settings:
	 * - postgate_preview_mode: Preview mode (paragraph/words)
	 * - postgate_preview_limit: Number of paragraphs/words to show
	 * - postgate_paywall_title: Custom paywall title
	 * - postgate_paywall_content_before: Content before paywall form
	 * - postgate_paywall_content_after: Content after paywall form
	 * - postgate_paywall_button_text: Custom button text
	 * - postgate_paywall_login_text: Custom login prompt text
	 * - postgate_paywall_layout: Paywall layout style (classic/compact)
	 * - postgate_paywall_badge: Badge text displayed above paywall title
	 * - postgate_paywall_description: Description text for paywall
	 * - postgate_login_badge: Badge text displayed above login form
	 * - postgate_login_title: Custom login form title
	 * - postgate_login_description: Description text for login form
	 * - postgate_login_button_text: Custom login button text
	 * - postgate_login_register_text: Custom register prompt text
	 * - postgate_login_forgot_text: Custom forgot password prompt text
	 * - postgate_archive_display_mode: Archive display mode (excerpt/archive_lock_indicator)
	 * - postgate_archive_lock_title: Lock indicator title on archives
	 * - postgate_archive_lock_button_text: Lock indicator button text on archives
	 *
	 * Advanced Settings:
	 * - postgate_remove_data_on_uninstall: Remove all plugin data on uninstall
	 *
	 * Email Notifications Settings:
	 * - postgate_email_notification_from_name: Sender name for notification emails
	 * - postgate_email_notification_from_email: Sender email address for notifications
	 * - postgate_manual_order_email_enabled: Enable email notifications for manual orders
	 * - postgate_manual_order_email_subject: Subject line for manual order emails
	 * - postgate_manual_order_email_message: Message body for manual order emails
	 *
	 * All settings include proper sanitization callbacks and REST API exposure.
	 *
	 * @since 1.0.0
	 *
	 * @return void
	 */
	public static function register_settings() {
		// ===== General =====
		register_setting(
			'postgate_general_settings',
			'postgate_lock_entire_site',
			[
				'type' => 'boolean',
				'default' => false,
				'sanitize_callback' => 'rest_sanitize_boolean',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_general_settings',
			'postgate_enabled_post_types',
			[
				'type' => 'array',
				'default' => [],
				'sanitize_callback' => [ __CLASS__, 'sanitize_string_array' ],
				'show_in_rest' => [
					'schema' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
				],
			]
		);

		register_setting(
			'postgate_general_settings',
			'postgate_lock_mode',
			[
				'type' => 'string',
				'default' => 'paywall',
				'sanitize_callback' => 'sanitize_title',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_general_settings',
			'postgate_bypass_roles',
			[
				'type' => 'array',
				'default' => [ 'administrator' ],
				'sanitize_callback' => [ __CLASS__, 'sanitize_string_array' ],
				'show_in_rest' => [
					'schema' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ],
				],
			]
		);

		// After Purchase Behavior (return-to-post or redirect-page)
		register_setting(
			'postgate_general_settings',
			'postgate_after_purchase_behavior',
			[
				'type' => 'string',
				'default' => 'return-to-post',
				'sanitize_callback' => [ __CLASS__, 'sanitize_after_purchase_behavior' ],
				'show_in_rest' => true,
			]
		);

		// After Purchase Page (page slug for redirect-page behavior)
		register_setting(
			'postgate_general_settings',
			'postgate_after_purchase_page',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_title',
				'show_in_rest' => true,
			]
		);

		// ===== Payments / Stripe =====
		register_setting(
			'postgate_payment_settings',
			'postgate_stripe_connect_mode',
			[
				'type' => 'string',
				'default' => 'sandbox',
				'sanitize_callback' => [ __CLASS__, 'sanitize_stripe_connect_mode' ],
				'show_in_rest' => true,
			]
		);

		// Display Currency
		register_setting(
			'postgate_payment_settings',
			'postgate_display_currency',
			[
				'type' => 'string',
				'default' => 'USD',
				'sanitize_callback' => [ __CLASS__, 'sanitize_currency_code' ],
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_payment_settings',
			'postgate_webhook_secret_live',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_payment_settings',
			'postgate_webhook_secret_sandbox',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		// Products map per mode (product-based architecture with nested prices)
		register_setting(
			'postgate_payment_settings',
			'postgate_stripe_products',
			[
				'type' => 'object',
				'default' => [ 'live' => null, 'sandbox' => null ],
				'sanitize_callback' => [ __CLASS__, 'sanitize_products_map' ],
				'show_in_rest' => [
					'schema' => [
						'type' => 'object',
						'properties' => [
							'live' => [
								'type' => [ 'object', 'null' ],
								'properties' => self::products_subobject_schema(),
								'additionalProperties' => false,
							],
							'sandbox' => [
								'type' => [ 'object', 'null' ],
								'properties' => self::products_subobject_schema(),
								'additionalProperties' => false,
							],
						],
						'additionalProperties' => false,
					],
				],
			]
		);

		// ===== Appearance (Paywall Layout) =====
		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_layout',
			[
				'type' => 'string',
				'default' => 'classic',
				'sanitize_callback' => [ __CLASS__, 'sanitize_paywall_layout' ],
				'show_in_rest' => [
					'schema' => [
						'type' => 'string',
						'enum' => [ 'classic', 'compact' ],
					],
				],
			]
		);

		// ===== Appearance (Preview) =====
		register_setting(
			'postgate_appearance_settings',
			'postgate_preview_mode',
			[
				'type' => 'string',
				'default' => 'paragraph',
				'sanitize_callback' => [ __CLASS__, 'sanitize_preview_mode' ],
				'show_in_rest' => [
					'schema' => [
						'type' => 'string',
						'enum' => [ 'paragraph', 'words' ],
					],
				],
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_preview_limit',
			[
				'type' => 'integer',
				'default' => 2,
				'sanitize_callback' => [ __CLASS__, 'sanitize_positive_int' ],
				'show_in_rest' => true,
			]
		);

		// ===== Appearance (Paywall Content) =====
		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_badge',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_title',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_description',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_content_before',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => [ __CLASS__, 'sanitize_html_content' ],
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_content_after',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => [ __CLASS__, 'sanitize_html_content' ],
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_button_text',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_paywall_login_text',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		// ===== Appearance (Login Gate Content) =====
		register_setting(
			'postgate_appearance_settings',
			'postgate_login_badge',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_login_title',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_login_description',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_login_button_text',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_login_register_text',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_login_forgot_text',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		// ===== Appearance (Archive Display) =====
		register_setting(
			'postgate_appearance_settings',
			'postgate_archive_display_mode',
			[
				'type' => 'string',
				'default' => 'excerpt',
				'sanitize_callback' => [ __CLASS__, 'sanitize_archive_display_mode' ],
				'show_in_rest' => [
					'schema' => [
						'type' => 'string',
						'enum' => [ 'excerpt', 'archive_lock_indicator' ],
					],
				],
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_archive_lock_title',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_appearance_settings',
			'postgate_archive_lock_button_text',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		// ===== Advanced =====
		register_setting(
			'postgate_advanced_settings',
			'postgate_remove_data_on_uninstall',
			[
				'type' => 'boolean',
				'default' => false,
				'sanitize_callback' => 'rest_sanitize_boolean',
				'show_in_rest' => true,
			]
		);

		// ===== Email Notifications - Shared Settings =====
		register_setting(
			'postgate_email_settings',
			'postgate_email_notification_from_name',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_email_settings',
			'postgate_email_notification_from_email',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_email',
				'show_in_rest' => true,
			]
		);

		// ===== Email Notifications - Manual Order Emails =====
		register_setting(
			'postgate_email_settings',
			'postgate_manual_order_email_enabled',
			[
				'type' => 'boolean',
				'default' => false,
				'sanitize_callback' => 'rest_sanitize_boolean',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_email_settings',
			'postgate_manual_order_email_subject',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_text_field',
				'show_in_rest' => true,
			]
		);

		register_setting(
			'postgate_email_settings',
			'postgate_manual_order_email_message',
			[
				'type' => 'string',
				'default' => '',
				'sanitize_callback' => 'sanitize_textarea_field',
				'show_in_rest' => true,
			]
		);
	}

	/**
	 * Sanitize an array of strings.
	 *
	 * Filters and sanitizes array values using sanitize_key(),
	 * removes empty values, and reindexes the array.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Value to sanitize (expected to be array).
	 *
	 * @return array Sanitized array of strings.
	 */
	public static function sanitize_string_array( $value ): array {
		if ( ! is_array( $value ) ) {
			return [];
		}
		return array_values( array_filter( array_map( 'sanitize_key', $value ) ) );
	}

	/**
	 * Sanitize Stripe connection mode.
	 *
	 * Validates that the mode is either 'live' or 'sandbox'.
	 * Defaults to 'sandbox' for any invalid value.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Mode value to sanitize.
	 *
	 * @return string Either 'live' or 'sandbox'.
	 */
	public static function sanitize_stripe_connect_mode( $value ): string {
		return 'live' === $value ? 'live' : 'sandbox';
	}

	/**
	 * Sanitize HTML content for paywall customization.
	 *
	 * Allows safe HTML tags using wp_kses_post() which permits:
	 * p, a, strong, em, ul, ol, li, br, and other safe formatting tags.
	 * Used for paywall content before/after settings.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Raw HTML content to sanitize.
	 *
	 * @return string Sanitized HTML content with safe tags only.
	 */
	public static function sanitize_html_content( $value ): string {
		if ( ! is_string( $value ) ) {
			return '';
		}
		// Allow safe HTML tags like p, a, strong, em, ul, ol, li, br
		return wp_kses_post( $value );
	}

	/**
	 * Sanitize after purchase behavior.
	 *
	 * Validates that the value is either 'return-to-post' or 'redirect-page'.
	 *
	 * @since 1.0.0
	 * @param string $value Behavior value to sanitize.
	 * @return string Sanitized behavior or 'return-to-post' if invalid.
	 */
	public static function sanitize_after_purchase_behavior( $value ): string {
		return in_array( $value, [ 'return-to-post', 'redirect-page' ], true )
			? (string) $value
			: 'return-to-post';
	}

	/**
	 * Sanitize currency code.
	 *
	 * Validates against Stripe-supported currencies and ensures proper format.
	 *
	 * @since 1.0.0
	 * @param string $value Currency code to sanitize.
	 * @return string Sanitized currency code or 'USD' if invalid.
	 */
	public static function sanitize_currency_code( $value ): string {
		$value = strtoupper( sanitize_text_field( $value ) );
		$supported = PostGate_Product_Utils::get_supported_currencies();
		return in_array( $value, $supported, true ) ? $value : 'USD';
	}

	/**
	 * Sanitize preview mode setting.
	 *
	 * Validates that the mode is either 'paragraph' or 'words'.
	 * Defaults to 'paragraph' for any invalid value.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Preview mode value to sanitize.
	 *
	 * @return string Either 'paragraph' or 'words'.
	 */
	public static function sanitize_preview_mode( $value ): string {
		return in_array( $value, [ 'paragraph', 'words' ], true ) ? (string) $value : 'paragraph';
	}

	/**
	 * Sanitize paywall layout.
	 *
	 * Validates that the layout is either 'classic' or 'compact'.
	 * Defaults to 'classic' for any invalid value.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Value to sanitize.
	 *
	 * @return string 'classic' or 'compact'.
	 */
	public static function sanitize_paywall_layout( $value ): string {
		return in_array( $value, [ 'classic', 'compact' ], true ) ? (string) $value : 'classic';
	}

	/**
	 * Sanitize archive display mode.
	 *
	 * Validates that the mode is either 'excerpt' or 'archive_lock_indicator'.
	 * Defaults to 'excerpt' for any invalid value.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Value to sanitize.
	 *
	 * @return string 'excerpt' or 'archive_lock_indicator'.
	 */
	public static function sanitize_archive_display_mode( $value ): string {
		return in_array( $value, [ 'excerpt', 'archive_lock_indicator' ], true ) ? (string) $value : 'excerpt';
	}

	/**
	 * Sanitize positive integer values.
	 *
	 * Ensures the value is an integer >= 1.
	 * Used for preview limits and other count settings.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Value to sanitize.
	 *
	 * @return int Sanitized positive integer (minimum 1).
	 */
	public static function sanitize_positive_int( $value ): int {
		$val = intval( $value );
		return ( $val < 1 ) ? 1 : $val;
	}

	/**
	 * Define the REST API schema for products subobject.
	 *
	 * This schema describes the structure of a single mode's products data
	 * (e.g., live or sandbox). It includes an updated_at timestamp and an array
	 * of product items with nested prices.
	 *
	 * @since 1.0.0
	 *
	 * @return array Schema definition for products subobject.
	 */
	private static function products_subobject_schema() {
		return [
			'updated_at' => [ 'type' => 'integer' ],
			'products' => [
				'type' => 'array',
				'items' => [
					'type' => 'object',
					'properties' => [
						'id' => [ 'type' => 'string' ],
						'name' => [ 'type' => 'string' ],
						'description' => [ 'type' => 'string' ],
						'default_price' => [ 'type' => 'string' ],
						'is_default' => [ 'type' => 'boolean' ],
						'metadata' => [
							'type' => 'object',
							'additionalProperties' => true,
						],
						'prices' => [
							'type' => 'array',
							'items' => [
								'type' => 'object',
								'properties' => [
									'id' => [ 'type' => 'string' ],
									'amount' => [ 'type' => 'integer' ],
									'currency' => [ 'type' => 'string' ],
									'interval' => [ 'type' => 'string' ],
									'interval_count' => [ 'type' => 'integer' ],
									'type' => [ 'type' => 'string' ],
									'nickname' => [ 'type' => 'string' ],
									'metadata' => [
										'type' => 'object',
										'additionalProperties' => true,
									],
								],
								'additionalProperties' => false,
							],
						],
					],
					'additionalProperties' => false,
				],
			],
		];
	}

	/**
	 * Sanitize products map (product-based architecture).
	 *
	 * Ensures proper structure for products data with live and sandbox modes.
	 * Each mode can be null or contain products data.
	 *
	 * @since 1.0.0
	 *
	 * @param mixed $value Products map to sanitize.
	 *
	 * @return array Sanitized products map with 'live' and 'sandbox' keys.
	 */
	public static function sanitize_products_map( $value ): array {
		$out = [ 'live' => null, 'sandbox' => null ];
		if ( is_array( $value ) ) {
			if ( array_key_exists( 'live', $value ) ) {
				$out['live'] = is_null( $value['live'] ) ? null : self::sanitize_products_subobject( $value['live'] );
			}
			if ( array_key_exists( 'sandbox', $value ) ) {
				$out['sandbox'] = is_null( $value['sandbox'] ) ? null : self::sanitize_products_subobject( $value['sandbox'] );
			}
		}
		return $out;
	}

	/**
	 * Sanitize products subobject for storage.
	 *
	 * Ensures proper data structure and sanitization for product items
	 * with nested prices before storing in WordPress options.
	 *
	 * @since 1.0.0
	 * @param mixed $sub The subobject data to sanitize.
	 * @return array Sanitized products subobject.
	 */
	private static function sanitize_products_subobject( $sub ): array {
		if ( ! is_array( $sub ) ) {
			$sub = [];
		}
		$products = [];
		$default_seen = false;

		if ( ! empty( $sub['products'] ) && is_array( $sub['products'] ) ) {
			foreach ( $sub['products'] as $product ) {
				if ( empty( $product['id'] ) ) {
					continue;
				}

				$is_default = ! empty( $product['is_default'] ) && false === $default_seen;
				if ( $is_default ) {
					$default_seen = true; // at most one default
				}

				// Sanitize nested prices
				$prices = [];
				if ( ! empty( $product['prices'] ) && is_array( $product['prices'] ) ) {
					foreach ( $product['prices'] as $price ) {
						if ( empty( $price['id'] ) ) {
							continue;
						}
						$type = ( isset( $price['type'] ) && $price['type'] === 'recurring' ) ? 'recurring' : 'one_time';

						$interval_count = 1;
						if ( 'recurring' === $type && isset( $price['interval_count'] ) ) {
							$interval_count = max( 1, intval( $price['interval_count'] ) );
						}

						$prices[] = [
							'id' => sanitize_text_field( (string) $price['id'] ),
							'amount' => isset( $price['amount'] ) ? intval( $price['amount'] ) : 0,
							'currency' => strtoupper( sanitize_text_field( (string) ( $price['currency'] ?? 'USD' ) ) ),
							'interval' => sanitize_text_field( (string) ( $price['interval'] ?? '' ) ),
							'interval_count' => $interval_count,
							'type' => $type,
							'nickname' => ! empty( $price['nickname'] ) ? sanitize_text_field( (string) $price['nickname'] ) : '',
							'metadata' => isset( $price['metadata'] ) && is_array( $price['metadata'] ) ? $price['metadata'] : [],
						];
					}
				}

				$products[] = [
					'id' => sanitize_text_field( (string) $product['id'] ),
					'name' => sanitize_text_field( (string) ( $product['name'] ?? '' ) ),
					'description' => sanitize_textarea_field( (string) ( $product['description'] ?? '' ) ),
					'default_price' => sanitize_text_field( (string) ( $product['default_price'] ?? '' ) ),
					'prices' => $prices,
					'is_default' => $is_default,
					'metadata' => isset( $product['metadata'] ) && is_array( $product['metadata'] ) ? $product['metadata'] : [],
				];
			}
		}

		return [
			'updated_at' => isset( $sub['updated_at'] ) ? intval( $sub['updated_at'] ) : time(),
			'products' => $products,
		];
	}

	/**
	 * REST endpoint to get available WordPress roles.
	 *
	 * Returns an array of role objects with value (slug) and label (translated name).
	 * Roles are sorted with 'administrator' first, followed by others alphabetically.
	 *
	 * Endpoint: GET /postgate/v1/roles
	 * Permission: manage_options
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request REST request object.
	 *
	 * @return WP_REST_Response|WP_Error Response with roles array or error.
	 */
	public static function rest_get_roles( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		if ( ! function_exists( 'wp_roles' ) ) {
			return new WP_Error( 'postgate_no_roles', 'Roles API unavailable', [ 'status' => 500 ] );
		}
		$roles_obj = wp_roles();
		$roles = $roles_obj ? $roles_obj->roles : [];
		$data = [];
		foreach ( $roles as $slug => $role ) {
			$data[] = [ 'value' => $slug, 'label' => translate_user_role( $role['name'] ) ];
		}
		usort(
			$data,
			function ( $a, $b ) {
				if ( $a['value'] === 'administrator' ) {
					return -1;
				}
				if ( $b['value'] === 'administrator' ) {
					return 1;
				}
				return strcmp( $a['label'], $b['label'] );
			}
		);
		return rest_ensure_response( $data );
	}

	/**
	 * REST endpoint to get supported Stripe currencies.
	 *
	 * Returns an array of currency objects with value (code) and label (name + code).
	 * Uses PostGate_Product_Utils for currency data.
	 *
	 * Endpoint: GET /postgate/v1/currencies
	 * Permission: manage_options
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request REST request object.
	 *
	 * @return WP_REST_Response Response with currencies array.
	 */
	public static function rest_get_currencies( WP_REST_Request $request ): WP_REST_Response {
		$currencies = PostGate_Product_Utils::get_supported_currencies();
		$names = PostGate_Product_Utils::get_currency_names();

		$data = [];
		foreach ( $currencies as $code ) {
			$data[] = [
				'value' => $code,
				'label' => isset( $names[ $code ] ) ? "{$names[ $code ]} ({$code})" : $code,
			];
		}

		return rest_ensure_response( $data );
	}

	/**
	 * REST endpoint: Fetch active Stripe products with prices and save to database.
	 *
	 * Retrieves products from Stripe API for the specified mode (live/sandbox),
	 * expands prices for each product, and stores them in the postgate_stripe_products option.
	 *
	 * Product-based architecture: Each product can have multiple prices (different currencies,
	 * billing intervals, etc.). This endpoint fetches products and their associated prices,
	 * storing them in a nested structure for the frontend to use.
	 *
	 * This method delegates the actual Stripe API operations to PostGate_Stripe_API,
	 * following the same delegation pattern used for currencies (PostGate_Product_Utils).
	 *
	 * Response includes:
	 * - products: Array of product objects with nested prices
	 * - saved: Boolean indicating if data was persisted
	 * - stored: Current mode's stored data
	 * - stored_all: Complete stored data for both modes
	 *
	 * Parameters:
	 * - mode: 'live' or 'sandbox' (default: current setting)
	 * - limit: Number of products to fetch (1-100, default: 100)
	 *
	 * @since 1.0.0
	 *
	 * @param WP_REST_Request $request REST request object with mode and limit params.
	 *
	 * @return WP_REST_Response|WP_Error Response with products data or error.
	 */
	public static function rest_fetch_products( WP_REST_Request $request ): WP_REST_Response|WP_Error {
		// Validate and sanitize parameters
		$mode = $request->get_param( 'mode' ) === 'live' ? 'live' : 'sandbox';
		$limit = max( 1, min( 100, intval( $request->get_param( 'limit' ) ?: 100 ) ) );

		// Verify Stripe access token
		$access_token = self::get_connected_access_token( $mode );
		if ( '' === $access_token ) {
			return new WP_Error(
				'postgate_no_token',
				sprintf( 'Missing Stripe access token for %s mode.', esc_html( strtoupper( $mode ) ) ),
				[ 'status' => 400 ]
			);
		}

		// Delegate to Stripe API class for product fetching and processing
		$result = PostGate_Stripe_API::fetch_and_process_products( $access_token, $mode, $limit );

		if ( is_wp_error( $result ) ) {
			return $result;
		}

		return rest_ensure_response( $result );
	}
	/**
	 * Retrieve the connected account access token for the given mode.
	 *
	 * Uses PostGate_Merchant_Auth to get the access token for Stripe API calls.
	 *
	 * @since 1.0.0
	 * @param string $mode Connection mode: 'live' or 'sandbox'.
	 * @return string Access token or empty string if not available.
	 */
	protected static function get_connected_access_token( string $mode ): string {
		$mode = ( 'live' === $mode ) ? 'live' : 'sandbox';
		return PostGate_Merchant_Auth::token( $mode );
	}
}