<?php
/**
 * Amazon SES integration bootstrap.
 *
 * @package Robotstxt_SMTP_AmazonSES
 */

namespace Robotstxt_SMTP_AmazonSES;

use Aws\Credentials\Credentials;
use Aws\Endpoint\PartitionEndpointProvider;
use Aws\Exception\AwsException;
use Aws\SesV2\SesV2Client;
use PHPMailer\PHPMailer\Exception as PHPMailerException;
use PHPMailer\PHPMailer\PHPMailer;
use Robotstxt_SMTP\Admin\Settings_Page;
use Robotstxt_SMTP\Plugin as SMTP_Plugin;
use Throwable;
use WP_Error;

/**
 * Amazon SES integration plugin class.
 */
class Plugin {
		/**
		 * Option key used to store the Amazon SES access key ID.
		 */
	private const OPTION_ACCESS_KEY = 'amazon_ses_access_key';

		/**
		 * Option key used to store the Amazon SES secret access key.
		 */
	private const OPTION_SECRET_KEY = 'amazon_ses_secret_key';

		/**
		 * Option key used to store the selected Amazon SES region.
		 */
	private const OPTION_REGION = 'amazon_ses_region';

		/**
		 * Identifier used for Amazon SES related settings notices.
		 */
	private const SETTINGS_ERROR_CREDENTIALS = 'robotstxt_smtp_amazon_ses_credentials';

		/**
		 * Holds the singleton instance.
		 *
		 * @var Plugin|null
		 */
	private static ?Plugin $instance = null;

	/**
	 * Cached list of available Amazon SES regions keyed by region code.
	 *
	 * @var array<string, string>|null
	 */
	private ?array $region_choices = null;

	/**
	 * Cached SES clients indexed by a hash of the credentials and region.
	 *
	 * @var array<string, SesV2Client>
	 */
	private array $client_cache = array();

		/**
		 * Retrieves the instance.
		 *
		 * @return Plugin
		 */
	public static function get_instance(): Plugin {
		if ( null === self::$instance ) {
				self::$instance = new self();
		}

			return self::$instance;
	}

		/**
		 * Registers WordPress hooks used by the integration.
		 *
		 * @return void
		 */
	public function run(): void {
			\add_filter( 'robotstxt_smtp_amazon_ses_active', array( $this, 'declare_active' ) );
			\add_filter( 'robotstxt_smtp_connection_field_definitions', array( $this, 'provide_field_definitions' ), 10, 2 );
			\add_filter( 'robotstxt_smtp_sanitized_options', array( $this, 'sanitize_settings' ), 10, 3 );
			\add_filter( 'pre_wp_mail', array( $this, 'maybe_route_mail_via_amazon_ses' ), 10, 2 );
	}

		/**
		 * Routes outgoing mail through Amazon SES when credentials are available.
		 *
		 * @param mixed                $pre_wp_mail Short-circuit value provided by earlier filters.
		 * @param array<string, mixed> $mail_data   Normalized email arguments from wp_mail().
		 *
		 * @return mixed
		 */
	public function maybe_route_mail_via_amazon_ses( $pre_wp_mail, array $mail_data ) {
		if ( null !== $pre_wp_mail ) {
				return $pre_wp_mail;
		}

		if ( ! SMTP_Plugin::is_amazon_ses_integration_active() ) {
				return $pre_wp_mail;
		}

			$settings   = $this->get_stored_settings();
			$access_key = (string) ( $settings[ self::OPTION_ACCESS_KEY ] ?? '' );
			$secret_key = (string) ( $settings[ self::OPTION_SECRET_KEY ] ?? '' );
			$region     = (string) ( $settings[ self::OPTION_REGION ] ?? '' );

		if ( '' === $access_key || '' === $secret_key || '' === $region ) {
				return $pre_wp_mail;
		}

			$mail_data = \wp_parse_args(
				$mail_data,
				array(
					'to'          => array(),
					'subject'     => '',
					'message'     => '',
					'headers'     => array(),
					'attachments' => array(),
				)
			);

			$recipients = $this->normalize_recipients( $mail_data['to'] );

		if ( empty( $recipients ) ) {
				$error = new WP_Error(
					'robotstxt_smtp_amazon_ses_missing_recipient',
					\esc_html__( 'Amazon SES could not send the email because no recipient was provided.', 'robotstxt-smtp-amazonses' ),
					array(
						'to'          => array(),
						'subject'     => (string) $mail_data['subject'],
						'message'     => (string) $mail_data['message'],
						'headers'     => $mail_data['headers'],
						'attachments' => $this->normalize_attachments( $mail_data['attachments'] ),
					)
				);

				\do_action( 'wp_mail_failed', $error );

				return $error;
		}

			$attachments = $this->normalize_attachments( $mail_data['attachments'] );
			$headers     = $this->normalize_headers( $mail_data['headers'] );
			$parsed      = $this->parse_headers( $headers );

			$mail_context = array(
				'to'          => $recipients,
				'subject'     => (string) $mail_data['subject'],
				'message'     => (string) $mail_data['message'],
				'headers'     => $mail_data['headers'],
				'attachments' => $attachments,
			);

			$send_error = $this->deliver_via_amazon_ses(
				$mail_context,
				$settings,
				$parsed,
				$access_key,
				$secret_key,
				$region
			);

		if ( $send_error instanceof WP_Error ) {
				\do_action( 'wp_mail_failed', $send_error );

				return $send_error;
		}

			\do_action( 'wp_mail_succeeded', $mail_context );

			return true;
	}

		/**
		 * Sends the email data to Amazon SES using the SDK client.
		 *
		 * @param array<string, mixed> $mail_context      Normalized mail data.
		 * @param array<string, mixed> $settings       Stored plugin settings.
		 * @param array<string, mixed> $parsed_headers Structured header data.
		 * @param string               $access_key        AWS access key ID.
		 * @param string               $secret_key        AWS secret access key.
		 * @param string               $region            AWS region identifier.
		 *
		 * @return WP_Error|null
		 */
	private function deliver_via_amazon_ses( array $mail_context, array $settings, array $parsed_headers, string $access_key, string $secret_key, string $region ): ?WP_Error {
			$phpmailer = $this->prepare_mailer_for_amazon_ses( $mail_context, $settings, $parsed_headers );

		if ( $phpmailer instanceof WP_Error ) {
				return $phpmailer;
		}

		try {
				$phpmailer->preSend();
		} catch ( PHPMailerException $exception ) {
				return new WP_Error(
					'robotstxt_smtp_amazon_ses_presend_failed',
					\sprintf(
								/* translators: %s: Error details describing the email preparation failure. */
						\esc_html__( 'Amazon SES could not prepare the email: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( $exception->getMessage() )
					),
					array(
						'to'                       => $mail_context['to'],
						'subject'                  => $mail_context['subject'],
						'message'                  => $mail_context['message'],
						'headers'                  => $mail_context['headers'],
						'attachments'              => $mail_context['attachments'],
						'phpmailer_exception_code' => $exception->getCode(),
					)
				);
		}

			$raw_message = $phpmailer->getSentMIMEMessage();

		try {
				$client = $this->get_ses_client( $access_key, $secret_key, $region );

			$client->sendEmail(
				array(
					'Content'          => array(
						'Raw' => array(
							'Data' => $raw_message,
						),
					),
					// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
					'FromEmailAddress' => $phpmailer->From,
				)
			);
		} catch ( AwsException $exception ) {
				$message = $exception->getAwsErrorMessage();

			if ( ! is_string( $message ) || '' === $message ) {
					$message = $exception->getMessage();
			}

				return new WP_Error(
					'robotstxt_smtp_amazon_ses_send_failed',
					\sprintf(
								/* translators: %s: Error message returned by Amazon SES. */
						\esc_html__( 'Amazon SES could not send the email: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( (string) $message )
					),
					array(
						'to'             => $mail_context['to'],
						'subject'        => $mail_context['subject'],
						'message'        => $mail_context['message'],
						'headers'        => $mail_context['headers'],
						'attachments'    => $mail_context['attachments'],
						'ses_error_code' => $exception->getAwsErrorCode(),
					)
				);
		} catch ( Throwable $exception ) {
				return new WP_Error(
					'robotstxt_smtp_amazon_ses_unexpected_send_error',
					\sprintf(
								/* translators: %s: Error message describing the unexpected failure. */
						\esc_html__( 'An unexpected error occurred while sending through Amazon SES: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( $exception->getMessage() )
					),
					array(
						'to'          => $mail_context['to'],
						'subject'     => $mail_context['subject'],
						'message'     => $mail_context['message'],
						'headers'     => $mail_context['headers'],
						'attachments' => $mail_context['attachments'],
					)
				);
		}

			return null;
	}

		/**
		 * Builds a PHPMailer instance configured with the provided mail data.
		 *
		 * @param array<string, mixed> $mail_context      Normalized mail data.
		 * @param array<string, mixed> $settings          Stored plugin settings.
		 * @param array<string, mixed> $parsed_headers Structured header data.
		 *
		 * @return PHPMailer|WP_Error
		 */
	private function prepare_mailer_for_amazon_ses( array $mail_context, array $settings, array $parsed_headers ) {
			$phpmailer = new PHPMailer( true );

			$default_content_type = \apply_filters( 'wp_mail_content_type', 'text/plain' );
			$default_charset      = \apply_filters( 'wp_mail_charset', \get_bloginfo( 'charset' ) );

			$content_type = is_string( $parsed_headers['content_type'] ?? '' ) && '' !== $parsed_headers['content_type'] ? $parsed_headers['content_type'] : ( is_string( $default_content_type ) ? $default_content_type : 'text/plain' );
			$charset      = is_string( $parsed_headers['charset'] ?? '' ) && '' !== $parsed_headers['charset'] ? $parsed_headers['charset'] : ( is_string( $default_charset ) ? $default_charset : 'utf-8' );

			// phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
			$phpmailer->CharSet  = $charset;
			$phpmailer->Encoding = '8bit';

			$from_email = $this->determine_from_email( $settings );
			$from_name  = $this->determine_from_name( $settings );

		try {
			$phpmailer->setFrom( $from_email, $from_name, false );
		} catch ( PHPMailerException $exception ) {
			return new WP_Error(
				'robotstxt_smtp_amazon_ses_invalid_from',
				\sprintf(
					/* translators: %s: Error details describing why the From header is invalid. */
					\esc_html__( 'Amazon SES rejected the "From" address: %s', 'robotstxt-smtp-amazonses' ),
					\wp_strip_all_tags( $exception->getMessage() )
				),
				array(
					'to'          => $mail_context['to'],
					'subject'     => $mail_context['subject'],
					'message'     => $mail_context['message'],
					'headers'     => $mail_context['headers'],
					'attachments' => $mail_context['attachments'],
				)
			);
		}

		foreach ( $mail_context['to'] as $recipient ) {
			try {
				$phpmailer->addAddress( $recipient );
			} catch ( PHPMailerException $exception ) {
				return new WP_Error(
					'robotstxt_smtp_amazon_ses_invalid_recipient',
					\sprintf(
						/* translators: %s: Invalid recipient email address. */
						\esc_html__( 'Amazon SES rejected the recipient address: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( $recipient )
					),
					array(
						'to'          => $mail_context['to'],
						'subject'     => $mail_context['subject'],
						'message'     => $mail_context['message'],
						'headers'     => $mail_context['headers'],
						'attachments' => $mail_context['attachments'],
					)
				);
			}
		}

		foreach ( $parsed_headers['cc'] ?? array() as $cc ) {
			try {
				$phpmailer->addCC( $cc );
			} catch ( PHPMailerException $exception ) {
				return new WP_Error(
					'robotstxt_smtp_amazon_ses_invalid_cc',
					\sprintf(
						/* translators: %s: Invalid CC email address. */
						\esc_html__( 'Amazon SES rejected the CC address: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( $cc )
					),
					array(
						'to'          => $mail_context['to'],
						'subject'     => $mail_context['subject'],
						'message'     => $mail_context['message'],
						'headers'     => $mail_context['headers'],
						'attachments' => $mail_context['attachments'],
					)
				);
			}
		}

		foreach ( $parsed_headers['bcc'] ?? array() as $bcc ) {
			try {
				$phpmailer->addBCC( $bcc );
			} catch ( PHPMailerException $exception ) {
				return new WP_Error(
					'robotstxt_smtp_amazon_ses_invalid_bcc',
					\sprintf(
						/* translators: %s: Invalid BCC email address. */
						\esc_html__( 'Amazon SES rejected the BCC address: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( $bcc )
					),
					array(
						'to'          => $mail_context['to'],
						'subject'     => $mail_context['subject'],
						'message'     => $mail_context['message'],
						'headers'     => $mail_context['headers'],
						'attachments' => $mail_context['attachments'],
					)
				);
			}
		}

		foreach ( $parsed_headers['reply_to'] ?? array() as $reply_to ) {
			try {
				$phpmailer->addReplyTo( $reply_to );
			} catch ( PHPMailerException $exception ) {
				return new WP_Error(
					'robotstxt_smtp_amazon_ses_invalid_reply_to',
					\sprintf(
						/* translators: %s: Invalid reply-to email address. */
						\esc_html__( 'Amazon SES rejected the reply-to address: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( $reply_to )
					),
					array(
						'to'          => $mail_context['to'],
						'subject'     => $mail_context['subject'],
						'message'     => $mail_context['message'],
						'headers'     => $mail_context['headers'],
						'attachments' => $mail_context['attachments'],
					)
				);
			}
		}

			$phpmailer->ContentType = $content_type;

		if ( 'text/html' === strtolower( $content_type ) ) {
			$phpmailer->isHTML( true );
		} else {
			$phpmailer->isHTML( false );
		}

		foreach ( $parsed_headers['custom'] ?? array() as $custom_header ) {
			if ( isset( $custom_header['name'], $custom_header['value'] ) ) {
				$phpmailer->addCustomHeader( (string) $custom_header['name'], (string) $custom_header['value'] );
			}
		}

			$phpmailer->Subject = \wp_specialchars_decode( $mail_context['subject'], ENT_QUOTES );

			$message = (string) $mail_context['message'];

		if ( $phpmailer->isHTML() ) {
			$phpmailer->msgHTML( $message );

			if ( '' === $phpmailer->AltBody ) {
				$phpmailer->AltBody = \wp_strip_all_tags( $message );
			}
		} else {
			$phpmailer->Body = $message;
		}

		foreach ( $mail_context['attachments'] as $attachment ) {
			try {
				$phpmailer->addAttachment( $attachment );
			} catch ( PHPMailerException $exception ) {
				return new WP_Error(
					'robotstxt_smtp_amazon_ses_invalid_attachment',
					\sprintf(
						/* translators: %s: Attachment path that could not be added. */
						\esc_html__( 'Amazon SES could not include the attachment: %s', 'robotstxt-smtp-amazonses' ),
						\wp_strip_all_tags( $attachment )
					),
					array(
						'to'                       => $mail_context['to'],
						'subject'                  => $mail_context['subject'],
						'message'                  => $mail_context['message'],
						'headers'                  => $mail_context['headers'],
						'attachments'              => $mail_context['attachments'],
						'phpmailer_exception_code' => $exception->getCode(),
					)
				);
			}
		}

			// phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase

			return $phpmailer;
	}
		/**
		 * Determines the From email address based on the stored settings and filters.
		 *
		 * @param array<string, mixed> $settings Stored plugin settings.
		 *
		 * @return string
		 */
	private function determine_from_email( array $settings ): string {
		$default_email = \get_bloginfo( 'admin_email' );

		if ( isset( $settings['from_email'] ) && \is_email( $settings['from_email'] ) ) {
				$default_email = $settings['from_email'];
		}

		$filtered = \apply_filters( 'wp_mail_from', $default_email );

		if ( ! \is_email( $filtered ) ) {
				return (string) $default_email;
		}

		return (string) $filtered;
	}

		/**
		 * Determines the From name based on stored settings and filters.
		 *
		 * @param array<string, mixed> $settings Stored plugin settings.
		 *
		 * @return string
		 */
	private function determine_from_name( array $settings ): string {
			$default_name = \get_bloginfo( 'name', 'display' );

		if ( isset( $settings['from_name'] ) && '' !== $settings['from_name'] ) {
				$default_name = $settings['from_name'];
		}

			$filtered = \apply_filters( 'wp_mail_from_name', $default_name );

			return (string) $filtered;
	}

		/**
		 * Converts different header representations into a clean array of header lines.
		 *
		 * @param mixed $headers Headers supplied to wp_mail().
		 *
		 * @return array<int, string>
		 */
	private function normalize_headers( $headers ): array {
		if ( empty( $headers ) ) {
				return array();
		}

		if ( is_string( $headers ) ) {
				$headers = preg_split( "/\r\n|\r|\n/", $headers );
		}

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

			$normalized = array();

		foreach ( $headers as $header ) {
			if ( is_array( $header ) ) {
					continue;
			}

				$line = trim( (string) $header );

			if ( '' !== $line ) {
					$normalized[] = $line;
			}
		}

			return $normalized;
	}

		/**
		 * Breaks down the headers into structured data used when building the PHPMailer instance.
		 *
		 * @param array<int, string> $headers Normalized header lines.
		 *
		 * @return array<string, mixed>
		 */
	private function parse_headers( array $headers ): array {
			$parsed = array(
				'content_type' => null,
				'charset'      => null,
				'cc'           => array(),
				'bcc'          => array(),
				'reply_to'     => array(),
				'custom'       => array(),
			);

			foreach ( $headers as $header ) {
				if ( false === strpos( $header, ':' ) ) {
						continue;
				}

					list( $name, $value ) = explode( ':', $header, 2 );
					$normalized_name      = strtolower( trim( $name ) );
					$original_name        = trim( $name );
					$value                = trim( $value );

				switch ( $normalized_name ) {
					case 'content-type':
						if ( false !== strpos( $value, ';' ) ) {
							$parts                  = explode( ';', $value );
							$parsed['content_type'] = trim( array_shift( $parts ) );

							foreach ( $parts as $part ) {
								if ( false === strpos( $part, '=' ) ) {
									continue;
								}

										list( $param, $param_value ) = explode( '=', $part, 2 );

								if ( 'charset' === strtolower( trim( $param ) ) ) {
										$parsed['charset'] = trim( $param_value, " 	\n\r\0\x0B\"'" );
								}
							}
						} else {
								$parsed['content_type'] = $value;
						}
						break;
					case 'cc':
							$parsed['cc'] = array_merge( $parsed['cc'], $this->normalize_recipients( $value ) );
						break;
					case 'bcc':
							$parsed['bcc'] = array_merge( $parsed['bcc'], $this->normalize_recipients( $value ) );
						break;
					case 'reply-to':
							$parsed['reply_to'] = array_merge( $parsed['reply_to'], $this->normalize_recipients( $value ) );
						break;
					default:
						if ( '' !== $original_name && '' !== $value ) {
								$parsed['custom'][] = array(
									'name'  => $original_name,
									'value' => $value,
								);
						}
				}
			}

			return $parsed;
	}

		/**
		 * Normalizes the list of recipients into an array of individual addresses.
		 *
		 * @param mixed $recipients Recipient data supplied to wp_mail().
		 *
		 * @return array<int, string>
		 */
	private function normalize_recipients( $recipients ): array {
		if ( empty( $recipients ) ) {
				return array();
		}

		if ( ! is_array( $recipients ) ) {
				$recipients = explode( ',', (string) $recipients );
		}

			$normalized = array();

		foreach ( $recipients as $recipient ) {
			if ( is_array( $recipient ) ) {
					continue;
			}

				$address = trim( (string) $recipient );

			if ( '' !== $address ) {
					$normalized[] = $address;
			}
		}

			return $normalized;
	}

		/**
		 * Ensures the attachments argument is converted into an array of paths.
		 *
		 * @param mixed $attachments Attachment data supplied to wp_mail().
		 *
		 * @return array<int, string>
		 */
	private function normalize_attachments( $attachments ): array {
		if ( empty( $attachments ) ) {
				return array();
		}

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

			$normalized = array();

		foreach ( $attachments as $attachment ) {
			if ( is_array( $attachment ) ) {
					continue;
			}

				$path = trim( (string) $attachment );

			if ( '' !== $path ) {
					$normalized[] = $path;
			}
		}

			return $normalized;
	}

		/**
		 * Retrieves a cached Amazon SES client or creates a new instance.
		 *
		 * @param string $access_key AWS access key ID.
		 * @param string $secret_key AWS secret access key.
		 * @param string $region     AWS region identifier.
		 *
		 * @return SesV2Client
		 */
	private function get_ses_client( string $access_key, string $secret_key, string $region ): SesV2Client {
			$cache_key = \md5( $access_key . '|' . $secret_key . '|' . $region );

		if ( isset( $this->client_cache[ $cache_key ] ) ) {
				return $this->client_cache[ $cache_key ];
		}

			$client = new SesV2Client(
				array(
					'version'     => 'latest',
					'region'      => $region,
					'credentials' => new Credentials( $access_key, $secret_key ),
				)
			);

			$this->client_cache[ $cache_key ] = $client;

			return $client;
	}

		/**
		 * Marks the Amazon SES integration as active.
		 *
		 * @param bool $is_active Current filter value.
		 *
		 * @return bool
		 */
	public function declare_active( bool $is_active ): bool {
			unset( $is_active );

			return true;
	}

		/**
		 * Replaces the SMTP connection fields with Amazon SES specific fields.
		 *
		 * @param array<int, array<string, mixed>> $fields        Registered field definitions.
		 * @param Settings_Page                    $settings_page Settings page instance.
		 *
		 * @return array<int, array<string, mixed>>
		 */
	public function provide_field_definitions( array $fields, Settings_Page $settings_page ): array {
		if ( ! SMTP_Plugin::is_amazon_ses_integration_active() ) {
				return $fields;
		}

			return array(
				array(
					'id'        => 'robotstxt_smtp_amazon_ses_access_key',
					'label'     => \esc_html__( 'Access Key ID', 'robotstxt-smtp-amazonses' ),
					'callback'  => array( $this, 'render_access_key_field' ),
					'label_for' => 'robotstxt_smtp_amazon_ses_access_key',
				),
				array(
					'id'        => 'robotstxt_smtp_amazon_ses_secret_key',
					'label'     => \esc_html__( 'Secret Access Key', 'robotstxt-smtp-amazonses' ),
					'callback'  => array( $this, 'render_secret_key_field' ),
					'label_for' => 'robotstxt_smtp_amazon_ses_secret_key',
				),
				array(
					'id'        => 'robotstxt_smtp_amazon_ses_region',
					'label'     => \esc_html__( 'Region', 'robotstxt-smtp-amazonses' ),
					'callback'  => array( $this, 'render_region_field' ),
					'label_for' => 'robotstxt_smtp_amazon_ses_region',
				),
				array(
					'id'        => 'robotstxt_smtp_from_email',
					'label'     => \esc_html__( 'From Email', 'robotstxt-smtp' ),
					'callback'  => array( $settings_page, 'render_from_email_field' ),
					'label_for' => 'robotstxt_smtp_from_email',
				),
				array(
					'id'        => 'robotstxt_smtp_from_name',
					'label'     => \esc_html__( 'From Name', 'robotstxt-smtp' ),
					'callback'  => array( $settings_page, 'render_from_name_field' ),
					'label_for' => 'robotstxt_smtp_from_name',
				),
			);
	}

		/**
		 * Outputs the Amazon SES access key field.
		 *
		 * @return void
		 */
	public function render_access_key_field(): void {
			$settings = $this->get_stored_settings();
		?>
						<input
								name="<?php echo \esc_attr( $this->get_field_name( self::OPTION_ACCESS_KEY ) ); ?>"
								type="text"
								id="robotstxt_smtp_amazon_ses_access_key"
								class="regular-text"
								value="<?php echo \esc_attr( $settings[ self::OPTION_ACCESS_KEY ] ); ?>"
								autocomplete="off"
						/>
						<p class="description"><?php \esc_html_e( 'Provide the IAM access key ID with permissions to send email through Amazon SES.', 'robotstxt-smtp-amazonses' ); ?></p>
				<?php
	}

		/**
		 * Outputs the Amazon SES secret key field.
		 *
		 * @return void
		 */
	public function render_secret_key_field(): void {
			$settings          = $this->get_stored_settings();
			$has_secret_key    = '' !== $settings[ self::OPTION_SECRET_KEY ];
			$placeholder_label = $has_secret_key ? \__( 'Leave empty to keep the stored secret access key.', 'robotstxt-smtp-amazonses' ) : '';
		?>
						<input
								name="<?php echo \esc_attr( $this->get_field_name( self::OPTION_SECRET_KEY ) ); ?>"
								type="password"
								id="robotstxt_smtp_amazon_ses_secret_key"
								class="regular-text"
								value=""
								autocomplete="new-password"
								placeholder="<?php echo \esc_attr( $placeholder_label ); ?>"
						/>
						<p class="description"><?php \esc_html_e( 'Enter the secret access key associated with the IAM user. Leave the field empty to retain the existing value.', 'robotstxt-smtp-amazonses' ); ?></p>
				<?php
	}

		/**
		 * Outputs the Amazon SES region selector.
		 *
		 * @return void
		 */
	public function render_region_field(): void {
			$settings       = $this->get_stored_settings();
			$current_region = $settings[ self::OPTION_REGION ];
			$regions        = $this->get_region_choices();
		?>
						<select name="<?php echo \esc_attr( $this->get_field_name( self::OPTION_REGION ) ); ?>" id="robotstxt_smtp_amazon_ses_region">
								<option value="" <?php \selected( '', $current_region ); ?>><?php \esc_html_e( 'Select a region', 'robotstxt-smtp-amazonses' ); ?></option>
							<?php foreach ( $regions as $region => $label ) : ?>
										<option value="<?php echo \esc_attr( $region ); ?>" <?php \selected( $current_region, $region ); ?>><?php echo \esc_html( $label ); ?></option>
								<?php endforeach; ?>
						</select>
						<p class="description"><?php \esc_html_e( 'Choose the AWS region where your Amazon SES account is hosted.', 'robotstxt-smtp-amazonses' ); ?></p>
				<?php
	}

		/**
		 * Sanitizes and validates the Amazon SES specific settings.
		 *
		 * @param array<string, mixed> $clean    Sanitized option values prepared by the core plugin.
		 * @param array<string, mixed> $options  Raw submitted option values after `wp_unslash()`.
		 * @param Settings_Page        $settings Settings page instance.
		 *
		 * @return array<string, mixed>
		 */
	public function sanitize_settings( array $clean, array $options, Settings_Page $settings ): array {
			unset( $settings );

		if ( ! SMTP_Plugin::is_amazon_ses_integration_active() ) {
				return $clean;
		}

		$stored_settings         = $this->get_stored_settings();
		$settings_error_slug     = $this->get_settings_option_name();
		$access_key              = $stored_settings[ self::OPTION_ACCESS_KEY ] ?? '';
			$secret_key          = $stored_settings[ self::OPTION_SECRET_KEY ] ?? '';
			$region              = $stored_settings[ self::OPTION_REGION ] ?? '';
			$regions             = $this->get_region_choices();
			$credentials_changed = false;

		if ( array_key_exists( self::OPTION_ACCESS_KEY, $options ) ) {
			$submitted_access_key = \trim( (string) $options[ self::OPTION_ACCESS_KEY ] );

			if ( '' !== $submitted_access_key ) {
					$sanitized_access_key = \sanitize_text_field( $submitted_access_key );

				if ( $sanitized_access_key !== $access_key ) {
					$credentials_changed = true;
				}

					$access_key = $sanitized_access_key;
			}
		}

		if ( array_key_exists( self::OPTION_SECRET_KEY, $options ) ) {
			$submitted_secret_key = \trim( (string) $options[ self::OPTION_SECRET_KEY ] );

			if ( '' !== $submitted_secret_key ) {
					$sanitized_secret_key = \sanitize_text_field( $submitted_secret_key );

				if ( $sanitized_secret_key !== $secret_key ) {
					$credentials_changed = true;
				}

					$secret_key = $sanitized_secret_key;
			}
		}

		if ( array_key_exists( self::OPTION_REGION, $options ) ) {
				$submitted_region = \sanitize_text_field( (string) $options[ self::OPTION_REGION ] );

			if ( '' === $submitted_region ) {
				if ( '' !== $region ) {
					$credentials_changed = true;
				}

					$region = '';
			} elseif ( isset( $regions[ $submitted_region ] ) ) {
				if ( $submitted_region !== $region ) {
						$credentials_changed = true;
				}

					$region = $submitted_region;
			} else {
				\add_settings_error(
					$settings_error_slug,
					self::SETTINGS_ERROR_CREDENTIALS,
					\esc_html__( 'The selected Amazon SES region is not available.', 'robotstxt-smtp-amazonses' ),
					'error'
				);

					return $this->restore_amazon_settings( $clean, $stored_settings );
			}
		}

			$clean[ self::OPTION_ACCESS_KEY ] = $access_key;
			$clean[ self::OPTION_SECRET_KEY ] = $secret_key;
			$clean[ self::OPTION_REGION ]     = $region;

		if ( $credentials_changed && ( '' === $access_key || '' === $secret_key || '' === $region ) ) {
			\add_settings_error(
				$settings_error_slug,
				self::SETTINGS_ERROR_CREDENTIALS,
				\esc_html__( 'Amazon SES credentials were not saved. Provide an access key, secret key, and region.', 'robotstxt-smtp-amazonses' ),
				'error'
			);

				return $this->restore_amazon_settings( $clean, $stored_settings );
		}

		if ( $credentials_changed && '' !== $access_key && '' !== $secret_key && '' !== $region ) {
				$validation_error = $this->validate_credentials( $access_key, $secret_key, $region );

			if ( $validation_error instanceof WP_Error ) {
				\add_settings_error(
					$settings_error_slug,
					self::SETTINGS_ERROR_CREDENTIALS,
					\esc_html( $validation_error->get_error_message() ),
					'error'
				);

					return $this->restore_amazon_settings( $clean, $stored_settings );
			}

			\add_settings_error(
				$settings_error_slug,
				self::SETTINGS_ERROR_CREDENTIALS,
				\esc_html__( 'Amazon SES credentials verified successfully.', 'robotstxt-smtp-amazonses' ),
				'updated'
			);
		}

			return $clean;
	}

		/**
		 * Restores the previously stored Amazon SES values in case validation fails.
		 *
		 * @param array<string, mixed> $clean            Current sanitized values.
		 * @param array<string, mixed> $stored_settings  Stored settings merged with defaults.
		 *
		 * @return array<string, mixed>
		 */
	private function restore_amazon_settings( array $clean, array $stored_settings ): array {
			$clean[ self::OPTION_ACCESS_KEY ] = $stored_settings[ self::OPTION_ACCESS_KEY ] ?? '';
			$clean[ self::OPTION_SECRET_KEY ] = $stored_settings[ self::OPTION_SECRET_KEY ] ?? '';
			$clean[ self::OPTION_REGION ]     = $stored_settings[ self::OPTION_REGION ] ?? '';

			return $clean;
	}

		/**
		 * Retrieves the stored settings merged with defaults for the active scope.
		 *
		 * @return array<string, mixed>
		 */
	private function get_stored_settings(): array {
			$option_name = $this->get_settings_option_name();

		if ( Settings_Page::NETWORK_OPTION_NAME === $option_name ) {
				$settings = \get_site_option( $option_name, array() );
		} else {
				$settings = \get_option( $option_name, array() );
		}

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

			return \wp_parse_args( $settings, Settings_Page::get_default_settings() );
	}

		/**
		 * Builds the settings field name for the current configuration scope.
		 *
		 * @param string $field Field identifier.
		 *
		 * @return string
		 */
	private function get_field_name( string $field ): string {
			return $this->get_settings_option_name() . '[' . $field . ']';
	}

		/**
		 * Determines the correct option name for the current configuration scope.
		 *
		 * @return string
		 */
	private function get_settings_option_name(): string {
		if ( \is_network_admin() && Settings_Page::is_network_mode_enabled() ) {
				return Settings_Page::NETWORK_OPTION_NAME;
		}

			return Settings_Page::OPTION_NAME;
	}

		/**
		 * Retrieves the list of available Amazon SES regions.
		 *
		 * @return array<string, string>
		 */
	private function get_region_choices(): array {
		if ( null !== $this->region_choices ) {
			return $this->region_choices;
		}

		$labels          = $this->get_known_region_labels();
		$choices         = $labels;
		$discovered_list = array();

		try {
			$provider = PartitionEndpointProvider::defaultProvider();

			foreach ( array( 'aws', 'aws-us-gov' ) as $partition_name ) {
				$partition = $provider->getPartitionByName( $partition_name );

				if ( ! $partition ) {
					continue;
				}

				$regions = $partition->getAvailableEndpoints( 'ses' );

				foreach ( $regions as $region ) {
					if ( is_string( $region ) && '' !== $region ) {
						$discovered_list[ $region ] = $labels[ $region ] ?? $this->format_region_label( $region );
					}
				}
			}
		} catch ( Throwable $exception ) {
			unset( $exception );
		}

		foreach ( $discovered_list as $region => $label ) {
			if ( isset( $choices[ $region ] ) ) {
				continue;
			}

			$choices[ $region ] = $label;
		}

		if ( empty( $choices ) ) {
			$choices = array(
				'us-east-1' => $this->format_region_label( 'us-east-1' ),
				'us-west-2' => $this->format_region_label( 'us-west-2' ),
			);
		}

		$this->region_choices = $choices;

		return $this->region_choices;
	}

		/**
		 * Provides translated labels for known Amazon SES regions.
		 *
		 * @return array<string, string>
		 */
	private function get_known_region_labels(): array {
		return array(
			'us-east-2'      => $this->format_named_region_label( \__( 'US East (Ohio)', 'robotstxt-smtp-amazonses' ), 'us-east-2' ),
			'us-east-1'      => $this->format_named_region_label( \__( 'US East (N. Virginia)', 'robotstxt-smtp-amazonses' ), 'us-east-1' ),
			'us-west-1'      => $this->format_named_region_label( \__( 'US West (N. California)', 'robotstxt-smtp-amazonses' ), 'us-west-1' ),
			'us-west-2'      => $this->format_named_region_label( \__( 'US West (Oregon)', 'robotstxt-smtp-amazonses' ), 'us-west-2' ),
			'af-south-1'     => $this->format_named_region_label( \__( 'Africa (Cape Town)', 'robotstxt-smtp-amazonses' ), 'af-south-1' ),
			'ap-south-2'     => $this->format_named_region_label( \__( 'Asia Pacific (Hyderabad)', 'robotstxt-smtp-amazonses' ), 'ap-south-2' ),
			'ap-southeast-3' => $this->format_named_region_label( \__( 'Asia Pacific (Jakarta)', 'robotstxt-smtp-amazonses' ), 'ap-southeast-3' ),
			'ap-south-1'     => $this->format_named_region_label( \__( 'Asia Pacific (Mumbai)', 'robotstxt-smtp-amazonses' ), 'ap-south-1' ),
			'ap-northeast-3' => $this->format_named_region_label( \__( 'Asia Pacific (Osaka)', 'robotstxt-smtp-amazonses' ), 'ap-northeast-3' ),
			'ap-northeast-2' => $this->format_named_region_label( \__( 'Asia Pacific (Seoul)', 'robotstxt-smtp-amazonses' ), 'ap-northeast-2' ),
			'ap-southeast-1' => $this->format_named_region_label( \__( 'Asia Pacific (Singapore)', 'robotstxt-smtp-amazonses' ), 'ap-southeast-1' ),
			'ap-southeast-2' => $this->format_named_region_label( \__( 'Asia Pacific (Sydney)', 'robotstxt-smtp-amazonses' ), 'ap-southeast-2' ),
			'ap-northeast-1' => $this->format_named_region_label( \__( 'Asia Pacific (Tokyo)', 'robotstxt-smtp-amazonses' ), 'ap-northeast-1' ),
			'ca-central-1'   => $this->format_named_region_label( \__( 'Canada (Central)', 'robotstxt-smtp-amazonses' ), 'ca-central-1' ),
			'eu-central-1'   => $this->format_named_region_label( \__( 'Europe (Frankfurt)', 'robotstxt-smtp-amazonses' ), 'eu-central-1' ),
			'eu-west-1'      => $this->format_named_region_label( \__( 'Europe (Ireland)', 'robotstxt-smtp-amazonses' ), 'eu-west-1' ),
			'eu-west-2'      => $this->format_named_region_label( \__( 'Europe (London)', 'robotstxt-smtp-amazonses' ), 'eu-west-2' ),
			'eu-south-1'     => $this->format_named_region_label( \__( 'Europe (Milan)', 'robotstxt-smtp-amazonses' ), 'eu-south-1' ),
			'eu-west-3'      => $this->format_named_region_label( \__( 'Europe (Paris)', 'robotstxt-smtp-amazonses' ), 'eu-west-3' ),
			'eu-north-1'     => $this->format_named_region_label( \__( 'Europe (Stockholm)', 'robotstxt-smtp-amazonses' ), 'eu-north-1' ),
			'eu-central-2'   => $this->format_named_region_label( \__( 'Europe (Zurich)', 'robotstxt-smtp-amazonses' ), 'eu-central-2' ),
			'il-central-1'   => $this->format_named_region_label( \__( 'Israel (Tel Aviv)', 'robotstxt-smtp-amazonses' ), 'il-central-1' ),
			'me-south-1'     => $this->format_named_region_label( \__( 'Middle East (Bahrain)', 'robotstxt-smtp-amazonses' ), 'me-south-1' ),
			'me-central-1'   => $this->format_named_region_label( \__( 'Middle East (UAE)', 'robotstxt-smtp-amazonses' ), 'me-central-1' ),
			'sa-east-1'      => $this->format_named_region_label( \__( 'South America (São Paulo)', 'robotstxt-smtp-amazonses' ), 'sa-east-1' ),
			'us-gov-east-1'  => $this->format_named_region_label( \__( 'AWS GovCloud (US-East)', 'robotstxt-smtp-amazonses' ), 'us-gov-east-1' ),
			'us-gov-west-1'  => $this->format_named_region_label( \__( 'AWS GovCloud (US-West)', 'robotstxt-smtp-amazonses' ), 'us-gov-west-1' ),
		);
	}

		/**
		 * Builds a formatted label for a named region.
		 *
		 * @param string $name Human-readable region name.
		 * @param string $code Region identifier.
		 *
		 * @return string
		 */
	private function format_named_region_label( string $name, string $code ): string {
		return \sprintf(
			/* translators: 1: AWS region display name. 2: AWS region code. */
			\__( '%1$s (%2$s)', 'robotstxt-smtp-amazonses' ),
			$name,
			$code
		);
	}

		/**
		 * Creates a fallback label for an AWS region code.
		 *
		 * @param string $code Region identifier.
		 *
		 * @return string
		 */
	private function format_region_label( string $code ): string {
		return \sprintf(
			/* translators: %s: AWS region code. */
			\__( 'Region %s', 'robotstxt-smtp-amazonses' ),
			$code
		);
	}

		/**
		 * Validates the provided Amazon SES credentials by calling the GetAccount endpoint.
		 *
		 * @param string $access_key Access key ID.
		 * @param string $secret_key Secret access key.
		 * @param string $region     Selected region.
		 *
		 * @return WP_Error|null
		 */
	private function validate_credentials( string $access_key, string $secret_key, string $region ): ?WP_Error {
		try {
				$client = new SesV2Client(
					array(
						'version'     => 'latest',
						'region'      => $region,
						'credentials' => new Credentials( $access_key, $secret_key ),
					)
				);

				$client->getAccount();
		} catch ( AwsException $exception ) {
				$message = $exception->getAwsErrorMessage();

			if ( ! is_string( $message ) || '' === $message ) {
					$message = $exception->getMessage();
			}

				$message = \wp_strip_all_tags( (string) $message );

				return new WP_Error(
					'robotstxt_smtp_amazon_ses_invalid_credentials',
					\sprintf(
								/* translators: %s: Error message from Amazon SES. */
						\esc_html__( 'Could not verify Amazon SES credentials: %s', 'robotstxt-smtp-amazonses' ),
						$message
					)
				);
		} catch ( Throwable $exception ) {
				$message = \wp_strip_all_tags( $exception->getMessage() );

				return new WP_Error(
					'robotstxt_smtp_amazon_ses_unexpected_error',
					\sprintf(
								/* translators: %s: Error message describing the unexpected failure. */
						\esc_html__( 'An unexpected error occurred while validating Amazon SES credentials: %s', 'robotstxt-smtp-amazonses' ),
						$message
					)
				);
		}

			return null;
	}
}
