<?php
/**
 * Settings management helpers.
 *
 * @package MailDoctor
 */

namespace MailDoctor\Helpers;

use MailDoctor\Mail\Config\API_Config;
use MailDoctor\Mail\Config\SMTP_Config;

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

class Settings {


	/**
	 * Option name.
	 */
	const OPTION_KEY = 'mail_doctor_settings';

	/**
	 * Network option name for defaults.
	 */
	const NETWORK_OPTION = 'mail_doctor_network_defaults';

	/**
	 * Fetch full settings array.
	 *
	 * @return array<string, mixed>
	 */
	public static function all(): array {
		$defaults = array_merge(
			SMTP_Config::defaults(),
			API_Config::defaults(),
			array(
				'override_scope'               => 'woocommerce',
				'async_mode'                   => 'sync',
				'active_profile'               => 'default',
				'smtp_profile_assignments'     => array(
					'emails' => array(),
					'roles'  => array(),
					'sites'  => array(),
				),
				'smtp_profile_map'             => array(), // Legacy compatibility.
				'webhook_token'                => '',
				'sendgrid_webhook_signing_key' => '',
				'webhook_enabled'              => false,
				'privacy_mode'                 => false,
				'hash_recipients'              => false,
				'retention_sent_days'          => 90,
				'retention_failed_days'        => 30,
				'retention_payload_days'       => 14,
				'max_attempts'                 => 4,
				'backoff_schedule'             => array( 300, 900, 3600, 14400, 43200 ), // 5m, 15m, 1h, 4h, 12h.
				'log_retention'                => 30,
				'attachment_total_max_mb'      => 10,
				'attachment_block_mimes'       => array(
					'application/x-php',
					'text/x-php',
					'application/x-msdownload',
					'application/x-msdos-program',
					'application/x-python',
				),
				'rate_limit_per_minute'        => 30,
				'rate_limit_pushback_cooldown' => 120,
				'idempotency_window'           => 300,
				'poison_attempt_threshold'     => 3,
				'poison_match_window'          => 900,
				'cleanup_on_uninstall'         => false,
				'compatibility_mode'           => false,
				'list_unsubscribe'             => false,
				'list_unsubscribe_mailto'      => '',
				'list_unsubscribe_url'         => '',
				'fallback_transport'           => false,
				'debug_logging'                => false,
				'alerts_enabled'               => false,
				'alert_threshold_percent'      => 20,
				'alert_window_minutes'         => 30,
				'alert_burst_failures'         => 5,
				'alert_burst_minutes'          => 5,
				'alert_email'                  => '',
				'alert_slack_webhook'          => '',
				'alert_generic_webhook'        => '',
				'alert_cooldown_minutes'       => 60,
				'digest_enabled'               => false,
				'digest_recipients'            => '',
				'digest_weekday'               => 'monday',
				'digest_hour'                  => 8,
				'structured_logging_enabled'   => false,
				'structured_logging_endpoint'  => '',
				'structured_logging_token'     => '',
			)
		);

		$stored = get_option( self::OPTION_KEY, array() );

		if ( ! is_array( $stored ) ) {
			$stored = array();
		}

		$settings = wp_parse_args( $stored, $defaults );

		if ( is_multisite() ) {
			$settings = self::apply_network_defaults( $settings );
		}

		return $settings;
	}

	/**
	 * Retrieve a specific setting value.
	 *
	 * @param string $key Setting key.
	 * @param mixed  $default Default value.
	 * @return mixed
	 */
	public static function get( string $key, $default = null ) {
		$settings = self::all();

		if ( ! array_key_exists( $key, $settings ) ) {
			return $default;
		}

		$value = $settings[ $key ];

		if ( is_bool( $default ) ) {
			return (bool) $value;
		}

		return $value;
	}

	/**
	 * Update settings with sanitized data.
	 *
	 * @param array<string, mixed> $data Posted settings.
	 * @return void
	 */
	public static function update( array $data ): void {
		update_option( self::OPTION_KEY, self::sanitize( $data ) );
	}

	/**
	 * Sanitize incoming settings before persisting.
	 *
	 * @param array<string, mixed> $data Raw data.
	 * @return array<string, mixed>
	 */
	public static function sanitize( array $data ): array {
		$current = self::all();

		$smtp_values = SMTP_Config::sanitize( $data, $current );
		$api_values  = API_Config::sanitize( $data, $current );

		$sanitized = array_merge(
			$smtp_values,
			$api_values,
			array(
				'override_scope'               => isset( $data['override_scope'] ) ? self::sanitize_scope( $data['override_scope'] ) : $current['override_scope'],
				'async_mode'                   => isset( $data['async_mode'] ) ? self::sanitize_async_mode( $data['async_mode'] ) : ( $current['async_mode'] ?? 'sync' ),
				'max_attempts'                 => isset( $data['max_attempts'] ) ? max( 1, Sanitize::int( $data['max_attempts'], 4 ) ) : $current['max_attempts'],
				'backoff_schedule'             => self::sanitize_schedule( $data['backoff_schedule'] ?? $current['backoff_schedule'] ),
				'log_retention'                => isset( $data['log_retention'] ) ? max( 7, Sanitize::int( $data['log_retention'], 30 ) ) : $current['log_retention'],
				'privacy_mode'                 => isset( $data['privacy_mode'] ) ? Sanitize::bool( $data['privacy_mode'] ) : ( $current['privacy_mode'] ?? false ),
				'hash_recipients'              => isset( $data['hash_recipients'] ) ? Sanitize::bool( $data['hash_recipients'] ) : ( $current['hash_recipients'] ?? false ),
				'retention_sent_days'          => isset( $data['retention_sent_days'] ) ? max( 1, Sanitize::int( $data['retention_sent_days'], $current['retention_sent_days'] ?? 90 ) ) : ( $current['retention_sent_days'] ?? 90 ),
				'retention_failed_days'        => isset( $data['retention_failed_days'] ) ? max( 1, Sanitize::int( $data['retention_failed_days'], $current['retention_failed_days'] ?? 30 ) ) : ( $current['retention_failed_days'] ?? 30 ),
				'retention_payload_days'       => isset( $data['retention_payload_days'] ) ? max( 1, Sanitize::int( $data['retention_payload_days'], $current['retention_payload_days'] ?? 14 ) ) : ( $current['retention_payload_days'] ?? 14 ),
				'cleanup_on_uninstall'         => isset( $data['cleanup_on_uninstall'] ) ? Sanitize::bool( $data['cleanup_on_uninstall'] ) : $current['cleanup_on_uninstall'],
				'compatibility_mode'           => isset( $data['compatibility_mode'] ) ? Sanitize::bool( $data['compatibility_mode'] ) : $current['compatibility_mode'],
				'list_unsubscribe'             => isset( $data['list_unsubscribe'] ) ? Sanitize::bool( $data['list_unsubscribe'] ) : $current['list_unsubscribe'],
				'list_unsubscribe_mailto'      => isset( $data['list_unsubscribe_mailto'] ) ? sanitize_email( $data['list_unsubscribe_mailto'] ) : $current['list_unsubscribe_mailto'],
				'list_unsubscribe_url'         => isset( $data['list_unsubscribe_url'] ) ? esc_url_raw( $data['list_unsubscribe_url'] ) : $current['list_unsubscribe_url'],
				'alerts_enabled'               => isset( $data['alerts_enabled'] ) ? Sanitize::bool( $data['alerts_enabled'] ) : ( $current['alerts_enabled'] ?? false ),
				'webhook_enabled'              => isset( $data['webhook_enabled'] ) ? Sanitize::bool( $data['webhook_enabled'] ) : ( $current['webhook_enabled'] ?? false ),
				'alert_threshold_percent'      => isset( $data['alert_threshold_percent'] )
					? max( 1, Sanitize::int( $data['alert_threshold_percent'], 20 ) )
					: ( isset( $data['alerts_threshold'] ) ? max( 1, Sanitize::int( $data['alerts_threshold'], 20 ) ) : ( $current['alert_threshold_percent'] ?? 20 ) ),
				'alert_window_minutes'         => isset( $data['alert_window_minutes'] )
					? max( 5, Sanitize::int( $data['alert_window_minutes'], 30 ) )
					: ( isset( $data['alerts_window'] ) ? max( 5, Sanitize::int( $data['alerts_window'], 30 ) ) : ( $current['alert_window_minutes'] ?? 30 ) ),
				'alert_burst_failures'         => isset( $data['alert_burst_failures'] ) ? max( 1, Sanitize::int( $data['alert_burst_failures'], 5 ) ) : ( $current['alert_burst_failures'] ?? 5 ),
				'alert_burst_minutes'          => isset( $data['alert_burst_minutes'] ) ? max( 1, Sanitize::int( $data['alert_burst_minutes'], 5 ) ) : ( $current['alert_burst_minutes'] ?? 5 ),
				'alert_email'                  => isset( $data['alert_email'] ) ? sanitize_email( $data['alert_email'] ) : ( $current['alert_email'] ?? '' ),
				'alert_slack_webhook'          => isset( $data['alert_slack_webhook'] ) ? esc_url_raw( $data['alert_slack_webhook'] ) : ( $current['alert_slack_webhook'] ?? '' ),
				'alert_generic_webhook'        => isset( $data['alert_generic_webhook'] ) ? esc_url_raw( $data['alert_generic_webhook'] ) : ( $current['alert_generic_webhook'] ?? '' ),
				'alert_cooldown_minutes'       => isset( $data['alert_cooldown_minutes'] ) ? max( 5, Sanitize::int( $data['alert_cooldown_minutes'], 60 ) ) : ( $current['alert_cooldown_minutes'] ?? 60 ),
				'digest_enabled'               => isset( $data['digest_enabled'] ) ? Sanitize::bool( $data['digest_enabled'] ) : ( $current['digest_enabled'] ?? false ),
				'digest_recipients'            => isset( $data['digest_recipients'] ) ? self::sanitize_digest_recipients( $data['digest_recipients'] ) : ( $current['digest_recipients'] ?? '' ),
				'digest_weekday'               => isset( $data['digest_weekday'] ) ? self::sanitize_digest_weekday( $data['digest_weekday'] ) : ( $current['digest_weekday'] ?? 'monday' ),
				'digest_hour'                  => isset( $data['digest_hour'] ) ? self::sanitize_digest_hour( $data['digest_hour'] ) : ( $current['digest_hour'] ?? 8 ),
				'sendgrid_webhook_signing_key' => isset( $data['sendgrid_webhook_signing_key'] ) ? self::sanitize_signing_key( $data['sendgrid_webhook_signing_key'] ) : ( $current['sendgrid_webhook_signing_key'] ?? '' ),
				'structured_logging_enabled'   => isset( $data['structured_logging_enabled'] ) ? Sanitize::bool( $data['structured_logging_enabled'] ) : ( $current['structured_logging_enabled'] ?? false ),
				'structured_logging_endpoint'  => isset( $data['structured_logging_endpoint'] ) ? esc_url_raw( $data['structured_logging_endpoint'] ) : ( $current['structured_logging_endpoint'] ?? '' ),
				'structured_logging_token'     => isset( $data['structured_logging_token'] ) ? sanitize_text_field( $data['structured_logging_token'] ) : ( $current['structured_logging_token'] ?? '' ),
				'rate_limit_per_minute'        => isset( $data['rate_limit_per_minute'] ) ? max( 0, Sanitize::int( $data['rate_limit_per_minute'], $current['rate_limit_per_minute'] ) ) : ( $current['rate_limit_per_minute'] ?? 30 ),
				'rate_limit_pushback_cooldown' => isset( $data['rate_limit_pushback_cooldown'] ) ? max( 30, Sanitize::int( $data['rate_limit_pushback_cooldown'], $current['rate_limit_pushback_cooldown'] ?? 120 ) ) : ( $current['rate_limit_pushback_cooldown'] ?? 120 ),
				'idempotency_window'           => isset( $data['idempotency_window'] ) ? max( 60, Sanitize::int( $data['idempotency_window'], $current['idempotency_window'] ?? 300 ) ) : ( $current['idempotency_window'] ?? 300 ),
				'poison_attempt_threshold'     => isset( $data['poison_attempt_threshold'] ) ? max( 2, Sanitize::int( $data['poison_attempt_threshold'], $current['poison_attempt_threshold'] ?? 3 ) ) : ( $current['poison_attempt_threshold'] ?? 3 ),
				'poison_match_window'          => isset( $data['poison_match_window'] ) ? max( 300, Sanitize::int( $data['poison_match_window'], $current['poison_match_window'] ?? 900 ) ) : ( $current['poison_match_window'] ?? 900 ),
				'attachment_total_max_mb'      => isset( $data['attachment_total_max_mb'] ) ? max( 1, Sanitize::int( $data['attachment_total_max_mb'], $current['attachment_total_max_mb'] ?? 10 ) ) : ( $current['attachment_total_max_mb'] ?? 10 ),
				'attachment_block_mimes'       => self::sanitize_mime_list( $data['attachment_block_mimes'] ?? ( $current['attachment_block_mimes'] ?? array() ) ),
				'fallback_transport'           => isset( $data['fallback_transport'] ) ? Sanitize::bool( $data['fallback_transport'] ) : ( $current['fallback_transport'] ?? false ),
				'debug_logging'                => isset( $data['debug_logging'] ) ? Sanitize::bool( $data['debug_logging'] ) : ( $current['debug_logging'] ?? false ),
			)
		);

		$transport = $sanitized['transport'];

		if ( 'smtp' !== $transport ) {
			$sanitized['smtp_host']       = '';
			$sanitized['smtp_username']   = '';
			$sanitized['smtp_password']   = '';
			$sanitized['smtp_encryption'] = 'none';
		}

		if ( 'mailgun_api' !== $transport ) {
			$sanitized['mailgun_api_key']      = '';
			$sanitized['mailgun_api_domain']   = '';
			$sanitized['mailgun_api_endpoint'] = 'api.mailgun.net';
		}

		if ( 'sendgrid_api' !== $transport ) {
			$sanitized['sendgrid_api_key'] = '';
		}

		if ( 'postmark_api' !== $transport ) {
			$sanitized['postmark_api_token'] = '';
		}

		if ( 'sparkpost_api' !== $transport ) {
			$sanitized['sparkpost_api_key']      = '';
			$sanitized['sparkpost_api_endpoint'] = 'https://api.sparkpost.com';
		}

		if ( 'ses_api' !== $transport ) {
			$sanitized['ses_api_key']           = '';
			$sanitized['ses_api_secret']        = '';
			$sanitized['ses_api_session_token'] = '';
		}

		if ( 'generic_http' !== $transport ) {
			$sanitized['generic_api_endpoint']   = '';
			$sanitized['generic_api_method']     = 'POST';
			$sanitized['generic_api_auth_token'] = '';
		}

		$sanitized['active_profile'] = self::sanitize_profile_reference( $data['active_profile'] ?? ( $current['active_profile'] ?? 'default' ) );

		$assignments_input = array();
		if ( isset( $data['smtp_profile_assignments'] ) && is_array( $data['smtp_profile_assignments'] ) ) {
			$assignments_input = $data['smtp_profile_assignments'];
		}

		if ( isset( $data['smtp_profile_map'] ) && is_array( $data['smtp_profile_map'] ) ) {
			if ( ! isset( $assignments_input['emails'] ) || ! is_array( $assignments_input['emails'] ) ) {
				$assignments_input['emails'] = $data['smtp_profile_map'];
			}
		}

		$sanitized['smtp_profile_assignments'] = self::sanitize_profile_assignments( $assignments_input );
		$sanitized['smtp_profile_map']         = array();

		if ( isset( $data['webhook_token'] ) ) {
			$token = sanitize_text_field( $data['webhook_token'] );
			if ( '' !== $token ) {
				$sanitized['webhook_token'] = substr( preg_replace( '/[^A-Za-z0-9]/', '', $token ), 0, 64 );
			} elseif ( isset( $current['webhook_token'] ) ) {
				$sanitized['webhook_token'] = $current['webhook_token'];
			}
		} elseif ( isset( $current['webhook_token'] ) ) {
			$sanitized['webhook_token'] = $current['webhook_token'];
		}

		if ( empty( $sanitized['webhook_token'] ) ) {
			$sanitized['webhook_token'] = self::maybe_generate_webhook_token();
		}

		if ( is_multisite() ) {
			$network  = self::network_config();
			$defaults = $network['defaults'];
			$locks    = $network['locks'];

			foreach ( $locks as $key => $locked ) {
				if ( $locked && isset( $defaults[ $key ] ) ) {
					$sanitized[ $key ] = $defaults[ $key ];
				}
			}
		}

		/**
		 * Filter sanitized Mail Doctor settings prior to persistence.
		 *
		 * @since 1.0.0
		 *
		 * @param array<string,mixed> $sanitized Sanitized values.
		 * @param array<string,mixed> $data      Raw submitted values.
		 */
		return apply_filters( 'mail_doctor_settings_sanitized', $sanitized, $data );
	}

	/**
	 * Public wrapper for profile reference sanitization.
	 *
	 * @param mixed $value Raw profile reference.
	 * @return string
	 */
	public static function sanitize_profile( $value ): string {
		return self::sanitize_profile_reference( $value );
	}

	/**
	 * Generate a fresh webhook token.
	 *
	 * @return string
	 */
	public static function new_webhook_token(): string {
		return self::maybe_generate_webhook_token();
	}

	/**
	 * Sanitize override scope.
	 *
	 * @param string $scope Raw scope.
	 * @return string
	 */
	private static function sanitize_scope( string $scope ): string {
		$scope = strtolower( sanitize_text_field( $scope ) );

		return in_array( $scope, array( 'woocommerce', 'all' ), true ) ? $scope : 'woocommerce';
	}

	/**
	 * Sanitize async delivery mode.
	 *
	 * @param string $mode Raw mode.
	 * @return string
	 */
	private static function sanitize_async_mode( string $mode ): string {
		$mode    = strtolower( sanitize_key( $mode ) );
		$allowed = array_keys( self::async_mode_choices() );

		if ( 'action_scheduler' === $mode && ! self::action_scheduler_available() ) {
			return 'shutdown';
		}

		return in_array( $mode, $allowed, true ) ? $mode : 'sync';
	}

	/**
	 * Sanitize backoff schedule array.
	 *
	 * @param mixed $schedule Raw schedule.
	 * @return array<int, int>
	 */
	private static function sanitize_schedule( $schedule ): array {
		if ( ! is_array( $schedule ) ) {
			return array( 300, 900, 3600, 14400, 43200 );
		}

		$clean = array();
		foreach ( $schedule as $offset ) {
			$offset = Sanitize::int( $offset );
			if ( $offset > 0 ) {
				$clean[] = $offset;
			}
		}

		if ( empty( $clean ) ) {
			return array( 300, 900, 3600, 14400, 43200 );
		}

		sort( $clean );

		return $clean;
	}

	/**
	 * Sanitize list of MIME types.
	 *
	 * @param mixed $input Raw input.
	 * @return array<int, string>
	 */
	private static function sanitize_mime_list( $input ): array {
		if ( is_string( $input ) ) {
			$input = preg_split( '/[\r\n,]+/', $input ) ?: array();
		}

		if ( ! is_array( $input ) ) {
			return array();
		}

		$sanitized = array();

		foreach ( $input as $value ) {
			if ( is_array( $value ) ) {
				$value = reset( $value );
			}

			$mime = strtolower( trim( (string) $value ) );

			if ( '' === $mime ) {
				continue;
			}

			$mime = preg_replace( '/[^a-z0-9\.\+\-\/]/', '', $mime );

			if ( '' !== $mime ) {
				$sanitized[] = $mime;
			}
		}

		return array_values( array_unique( $sanitized ) );
	}

	/**
	 * Sanitize digest recipient list.
	 *
	 * @param mixed $input Raw recipient input.
	 * @return string
	 */
	private static function sanitize_digest_recipients( $input ): string {
		if ( is_array( $input ) ) {
			$input = implode( ',', $input );
		}

		$input = (string) $input;

		if ( '' === trim( $input ) ) {
			return '';
		}

		$parts = preg_split( '/[\s,]+/', $input );

		if ( ! $parts ) {
			return '';
		}

		$valid = array();

		foreach ( $parts as $part ) {
			$email = sanitize_email( trim( $part ) );
			if ( $email ) {
				$valid[] = strtolower( $email );
			}
		}

		$valid = array_unique( $valid );

		return implode( ', ', $valid );
	}

	/**
	 * Sanitize weekday string for digest scheduling.
	 *
	 * @param mixed $weekday Raw weekday value.
	 * @return string
	 */
	private static function sanitize_digest_weekday( $weekday ): string {
		$weekday = strtolower( sanitize_key( (string) $weekday ) );
		$allowed = array( 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday' );

		return in_array( $weekday, $allowed, true ) ? $weekday : 'monday';
	}

	/**
	 * Sanitize digest hour.
	 *
	 * @param mixed $hour Raw hour value.
	 * @return int
	 */
	private static function sanitize_digest_hour( $hour ): int {
		$hour = (int) $hour;

		if ( $hour < 0 ) {
			$hour = 0;
		}

		if ( $hour > 23 ) {
			$hour = 23;
		}

		return $hour;
	}

	/**
	 * Normalize webhook signing key values.
	 *
	 * @param mixed $value Raw signing key.
	 * @return string
	 */
	protected static function sanitize_signing_key( $value ): string {
		if ( ! is_string( $value ) ) {
			return '';
		}

		$key = preg_replace( '/\s+/', '', trim( $value ) );

		if ( '' === $key ) {
			return '';
		}

		return preg_replace( '/[^A-Za-z0-9+\/=]/', '', $key );
	}

	/**
	 * Retrieve configured SMTP profiles keyed by slug.
	 *
	 * @return array<string, array<string, mixed>>
	 */
	public static function get_profiles(): array {
		if ( ! class_exists( '\MailDoctor\Profiles' ) ) {
			return array();
		}

		return \MailDoctor\Profiles::all();
	}

	/**
	 * Retrieve mapped profile assignments for Woo email IDs.
	 *
	 * @return array<string, string>
	 */
	public static function get_profile_map(): array {
		$assignments = self::get_profile_assignments();

		return $assignments['emails'];
	}

	/**
	 * Retrieve structured profile assignments.
	 *
	 * @return array{
	 *     emails: array<string, string>,
	 *     roles: array<string, string>,
	 *     sites: array<string, string>
	 * }
	 */
	public static function get_profile_assignments(): array {
		$settings    = self::all();
		$assignments = isset( $settings['smtp_profile_assignments'] ) && is_array( $settings['smtp_profile_assignments'] ) ? $settings['smtp_profile_assignments'] : array();

		$emails = isset( $assignments['emails'] ) && is_array( $assignments['emails'] ) ? $assignments['emails'] : array();
		$roles  = isset( $assignments['roles'] ) && is_array( $assignments['roles'] ) ? $assignments['roles'] : array();
		$sites  = isset( $assignments['sites'] ) && is_array( $assignments['sites'] ) ? $assignments['sites'] : array();

		return array(
			'emails' => $emails,
			'roles'  => $roles,
			'sites'  => array_map( 'strval', $sites ),
		);
	}

	/**
	 * Determine if privacy mode is enabled.
	 *
	 * @return bool
	 */
	public static function privacy_mode_enabled(): bool {
		return (bool) self::get( 'privacy_mode', false );
	}

	/**
	 * Retrieve async delivery mode.
	 *
	 * @return string
	 */
	public static function async_mode(): string {
		$mode    = self::get( 'async_mode', 'sync' );
		$allowed = array_keys( self::async_mode_choices() );

		if ( ! in_array( $mode, $allowed, true ) ) {
			return 'sync';
		}

		if ( 'action_scheduler' === $mode && ! self::action_scheduler_available() ) {
			return 'shutdown';
		}

		return $mode;
	}

	/**
	 * Expose async mode labels.
	 *
	 * @return array<string, string>
	 */
	public static function async_mode_choices(): array {
		return array(
			'sync'             => __( 'Immediate (synchronous)', 'mail-doctor' ),
			'shutdown'         => __( 'Deferred until shutdown', 'mail-doctor' ),
			'action_scheduler' => __( 'Background queue (Action Scheduler)', 'mail-doctor' ),
		);
	}

	/**
	 * Provide attachment policy configuration.
	 *
	 * @return array<string, mixed>
	 */
	public static function attachment_policy(): array {
		$max_mb = (int) self::get( 'attachment_total_max_mb', 10 );

		if ( $max_mb <= 0 ) {
			$max_mb = 1;
		}

		$mimes = self::get( 'attachment_block_mimes', array() );

		if ( ! is_array( $mimes ) ) {
			$mimes = self::sanitize_mime_list( $mimes );
		}

		return array(
			'max_total_bytes' => $max_mb * MB_IN_BYTES,
			'max_total_mb'    => $max_mb,
			'blocked_mimes'   => $mimes,
		);
	}

	/**
	 * Determine if fallback transport is enabled.
	 *
	 * @return bool
	 */
	public static function fallback_transport_enabled(): bool {
		return (bool) self::get( 'fallback_transport', false );
	}

	/**
	 * Check if Action Scheduler functions are available.
	 *
	 * @return bool
	 */
	public static function action_scheduler_available(): bool {
		return function_exists( 'as_enqueue_async_action' ) || class_exists( 'ActionScheduler' );
	}

	/**
	 * Determine if recipient hashing is active.
	 *
	 * @return bool
	 */
	public static function hash_recipients_enabled(): bool {
		return (bool) self::get( 'hash_recipients', false );
	}

	/**
	 * Produce hash for recipient email using privacy salt.
	 *
	 * @param string $email Recipient email address.
	 * @return string
	 */
	public static function hash_email( string $email ): string {
		$email = strtolower( trim( $email ) );

		if ( '' === $email ) {
			return '';
		}

		$hash = hash_hmac( 'sha256', $email, self::privacy_salt() );

		return $hash ?: $email;
	}

	/**
	 * Retrieve configured retention windows.
	 *
	 * @return array<string, int>
	 */
	public static function retention_periods(): array {
		return array(
			'sent'     => max( 1, (int) self::get( 'retention_sent_days', 90 ) ),
			'failed'   => max( 1, (int) self::get( 'retention_failed_days', 30 ) ),
			'payload'  => max( 1, (int) self::get( 'retention_payload_days', 14 ) ),
			'fallback' => max( 1, (int) self::get( 'log_retention', 30 ) ),
		);
	}

	/**
	 * Resolve the assigned profile slug for a Woo email identifier.
	 *
	 * @param string|null $email_id Woo email identifier.
	 * @return string|null
	 */
	public static function resolve_profile_for_email( ?string $email_id, array $context = array() ): string {
		$assignments = self::get_profile_assignments();

		if ( $email_id ) {
			$email_key = sanitize_key( $email_id );
			if ( isset( $assignments['emails'][ $email_key ] ) ) {
				return $assignments['emails'][ $email_key ];
			}
		}

		$roles = isset( $context['roles'] ) && is_array( $context['roles'] ) ? $context['roles'] : self::current_user_roles();

		foreach ( $roles as $role ) {
			$role_key = sanitize_key( $role );
			if ( isset( $assignments['roles'][ $role_key ] ) ) {
				return $assignments['roles'][ $role_key ];
			}
		}

		$site_id = isset( $context['site_id'] ) ? (int) $context['site_id'] : ( is_multisite() ? (int) get_current_blog_id() : 0 );
		if ( $site_id > 0 ) {
			$site_key = (string) $site_id;
			if ( isset( $assignments['sites'][ $site_key ] ) ) {
				return $assignments['sites'][ $site_key ];
			}
		}

		$active = self::get_active_profile();
		if ( $active && 'default' !== $active ) {
			return $active;
		}

		if ( class_exists( '\MailDoctor\Profiles' ) ) {
			$match = \MailDoctor\Profiles::resolve_for_context(
				array(
					'email_id' => $email_id ? sanitize_key( $email_id ) : null,
					'roles'    => $roles,
					'site_id'  => $site_id,
				)
			);

			if ( $match && ! empty( $match['id'] ) ) {
				return (string) $match['id'];
			}
		}

		return 'default';
	}

	/**
	 * Fetch SMTP credentials for a profile or the default configuration.
	 *
	 * @param string|null $profile_slug Profile slug.
	 * @return array{
	 *     label: string,
	 *     host: string,
	 *     port: int,
	 *     encryption: string,
	 *     username: string,
	 *     password: string,
	 *     slug: string|null
	 * }
	 */
	public static function smtp_credentials( ?string $profile_slug = null ): array {
		$settings = self::all();

		$base = array(
			'label'      => __( 'Default SMTP Settings', 'mail-doctor' ),
			'host'       => $settings['smtp_host'] ?? '',
			'port'       => (int) ( $settings['smtp_port'] ?? 587 ),
			'encryption' => $settings['smtp_encryption'] ?? 'tls',
			'username'   => $settings['smtp_username'] ?? '',
			'password'   => $settings['smtp_password'] ?? '',
			'slug'       => 'default',
			'id'         => null,
			'auth'       => ( $settings['smtp_username'] ?? '' ) ? 'password' : 'none',
			'secret'     => array(
				'type'     => 'password',
				'password' => $settings['smtp_password'] ?? '',
			),
			'from_email' => $settings['from_email'] ?? '',
			'from_name'  => $settings['from_name'] ?? '',
		);

		$key = $profile_slug;

		if ( null === $key || '' === $key ) {
			$key = self::get_active_profile();
		}

		if ( ! $key || 'default' === $key ) {
			return $base;
		}

		if ( ! class_exists( '\MailDoctor\Profiles' ) ) {
			return $base;
		}

		$profile = \MailDoctor\Profiles::credentials( (int) $key );

		if ( empty( $profile ) ) {
			return $base;
		}

		$profile_password = '';
		if ( isset( $profile['secret']['password'] ) && '' !== $profile['secret']['password'] ) {
			$profile_password = $profile['secret']['password'];
		} elseif ( isset( $profile['password'] ) && '' !== $profile['password'] ) {
			$profile_password = $profile['password'];
		}

		return array(
			'label'      => $profile['name'],
			'host'       => $profile['host'] ?? '',
			'port'       => isset( $profile['port'] ) ? (int) $profile['port'] : $base['port'],
			'encryption' => $profile['secure'] ?? $base['encryption'],
			'username'   => $profile['username'] ?? '',
			'password'   => $profile_password,
			'slug'       => (string) $profile['id'],
			'id'         => (int) $profile['id'],
			'auth'       => $profile['auth'] ?? 'password',
			'secret'     => $profile['secret'] ?? array(),
			'from_email' => $profile['from_email'] ?? '',
			'from_name'  => $profile['from_name'] ?? '',
		);
	}

	/**
	 * Provide options suitable for select dropdowns (default + profiles).
	 *
	 * @return array<string, string>
	 */
	public static function profile_choices(): array {
		if ( class_exists( '\MailDoctor\Profiles' ) ) {
			return \MailDoctor\Profiles::choices();
		}

		return array(
			'default' => __( 'Default SMTP Settings', 'mail-doctor' ),
		);
	}

	/**
	 * Provide human readable label for a transport key.
	 *
	 * @param string|null $transport Transport identifier.
	 * @return string
	 */
	public static function transport_label( ?string $transport = null ): string {
		$key = $transport ? sanitize_key( (string) $transport ) : (string) self::get( 'transport', 'smtp' );

		if ( '' === $key ) {
			$key = 'smtp';
		}

		$map = array(
			'smtp'          => __( 'SMTP', 'mail-doctor' ),
			'mailgun_api'   => __( 'Mailgun REST API', 'mail-doctor' ),
			'sendgrid_api'  => __( 'SendGrid Web API', 'mail-doctor' ),
			'postmark_api'  => __( 'Postmark API', 'mail-doctor' ),
			'sparkpost_api' => __( 'SparkPost API', 'mail-doctor' ),
			'ses_api'       => __( 'Amazon SES API', 'mail-doctor' ),
			'generic_http'  => __( 'Generic HTTP Endpoint', 'mail-doctor' ),
		);

		if ( isset( $map[ $key ] ) ) {
			return $map[ $key ];
		}

		$readable = ucwords( str_replace( array( '_', '-' ), ' ', $key ) );

		return $readable ?: $key;
	}

	/**
	 * Retrieve active profile selection.
	 *
	 * @return string
	 */
	public static function get_active_profile(): string {
		$settings = self::all();

		return self::sanitize_profile_reference( $settings['active_profile'] ?? 'default' );
	}

	/**
	 * Sanitize profile reference ensuring the target exists.
	 *
	 * @param mixed $value Raw profile reference.
	 * @return string
	 */
	protected static function sanitize_profile_reference( $value ): string {
		$reference = is_numeric( $value ) ? (string) (int) $value : sanitize_text_field( (string) $value );

		if ( '' === $reference || 'default' === $reference ) {
			return 'default';
		}

		$choices = self::profile_choices();

		return isset( $choices[ $reference ] ) ? $reference : 'default';
	}

	/**
	 * Sanitize nested assignment arrays.
	 *
	 * @param array<string, mixed> $input Raw assignment data.
	 * @return array{
	 *     emails: array<string, string>,
	 *     roles: array<string, string>,
	 *     sites: array<string, string>
	 * }
	 */
	protected static function sanitize_profile_assignments( array $input ): array {
		$result = array(
			'emails' => array(),
			'roles'  => array(),
			'sites'  => array(),
		);

		if ( isset( $input['emails'] ) && is_array( $input['emails'] ) ) {
			foreach ( $input['emails'] as $email_id => $profile_value ) {
				$email_id = sanitize_key( $email_id );
				$profile  = self::sanitize_profile_reference( $profile_value );

				if ( '' === $email_id || 'default' === $profile ) {
					continue;
				}

				$result['emails'][ $email_id ] = $profile;
			}
		}

		if ( isset( $input['roles'] ) && is_array( $input['roles'] ) ) {
			$available_roles = self::available_roles();

			foreach ( $input['roles'] as $role => $profile_value ) {
				$role_key = sanitize_key( $role );
				if ( '' === $role_key || ! isset( $available_roles[ $role_key ] ) ) {
					continue;
				}

				$profile = self::sanitize_profile_reference( $profile_value );
				if ( 'default' === $profile ) {
					continue;
				}

				$result['roles'][ $role_key ] = $profile;
			}
		}

		if ( isset( $input['sites'] ) && is_array( $input['sites'] ) ) {
			foreach ( $input['sites'] as $site_id => $profile_value ) {
				$site_key = (int) $site_id;
				if ( $site_key <= 0 ) {
					continue;
				}

				$profile = self::sanitize_profile_reference( $profile_value );
				if ( 'default' === $profile ) {
					continue;
				}

				$result['sites'][ (string) $site_key ] = $profile;
			}
		}

		return $result;
	}

	/**
	 * Generate webhook token if missing.
	 *
	 * @return string
	 */
	protected static function maybe_generate_webhook_token(): string {
		try {
			return bin2hex( \random_bytes( 16 ) );
		} catch ( \Exception $exception ) {
			return strtolower( \wp_generate_password( 24, false, false ) );
		}
	}

	/**
	 * Enumerate available WordPress roles.
	 *
	 * @return array<string, string>
	 */
	protected static function available_roles(): array {
		if ( ! function_exists( 'wp_roles' ) ) {
			return array();
		}

		return wp_roles()->role_names;
	}

	/**
	 * Determine roles for the current user session.
	 *
	 * @return array<int, string>
	 */
	public static function current_user_roles(): array {
		if ( ! function_exists( 'wp_get_current_user' ) ) {
			return array();
		}

		$user = wp_get_current_user();

		if ( ! $user || empty( $user->roles ) || ! is_array( $user->roles ) ) {
			return array();
		}

		return array_map( 'sanitize_key', $user->roles );
	}

	/**
	 * Provide consistent privacy salt for hashing operations.
	 *
	 * @return string
	 */
	protected static function privacy_salt(): string {
		$components = array();

		if ( defined( 'AUTH_SALT' ) && AUTH_SALT ) {
			$components[] = AUTH_SALT;
		}

		if ( defined( 'LOGGED_IN_SALT' ) && LOGGED_IN_SALT ) {
			$components[] = LOGGED_IN_SALT;
		}

		if ( empty( $components ) ) {
			$components[] = wp_salt( 'auth' );
		}

		return hash( 'sha256', implode( '|', $components ) . '|mail_doctor_privacy', false );
	}

	/**
	 * Apply network defaults and locks.
	 *
	 * @param array<string, mixed> $settings Site settings.
	 * @return array<string, mixed>
	 */
	protected static function apply_network_defaults( array $settings ): array {
		$network  = self::network_config();
		$defaults = $network['defaults'];
		$locks    = $network['locks'];

		foreach ( $defaults as $key => $value ) {
			if ( isset( $locks[ $key ] ) && $locks[ $key ] ) {
				$settings[ $key ] = $value;
				continue;
			}

			if ( ! isset( $settings[ $key ] ) || '' === $settings[ $key ] ) {
				$settings[ $key ] = $value;
			}
		}

		return $settings;
	}

	/**
	 * Determine whether debug logging is enabled.
	 *
	 * @return bool
	 */
	public static function debug_logging_enabled(): bool {
		if ( defined( 'MAILDOCTOR_DEBUG_LOG' ) ) {
			return (bool) MAILDOCTOR_DEBUG_LOG;
		}

		$settings = self::all();
		$enabled  = ! empty( $settings['debug_logging'] );

		/**
		 * Filter whether Mail Doctor debug logging is enabled.
		 *
		 * @since 2.1.0
		 *
		 * @param bool $enabled Current enabled state.
		 */
		return (bool) apply_filters( 'mail_doctor_debug_logging_enabled', $enabled );
	}

	/**
	 * Retrieve network defaults configuration.
	 *
	 * @return array{
	 *     defaults: array<string, mixed>,
	 *     locks: array<string, bool>
	 * }
	 */
	public static function network_config(): array {
		if ( ! is_multisite() ) {
			return array(
				'defaults' => array(),
				'locks'    => array(),
			);
		}

		$option = get_site_option( self::NETWORK_OPTION, array() );

		if ( ! is_array( $option ) ) {
			$option = array();
		}

		$option = wp_parse_args(
			$option,
			array(
				'defaults' => array(),
				'locks'    => array(),
			)
		);

		return $option;
	}

	/**
	 * Persist network defaults and locks.
	 *
	 * @param array<string, mixed> $defaults Default values.
	 * @param array<string, bool>  $locks Locked values.
	 * @return void
	 */
	public static function set_network_defaults( array $defaults, array $locks ): void {
		if ( ! is_multisite() ) {
			return;
		}

		$allowed = array(
			'smtp_host',
			'smtp_port',
			'smtp_encryption',
			'smtp_username',
			'smtp_password',
			'from_email',
			'from_name',
			'override_scope',
			'max_attempts',
			'backoff_schedule',
			'log_retention',
			'cleanup_on_uninstall',
			'compatibility_mode',
			'list_unsubscribe',
			'list_unsubscribe_mailto',
			'list_unsubscribe_url',
			'alerts_enabled',
			'webhook_enabled',
			'alert_threshold_percent',
			'alert_window_minutes',
			'alert_burst_failures',
			'alert_burst_minutes',
			'alert_email',
			'alert_slack_webhook',
			'alert_generic_webhook',
			'alert_cooldown_minutes',
			'structured_logging_enabled',
			'structured_logging_endpoint',
			'structured_logging_token',
		);

		$defaults = array_intersect_key( $defaults, array_flip( $allowed ) );
		$locks    = array_intersect_key( $locks, array_flip( $allowed ) );

		$config = self::network_config();

		foreach ( $defaults as $key => $value ) {
			if ( 'backoff_schedule' === $key && ! is_array( $value ) ) {
				unset( $defaults[ $key ] );
			}
		}

		$config['defaults'] = $defaults;
		$config['locks']    = array_map(
			static function ( $locked ) {
				return (bool) $locked;
			},
			$locks
		);

		update_site_option( self::NETWORK_OPTION, $config );
	}
}
