<?php
/**
 * Email Delivery Module
 *
 * Provider-based WordPress email delivery (SMTP, API, and OAuth providers).
 *
 * @package EmailSmtp
 * @since 1.0.0
 */

namespace EmailSmtp;

use EmailSmtp\Includes\AdminUi;
use EmailSmtp\Includes\ConnectionStore;
use EmailSmtp\Includes\CredentialManager;
use EmailSmtp\Includes\DeliveryAttemptContext;
use EmailSmtp\Includes\DiagnosticsService;
use EmailSmtp\Includes\DomainHealth;
use EmailSmtp\Includes\EmailRouter;
use EmailSmtp\Includes\DeliveryResult;
use EmailSmtp\Includes\ErrorNormalizer;
use EmailSmtp\Includes\FailureNotifier;
use EmailSmtp\Includes\MailInterceptor;
use EmailSmtp\Includes\OAuth\AbstractOAuthMailProvider;
use EmailSmtp\Includes\OAuth\OAuthCallbackHandler;
use EmailSmtp\Includes\OAuth\OAuthState;
use EmailSmtp\Includes\ProviderRegistry;
use EmailSmtp\Includes\Webhooks\WebhookRouter;
use EmailSmtp\Includes\SettingsSchema;
use EmailSmtp\Includes\SettingsImporter;
use EmailSmtp\Includes\SmtpRelayProfiles;
use EmailSmtp\Includes\SmtpSettingsHelper;
use WP_PowerSuite\Core\AbstractModule;
use WP_PowerSuite\Core\NetworkScope;
use WP_PowerSuite\Core\OptionStoreModuleSettingsRepository;
use WP_PowerSuite\Email\EmailComponents;
use WP_PowerSuite\Email\EmailRenderer;

require_once __DIR__ . '/includes/bootstrap.php';

class Module extends AbstractModule {

	private const MODULE_ID      = 'email-smtp';
	private const MODULE_VERSION = '1.10.42';
	private const ENCRYPT_KEY    = 'wp_powersuite_smtp_key_v1';
	private const PASSWORD_MASK  = '••••••••••••';
	public const PAGE_SLUG       = 'powersuite-email-delivery';

	private ?array $settings_cache = null;

	private string $last_mail_error = '';

	/** @var array<string, mixed>|null */
	private static ?array $test_settings_overlay = null;

	/**
	 * White-label-aware product name for outbound copy and logs.
	 */
	private static function brandPluginName(): string {
		return function_exists( 'wp_powersuite_get_plugin_name' )
			? wp_powersuite_get_plugin_name()
			: __( 'WP PowerSuite', 'wp-powersuite' );
	}

	/**
	 * Lazy-load admin-only helpers (not needed on the mail transport path).
	 */
	public static function load_admin_includes(): void {
		static $loaded = false;
		if ( $loaded ) {
			return;
		}
		$loaded = true;
		$dir    = __DIR__ . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR;
		foreach ( [ 'AdminUi.php', 'DiagnosticsService.php', 'SettingsImporter.php', 'DomainHealth.php' ] as $file ) {
			$path = $dir . $file;
			if ( is_readable( $path ) ) {
				require_once $path;
			}
		}
	}

	public function getId(): string {
		return self::MODULE_ID;
	}

	public function getName(): string {
		return __( 'Email Delivery', 'wp-powersuite' );
	}

	public function getVersion(): string {
		return self::MODULE_VERSION;
	}

	public function getDescription(): string {
		return __( 'Route WordPress email through SMTP or transactional providers for better deliverability.', 'wp-powersuite' );
	}

	public function getContext(): string|array {
		return [ 'admin', 'frontend', 'ajax', 'cron', 'rest', 'cli' ];
	}

	public function getDependencies(): array {
		return [];
	}

	protected function registerHooks(): void {
		self::registerPhpMailerHook();
		OAuthCallbackHandler::register();
		WebhookRouter::register();
		add_action( 'wp_ajax_wpps_email_delivery_test_connection', [ $this, 'ajaxTestConnection' ] );
		add_action( 'wp_ajax_wpps_email_delivery_test_email', [ $this, 'ajaxTestEmail' ] );
		add_action( 'wp_ajax_wpps_email_delivery_connection_form', [ $this, 'ajaxConnectionForm' ] );
		add_action( 'wp_ajax_wpps_email_delivery_domain_health', [ $this, 'ajaxDomainHealth' ] );
		add_action( 'wp_ajax_wpps_email_delivery_overview_stats', [ $this, 'ajaxOverviewStats' ] );
		add_action( 'wp_ajax_wpps_email_delivery_tab', [ $this, 'ajaxTab' ] );
		add_action( 'wp_mail_failed', [ $this, 'captureMailError' ] );
		add_action( 'wp_mail_succeeded', [ $this, 'captureMailSuccess' ] );

		if ( is_admin() ) {
			add_action( $this->adminMenuHook(), [ $this, 'addAdminMenu' ], 20 );
			add_action( 'admin_enqueue_scripts', [ $this, 'enqueueAdminAssets' ] );
			add_action( 'admin_notices', [ $this, 'maybeShowFailureNotice' ] );
			add_action( 'network_admin_notices', [ $this, 'maybeShowFailureNotice' ] );
			add_action( 'admin_post_wpps_email_delivery_save', [ $this, 'handleSaveSettings' ] );
			add_action( 'admin_post_wpps_email_delivery_save_connection', [ $this, 'handleSaveConnection' ] );
			add_action( 'admin_post_wpps_email_delivery_delete_connection', [ $this, 'handleDeleteConnection' ] );
			add_action( 'admin_post_wpps_email_delivery_set_connection_role', [ $this, 'handleSetConnectionRole' ] );
			add_action( 'admin_post_wpps_email_delivery_oauth_connect', [ $this, 'handleOAuthConnect' ] );
			add_action( 'admin_post_wpps_email_delivery_oauth_disconnect', [ $this, 'handleOAuthDisconnect' ] );
			add_action( 'admin_post_wpps_email_delivery_import_settings', [ $this, 'handleImportSettings' ] );
			add_action( 'admin_post_wpps_email_delivery_dismiss_import', [ $this, 'handleDismissImport' ] );
			add_action( 'admin_post_wpps_email_delivery_dismiss_failure_notice', [ $this, 'handleDismissFailureNotice' ] );
		}
	}

	/**
	 * Surface a persistent notice when recent delivery failures were recorded.
	 */
	public function maybeShowFailureNotice(): void {
		if ( ! NetworkScope::canManagePlugin() ) {
			return;
		}
		if ( ! empty( $_GET['wpps_ed_dismiss_fail'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			return;
		}
		if ( get_transient( 'wpps_ed_fail_notice_dismissed' ) ) {
			return;
		}

		// Runtime success/failure is stored in a side-channel option, not module settings.
		$runtime    = get_option( 'wp_powersuite_email_delivery_meta', [] );
		$runtime    = is_array( $runtime ) ? $runtime : [];
		$settings   = $this->getSettings();
		$failed_at  = (string) ( $runtime['last_failure_at'] ?? $settings['last_failure_at'] ?? '' );
		$success_at = (string) ( $runtime['last_success_at'] ?? $settings['last_success_at'] ?? '' );
		if ( $failed_at === '' ) {
			return;
		}
		$failed_ts  = strtotime( $failed_at );
		$success_ts = $success_at !== '' ? strtotime( $success_at ) : 0;
		if ( ! $failed_ts || ( $success_ts && $success_ts >= $failed_ts ) ) {
			return;
		}
		// Only show for failures in the last 48 hours.
		if ( $failed_ts < ( time() - 2 * DAY_IN_SECONDS ) ) {
			return;
		}

		$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
		if ( $screen && isset( $screen->id ) && str_contains( (string) $screen->id, 'powersuite-email-delivery' ) ) {
			return;
		}

		$logs_url = NetworkScope::adminUrl( 'admin.php', [ 'page' => 'powersuite-email-logs', 'status' => 'failed' ] );
		$fix_url = NetworkScope::adminUrl( 'admin.php', [ 'page' => self::PAGE_SLUG, 'tab' => 'settings' ] );
		$dismiss  = wp_nonce_url(
			admin_url( 'admin-post.php?action=wpps_email_delivery_dismiss_failure_notice' ),
			'wpps_email_delivery_dismiss_failure_notice'
		);
		$message  = ErrorNormalizer::redact( (string) ( $runtime['last_failure_message'] ?? $settings['last_failure_message'] ?? '' ) );
		?>
		<div class="notice notice-error">
			<p>
				<strong><?php esc_html_e( 'Email delivery is failing.', 'wp-powersuite' ); ?></strong>
				<?php if ( $message !== '' ) : ?>
					<?php echo esc_html( $message ); ?>
				<?php endif; ?>
				<a href="<?php echo esc_url( $fix_url ); ?>"><?php esc_html_e( 'Fix settings', 'wp-powersuite' ); ?></a>
				|
				<a href="<?php echo esc_url( $logs_url ); ?>"><?php esc_html_e( 'View failed logs', 'wp-powersuite' ); ?></a>
				|
				<a href="<?php echo esc_url( $dismiss ); ?>"><?php esc_html_e( 'Dismiss', 'wp-powersuite' ); ?></a>
			</p>
		</div>
		<?php
	}

	public function handleDismissFailureNotice(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_dismiss_failure_notice' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}
		set_transient( 'wpps_ed_fail_notice_dismissed', 1, DAY_IN_SECONDS );
		wp_safe_redirect( wp_get_referer() ? wp_get_referer() : NetworkScope::adminUrl( 'admin.php', [ 'page' => self::PAGE_SLUG ] ) );
		exit;
	}

	/**
	 * Register mail interception once (also called from early-bootstrap.php).
	 */
	public static function registerPhpMailerHook(): void {
		MailInterceptor::register();
	}

	/**
	 * Static PHPMailer configurator — BC entry point used by early bootstrap / helpers.
	 *
	 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer PHPMailer instance.
	 */
	public static function configurePhpMailerStatic( $phpmailer ): void {
		MailInterceptor::configure_phpmailer( $phpmailer );
	}

	/**
	 * Apply SMTP settings to a PHPMailer instance.
	 *
	 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
	 * @param array<string, mixed>           $settings
	 */
	public static function configurePhpMailerFromSettings( $phpmailer, array $settings ): void {
		$phpmailer->isSMTP();
		$phpmailer->Host = SmtpSettingsHelper::sanitizeHost( (string) ( $settings['smtp_host'] ?? '' ) );

		$port = (int) ( $settings['smtp_port'] ?? 587 );
		if ( $port < 1 || $port > 65535 ) {
			$port = 587;
		}
		$phpmailer->Port = $port;

		$smtp_auth           = ( $settings['smtp_auth'] ?? '1' ) === '1' || ( $settings['smtp_auth'] ?? '' ) === true;
		$phpmailer->SMTPAuth = $smtp_auth;

		if ( $smtp_auth ) {
			$phpmailer->Username = sanitize_text_field( (string) ( $settings['smtp_username'] ?? '' ) );
			$password            = (string) ( $settings['smtp_password'] ?? '' );
			if ( CredentialManager::is_from_constant( 'WP_POWERSUITE_SMTP_PASSWORD' ) ) {
				$password = (string) constant( 'WP_POWERSUITE_SMTP_PASSWORD' );
			} elseif ( CredentialManager::is_encrypted( $password ) ) {
				$password = self::decryptPasswordStatic( $password );
			}
			$phpmailer->Password = $password;
		}

		$encryption            = (string) ( $settings['smtp_encryption'] ?? 'tls' );
		$phpmailer->SMTPSecure = in_array( $encryption, [ 'tls', 'ssl', '' ], true ) ? $encryption : 'tls';

		$autotls = ! isset( $settings['smtp_autotls'] )
			? ( $encryption === 'tls' || ( $encryption === '' && $port === 587 ) )
			: ( (string) $settings['smtp_autotls'] === '1' );
		$phpmailer->SMTPAutoTLS = $autotls;

		$timeout           = max( 5, min( 120, (int) ( $settings['smtp_timeout'] ?? 30 ) ) );
		$phpmailer->Timeout = $timeout;

		if ( ! empty( $settings['smtp_allow_self_signed'] ) && (string) $settings['smtp_allow_self_signed'] !== '0' ) {
			$phpmailer->SMTPOptions = [
				'ssl' => [
					'verify_peer'       => false,
					'verify_peer_name'  => false,
					'allow_self_signed' => true,
				],
			];
		}

		self::maybeApplyFromIdentity( $phpmailer, $settings );
		self::maybeApplyReplyTo( $phpmailer, $settings );

		$set_return_path = ! isset( $settings['set_return_path'] )
			|| ( ! empty( $settings['set_return_path'] ) && (string) $settings['set_return_path'] !== '0' );
		if ( $set_return_path ) {
			$return_path = (string) ( $settings['return_path'] ?? '' );
			if ( is_email( $return_path ) ) {
				$phpmailer->Sender = $return_path;
			} elseif ( is_email( (string) ( $settings['from_email'] ?? '' ) ) && empty( $phpmailer->Sender ) ) {
				$phpmailer->Sender = (string) $settings['from_email'];
			}
		}

		self::maybeEnableDebug( $phpmailer, $settings );
	}

	/**
	 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
	 * @param array<string, mixed>           $settings
	 */
	private static function maybeApplyFromIdentity( $phpmailer, array $settings ): void {
		$force_email = ! empty( $settings['force_from_email'] ) && (string) $settings['force_from_email'] !== '0';
		$force_name  = ! empty( $settings['force_from_name'] ) && (string) $settings['force_from_name'] !== '0';
		$override    = ! empty( $settings['override_from'] ) && (string) $settings['override_from'] !== '0';
		if ( $override ) {
			$force_email = true;
			$force_name  = true;
		}
		if ( ! $force_email && ! $force_name ) {
			return;
		}

		$from_email = sanitize_email( (string) ( $settings['from_email'] ?? '' ) );
		if ( ! is_email( $from_email ) ) {
			$from_email = sanitize_email( (string) get_option( 'admin_email' ) );
		}
		if ( ! is_email( $from_email ) ) {
			return;
		}

		$default_from   = apply_filters( 'wp_mail_from', get_option( 'admin_email' ) );
		$current_from   = (string) $phpmailer->From;
		$already_custom = $current_from !== ''
			&& $current_from !== $default_from
			&& $current_from !== $from_email;

		if ( $already_custom && ! apply_filters( 'wp_powersuite_smtp_force_from_override', false, $phpmailer, $settings ) ) {
			if ( $force_name ) {
				$phpmailer->FromName = SmtpSettingsHelper::sanitizeFromName( (string) ( $settings['from_name'] ?? '' ) );
			}
			return;
		}

		if ( $force_email ) {
			$phpmailer->From = $from_email;
		}
		if ( $force_name || $force_email ) {
			$phpmailer->FromName = SmtpSettingsHelper::sanitizeFromName( (string) ( $settings['from_name'] ?? '' ) );
		}
	}

	/**
	 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
	 * @param array<string, mixed>           $settings
	 */
	private static function maybeApplyReplyTo( $phpmailer, array $settings ): void {
		$reply_email = sanitize_email( (string) ( $settings['reply_to_email'] ?? '' ) );
		if ( ! is_email( $reply_email ) ) {
			return;
		}
		$reply_name = SmtpSettingsHelper::sanitizeFromName( (string) ( $settings['reply_to_name'] ?? '' ) );
		$phpmailer->addReplyTo( $reply_email, $reply_name );
	}

	/**
	 * @param \PHPMailer\PHPMailer\PHPMailer $phpmailer
	 * @param array<string, mixed>           $settings
	 */
	private static function maybeEnableDebug( $phpmailer, array $settings ): void {
		$level = (int) ( $settings['smtp_debug'] ?? 0 );
		if ( $level < 1 || $level > 2 ) {
			return;
		}
		if ( ! ( is_admin() || ( defined( 'WP_DEBUG' ) && WP_DEBUG ) ) ) {
			return;
		}
		if ( ! NetworkScope::canManagePlugin() ) {
			return;
		}

		$phpmailer->SMTPDebug  = $level;
		$phpmailer->Debugoutput = static function ( string $str, int $level ): void {
			unset( $level );
			$str = ErrorNormalizer::redact( $str );
			error_log( self::brandPluginName() . ' Email Delivery debug: ' . $str ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
		};
	}

	public static function decryptPasswordStatic( string $value ): string {
		if ( $value === '' ) {
			return $value;
		}
		if ( str_starts_with( $value, 'ENC2:' ) ) {
			return self::decryptV2Static( substr( $value, 5 ) );
		}
		if ( str_starts_with( $value, 'ENC:' ) ) {
			return self::decryptV1Static( substr( $value, 4 ) );
		}
		return '';
	}

	private static function decryptV2Static( string $encoded ): string {
		$auth_key = defined( 'AUTH_KEY' ) && AUTH_KEY ? AUTH_KEY : ABSPATH;
		$key      = hash( 'sha256', self::ENCRYPT_KEY . $auth_key, true );
		$data     = base64_decode( $encoded, true );
		if ( $data === false || strlen( $data ) < 17 ) {
			return '';
		}
		$iv        = substr( $data, 0, 16 );
		$encrypted = substr( $data, 16 );
		$decrypted = openssl_decrypt( $encrypted, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
		return $decrypted !== false ? $decrypted : '';
	}

	private static function decryptV1Static( string $encoded ): string {
		$auth_key    = defined( 'AUTH_KEY' ) && AUTH_KEY ? AUTH_KEY : ABSPATH;
		$secure_auth = defined( 'SECURE_AUTH_KEY' ) && SECURE_AUTH_KEY ? SECURE_AUTH_KEY : ABSPATH;
		$key         = substr( hash( 'sha256', self::ENCRYPT_KEY . $auth_key ), 0, 32 );
		$iv          = substr( hash( 'sha256', self::ENCRYPT_KEY . $secure_auth ), 0, 16 );
		$decrypted   = openssl_decrypt( base64_decode( $encoded ), 'AES-128-CBC', $key, OPENSSL_RAW_DATA, $iv );
		return $decrypted !== false ? $decrypted : '';
	}

	public function captureMailError( $wp_error ): void {
		if ( is_wp_error( $wp_error ) ) {
			$this->last_mail_error = $wp_error->get_error_message();
			$this->persistDeliveryMeta( false, ErrorNormalizer::redact( $this->last_mail_error ) );
			if ( ! apply_filters( 'wp_powersuite_email_delivery_skip_failure_alert', false ) ) {
				$settings = $this->getSettings();
				$provider = ProviderRegistry::get( (string) ( $settings['provider'] ?? 'wordpress' ) );
				if ( $provider ) {
					FailureNotifier::maybe_notify(
						DeliveryResult::failure( ErrorNormalizer::redact( $this->last_mail_error ), '', 'unknown' ),
						$provider,
						$settings
					);
				}
			}
		}
	}

	public function captureMailSuccess( $mail_data = null ): void {
		unset( $mail_data );
		// Simulation must not mark the connection as "tested / ready".
		if (
			class_exists( DeliveryAttemptContext::class )
			&& sanitize_key( (string) DeliveryAttemptContext::get( 'transport', '' ) ) === 'simulation'
		) {
			return;
		}
		$this->persistDeliveryMeta( true, '' );
	}

	private function persistDeliveryMeta( bool $success, string $message ): void {
		// Lightweight side-channel so every wp_mail() does not rewrite the full settings blob.
		$meta = [
			'last_success_at'      => '',
			'last_failure_at'      => '',
			'last_failure_message' => '',
			'connection_status'    => 'not_configured',
		];
		$stored = get_option( 'wp_powersuite_email_delivery_meta', [] );
		if ( is_array( $stored ) ) {
			$meta = array_merge( $meta, $stored );
		}
		if ( $success ) {
			$meta['last_success_at']   = gmdate( 'c' );
			$meta['connection_status'] = 'connected';
		} else {
			$meta['last_failure_at']      = gmdate( 'c' );
			$meta['last_failure_message'] = $message;
			$meta['connection_status']    = 'failed';
		}
		update_option( 'wp_powersuite_email_delivery_meta', $meta, false );
	}

	public function addAdminMenu(): void {
		$this->registerPowerSuiteSubmenu(
			__( 'Email Delivery', 'wp-powersuite' ),
			__( 'Email Delivery', 'wp-powersuite' ),
			self::PAGE_SLUG,
			[ $this, 'renderAdminPage' ]
		);
	}

	public function enqueueAdminAssets( string $hook ): void {
		if ( strpos( $hook, self::PAGE_SLUG ) === false ) {
			return;
		}
		$base = defined( 'WP_POWERSUITE_PLUGIN_URL' ) ? WP_POWERSUITE_PLUGIN_URL : '';
		wp_enqueue_style( 'dashicons' );
		wp_enqueue_style( 'wp-powersuite-admin' );
		wp_enqueue_style(
			'wpps-email-delivery-admin',
			$base . 'modules/email-smtp/assets/email-delivery-admin.css',
			[ 'wp-powersuite-admin' ],
			self::MODULE_VERSION
		);
		wp_enqueue_script(
			'wpps-email-delivery-admin',
			$base . 'modules/email-smtp/assets/email-delivery-admin.js',
			[ 'jquery' ],
			self::MODULE_VERSION,
			true
		);
		wp_localize_script(
			'wpps-email-delivery-admin',
			'wppsEmailDelivery',
			[
				'ajaxUrl'   => admin_url( 'admin-ajax.php' ),
				'nonce'     => wp_create_nonce( 'wp_powersuite_ajax' ),
				'siteToday' => ( new \DateTimeImmutable( 'today', wp_timezone() ) )->format( 'Y-m-d' ),
				'i18n'      => [
					'testing'     => __( 'Testing…', 'wp-powersuite' ),
					'sending'     => __( 'Sending…', 'wp-powersuite' ),
					'loading'     => __( 'Loading…', 'wp-powersuite' ),
					'refreshing'  => __( 'Refreshing…', 'wp-powersuite' ),
					'refresh'     => __( 'Refresh', 'wp-powersuite' ),
					'copied'      => __( 'Copied', 'wp-powersuite' ),
					'error'       => __( 'Something went wrong. Please try again.', 'wp-powersuite' ),
					'scoreOf'     => __( '%1$d of %2$d checks passed', 'wp-powersuite' ),
					'justNow'     => __( 'just now', 'wp-powersuite' ),
					'emailSent'   => __( 'Emails sent', 'wp-powersuite' ),
					'emailFailed' => __( 'Emails failed', 'wp-powersuite' ),
				],
				'pageUrl'     => NetworkScope::adminUrl( 'admin.php', [ 'page' => self::PAGE_SLUG ] ),
				'settingsUrl' => NetworkScope::adminUrl( 'admin.php', [ 'page' => self::PAGE_SLUG, 'tab' => 'settings' ] ),
				'currentTab'  => isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( (string) $_GET['tab'] ) ) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended
			]
		);
	}

	/**
	 * Soft-load a Settings / Overview / Test tab without a full admin reload.
	 */
	public function ajaxTab(): void {
		check_ajax_referer( 'wp_powersuite_ajax', 'nonce' );
		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ], 403 );
		}

		$requested = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( (string) $_POST['tab'] ) ) : '';
		$ctx       = $this->buildAdminViewContext( $requested );
		$tab       = (string) $ctx['tab'];

		ob_start();
		$this->renderTabPartial( $tab, $ctx );
		$html = (string) ob_get_clean();

		wp_send_json_success(
			[
				'tab'  => $tab,
				'html' => $html,
				'url'  => NetworkScope::adminUrl( 'admin.php', [ 'page' => self::PAGE_SLUG, 'tab' => $tab ] ),
			]
		);
	}

	/**
	 * Overview chart series for a selected date range (Email Logs required).
	 */
	public function ajaxOverviewStats(): void {
		check_ajax_referer( 'wp_powersuite_ajax', 'nonce' );
		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied.', 'wp-powersuite' ) ], 403 );
		}
		if ( ! OptionStoreModuleSettingsRepository::isModuleEnabled( 'email-logs' ) ) {
			wp_send_json_error( [ 'message' => __( 'Enable the Email Logs module to view overview analytics.', 'wp-powersuite' ) ], 400 );
		}
		if ( ! class_exists( '\EmailLogs\Includes\EmailLogService' ) ) {
			wp_send_json_error( [ 'message' => __( 'Email Logs is not available.', 'wp-powersuite' ) ], 400 );
		}

		$start = isset( $_POST['start'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['start'] ) ) : '';
		$end   = isset( $_POST['end'] ) ? sanitize_text_field( wp_unslash( (string) $_POST['end'] ) ) : '';
		if ( $start !== '' && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $start ) ) {
			$start = '';
		}
		if ( $end !== '' && ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $end ) ) {
			$end = '';
		}

		$series = \EmailLogs\Includes\EmailLogService::get_daily_status_series( $start, $end );
		wp_send_json_success( $series );
	}

	/**
	 * Refresh domain health DNS report (bypasses cache when requested).
	 */
	public function ajaxDomainHealth(): void {
		check_ajax_referer( 'wp_powersuite_ajax', 'nonce' );
		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied', 'wp-powersuite' ) ] );
		}

		self::load_admin_includes();
		$force  = ! empty( $_POST['force'] ) && (string) wp_unslash( $_POST['force'] ) !== '0'; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$report = DomainHealth::report_for_settings( $this->getSettings(), $force, true );
		if ( empty( $report['show'] ) ) {
			wp_send_json_success(
				[
					'show' => false,
				]
			);
		}

		$checked_label = '';
		if ( ! empty( $report['checked_at'] ) ) {
			$ts = strtotime( (string) $report['checked_at'] );
			if ( $ts ) {
				$checked_label = sprintf(
					/* translators: %s: human time diff */
					__( 'Checked %s ago', 'wp-powersuite' ),
					human_time_diff( $ts, time() )
				);
			}
		}

		wp_send_json_success(
			[
				'show'          => true,
				'domain'        => (string) ( $report['domain'] ?? '' ),
				'from_email'    => (string) ( $report['from_email'] ?? '' ),
				'score'         => (int) ( $report['score'] ?? 0 ),
				'total'         => (int) ( $report['total'] ?? 0 ),
				'checked_at'    => (string) ( $report['checked_at'] ?? '' ),
				'checked_label' => $checked_label,
				'checks'        => $report['checks'],
			]
		);
	}

	/**
	 * Return connection form HTML for a provider / existing connection (no full page reload).
	 */
	public function ajaxConnectionForm(): void {
		check_ajax_referer( 'wp_powersuite_ajax', 'nonce' );
		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied', 'wp-powersuite' ) ] );
		}

		$settings           = $this->getSettings();
		$connections        = ConnectionStore::all( $settings );
		$provider_id        = isset( $_POST['provider'] ) ? sanitize_key( wp_unslash( (string) $_POST['provider'] ) ) : '';
		$edit_connection_id = isset( $_POST['connection'] ) ? sanitize_key( wp_unslash( (string) $_POST['connection'] ) ) : '';
		$connection         = [];

		if ( $edit_connection_id !== '' && isset( $connections[ $edit_connection_id ] ) ) {
			$connection  = $connections[ $edit_connection_id ];
			$provider_id = sanitize_key( (string) ( $connection['provider'] ?? $provider_id ) );
		}

		if ( ! in_array( $provider_id, ProviderRegistry::ids(), true ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid provider.', 'wp-powersuite' ) ] );
		}

		$page_url = NetworkScope::adminUrl( 'admin.php', [ 'page' => self::PAGE_SLUG, 'tab' => 'settings' ] );

		ob_start();
		include __DIR__ . '/views/partials/connection-form.php';
		$html = (string) ob_get_clean();

		wp_send_json_success(
			[
				'html'       => $html,
				'provider'   => $provider_id,
				'connection' => $edit_connection_id,
			]
		);
	}

	public function renderAdminPage(): void {
		if ( ! $this->canManagePlugin() ) {
			wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'wp-powersuite' ) );
		}

		$raw_tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( (string) $_GET['tab'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$ctx     = $this->buildAdminViewContext( $raw_tab );
		extract( $ctx, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract -- View locals for admin-page.php.

		include __DIR__ . '/views/admin-page.php';
	}

	/**
	 * Shared view data for full page and soft tab loads.
	 *
	 * @return array<string, mixed>
	 */
	private function buildAdminViewContext( string $requested_tab = '' ): array {
		self::load_admin_includes();
		$settings              = $this->getSettings();
		$providers             = ProviderRegistry::cards();
		$active                = ProviderRegistry::get( (string) ( $settings['provider'] ?? 'wordpress' ) );
		$diagnostics           = DiagnosticsService::collect();
		$email_logs_enabled    = OptionStoreModuleSettingsRepository::isModuleEnabled( 'email-logs' );
		$email_logs_url        = NetworkScope::adminUrl( 'admin.php', [ 'page' => 'powersuite-email-logs' ] );
		$email_reports_enabled = OptionStoreModuleSettingsRepository::isModuleEnabled( 'email-reports' );
		$email_reports_url     = NetworkScope::adminUrl(
			'admin.php',
			[
				'page'        => 'powersuite',
				'wpps_module' => 'email-reports',
			]
		);
		$conflicting    = SmtpSettingsHelper::detectConflictingPlugins();
		$import_sources = SettingsImporter::banner_sources();
		$saved          = isset( $_GET['saved'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$health         = AdminUi::health_summary( $active, $settings );
		$checklist      = AdminUi::checklist( $active, $settings, $diagnostics, $email_logs_enabled );
		$connections    = ConnectionStore::all( $settings );
		// Cache-only on page paint — live DNS runs via AJAX refresh / panel open.
		$domain_health  = DomainHealth::report_for_settings( $settings, false, false );
		$tab            = AdminUi::normalize_tab( $requested_tab, $health );
		$edit_connection_id = isset( $_GET['connection'] ) ? sanitize_key( wp_unslash( (string) $_GET['connection'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
		$pick_provider      = isset( $_GET['provider'] ) ? sanitize_key( wp_unslash( (string) $_GET['provider'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended

		// Soft tab loads should not reopen a deep-linked connection form from the original URL.
		if ( defined( 'DOING_AJAX' ) && DOING_AJAX ) {
			$edit_connection_id = isset( $_POST['connection'] ) ? sanitize_key( wp_unslash( (string) $_POST['connection'] ) ) : '';
			$pick_provider      = isset( $_POST['provider'] ) ? sanitize_key( wp_unslash( (string) $_POST['provider'] ) ) : '';
		}

		$overview_series      = null;
		$overview_recent_logs = [];
		if ( $tab === 'overview' && $email_logs_enabled ) {
			if ( ! class_exists( '\EmailLogs\Includes\EmailLogService' ) && defined( 'WP_POWERSUITE_MODULES_DIR' ) ) {
				$logs_service = WP_POWERSUITE_MODULES_DIR . 'email-logs' . DIRECTORY_SEPARATOR . 'includes' . DIRECTORY_SEPARATOR . 'EmailLogService.php';
				if ( is_readable( $logs_service ) ) {
					require_once $logs_service;
				}
			}
			if ( class_exists( '\EmailLogs\Includes\EmailLogService' ) ) {
				$overview_series      = \EmailLogs\Includes\EmailLogService::get_daily_status_series();
				$overview_recent_logs = \EmailLogs\Includes\EmailLogService::get_recent_logs( 6 );
			}
		}

		return compact(
			'settings',
			'providers',
			'active',
			'diagnostics',
			'email_logs_enabled',
			'email_logs_url',
			'email_reports_enabled',
			'email_reports_url',
			'conflicting',
			'import_sources',
			'saved',
			'health',
			'checklist',
			'connections',
			'domain_health',
			'tab',
			'edit_connection_id',
			'pick_provider',
			'overview_series',
			'overview_recent_logs'
		);
	}

	/**
	 * @param array<string, mixed> $ctx
	 */
	private function renderTabPartial( string $tab, array $ctx ): void {
		extract( $ctx, EXTR_SKIP ); // phpcs:ignore WordPress.PHP.DontExtract.extract_extract
		$page_url = NetworkScope::adminUrl( 'admin.php', [ 'page' => self::PAGE_SLUG ] );

		if ( $tab === 'test' ) {
			include __DIR__ . '/views/partials/test-email.php';
			return;
		}
		if ( $tab === 'overview' ) {
			include __DIR__ . '/views/partials/overview.php';
			return;
		}
		include __DIR__ . '/views/partials/configure.php';
	}

	/**
	 * Import connections from a detected third-party email plugin.
	 */
	public function handleImportSettings(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_import_settings' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		self::load_admin_includes();
		$source = isset( $_GET['source'] ) ? sanitize_key( wp_unslash( (string) $_GET['source'] ) ) : '';
		$result = SettingsImporter::import( $source, $this->getSettings() );

		if ( is_wp_error( $result ) ) {
			wp_safe_redirect(
				add_query_arg(
					[
						'page'  => self::PAGE_SLUG,
						'tab'   => 'settings',
						'error' => rawurlencode( $result->get_error_message() ),
					],
					NetworkScope::adminUrl( 'admin.php' )
				)
			);
			exit;
		}

		OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->save( $result['settings'] );
		$this->settings_cache = null;

		$message = sprintf(
			/* translators: 1: imported count, 2: skipped count */
			__( 'Imported %1$d connection(s). Skipped %2$d.', 'wp-powersuite' ),
			(int) $result['imported'],
			(int) $result['skipped']
		);

		wp_safe_redirect(
			add_query_arg(
				[
					'page'    => self::PAGE_SLUG,
					'tab'     => 'settings',
					'saved'   => '1',
					'imported'=> rawurlencode( $message ),
				],
				NetworkScope::adminUrl( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Dismiss the import banner for a source.
	 */
	public function handleDismissImport(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_dismiss_import' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		self::load_admin_includes();
		$source = isset( $_GET['source'] ) ? sanitize_key( wp_unslash( (string) $_GET['source'] ) ) : '';
		if ( $source !== '' ) {
			SettingsImporter::dismiss( $source );
		}

		wp_safe_redirect(
			add_query_arg(
				[
					'page' => self::PAGE_SLUG,
					'tab'  => 'settings',
				],
				NetworkScope::adminUrl( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Save routing (default / fallback / retries) and light module options.
	 */
	public function handleSaveSettings(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_save' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		$existing = $this->getSettings();
		$raw      = isset( $_POST['settings'] ) && is_array( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$raw      = is_array( $raw ) ? $raw : [];

		if ( array_key_exists( 'default_connection_id', $raw ) ) {
			$existing['default_connection_id'] = sanitize_key( (string) $raw['default_connection_id'] );
		}
		if ( array_key_exists( 'fallback_connection_id', $raw ) ) {
			$existing['fallback_connection_id'] = sanitize_key( (string) $raw['fallback_connection_id'] );
		}
		if ( array_key_exists( 'fallback_enabled', $raw ) ) {
			$existing['fallback_enabled'] = ! empty( $raw['fallback_enabled'] ) && (string) $raw['fallback_enabled'] !== '0' ? '1' : '0';
		}
		if ( array_key_exists( 'retry_max_attempts', $raw ) ) {
			$existing['retry_max_attempts'] = (string) max( 0, min( 2, (int) $raw['retry_max_attempts'] ) );
		}
		if ( array_key_exists( 'fallback_on', $raw ) ) {
			$mode = sanitize_key( (string) $raw['fallback_on'] );
			$existing['fallback_on'] = in_array( $mode, [ 'retry_safe', 'always' ], true ) ? $mode : 'retry_safe';
		}

		if ( array_key_exists( 'webhook_enabled', $raw ) ) {
			$existing['webhook_enabled'] = ! empty( $raw['webhook_enabled'] ) && (string) $raw['webhook_enabled'] !== '0' ? '1' : '0';
		}
		if ( isset( $raw['webhook_secret'] ) ) {
			$posted_secret = (string) $raw['webhook_secret'];
			if ( $posted_secret !== '' && ! CredentialManager::is_mask( $posted_secret ) ) {
				$existing['webhook_secret'] = CredentialManager::encrypt( $posted_secret );
			}
		}
		if ( isset( $raw['webhook_bird_signing_secret'] ) ) {
			$posted_bird = (string) $raw['webhook_bird_signing_secret'];
			if ( $posted_bird !== '' && ! CredentialManager::is_mask( $posted_bird ) ) {
				$existing['webhook_bird_signing_secret'] = CredentialManager::encrypt( $posted_bird );
			}
		}
		if ( array_key_exists( 'webhook_sendgrid_verification_key', $raw ) ) {
			$existing['webhook_sendgrid_verification_key'] = sanitize_textarea_field( (string) $raw['webhook_sendgrid_verification_key'] );
		}
		if ( array_key_exists( 'failure_alert_enabled', $raw ) ) {
			$existing['failure_alert_enabled'] = ! empty( $raw['failure_alert_enabled'] ) && (string) $raw['failure_alert_enabled'] !== '0' ? '1' : '0';
		}
		if ( array_key_exists( 'failure_alert_email', $raw ) ) {
			$alert = sanitize_email( (string) $raw['failure_alert_email'] );
			$existing['failure_alert_email'] = is_email( $alert ) ? $alert : '';
		}
		if ( array_key_exists( 'failure_alert_webhook', $raw ) ) {
			$webhook = esc_url_raw( trim( (string) $raw['failure_alert_webhook'] ) );
			$existing['failure_alert_webhook'] = ( $webhook !== '' && FailureNotifier::is_allowed_webhook_url( $webhook ) ) ? $webhook : '';
		}
		if ( array_key_exists( 'auto_retry_failed', $raw ) ) {
			$existing['auto_retry_failed'] = ! empty( $raw['auto_retry_failed'] ) && (string) $raw['auto_retry_failed'] !== '0' ? '1' : '0';
		}
		if ( array_key_exists( 'simulation_mode', $raw ) ) {
			$existing['simulation_mode'] = ! empty( $raw['simulation_mode'] ) && (string) $raw['simulation_mode'] !== '0' ? '1' : '0';
		}

		$settings = SettingsSchema::normalize( $existing );
		unset( $settings['_migration_dirty'] );
		OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->save( $settings );
		$this->settings_cache = null;

		wp_safe_redirect(
			add_query_arg(
				[
					'page'  => self::PAGE_SLUG,
					'tab' => 'settings',
					'saved' => '1',
				],
				NetworkScope::adminUrl( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Create or update a named connection (provider + From email + credentials).
	 */
	public function handleSaveConnection(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_save_connection' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		$existing = $this->getSettings();
		$existing = ConnectionStore::ensure( $existing );
		$raw      = isset( $_POST['connection'] ) && is_array( $_POST['connection'] ) ? wp_unslash( $_POST['connection'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$raw      = is_array( $raw ) ? $raw : [];

		$provider = sanitize_key( (string) ( $raw['provider'] ?? '' ) );
		if ( ! in_array( $provider, ProviderRegistry::ids(), true ) ) {
			wp_safe_redirect(
				add_query_arg(
					[
						'page'  => self::PAGE_SLUG,
						'tab' => 'settings',
						'error' => rawurlencode( __( 'Invalid provider.', 'wp-powersuite' ) ),
					],
					NetworkScope::adminUrl( 'admin.php' )
				)
			);
			exit;
		}

		$conn_id = sanitize_key( (string) ( $raw['id'] ?? '' ) );
		$is_new  = ( $conn_id === '' || ! isset( $existing['connections'][ $conn_id ] ) );
		if ( $is_new ) {
			$conn_id = ConnectionStore::new_id();
		}

		$prev = is_array( $existing['connections'][ $conn_id ] ?? null ) ? $existing['connections'][ $conn_id ] : [];
		$name = sanitize_text_field( (string) ( $raw['name'] ?? '' ) );
		if ( $name === '' ) {
			$name = ProviderRegistry::get( $provider )?->get_name() ?? $provider;
		}
		$from_email = sanitize_email( (string) ( $raw['from_email'] ?? '' ) );
		if ( ! is_email( $from_email ) ) {
			wp_safe_redirect(
				add_query_arg(
					[
						'page'       => self::PAGE_SLUG,
						'tab' => 'settings',
						'provider'   => $provider,
						'connection' => $is_new ? '' : $conn_id,
						'error'      => rawurlencode( __( 'A valid From email is required.', 'wp-powersuite' ) ),
					],
					NetworkScope::adminUrl( 'admin.php' )
				)
			);
			exit;
		}

		if ( ConnectionStore::from_email_in_use( $existing, $from_email, $is_new ? '' : $conn_id ) ) {
			wp_safe_redirect(
				add_query_arg(
					[
						'page'       => self::PAGE_SLUG,
						'tab'        => 'settings',
						'provider'   => $provider,
						'connection' => $is_new ? '' : $conn_id,
						'error'      => rawurlencode( __( 'This From email is already used by another connection. Each connection needs a unique From email.', 'wp-powersuite' ) ),
					],
					NetworkScope::adminUrl( 'admin.php' )
				)
			);
			exit;
		}

		// Named providers are API-only; SMTP relay is Custom SMTP only.
		$transport = $provider === 'smtp' ? 'smtp' : 'api';

		$conn = array_merge(
			$prev,
			[
				'id'               => $conn_id,
				'name'             => $name,
				'provider'         => $provider,
				'transport'        => $transport,
				'from_email'       => $from_email,
				'from_name'        => sanitize_text_field( (string) ( $raw['from_name'] ?? get_option( 'blogname' ) ) ),
				'force_from_email' => ! empty( $raw['force_from_email'] ) ? '1' : '0',
				'force_from_name'  => ! empty( $raw['force_from_name'] ) ? '1' : '0',
				'set_return_path'  => ! empty( $raw['set_return_path'] ) ? '1' : '0',
				'override_from'    => ! empty( $raw['force_from_email'] ) ? '1' : '0',
			]
		);

		// Provider credential fields are posted under connection[fields][...].
		$fields = isset( $raw['fields'] ) && is_array( $raw['fields'] ) ? $raw['fields'] : [];
		foreach ( $fields as $key => $value ) {
			$key = sanitize_key( (string) $key );
			if ( $key === '' ) {
				continue;
			}
			$conn[ $key ] = is_scalar( $value ) ? (string) $value : '';
		}
		$conn = ConnectionStore::merge_connection_secrets( $prev, $conn );

		foreach ( CredentialManager::constant_map() as $key => $constant ) {
			if ( CredentialManager::is_from_constant( $constant ) && $key !== 'provider' ) {
				$conn[ $key ] = $existing[ $key ] ?? ( $prev[ $key ] ?? '' );
			}
		}

		$provider_obj = ProviderRegistry::get( $provider );
		if ( $provider_obj ) {
			if ( $provider === 'smtp' ) {
				$host  = trim( (string) ( $conn['smtp_host'] ?? '' ) );
				$auth  = ! isset( $conn['smtp_auth'] ) || (string) $conn['smtp_auth'] !== '0';
				$user  = trim( (string) ( $conn['smtp_username'] ?? '' ) );
				$pass  = (string) ( $conn['smtp_password'] ?? '' );
				$valid = true;
				if ( $host === '' ) {
					$valid = new \WP_Error( 'missing_smtp_host', __( 'SMTP host is required.', 'wp-powersuite' ) );
				} elseif ( $auth && ( $user === '' || $pass === '' ) ) {
					$valid = new \WP_Error( 'missing_smtp_auth', __( 'SMTP username and password are required.', 'wp-powersuite' ) );
				}
			} else {
				$bag   = SettingsSchema::provider_bag( $conn, $provider );
				$valid = $provider_obj->validate_settings( $bag );
			}
			if ( is_wp_error( $valid ) && $provider !== 'wordpress' ) {
				wp_safe_redirect(
					add_query_arg(
						[
							'page'       => self::PAGE_SLUG,
							'tab' => 'settings',
							'provider'   => $provider,
							'connection' => $is_new ? '' : $conn_id,
							'error'      => rawurlencode( $valid->get_error_message() ),
						],
						NetworkScope::adminUrl( 'admin.php' )
					)
				);
				exit;
			}
		}

		$existing['connections'][ $conn_id ] = $conn;
		if ( empty( $existing['default_connection_id'] ) || ( $is_new && count( $existing['connections'] ) === 1 ) ) {
			$existing['default_connection_id'] = $conn_id;
		}

		$settings = SettingsSchema::normalize( $existing );
		unset( $settings['_migration_dirty'] );
		OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->save( $settings );
		$this->settings_cache = null;

		wp_safe_redirect(
			add_query_arg(
				[
					'page'       => self::PAGE_SLUG,
					'tab' => 'settings',
					'connection' => $conn_id,
					'saved'      => '1',
				],
				NetworkScope::adminUrl( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Set a connection as primary or fallback (or clear fallback).
	 */
	public function handleSetConnectionRole(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_set_connection_role' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		$conn_id  = isset( $_GET['connection'] ) ? sanitize_key( wp_unslash( (string) $_GET['connection'] ) ) : '';
		$role     = isset( $_GET['role'] ) ? sanitize_key( wp_unslash( (string) $_GET['role'] ) ) : '';
		$existing = ConnectionStore::ensure( $this->getSettings() );

		if ( $conn_id !== '' && isset( $existing['connections'][ $conn_id ] ) && is_array( $existing['connections'][ $conn_id ] ) ) {
			if ( $role === 'primary' ) {
				$existing['default_connection_id'] = $conn_id;
				$existing = ConnectionStore::sync_flat_from_connection( $existing, $existing['connections'][ $conn_id ] );
				// Primary cannot also be fallback.
				if ( (string) ( $existing['fallback_connection_id'] ?? '' ) === $conn_id ) {
					$existing['fallback_connection_id'] = '';
					$existing['fallback_enabled']       = '0';
				}
			} elseif ( $role === 'fallback' ) {
				if ( (string) ( $existing['default_connection_id'] ?? '' ) !== $conn_id ) {
					$existing['fallback_connection_id'] = $conn_id;
					$existing['fallback_enabled']       = '1';
					$existing['fallback_on']            = 'retry_safe';
				}
			} elseif ( $role === 'clear_fallback' ) {
				if ( (string) ( $existing['fallback_connection_id'] ?? '' ) === $conn_id ) {
					$existing['fallback_connection_id'] = '';
					$existing['fallback_enabled']       = '0';
				}
			}
		}

		$settings = SettingsSchema::normalize( $existing );
		unset( $settings['_migration_dirty'] );
		OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->save( $settings );
		$this->settings_cache = null;

		wp_safe_redirect(
			add_query_arg(
				[
					'page'  => self::PAGE_SLUG,
					'tab'   => 'settings',
					'saved' => '1',
				],
				NetworkScope::adminUrl( 'admin.php' )
			)
		);
		exit;
	}

	public function handleDeleteConnection(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_delete_connection' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		$conn_id  = isset( $_GET['connection'] ) ? sanitize_key( wp_unslash( (string) $_GET['connection'] ) ) : '';
		$existing = ConnectionStore::ensure( $this->getSettings() );
		if ( $conn_id !== '' && isset( $existing['connections'][ $conn_id ] ) ) {
			unset( $existing['connections'][ $conn_id ] );
			if ( (string) ( $existing['default_connection_id'] ?? '' ) === $conn_id ) {
				$existing['default_connection_id'] = $existing['connections'] !== []
					? (string) array_key_first( $existing['connections'] )
					: '';
			}
			if ( (string) ( $existing['fallback_connection_id'] ?? '' ) === $conn_id ) {
				$existing['fallback_connection_id'] = '';
				$existing['fallback_enabled']       = '0';
			}
			// Keep multi-connection schema so an empty list is not re-migrated from flat aliases.
			$existing['migration_version'] = SettingsSchema::MIGRATION_VERSION;
			$existing['connections']       = is_array( $existing['connections'] ?? null ) ? $existing['connections'] : [];
			if ( $existing['connections'] === [] ) {
				$existing['default_connection_id']  = '';
				$existing['fallback_connection_id'] = '';
				$existing['fallback_enabled']       = '0';
				$existing['provider']               = 'wordpress';
			}
		}

		$settings = SettingsSchema::normalize( $existing );
		unset( $settings['_migration_dirty'] );
		OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->save( $settings );
		$this->settings_cache = null;

		wp_safe_redirect(
			add_query_arg(
				[
					'page'  => self::PAGE_SLUG,
					'tab' => 'settings',
					'saved' => '1',
				],
				NetworkScope::adminUrl( 'admin.php' )
			)
		);
		exit;
	}

	/**
	 * Save OAuth client credentials (if posted) and redirect to the provider authorize URL.
	 */
	public function handleOAuthConnect(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_oauth', 'wpps_ed_oauth_nonce' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		$provider_id = isset( $_POST['oauth_provider'] ) ? sanitize_key( wp_unslash( (string) $_POST['oauth_provider'] ) ) : '';
		if ( ! in_array( $provider_id, [ 'google', 'microsoft' ], true ) ) {
			wp_die( esc_html__( 'Invalid OAuth provider.', 'wp-powersuite' ) );
		}

		$provider = ProviderRegistry::get( $provider_id );
		if ( ! $provider instanceof AbstractOAuthMailProvider ) {
			wp_die( esc_html__( 'OAuth provider unavailable.', 'wp-powersuite' ) );
		}

		$existing      = ConnectionStore::ensure( $this->getSettings() );
		$conn_id       = isset( $_POST['oauth_connection_id'] ) ? sanitize_key( wp_unslash( (string) $_POST['oauth_connection_id'] ) ) : '';
		$raw           = isset( $_POST['settings'] ) && is_array( $_POST['settings'] ) ? wp_unslash( $_POST['settings'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$raw           = is_array( $raw ) ? $raw : [];

		if ( $conn_id !== '' && isset( $existing['connections'][ $conn_id ] ) && is_array( $existing['connections'][ $conn_id ] ) ) {
			$conn = $existing['connections'][ $conn_id ];
			$conn['provider'] = $provider_id;
			foreach ( $raw as $key => $value ) {
				$key = sanitize_key( (string) $key );
				if ( $key === '' || ! is_scalar( $value ) ) {
					continue;
				}
				$conn[ $key ] = (string) $value;
			}
			$conn = ConnectionStore::merge_connection_secrets( $existing['connections'][ $conn_id ], $conn );
			$existing['connections'][ $conn_id ] = $conn;
			if ( (string) ( $existing['default_connection_id'] ?? '' ) === $conn_id ) {
				$existing = ConnectionStore::sync_flat_from_connection( $existing, $conn );
			}
		} else {
			$merged = array_merge( $existing, $raw );
			$merged['provider'] = $provider_id;
			$merged = CredentialManager::merge_secrets( $merged, $existing, CredentialManager::secret_keys() );
			$existing = $merged;
		}

		foreach ( CredentialManager::constant_map() as $key => $constant ) {
			if ( CredentialManager::is_from_constant( $constant ) && $key !== 'provider' ) {
				$existing[ $key ] = $this->getSettings()[ $key ] ?? '';
			}
			if ( $key === 'provider' && CredentialManager::is_from_constant( $constant ) ) {
				$existing['provider'] = sanitize_key( (string) constant( $constant ) );
			}
		}

		$settings = SettingsSchema::normalize( $existing );
		unset( $settings['_migration_dirty'] );

		$bag = $conn_id !== '' && isset( $settings['connections'][ $conn_id ] )
			? SettingsSchema::provider_bag( $settings['connections'][ $conn_id ], $provider_id )
			: SettingsSchema::provider_bag( $settings, $provider_id );
		$valid = $provider->validate_settings( $bag );
		if ( is_wp_error( $valid ) ) {
			wp_safe_redirect(
				add_query_arg(
					[
						'page'         => self::PAGE_SLUG,
						'tab'          => 'settings',
						'connection'   => $conn_id,
						'oauth_error'  => 'missing_credentials',
						'oauth_detail' => rawurlencode( $valid->get_error_message() ),
					],
					NetworkScope::adminUrl( 'admin.php' )
				)
			);
			exit;
		}

		OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->save( $settings );
		$this->settings_cache = null;

		$redirect_after = NetworkScope::adminUrl(
			'admin.php',
			array_filter(
				[
					'page'       => self::PAGE_SLUG,
					'tab'        => 'settings',
					'connection' => $conn_id !== '' ? $conn_id : null,
				]
			)
		);
		$pkce = OAuthState::create( $provider_id, $redirect_after, $conn_id );
		$url  = $provider->get_authorize_url( $bag, $pkce['state'], $pkce['code_challenge'] );

		$host = (string) wp_parse_url( $url, PHP_URL_HOST );
		if ( ! in_array( $host, [ 'accounts.google.com', 'login.microsoftonline.com' ], true ) ) {
			wp_die( esc_html__( 'Unexpected OAuth authorize host.', 'wp-powersuite' ) );
		}

		// External IdP — wp_safe_redirect would block these hosts.
		wp_redirect( $url ); // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect
		exit;
	}

	/**
	 * Clear OAuth tokens while keeping client credentials.
	 */
	public function handleOAuthDisconnect(): void {
		if ( ! $this->canManagePlugin() || ! check_admin_referer( 'wpps_email_delivery_oauth', 'wpps_ed_oauth_nonce' ) ) {
			wp_die( esc_html__( 'Invalid request.', 'wp-powersuite' ) );
		}

		$provider_id = isset( $_POST['oauth_provider'] ) ? sanitize_key( wp_unslash( (string) $_POST['oauth_provider'] ) ) : '';
		if ( ! in_array( $provider_id, [ 'google', 'microsoft' ], true ) ) {
			wp_die( esc_html__( 'Invalid OAuth provider.', 'wp-powersuite' ) );
		}

		$provider = ProviderRegistry::get( $provider_id );
		if ( ! $provider instanceof AbstractOAuthMailProvider ) {
			wp_die( esc_html__( 'OAuth provider unavailable.', 'wp-powersuite' ) );
		}

		$conn_id  = isset( $_POST['oauth_connection_id'] ) ? sanitize_key( wp_unslash( (string) $_POST['oauth_connection_id'] ) ) : '';
		$settings = ConnectionStore::ensure( $this->getSettings() );

		if ( $conn_id !== '' && isset( $settings['connections'][ $conn_id ] ) && is_array( $settings['connections'][ $conn_id ] ) ) {
			$conn = $provider->disconnect( $settings['connections'][ $conn_id ] );
			$settings['connections'][ $conn_id ] = is_array( $conn ) ? $conn : $settings['connections'][ $conn_id ];
			if ( (string) ( $settings['default_connection_id'] ?? '' ) === $conn_id ) {
				$settings = ConnectionStore::sync_flat_from_connection( $settings, $settings['connections'][ $conn_id ] );
			}
		} else {
			$settings = $provider->disconnect( $settings );
		}

		$settings = SettingsSchema::normalize( $settings );
		unset( $settings['_migration_dirty'] );

		OptionStoreModuleSettingsRepository::forModule( self::MODULE_ID )->save( $settings );
		$this->settings_cache = null;

		wp_safe_redirect(
			add_query_arg(
				array_filter(
					[
						'page'       => self::PAGE_SLUG,
						'tab' => 'settings',
						'connection' => $conn_id !== '' ? $conn_id : null,
						'oauth'      => 'disconnected',
					]
				),
				NetworkScope::adminUrl( 'admin.php' )
			)
		);
		exit;
	}

	public function ajaxTestConnection(): void {
		check_ajax_referer( 'wp_powersuite_ajax', 'nonce' );
		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied', 'wp-powersuite' ) ] );
		}

		$user_id  = get_current_user_id();
		$rate_key = 'wpps_ed_conn_' . $user_id;
		$attempts = (int) get_transient( $rate_key );
		if ( $attempts >= 20 ) {
			wp_send_json_error( [ 'message' => __( 'Rate limit exceeded. Please wait a few minutes.', 'wp-powersuite' ) ] );
		}
		set_transient( $rate_key, $attempts + 1, 5 * MINUTE_IN_SECONDS );

		$settings = ConnectionStore::ensure( $this->getSettings() );
		$raw      = isset( $_POST['connection'] ) && is_array( $_POST['connection'] ) ? wp_unslash( $_POST['connection'] ) : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		$raw      = is_array( $raw ) ? $raw : [];

		$conn_id = sanitize_key( (string) ( $raw['id'] ?? ( $_POST['connection_id'] ?? '' ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
		if ( $conn_id !== '' && isset( $settings['connections'][ $conn_id ] ) ) {
			$settings = ConnectionStore::settings_for_connection( $settings, $conn_id );
			$prev     = is_array( $settings['connections'][ $conn_id ] ?? null ) ? $settings['connections'][ $conn_id ] : [];
		} else {
			$prev = [];
		}

		if ( $raw !== [] ) {
			$provider_id = sanitize_key( (string) ( $raw['provider'] ?? ( $settings['provider'] ?? 'wordpress' ) ) );
			if ( in_array( $provider_id, ProviderRegistry::ids(), true ) ) {
				$settings['provider'] = $provider_id;
			}
			$settings['transport'] = ( ( $settings['provider'] ?? '' ) === 'smtp' ) ? 'smtp' : 'api';

			$fields = isset( $raw['fields'] ) && is_array( $raw['fields'] ) ? $raw['fields'] : [];
			$draft  = $prev;
			foreach ( $fields as $key => $value ) {
				$key = sanitize_key( (string) $key );
				if ( $key === '' ) {
					continue;
				}
				$draft[ $key ] = is_scalar( $value ) ? (string) $value : '';
			}
			$draft = ConnectionStore::merge_connection_secrets( $prev, $draft );
			foreach ( $draft as $key => $value ) {
				if ( in_array( $key, [ 'id', 'name' ], true ) ) {
					continue;
				}
				$settings[ $key ] = $value;
			}
		}

		$provider = ProviderRegistry::get( (string) ( $settings['provider'] ?? 'wordpress' ) );
		if ( ! $provider ) {
			wp_send_json_error( [ 'message' => __( 'Unknown provider.', 'wp-powersuite' ) ] );
		}

		if ( SmtpRelayProfiles::connection_uses_smtp( $settings, $provider ) ) {
			$settings = SmtpRelayProfiles::merge_smtp_settings( $settings, $provider->get_id() );
			$smtp     = ProviderRegistry::get( 'smtp' );
			if ( ! $smtp ) {
				wp_send_json_error( [ 'message' => __( 'SMTP transport is unavailable.', 'wp-powersuite' ) ] );
			}
			$result = $smtp->connect( array_merge( $settings, SettingsSchema::provider_bag( $settings, 'smtp' ) ) );
		} else {
			$result = $provider->connect( SettingsSchema::provider_bag( $settings, $provider->get_id() ) );
		}

		$payload             = $result->to_array();
		$payload['provider'] = $provider->get_id();
		$payload['transport'] = sanitize_key( (string) ( $settings['transport'] ?? 'api' ) );

		if ( $result->is_success() ) {
			wp_send_json_success( $payload );
		}
		wp_send_json_error( $payload );
	}

	public function ajaxTestEmail(): void {
		check_ajax_referer( 'wp_powersuite_ajax', 'nonce' );
		if ( ! NetworkScope::canManagePlugin() ) {
			wp_send_json_error( [ 'message' => __( 'Permission denied', 'wp-powersuite' ) ] );
		}

		$user_id  = get_current_user_id();
		$rate_key = 'wpps_smtp_test_' . $user_id;
		$attempts = (int) get_transient( $rate_key );
		if ( $attempts >= 10 ) {
			wp_send_json_error( [ 'message' => __( 'Rate limit exceeded. Please wait a few minutes before sending another test email.', 'wp-powersuite' ) ] );
		}

		$test_email = isset( $_POST['test_email'] ) ? sanitize_email( wp_unslash( $_POST['test_email'] ) ) : '';
		if ( ! is_email( $test_email ) ) {
			wp_send_json_error( [ 'message' => __( 'Invalid email address', 'wp-powersuite' ) ] );
		}

		$plugin_name = self::brandPluginName();

		$subject = isset( $_POST['subject'] ) ? sanitize_text_field( wp_unslash( $_POST['subject'] ) ) : '';
		if ( $subject === '' ) {
			$subject = sprintf( __( 'Email delivery check – %s', 'wp-powersuite' ), $plugin_name );
		}

		$custom = isset( $_POST['message'] ) ? sanitize_textarea_field( wp_unslash( $_POST['message'] ) ) : '';
		$is_html = ! isset( $_POST['format'] ) || sanitize_key( wp_unslash( $_POST['format'] ) ) !== 'plain';

		$greeting    = __( 'Hello,', 'wp-powersuite' );
		$body_text   = $custom !== '' ? $custom : sprintf(
			__( 'You requested to verify that emails from %s are being delivered correctly.', 'wp-powersuite' ),
			$plugin_name
		);
		$inbox_note  = __( 'If you received this in your inbox (and not in spam), your configuration is set up properly.', 'wp-powersuite' );
		$provider_id = (string) ( $this->getSettings()['provider'] ?? 'wordpress' );

		if ( $is_html ) {
			$message = $this->buildTestEmailHtml( $greeting, $body_text, $inbox_note, __( 'Best regards,', 'wp-powersuite' ), $plugin_name );
		} else {
			$message = $greeting . "\n\n" . $body_text . "\n\n" . $inbox_note . "\n\n" . $plugin_name . "\n";
		}

		$result = EmailRouter::send_test_email( $test_email, $subject, $message, $is_html );
		set_transient( $rate_key, $attempts + 1, 5 * MINUTE_IN_SECONDS );

		$payload = $result->to_array();
		$payload['provider'] = $provider_id;

		if ( $result->is_success() ) {
			wp_send_json_success( $payload );
		}
		if ( $this->last_mail_error !== '' ) {
			$payload['technical_message'] = ErrorNormalizer::redact( $this->last_mail_error );
		}
		wp_send_json_error( $payload );
	}

	private function buildTestEmailHtml( string $greeting, string $body_text, string $inbox_note, string $signoff, string $plugin_name ): string {
		$content  = EmailComponents::paragraph( esc_html( $greeting ), [ 'margin' => '0 0 12px' ] );
		$content .= EmailComponents::paragraph( esc_html( $body_text ) );
		$content .= EmailComponents::notice( esc_html( $inbox_note ), 'info' );
		$content .= EmailComponents::paragraph(
			esc_html( $signoff ) . '<br><strong>' . esc_html( $plugin_name ) . '</strong>',
			[ 'margin' => '16px 0 0' ]
		);

		return EmailRenderer::wrap(
			[
				'title'        => __( 'Email delivery check', 'wp-powersuite' ),
				'site_name'    => $plugin_name,
				'logo_url'     => EmailRenderer::brandingLogoUrl(),
				'content'      => $content,
				'footer_brand' => sprintf(
					/* translators: %s: plugin / white-label product name */
					__( 'Sent via %s Email Delivery', 'wp-powersuite' ),
					$plugin_name
				),
			]
		);
	}

	public static function encryptPassword( string $value ): string {
		if ( $value === '' ) {
			return '';
		}
		$auth_key = defined( 'AUTH_KEY' ) && AUTH_KEY ? AUTH_KEY : ABSPATH;
		$key      = hash( 'sha256', self::ENCRYPT_KEY . $auth_key, true );
		$iv       = random_bytes( 16 );
		$encrypted = openssl_encrypt( $value, 'AES-256-CBC', $key, OPENSSL_RAW_DATA, $iv );
		if ( $encrypted === false ) {
			return '';
		}
		return 'ENC2:' . base64_encode( $iv . $encrypted );
	}

	public static function getPasswordMask(): string {
		return self::PASSWORD_MASK;
	}

	/**
	 * @return array<string, mixed>
	 */
	public static function getDefaultSettings(): array {
		return SettingsSchema::defaults();
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public static function normalizeSettings( array $settings ): array {
		return SettingsSchema::normalize( $settings );
	}

	/**
	 * @param array<string, mixed> $settings
	 * @return array<string, mixed>
	 */
	public static function redactSettingsForResponse( array $settings ): array {
		return CredentialManager::redact( $settings, CredentialManager::secret_keys() );
	}

	/**
	 * @return array<string, mixed>
	 */
	private function getSettings(): array {
		if ( $this->settings_cache !== null ) {
			return $this->settings_cache;
		}
		$settings = $this->moduleSettings()->all( [] );
		$settings = is_array( $settings ) ? $settings : [];
		$this->settings_cache = SettingsSchema::normalize( $settings );
		unset( $this->settings_cache['_migration_dirty'] );
		return $this->settings_cache;
	}
}
