<?php
/**
 * Settings store for Mersal Notifications.
 *
 * One option array (`mnotif_settings`) holds the whole config. Static helpers
 * provide typed access with sane defaults so the rest of the plugin never has
 * to think about missing keys.
 *
 * @package Mersal_Notif
 */

if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

class Mersal_Notif_Settings {

	const OPTION = 'mnotif_settings';

	/**
	 * Per-request WhatsApp method override (set when the user picks a method on
	 * the login screen). When set, wa_style() returns it instead of the saved
	 * default. Never persisted.
	 *
	 * @var string|null
	 */
	private static $wa_style_override = null;

	/**
	 * Default settings.
	 *
	 * @return array
	 */
	public static function defaults() {
		return array(
			// Appearance.
			'theme'             => 'light',   // green | cyan | light (Quiet Light = default).
			'lang'              => 'ar',      // ar | en — admin UI + form default language.
			'dark'              => 0,         // Admin dashboard dark mode only (topbar toggle).
			'form_dark'         => 0,         // Default dark mode on the public login form.
			'form_dark_toggle'  => 1,         // Show dark-mode switch on the login form.
			'form_lang_toggle'  => 1,         // Show language switch on the login form.
			'ui_font'           => 'tajawal', // Any key in self::font_registry().

			// OTP behaviour.
			'otp_length'        => 6,
			'otp_expiry'        => 10,        // minutes.
			'rate_limit'        => 3,         // legacy per-minute (unused when rate_limit_enabled).
			'rate_limit_enabled' => 1,
			'rate_limit_ip_hour' => 10,
			'rate_limit_phone_day' => 20,
			'rate_limit_cooldown' => 1,      // hours after limit hit.

			// Channels.
			'channel_email'     => 1,
			'channel_whatsapp'  => 0,
			'channel_sms'       => 0,
			'channel_google'    => 0,
			'default_method'    => 'email',   // email | whatsapp | sms.
			'login_method_preset' => 'phone', // phone | email | choice — first screen on login form.

			// Flow.
			'allow_signup'      => 1,
			'auto_redirect'     => 1,
			'one_session'       => 0,
			'remember_last_method' => 0,

			// Sessions.
			'session_length'    => '14d',     // 1d | 7d | 14d | 30d | lifetime.
			'remember_me_length'=> '30d',     // 14d | 30d | 90d.
			'logout_on_password_change' => 1,
			'allow_device_management'   => 1,
			'device_login_alert'        => 0,

			// Advanced behavior.
			'auto_link_by_email'        => 1,
			'allow_unverified_email'    => 0,
			'sync_phone_woocommerce'    => 1,
			'redirect_url'      => '',        // after successful login.

			// Messages — legacy flat keys kept in sync with msg_events.login.
			'msg_email_subject' => 'Your verification code',
			'msg_email_body'    => 'Your one-time code for {{site_name}} is: {{otp}}. It expires in {{expiry}} minutes.',
			'msg_whatsapp'      => "Your {{site_name}} verification code is: {{otp}}\nOr tap to sign in: {{link}}",
			'msg_sms'           => 'Your {{site_name}} code: {{otp}} — or sign in: {{link}}',

			// One-tap "confirm to log in" link (WhatsApp / SMS).
			'confirm_login'     => 1,
			'confirm_expiry'    => 10,  // minutes the short link stays valid.
			'confirm_poll'      => 3,   // seconds between browser status polls.
			'msg_events'        => array(), // Filled via default_msg_events() on read.

			// Branding.
			'btn_color'         => '#0f7a4a',
			'form_title'        => '',        // empty -> localized default.
			'form_subtitle'     => '',
			'custom_css'        => '',
			'logo_ar_light'     => '',
			'logo_ar_dark'      => '',
			'logo_en_light'     => '',
			'logo_en_dark'      => '',
			'form_logo_height'  => '34',      // px height of the brand logo in the form.

			// Email delivery.
			'email_from_name'   => '',        // empty -> site name.
			'email_from'        => '',        // empty -> WP default.
			'email_provider'    => 'wordpress', // wordpress | mersal.
			'email_gateway'     => '',        // mersal.it email gateway uid (optional).

			// Gateways (uid pulled from the connected Mersal account).
			'wa_gateway'        => '',
			'sms_gateway'       => '',

			// WhatsApp verification message style:
			//   standard → plain OTP code text
			//   poll     → interactive Poll (Approve / Reject — taps log the user in)
			//   link     → one-tap confirmation link message
			'wa_msg_style'      => 'standard', // standard | poll | button | link (default/primary).
			// Methods offered to the user on the login screen. When more than one
			// is enabled the user picks; empty => just use wa_msg_style.
			'wa_methods'        => array(),    // subset of: standard, poll, button, link.
			'wa_link_msg'       => '',         // link-style message body (empty -> localized default).
			// List Interface Design (empty -> localized defaults at render time).
			'wa_list_title'      => '',
			'wa_list_desc'       => '',
			'wa_list_item1_title'=> '',
			'wa_list_item1_desc' => '',
			'wa_list_item2_title'=> '',
			'wa_list_item2_desc' => '',
			'wa_list_button'     => '',
			'wa_list_footer'     => '',

			// Advanced Country Code — auto-correct phone numbers (intl-tel-input).
			'intl_enabled'         => 0,
			'intl_default_country' => 'eg',     // ISO2 lowercase.
			'intl_ip_detection'    => 1,        // detect country by visitor IP.
			'intl_carrier_lookup'  => 0,        // real-time carrier name lookup.
			'intl_alignment'       => 'auto',   // auto | left | right (flag side).
			'intl_countries'       => array(),  // allowed ISO2 list (empty = all).
			'intl_fields'          => array(),  // extra CSS selectors (empty = built-in defaults).

			// Google sign-in (OAuth).
			'google_client_id'     => '',
			'google_client_secret' => '',

			// Security (rate limit keys above under OTP behaviour).
			'recaptcha_enabled'    => 0,
			'captcha_provider'     => 'recaptcha', // recaptcha | turnstile | hcaptcha.
			'recaptcha_site'       => '',
			'recaptcha_secret'     => '',
			'block_ips_enabled'    => 1,
			'blocked_ips'          => '',
			'block_countries_enabled' => 1,
			'blocked_countries'    => '',
			'block_disposable'     => 0,
			'disposable_prefixes'  => '',  // Optional extra E.164 prefixes to treat as disposable.
			'numverify_key'        => '',  // Optional numverify key → VoIP line-type detection.
			'block_vpn_proxy'      => 0,
			'honeypot_enabled'     => 0,
			// Privacy: hide whether an account exists on the login OTP step (anti-
			// enumeration). OFF keeps the friendly "no account → sign up" prompt.
			'privacy_login'        => 0,
			// Account phone: show the linked number on the WP profile screen, and an
			// optional "confirm your number" bar for logged-in users.
			'profile_show_phone'   => 1,
			'verify_bar_account'   => 0,   // Auto-render the bar on WooCommerce My Account.
			'verify_bar_intro'     => '',  // Custom bar heading (empty → localized default).
			// Trust X-Forwarded-For / CF-Connecting-IP for the client IP. Leave OFF
			// unless the site is strictly behind a trusted proxy/CDN — otherwise a
			// spoofed header lets an attacker rotate IPs to bypass rate limits.
			'trust_proxy'          => 0,
			'force_2fa_roles'      => 0,

			// Post-login redirect rules: [ ['role'=>'all','url'=>''], ... ].
			'redirect_rules'    => array(),

			// Safety net for demos with no WhatsApp/SMS gateway configured.
			'test_mode'         => 0,

			// Auth page / site gate.
			'auth_page_id'          => 0,
			'auto_create_auth_page' => 0,
			'force_login'           => 0,
			'blank_auth_template'   => 1,

			// WooCommerce (UI toggles; checkout hook wiring can extend later).
			'woo_checkout_otp'      => 0,
			'woo_otp_before_pay'    => 1,
			'woo_restore_cart'      => 1,

			// Email login (method + SMTP + verification).
			'email_method'              => 'otp',   // otp | password | magic.
			'email_otp_on'              => 1,
			'email_password_on'         => 1,
			'email_magic_on'            => 0,
			'smtp_host'                 => '',
			'smtp_port'                 => 587,
			'smtp_enc'                  => 'tls',    // tls | ssl | none.
			'smtp_user'                 => '',
			'smtp_pass'                 => '',
			'require_email_verify'      => 0,
			'block_checkout_unverified' => 0,

			// Phone login (channel + countries + validation).
			'phone_channel'         => 'both',   // sms | whatsapp | both.
			'fallback_on'           => 1,
			'countries_mode'        => 'allow',  // allow | block.
			'default_country'       => 'EG',
			'phone_format'          => 'intl',   // intl | local | e164.
			'validate_format'       => 1,
			'require_country_code'  => 1,
			'allow_landline'        => 0,
			'hlr_lookup'            => 0,

			// OTP advanced.
			'otp_type'          => 'numeric',    // numeric | alphanum | alpha.
			'resend_wait'       => 60,           // seconds before resend.
			'otp_max_attempts'  => 5,
			'lockout_duration'  => 15,           // minutes: 5 | 15 | 60 | 1440.
			'one_active_otp'    => 1,
			'allow_reuse_once'  => 0,
			'case_sensitive'    => 0,

			// Google behavior.
			'google_autolink'         => 1,
			'google_ask_mobile'       => 1,
			'google_restrict_domains' => 0,
			'google_button_label'     => '',

			// Abandoned cart recovery.
			'abandoned_cart_enabled'   => 0,
			'ac_threshold_minutes'     => 60,
			'ac_track_pages'           => 1,
			'ac_capture_checkout'      => 1,
			'ac_prefill_checkout'      => 1,
			'ac_message_template'      => '',
			'ac_generate_coupon'       => 0,
			'ac_coupon_prefix'         => 'REC',
			'ac_coupon_type'           => 'percent',   // percent | fixed_cart.
			'ac_coupon_discount'       => 10,
			'ac_coupon_expiry_days'    => 7,
			'ac_coupon_min_spend'      => 0,
			'ac_coupon_max_discount'   => 0,
			'ac_coupon_free_shipping'  => 0,
			'ac_coupon_email_restrict' => 0,
			'ac_coupon_products'       => '',
			'ac_coupon_cats'           => '',

			// Phone-capture popup.
			'ac_popup_enabled'         => 0,
			'ac_popup_title'           => '',
			'ac_popup_desc'            => '',
			'ac_popup_btn'             => '',
			'ac_popup_success_title'   => '',
			'ac_popup_success_desc'    => '',
			'ac_popup_image'           => '',
			'ac_popup_color'           => '#0f8f6f',
			'ac_popup_display'         => 'all',       // all | desktop | mobile.
			'ac_popup_trigger_cart'    => 1,
			'ac_popup_trigger_exit'    => 1,
			'ac_popup_delay'           => 4,           // seconds; 0 = no timed trigger.
			'ac_popup_frequency'       => 'days',      // once | days | always.
			'ac_popup_frequency_days'  => 7,
			'ac_popup_min_cart'        => 0,
			'ac_popup_exclude'         => '',          // page IDs CSV.

			// WhatsApp chat widget (front-end).
			'chat_widget_enabled'      => 0,

			// Notification blacklist (numbers / emails never contacted).
			'notif_blocked_numbers'    => '',
			// Rotate a rule's saved wordings instead of repeating one text — the
			// single biggest thing that keeps a WhatsApp sender out of trouble.
			'variant_rotation'         => 1,
			// Hours a customer is left alone after any notification (0 = off).
			'notif_min_gap'            => 0,
			'notif_blocked_emails'     => '',
		);
	}

	/**
	 * Get the full settings array, merged over defaults.
	 *
	 * @return array
	 */
	public static function all() {
		$saved = get_option( self::OPTION, array() );
		if ( ! is_array( $saved ) ) {
			$saved = array();
		}
		$merged = wp_parse_args( $saved, self::defaults() );
		// Legacy: form dark lived under `dark` before the split.
		if ( ! array_key_exists( 'form_dark', $saved ) ) {
			$merged['form_dark'] = ! empty( $saved['dark'] ) ? 1 : 0;
		}
		if ( ! array_key_exists( 'form_dark_toggle', $saved ) ) {
			$merged['form_dark_toggle'] = 1;
		}
		if ( ! array_key_exists( 'form_lang_toggle', $saved ) ) {
			$merged['form_lang_toggle'] = 1;
		}
		return self::normalize_msg_events( $merged );
	}

	/**
	 * Template event ids shown on the Message Templates admin page.
	 *
	 * @return array<string, string> id => internal key.
	 */
	public static function template_event_ids() {
		return array( 'login', 'signup', 'woo', 'password', 'welcome' );
	}

	/**
	 * Default message templates per event/channel.
	 *
	 * @param string $lang ar|en.
	 * @return array
	 */
	public static function default_msg_events( $lang = 'en' ) {
		$ar = 'ar' === $lang;
		$site = '{{site_name}}';

		return array(
			'login'    => array(
				'email_subject' => $ar ? 'كود تأكيد الدخول' : 'Your login verification code',
				'email'         => $ar
					? "كود تأكيد دخولك على {$site} هو {{otp}}.\nالكود صالح لمدة {{expiry}} دقايق، متشاركوش مع حد."
					: "Your login code for {$site} is {{otp}}.\nValid for {{expiry}} minutes. Do not share it.",
				'whatsapp'      => $ar
					? "كود تأكيد دخولك على {$site} هو {{otp}}. الكود صالح لمدة {{expiry}} دقايق، متشاركوش مع حد."
					: "Your login code for {$site} is {{otp}}. Valid for {{expiry}} minutes.",
				'sms'           => $ar
					? "كود الدخول لـ {$site}: {{otp}} (صالح {{expiry}} د)"
					: "{$site} login code: {{otp}} ({{expiry}} min)",
			),
			'signup'   => array(
				'email_subject' => $ar ? 'فعّل حسابك' : 'Activate your account',
				'email'         => $ar
					? "كود تفعيل حسابك على {$site}: {{otp}} (صالح {{expiry}} دقايق)."
					: "Your {$site} activation code is {{otp}} (expires in {{expiry}} min).",
				'whatsapp'      => $ar
					? "كود تفعيل حسابك على {$site}: {{otp}}"
					: "Activate {$site}: {{otp}}",
				'sms'           => $ar ? "تفعيل {$site}: {{otp}}" : "Activate {$site}: {{otp}}",
			),
			'woo'      => array(
				'email_subject' => $ar ? 'تأكيد طلبك' : 'Confirm your order',
				'email'         => $ar
					? "كود تأكيد طلبك على {$site}: {{otp}}"
					: "Your {$site} order verification code: {{otp}}",
				'whatsapp'      => $ar ? "كود تأكيد طلبك: {{otp}}" : "Order code: {{otp}}",
				'sms'           => $ar ? "طلب {$site}: {{otp}}" : "Order {$site}: {{otp}}",
			),
			'password' => array(
				'email_subject' => $ar ? 'تغيير كلمة السر' : 'Password reset',
				'email'         => $ar
					? "كود تغيير كلمة السر على {$site}: {{otp}}"
					: "Your {$site} password reset code: {{otp}}",
				'whatsapp'      => $ar ? "كود تغيير كلمة السر: {{otp}}" : "Password reset: {{otp}}",
				'sms'           => $ar ? "تغيير كلمة السر: {{otp}}" : "Reset code: {{otp}}",
			),
			'welcome'  => array(
				'email_subject' => $ar ? 'أهلاً بك' : 'Welcome',
				'email'         => $ar
					? "أهلاً {{user_name}}! تم تفعيل حسابك على {$site}."
					: "Hi {{user_name}}, welcome to {$site}!",
				'whatsapp'      => $ar ? "أهلاً {{user_name}} في {$site}!" : "Welcome to {$site}, {{user_name}}!",
				'sms'           => $ar ? "أهلاً بك في {$site}" : "Welcome to {$site}",
			),
		);
	}

	/**
	 * Default "Interactive List" interface design per language.
	 *
	 * Mirrors the Wawp List Interface Design fields. Rendered as styled
	 * WhatsApp text (native list buttons are deprecated for non-business
	 * accounts), so placeholders like {{otp}} / {{site_name}} are supported.
	 *
	 * @param string $lang ar|en.
	 * @return array<string,string>
	 */
	public static function whatsapp_list_defaults( $lang = 'en' ) {
		$ar   = 'ar' === $lang;
		$site = '{{site_name}}';

		if ( $ar ) {
			return array(
				'title'       => 'تأكيد آمن',
				'desc'        => "تم رصد محاولة دخول على {$site}. تأكيد إنك إنت؟",
				'item1_title' => 'أنا — تابع الدخول',
				'item1_desc'  => 'أنا اللي بسجّل دخول دلوقتي',
				'item2_title' => 'مش أنا',
				'item2_desc'  => 'تجاهل الرسالة دي ولا تشاركها',
				'button'      => 'تأكيد الدخول',
				'footer'      => "حماية الدخول عبر {$site}",
			);
		}

		return array(
			'title'       => 'Secure Verification',
			'desc'        => "A login attempt on {$site} was detected. Is it you?",
			'item1_title' => "It's me — continue",
			'item1_desc'  => "I'm signing in right now",
			'item2_title' => "It wasn't me",
			'item2_desc'  => 'Ignore this message — do not share it',
			'button'      => 'Verify Now',
			'footer'      => "Secure authentication via {$site}",
		);
	}

	/**
	 * WhatsApp login methods offered to the user, in order. Returns the saved
	 * `wa_methods` (validated), always including the default `wa_msg_style`.
	 * When this has >1 entry the login screen shows a method chooser.
	 *
	 * @return string[] Subset/ordering of: standard, poll, button, link.
	 */
	public static function wa_methods() {
		$valid   = array( 'standard', 'poll', 'button', 'link' );
		$default = self::get( 'wa_msg_style', 'standard' );
		$default = in_array( $default, $valid, true ) ? $default : 'standard';

		$saved = self::get( 'wa_methods', array() );
		$saved = is_array( $saved ) ? array_values( array_intersect( $valid, $saved ) ) : array();

		// Always make the default method available; keep it first.
		$methods = array_values( array_unique( array_merge( array( $default ), $saved ) ) );
		return $methods;
	}

	/**
	 * The active WhatsApp message style for the current send — the per-request
	 * override (user's choice) if set, else the saved default `wa_msg_style`.
	 *
	 * @return string
	 */
	public static function wa_style() {
		if ( null !== self::$wa_style_override ) {
			return self::$wa_style_override;
		}
		return self::get( 'wa_msg_style', 'standard' );
	}

	/**
	 * Set the per-request WhatsApp method override (validated against the
	 * methods the admin enabled). No-op for invalid/unavailable methods.
	 *
	 * @param string $method standard|poll|button|link.
	 */
	public static function set_wa_style_override( $method ) {
		$method = is_string( $method ) ? $method : '';
		if ( '' !== $method && in_array( $method, self::wa_methods(), true ) ) {
			self::$wa_style_override = $method;
		}
	}

	/**
	 * Force a specific WhatsApp style for the current request, bypassing the
	 * user-facing wa_methods allow-list. For internal flows (e.g. the profile
	 * verification bar) that always want a plain OTP code.
	 *
	 * @param string $style standard|poll|button|link.
	 */
	public static function force_wa_style( $style ) {
		if ( in_array( $style, array( 'standard', 'poll', 'button', 'link' ), true ) ) {
			self::$wa_style_override = $style;
		}
	}

	/**
	 * Resolved List Interface Design (saved values, falling back to localized
	 * defaults for any empty field).
	 *
	 * @return array<string,string> Keys: title, desc, item1_title, item1_desc,
	 *                              item2_title, item2_desc, button, footer.
	 */
	public static function whatsapp_list_config() {
		$lang     = 'en' === self::get( 'lang', 'ar' ) ? 'en' : 'ar';
		$defaults = self::whatsapp_list_defaults( $lang );
		$map      = array(
			'title'       => 'wa_list_title',
			'desc'        => 'wa_list_desc',
			'item1_title' => 'wa_list_item1_title',
			'item1_desc'  => 'wa_list_item1_desc',
			'item2_title' => 'wa_list_item2_title',
			'item2_desc'  => 'wa_list_item2_desc',
			'button'      => 'wa_list_button',
			'footer'      => 'wa_list_footer',
		);
		$out = array();
		foreach ( $map as $field => $key ) {
			$val          = (string) self::get( $key, '' );
			$out[ $field ] = '' !== trim( $val ) ? $val : $defaults[ $field ];
		}
		return $out;
	}

	/**
	 * Resolved link-style WhatsApp message body (saved value or localized default).
	 *
	 * @return string
	 */
	public static function whatsapp_link_message() {
		$saved = (string) self::get( 'wa_link_msg', '' );
		if ( '' !== trim( $saved ) ) {
			return $saved;
		}
		$ar = 'en' !== self::get( 'lang', 'ar' );
		return $ar
			? "لتأكيد دخولك على {{site_name}} اضغط الرابط:\n{{link}}\n\nأو استخدم الكود: {{otp}} (صالح {{expiry}} دقايق)."
			: "Tap to confirm your sign-in to {{site_name}}:\n{{link}}\n\nOr use the code: {{otp}} (valid {{expiry}} min).";
	}

	/**
	 * Merge saved + default event templates; sync legacy msg_* keys from login event.
	 *
	 * @param array $settings Settings array.
	 * @return array
	 */
	public static function normalize_msg_events( array $settings ) {
		$lang     = isset( $settings['lang'] ) && 'en' === $settings['lang'] ? 'en' : 'ar';
		$defaults = self::default_msg_events( $lang );
		$events   = isset( $settings['msg_events'] ) && is_array( $settings['msg_events'] ) ? $settings['msg_events'] : array();

		foreach ( self::template_event_ids() as $event_id ) {
			$base = isset( $defaults[ $event_id ] ) ? $defaults[ $event_id ] : array();
			$cur  = isset( $events[ $event_id ] ) && is_array( $events[ $event_id ] ) ? $events[ $event_id ] : array();
			$events[ $event_id ] = wp_parse_args( $cur, $base );
		}

		// Migrate legacy flat keys into login event once.
		if ( ! empty( $settings['msg_email_body'] ) && empty( $events['login']['email'] ) ) {
			$events['login']['email'] = $settings['msg_email_body'];
		}
		if ( ! empty( $settings['msg_email_subject'] ) ) {
			$events['login']['email_subject'] = $settings['msg_email_subject'];
		}
		if ( ! empty( $settings['msg_whatsapp'] ) ) {
			$events['login']['whatsapp'] = $settings['msg_whatsapp'];
		}
		if ( ! empty( $settings['msg_sms'] ) ) {
			$events['login']['sms'] = $settings['msg_sms'];
		}

		$settings['msg_events'] = $events;

		// Keep legacy keys in sync (OTP send still reads these as fallback).
		$settings['msg_email_subject'] = $events['login']['email_subject'];
		$settings['msg_email_body']    = $events['login']['email'];
		$settings['msg_whatsapp']      = $events['login']['whatsapp'];
		$settings['msg_sms']           = $events['login']['sms'];

		return $settings;
	}

	/**
	 * Get one event's channel template text.
	 *
	 * @param string $event   Event id.
	 * @param string $channel email|whatsapp|sms.
	 * @return string
	 */
	public static function get_event_template( $event, $channel ) {
		$events = self::get( 'msg_events', array() );
		if ( ! isset( $events[ $event ] ) || ! is_array( $events[ $event ] ) ) {
			return '';
		}
		$key = 'email' === $channel ? 'email' : $channel;
		return isset( $events[ $event ][ $key ] ) ? (string) $events[ $event ][ $key ] : '';
	}

	/**
	 * Get a single setting.
	 *
	 * @param string $key     Setting key.
	 * @param mixed  $fallback Optional fallback if key missing.
	 * @return mixed
	 */
	public static function get( $key, $fallback = null ) {
		$all = self::all();
		if ( array_key_exists( $key, $all ) ) {
			return $all[ $key ];
		}
		return $fallback;
	}

	/**
	 * Persist a (sanitised) settings array, merging over what already exists.
	 *
	 * @param array $new Raw incoming values.
	 * @return array The stored settings.
	 */
	public static function update( array $new ) {
		$current = self::all();
		$merged  = self::normalize_msg_events( array_merge( $current, self::sanitize( $new ) ) );
		update_option( self::OPTION, $merged );
		return $merged;
	}

	/**
	 * Seed defaults on activation (without clobbering existing config).
	 */
	public static function seed_defaults() {
		if ( false === get_option( self::OPTION, false ) ) {
			add_option( self::OPTION, self::defaults() );
		}
	}

	/**
	 * Sanitise an incoming settings array. Only known keys are kept.
	 *
	 * @param array $in Raw values.
	 * @return array
	 */
	public static function sanitize( array $in ) {
		$out      = array();
		$defaults = self::defaults();

		// Enums.
		if ( isset( $in['theme'] ) ) {
			$out['theme'] = in_array( $in['theme'], array( 'green', 'cyan', 'light' ), true ) ? $in['theme'] : 'light';
		}
		if ( isset( $in['lang'] ) ) {
			$out['lang'] = in_array( $in['lang'], array( 'ar', 'en' ), true ) ? $in['lang'] : 'ar';
		}
		if ( isset( $in['ui_font'] ) ) {
			$font = sanitize_key( $in['ui_font'] );
			$out['ui_font'] = array_key_exists( $font, self::font_registry() ) ? $font : 'tajawal';
		}
		if ( isset( $in['default_method'] ) ) {
			$out['default_method'] = in_array( $in['default_method'], array( 'email', 'whatsapp', 'sms' ), true ) ? $in['default_method'] : 'email';
		}
		if ( isset( $in['login_method_preset'] ) ) {
			$out['login_method_preset'] = in_array( $in['login_method_preset'], array( 'phone', 'email', 'choice' ), true ) ? $in['login_method_preset'] : 'phone';
			if ( 'phone' === $out['login_method_preset'] ) {
				$ch = self::enabled_channels();
				if ( in_array( 'whatsapp', $ch, true ) ) {
					$out['default_method'] = 'whatsapp';
				} elseif ( in_array( 'sms', $ch, true ) ) {
					$out['default_method'] = 'sms';
				}
			} elseif ( 'email' === $out['login_method_preset'] ) {
				$out['default_method'] = 'email';
			}
		}
		if ( isset( $in['session_length'] ) ) {
			$out['session_length'] = in_array( $in['session_length'], array( '1d', '7d', '14d', '30d', 'lifetime' ), true ) ? $in['session_length'] : '14d';
		}
		if ( isset( $in['remember_me_length'] ) ) {
			$out['remember_me_length'] = in_array( $in['remember_me_length'], array( '14d', '30d', '90d' ), true ) ? $in['remember_me_length'] : '30d';
		}

		// Booleans.
		foreach ( array(
			'dark', 'form_dark', 'form_dark_toggle', 'form_lang_toggle',
			'channel_email', 'channel_whatsapp', 'channel_sms', 'channel_google',
			'allow_signup', 'test_mode', 'recaptcha_enabled', 'rate_limit_enabled', 'block_ips_enabled', 'block_countries_enabled',
			'block_disposable', 'block_vpn_proxy', 'honeypot_enabled', 'privacy_login', 'trust_proxy',
			'profile_show_phone', 'verify_bar_account', 'force_2fa_roles',
			'auto_create_auth_page', 'force_login', 'blank_auth_template',
			'auto_redirect', 'one_session', 'remember_last_method',
			'logout_on_password_change', 'allow_device_management', 'device_login_alert',
			'auto_link_by_email', 'allow_unverified_email', 'sync_phone_woocommerce',
			'woo_checkout_otp', 'woo_otp_before_pay', 'woo_restore_cart',
			'email_otp_on', 'email_password_on', 'email_magic_on', 'require_email_verify', 'block_checkout_unverified',
			'fallback_on', 'validate_format', 'require_country_code', 'allow_landline', 'hlr_lookup',
			'one_active_otp', 'allow_reuse_once', 'case_sensitive',
			'google_autolink', 'google_ask_mobile', 'google_restrict_domains',
			'confirm_login', 'variant_rotation',
			'intl_enabled', 'intl_ip_detection', 'intl_carrier_lookup',
		) as $bool_key ) {
			if ( array_key_exists( $bool_key, $in ) ) {
				$out[ $bool_key ] = empty( $in[ $bool_key ] ) ? 0 : 1;
			}
		}

		// WhatsApp message style + List/Poll Interface Design.
		if ( isset( $in['wa_msg_style'] ) ) {
			$style = 'list' === $in['wa_msg_style'] ? 'poll' : $in['wa_msg_style']; // migrate legacy 'list'.
			$out['wa_msg_style'] = in_array( $style, array( 'standard', 'poll', 'link', 'button' ), true ) ? $style : 'standard';
		}
		// Methods the user may choose from on the login screen (multi-select).
		if ( isset( $in['wa_methods'] ) ) {
			$valid = array( 'standard', 'poll', 'button', 'link' );
			$picked = is_array( $in['wa_methods'] ) ? array_map( 'sanitize_key', wp_unslash( $in['wa_methods'] ) ) : array();
			$out['wa_methods'] = array_values( array_intersect( $valid, $picked ) );
		} elseif ( ! empty( $in['wa_methods_present'] ) ) {
			// The WhatsApp form was submitted with every box unticked → clear it.
			// (Without this marker, saving any OTHER settings page would wipe the
			// admin's chosen methods, since wa_methods only lives on that one form.)
			$out['wa_methods'] = array();
		}
		if ( isset( $in['wa_link_msg'] ) ) {
			$out['wa_link_msg'] = sanitize_textarea_field( wp_unslash( $in['wa_link_msg'] ) );
		}
		foreach ( array( 'wa_list_title', 'wa_list_item1_title', 'wa_list_item1_desc', 'wa_list_item2_title', 'wa_list_item2_desc', 'wa_list_button', 'wa_list_footer' ) as $lk ) {
			if ( isset( $in[ $lk ] ) ) {
				$out[ $lk ] = sanitize_text_field( wp_unslash( $in[ $lk ] ) );
			}
		}
		if ( isset( $in['wa_list_desc'] ) ) {
			$out['wa_list_desc'] = sanitize_textarea_field( wp_unslash( $in['wa_list_desc'] ) );
		}

		// Advanced Country Code (intl-tel-input).
		if ( isset( $in['intl_default_country'] ) ) {
			$out['intl_default_country'] = sanitize_key( $in['intl_default_country'] );
		}
		if ( isset( $in['intl_alignment'] ) ) {
			$out['intl_alignment'] = in_array( $in['intl_alignment'], array( 'auto', 'left', 'right' ), true ) ? $in['intl_alignment'] : 'auto';
		}
		if ( isset( $in['intl_countries'] ) ) {
			$list = is_array( $in['intl_countries'] ) ? $in['intl_countries'] : explode( ',', (string) $in['intl_countries'] );
			$out['intl_countries'] = array_values( array_unique( array_filter( array_map( 'sanitize_key', $list ) ) ) );
		}
		if ( isset( $in['intl_fields'] ) ) {
			if ( is_array( $in['intl_fields'] ) ) {
				$rows = $in['intl_fields'];
			} else {
				$rows = preg_split( '/[\r\n,]+/', (string) wp_unslash( $in['intl_fields'] ) );
			}
			$clean = array();
			foreach ( (array) $rows as $sel ) {
				$sel = trim( wp_strip_all_tags( (string) $sel ) );
				if ( '' !== $sel ) {
					$clean[] = $sel;
				}
			}
			$out['intl_fields'] = array_values( array_unique( $clean ) );
		}

		// Plain text / keys.
		foreach ( array( 'email_from_name', 'email_gateway', 'wa_gateway', 'sms_gateway', 'google_client_id', 'google_client_secret', 'recaptcha_site', 'recaptcha_secret', 'smtp_host', 'smtp_user', 'default_country', 'google_button_label', 'numverify_key', 'disposable_prefixes' ) as $tk ) {
			if ( isset( $in[ $tk ] ) ) {
				$out[ $tk ] = sanitize_text_field( $in[ $tk ] );
			}
		}
		// SMTP password — preserve special characters that sanitize_text_field would strip.
		if ( isset( $in['smtp_pass'] ) ) {
			$out['smtp_pass'] = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', wp_unslash( $in['smtp_pass'] ) );
		}
		foreach ( array( 'logo_ar_light', 'logo_ar_dark', 'logo_en_light', 'logo_en_dark' ) as $lk ) {
			if ( isset( $in[ $lk ] ) ) {
				$out[ $lk ] = esc_url_raw( $in[ $lk ] );
			}
		}
		if ( isset( $in['form_logo_height'] ) ) {
			$out['form_logo_height'] = in_array( $in['form_logo_height'], array( '28', '34', '40', '48' ), true ) ? $in['form_logo_height'] : '34';
		}
		if ( isset( $in['confirm_expiry'] ) ) {
			$out['confirm_expiry'] = min( 60, max( 1, (int) $in['confirm_expiry'] ) );
		}
		if ( isset( $in['confirm_poll'] ) ) {
			$out['confirm_poll'] = min( 15, max( 2, (int) $in['confirm_poll'] ) );
		}
		if ( isset( $in['email_from'] ) ) {
			$out['email_from'] = $in['email_from'] ? sanitize_email( $in['email_from'] ) : '';
		}

		// Redirect rules (array of role => url).
		if ( isset( $in['redirect_rules'] ) && is_array( $in['redirect_rules'] ) ) {
			$rules = array();
			foreach ( $in['redirect_rules'] as $rule ) {
				if ( empty( $rule['url'] ) ) {
					continue;
				}
				$rules[] = array(
					'role' => isset( $rule['role'] ) ? sanitize_key( $rule['role'] ) : 'all',
					'url'  => esc_url_raw( $rule['url'] ),
				);
			}
			$out['redirect_rules'] = $rules;
		}

		// Integers (clamped).
		if ( isset( $in['otp_length'] ) ) {
			$out['otp_length'] = max( 4, min( 8, (int) $in['otp_length'] ) );
		}
		if ( isset( $in['otp_expiry'] ) ) {
			$out['otp_expiry'] = max( 1, min( 60, (int) $in['otp_expiry'] ) );
		}
		if ( isset( $in['rate_limit'] ) ) {
			$out['rate_limit'] = max( 1, min( 20, (int) $in['rate_limit'] ) );
		}
		if ( isset( $in['rate_limit_ip_hour'] ) ) {
			$out['rate_limit_ip_hour'] = max( 1, min( 100, (int) $in['rate_limit_ip_hour'] ) );
		}
		if ( isset( $in['rate_limit_phone_day'] ) ) {
			$out['rate_limit_phone_day'] = max( 1, min( 200, (int) $in['rate_limit_phone_day'] ) );
		}
		if ( isset( $in['rate_limit_cooldown'] ) ) {
			$out['rate_limit_cooldown'] = max( 1, min( 72, (int) $in['rate_limit_cooldown'] ) );
		}
		if ( isset( $in['captcha_provider'] ) ) {
			$out['captcha_provider'] = in_array( $in['captcha_provider'], array( 'recaptcha', 'turnstile', 'hcaptcha' ), true )
				? $in['captcha_provider'] : 'recaptcha';
		}
		if ( isset( $in['blocked_ips'] ) ) {
			$ips = Mersal_Notif_Security::parse_lines( wp_unslash( $in['blocked_ips'] ) );
			$out['blocked_ips'] = implode( "\n", $ips );
		}
		if ( isset( $in['blocked_countries'] ) ) {
			$out['blocked_countries'] = implode( ',', Mersal_Notif_Security::parse_countries( wp_unslash( $in['blocked_countries'] ) ) );
		}
		if ( isset( $in['auth_page_id'] ) ) {
			$out['auth_page_id'] = max( 0, (int) $in['auth_page_id'] );
		}

		// Enums (method/channel/format pickers).
		$enums = array(
			'email_method'   => array( 'otp', 'password', 'magic' ),
			'email_provider' => array( 'wordpress', 'mersal' ),
			'phone_channel'  => array( 'sms', 'whatsapp', 'both' ),
			'countries_mode' => array( 'allow', 'block' ),
			'phone_format'   => array( 'intl', 'local', 'e164' ),
			'otp_type'       => array( 'numeric', 'alphanum', 'alpha' ),
			'smtp_enc'       => array( 'tls', 'ssl', 'none' ),
		);
		foreach ( $enums as $ek => $allowed ) {
			if ( isset( $in[ $ek ] ) ) {
				$out[ $ek ] = in_array( $in[ $ek ], $allowed, true ) ? $in[ $ek ] : $defaults[ $ek ];
			}
		}

		// Integers (clamped) — advanced OTP + SMTP.
		$ints = array(
			'smtp_port'        => array( 1, 65535 ),
			'resend_wait'      => array( 30, 180 ),
			'otp_max_attempts' => array( 3, 10 ),
		);
		foreach ( $ints as $ik => $mm ) {
			if ( isset( $in[ $ik ] ) ) {
				$out[ $ik ] = max( $mm[0], min( $mm[1], (int) $in[ $ik ] ) );
			}
		}
		if ( isset( $in['lockout_duration'] ) ) {
			$out['lockout_duration'] = in_array( (int) $in['lockout_duration'], array( 5, 15, 60, 1440 ), true ) ? (int) $in['lockout_duration'] : 15;
		}

		// Colors.
		if ( isset( $in['btn_color'] ) ) {
			$color             = sanitize_hex_color( $in['btn_color'] );
			$out['btn_color']  = $color ? $color : $defaults['btn_color'];
		}

		// Plain text.
		foreach ( array( 'msg_email_subject', 'form_title', 'form_subtitle', 'verify_bar_intro' ) as $text_key ) {
			if ( isset( $in[ $text_key ] ) ) {
				$out[ $text_key ] = sanitize_text_field( $in[ $text_key ] );
			}
		}

		// URL.
		if ( isset( $in['redirect_url'] ) ) {
			$out['redirect_url'] = $in['redirect_url'] ? esc_url_raw( $in['redirect_url'] ) : '';
		}

		// Multi-line message bodies (allow line breaks, strip tags).
		foreach ( array( 'msg_email_body', 'msg_whatsapp', 'msg_sms' ) as $area_key ) {
			if ( isset( $in[ $area_key ] ) ) {
				$out[ $area_key ] = sanitize_textarea_field( $in[ $area_key ] );
			}
		}

		if ( isset( $in['msg_events'] ) && is_array( $in['msg_events'] ) ) {
			$lang     = isset( $in['lang'] ) ? $in['lang'] : self::get( 'lang', 'ar' );
			$defaults = self::default_msg_events( 'en' === $lang ? 'en' : 'ar' );
			$clean    = array();
			foreach ( self::template_event_ids() as $event_id ) {
				if ( ! isset( $in['msg_events'][ $event_id ] ) || ! is_array( $in['msg_events'][ $event_id ] ) ) {
					continue;
				}
				$row = $in['msg_events'][ $event_id ];
				$clean[ $event_id ] = array(
					'email_subject' => isset( $row['email_subject'] ) ? sanitize_text_field( $row['email_subject'] ) : ( $defaults[ $event_id ]['email_subject'] ?? '' ),
					'email'         => isset( $row['email'] ) ? sanitize_textarea_field( $row['email'] ) : ( $defaults[ $event_id ]['email'] ?? '' ),
					'whatsapp'      => isset( $row['whatsapp'] ) ? sanitize_textarea_field( $row['whatsapp'] ) : ( $defaults[ $event_id ]['whatsapp'] ?? '' ),
					'sms'           => isset( $row['sms'] ) ? sanitize_textarea_field( $row['sms'] ) : ( $defaults[ $event_id ]['sms'] ?? '' ),
				);
			}
			$out['msg_events'] = $clean;
			if ( isset( $clean['login'] ) ) {
				$out['msg_email_subject'] = $clean['login']['email_subject'];
				$out['msg_email_body']    = $clean['login']['email'];
				$out['msg_whatsapp']      = $clean['login']['whatsapp'];
				$out['msg_sms']           = $clean['login']['sms'];
			}
		}

		// Custom CSS — strip tags but keep braces/quotes.
		if ( isset( $in['custom_css'] ) ) {
			$out['custom_css'] = wp_strip_all_tags( (string) $in['custom_css'] );
		}

		// --- Abandoned cart + phone-capture popup + chat widget ---
		foreach ( array(
			'abandoned_cart_enabled', 'ac_track_pages', 'ac_capture_checkout', 'ac_prefill_checkout',
			'ac_generate_coupon', 'ac_coupon_free_shipping', 'ac_coupon_email_restrict',
			'ac_popup_enabled', 'ac_popup_trigger_cart', 'ac_popup_trigger_exit',
			'chat_widget_enabled',
		) as $bk ) {
			if ( array_key_exists( $bk, $in ) ) {
				$out[ $bk ] = empty( $in[ $bk ] ) ? 0 : 1;
			}
		}
		$ac_ints = array(
			'ac_threshold_minutes'    => array( 5, 2880 ),
			'ac_coupon_discount'      => array( 1, 100000 ),
			'ac_coupon_expiry_days'   => array( 1, 90 ),
			'ac_coupon_min_spend'     => array( 0, 1000000 ),
			'ac_coupon_max_discount'  => array( 0, 1000000 ),
			'ac_popup_delay'          => array( 0, 300 ),
			'ac_popup_frequency_days' => array( 1, 90 ),
			'ac_popup_min_cart'       => array( 0, 1000000 ),
		);
		foreach ( $ac_ints as $ik => $mm ) {
			if ( isset( $in[ $ik ] ) ) {
				$out[ $ik ] = max( $mm[0], min( $mm[1], (int) $in[ $ik ] ) );
			}
		}
		if ( isset( $in['ac_coupon_type'] ) ) {
			$out['ac_coupon_type'] = in_array( $in['ac_coupon_type'], array( 'percent', 'fixed_cart' ), true ) ? $in['ac_coupon_type'] : 'percent';
		}
		if ( isset( $in['ac_popup_display'] ) ) {
			$out['ac_popup_display'] = in_array( $in['ac_popup_display'], array( 'all', 'desktop', 'mobile' ), true ) ? $in['ac_popup_display'] : 'all';
		}
		if ( isset( $in['ac_popup_frequency'] ) ) {
			$out['ac_popup_frequency'] = in_array( $in['ac_popup_frequency'], array( 'once', 'days', 'always' ), true ) ? $in['ac_popup_frequency'] : 'days';
		}
		foreach ( array( 'ac_coupon_prefix', 'ac_popup_title', 'ac_popup_desc', 'ac_popup_btn', 'ac_popup_success_title', 'ac_popup_success_desc' ) as $tk ) {
			if ( isset( $in[ $tk ] ) ) {
				$out[ $tk ] = sanitize_text_field( $in[ $tk ] );
			}
		}
		foreach ( array( 'ac_coupon_products', 'ac_coupon_cats', 'ac_popup_exclude' ) as $ck ) {
			if ( isset( $in[ $ck ] ) ) {
				$ids          = array_filter( array_map( 'absint', preg_split( '/[\s,]+/', (string) $in[ $ck ] ) ) );
				$out[ $ck ]   = implode( ',', $ids );
			}
		}
		if ( isset( $in['ac_popup_image'] ) ) {
			$out['ac_popup_image'] = $in['ac_popup_image'] ? esc_url_raw( $in['ac_popup_image'] ) : '';
		}
		if ( isset( $in['ac_popup_color'] ) ) {
			$color                  = sanitize_hex_color( $in['ac_popup_color'] );
			$out['ac_popup_color']  = $color ? $color : '#0f8f6f';
		}
		if ( isset( $in['ac_message_template'] ) ) {
			$out['ac_message_template'] = sanitize_textarea_field( $in['ac_message_template'] );
		}
		if ( isset( $in['notif_min_gap'] ) ) {
			$out['notif_min_gap'] = max( 0, min( 720, (int) $in['notif_min_gap'] ) );
		}
		foreach ( array( 'notif_blocked_numbers', 'notif_blocked_emails' ) as $blk ) {
			if ( isset( $in[ $blk ] ) ) {
				$lines        = preg_split( '/[\r\n,]+/', (string) wp_unslash( $in[ $blk ] ) );
				$lines        = array_filter( array_map( 'trim', (array) $lines ), 'strlen' );
				$out[ $blk ]  = implode( "\n", array_unique( array_map( 'sanitize_text_field', $lines ) ) );
			}
		}

		return $out;
	}

	/**
	 * Channels that are currently enabled, in display order.
	 *
	 * @return array List of channel keys (email|whatsapp|sms).
	 */
	public static function enabled_channels() {
		$all      = self::all();
		$channels = array();
		foreach ( array( 'email', 'whatsapp', 'sms' ) as $ch ) {
			if ( ! empty( $all[ 'channel_' . $ch ] ) ) {
				$channels[] = $ch;
			}
		}
		if ( empty( $channels ) ) {
			$channels[] = 'email'; // never leave the user with nothing.
		}
		return $channels;
	}
	/**
	 * Available admin/form UI fonts (key => label).
	 *
	 * @return array<string,string>
	 */
	public static function font_registry() {
		return array(
			'tajawal' => 'Tajawal',
			'cairo'   => 'Cairo',
			'system'  => 'System',
		);
	}
}
