<?php
/**
 * Log management helpers for the Mail Doctor admin UI.
 *
 * @package MailDoctor
 */

namespace MailDoctor\Admin\Modules;

use MailDoctor\Admin;
use MailDoctor\Helpers\Formatter;
use MailDoctor\Helpers\Settings;
use MailDoctor\Logger;
use MailDoctor\Logs_Table;
use MailDoctor\Retry;

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

/**
 * Provides rendering and AJAX utilities for email logs.
 */
trait Logs_Trait {


	/**
	 * Render logs table.
	 *
	 * @return void
	 */
	protected static function render_logs_tab(): void {
		$mail_doctor_active_tab = 'logs';
		$mail_doctor_base_url   = self::page_url();

		// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing.
		if ( isset( $_GET['action'] ) && 'view' === $_GET['action'] && ! empty( $_GET['log_id'] ) ) {
			// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only view routing.
			$log_id = absint( $_GET['log_id'] );
			$row    = Logger::get_entry( $log_id );
			
			if ( $row ) {
				$mail_doctor_log_item = self::prepare_log_item( (array) $row );
				include MAIL_DOCTOR_PATH . 'templates/admin/log-view.php';
				return;
			}
		}

		$mail_doctor_logs_table = new Logs_Table();
		$mail_doctor_logs_table->prepare_items();

		include MAIL_DOCTOR_PATH . 'templates/admin/logs-list.php';
	}

	/**
	 * Build sanitized arguments for log queries.
	 *
	 * @return array<string,mixed>
	 */
	public static function get_log_query_args(): array {
		$args = array();

		if ( isset( $_GET['status'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$status = sanitize_key( wp_unslash( $_GET['status'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			if ( $status ) {
				$args['status'] = $status;
			}
		}

		if ( isset( $_GET['event'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$event = sanitize_key( wp_unslash( $_GET['event'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			if ( $event ) {
				$args['event'] = $event;
			}
		}

		if ( isset( $_GET['recipient'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$recipient = sanitize_text_field( wp_unslash( $_GET['recipient'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			if ( $recipient ) {
				$args['recipient_domain'] = $recipient;
			}
		}

		if ( isset( $_GET['provider'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$provider = sanitize_text_field( wp_unslash( $_GET['provider'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			if ( $provider ) {
				$args['provider_pattern'] = $provider;
			}
		}

		if ( isset( $_GET['date_from'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$date_from_raw = sanitize_text_field( wp_unslash( $_GET['date_from'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$date_from     = self::normalize_date_param( $date_from_raw );
			if ( $date_from ) {
				$args['date_from'] = $date_from;
			}
		}

		if ( isset( $_GET['date_to'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$date_to_raw = sanitize_text_field( wp_unslash( $_GET['date_to'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$date_to     = self::normalize_date_param( $date_to_raw );
			if ( $date_to ) {
				$args['date_to'] = $date_to;
			}
		}

		if ( isset( $_GET['s'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			$search = sanitize_text_field( wp_unslash( $_GET['s'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- view context.
			if ( $search ) {
				$args['search'] = $search;
			}
		}

		/**
		 * Filter log query arguments assembled from the admin screen.
		 *
		 * @since 1.3.0
		 *
		 * @param array<string,mixed> $args Log query arguments.
		 */
		return apply_filters( 'mail_doctor_log_query_args', $args );
	}

	/**
	 * Normalize a Y-m-d string.
	 *
	 * @param string $value Raw date string.
	 * @return string|null
	 */
	protected static function normalize_date_param( string $value ): ?string {
		$value = trim( $value );

		if ( '' === $value ) {
			return null;
		}

		if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) {
			return null;
		}

		list($year, $month, $day) = array_map( 'intval', explode( '-', $value ) );

		return wp_checkdate( $month, $day, $year, $value ) ? $value : null;
	}

	/**
	 * Normalize a raw log row for UI rendering.
	 *
	 * @param array<string,mixed> $row Raw database row.
	 * @return array<string,mixed>
	 */
	public static function prepare_log_item( array $row ): array {
		$id      = isset( $row['id'] ) ? (int) $row['id'] : 0;
		$created = isset( $row['created_at'] ) ? (string) $row['created_at'] : '';

		$time_display = $created ? Formatter::local_time( $created ) : '';
		if ( '' === $time_display && $created ) {
			$time_display = sanitize_text_field( $created );
		}

		$payload = array();
		if ( isset( $row['payload'] ) && $row['payload'] ) {
			$decoded = json_decode( (string) $row['payload'], true );
			if ( is_array( $decoded ) ) {
				$payload = $decoded;
			}
		} elseif ( $id ) {
			$payload = Logger::get_payload( $id );
		}

		$meta      = isset( $payload['meta'] ) && is_array( $payload['meta'] ) ? $payload['meta'] : array();
		$retry     = isset( $payload['retry'] ) && is_array( $payload['retry'] ) ? $payload['retry'] : array();
		$obfuscate = Settings::privacy_mode_enabled();

		$recipients        = self::resolve_recipients( $meta, $row['to_email'] ?? '', $obfuscate );
		$recipient_display = trim( implode( ', ', $recipients ) );
		$status            = sanitize_key( $row['status'] ?? 'queued' );
		if ( '' === $status ) {
			$status = 'queued';
		}

		switch ( $status ) {
			case 'sent':
				$status_label = __( 'Sent', 'mail-doctor' );
				break;
			case 'failed':
				$status_label = __( 'Failed', 'mail-doctor' );
				break;
			case 'skipped':
				$status_label = __( 'Skipped', 'mail-doctor' );
				break;
			default:
				$status_label = __( 'Queued', 'mail-doctor' );
				$status       = 'queued';
				break;
		}

		if ( $obfuscate && '' === $recipient_display ) {
			$recipient_display = __( '[redacted]', 'mail-doctor' );
		}

		$subject  = isset( $row['subject'] ) ? sanitize_text_field( $row['subject'] ) : '';
		$error    = isset( $row['error'] ) ? sanitize_textarea_field( (string) $row['error'] ) : '';
		$provider = isset( $row['provider_code'] ) ? sanitize_text_field( (string) $row['provider_code'] ) : '';
		$latency  = isset( $row['duration_ms'] ) ? max( 0, (int) $row['duration_ms'] ) : 0;
		$attempts = isset( $retry['attempts'] ) ? max( 0, (int) $retry['attempts'] ) : 0;

		$transport_key   = isset( $meta['transport'] ) ? sanitize_key( (string) $meta['transport'] ) : '';
		$transport_label = isset( $meta['transport_label'] ) ? sanitize_text_field( (string) $meta['transport_label'] ) : '';
		$profile_slug    = isset( $meta['smtp_profile'] ) ? sanitize_text_field( (string) $meta['smtp_profile'] ) : '';
		$profile_label   = isset( $meta['smtp_profile_label'] ) ? sanitize_text_field( (string) $meta['smtp_profile_label'] ) : '';
		$smtp_host       = isset( $meta['smtp_host'] ) ? sanitize_text_field( (string) $meta['smtp_host'] ) : '';
		$envelope        = isset( $meta['envelope'] ) && is_array( $meta['envelope'] ) ? $meta['envelope'] : array();
		$content_meta    = isset( $meta['content'] ) && is_array( $meta['content'] ) ? $meta['content'] : array();
		$transport_debug = isset( $meta['transport_debug'] ) && is_array( $meta['transport_debug'] ) ? $meta['transport_debug'] : array();
		$environment     = isset( $meta['environment'] ) && is_array( $meta['environment'] ) ? $meta['environment'] : array();

		if ( '' === $transport_label && '' !== $transport_key ) {
			$transport_label = Settings::transport_label( $transport_key );
		}

		$transport_display = $transport_label;

		if ( $transport_key && 'smtp' === $transport_key && $smtp_host ) {
			$transport_display = $transport_display
				? sprintf(
					/* translators: 1: delivery method label, 2: SMTP host */
					__( '%1$s via %2$s', 'mail-doctor' ),
					$transport_display,
					$smtp_host
				)
				: $smtp_host;
		}

		if ( '' === $transport_display && '' !== $transport_key ) {
			$transport_display = Settings::transport_label( $transport_key );
		}

		$profile_display = $profile_label;

		if ( '' === $profile_display && '' !== $profile_slug ) {
			$profile_display = $profile_slug;
		}

		$headers    = self::format_headers_for_modal( $row, $payload, $obfuscate );
		$history    = self::format_attempt_history( isset( $retry['history'] ) && is_array( $retry['history'] ) ? $retry['history'] : array() );
		$next_retry = '';
		if ( ! empty( $retry['next_attempt_gmt'] ) ) {
			$next_retry = Formatter::local_time( (string) $retry['next_attempt_gmt'] );
		}

		$from        = isset( $envelope['from'] ) ? sanitize_text_field( (string) $envelope['from'] ) : '';
		$reply_to    = isset( $envelope['reply_to'] ) ? sanitize_text_field( (string) $envelope['reply_to'] ) : '';
		$return_path = isset( $envelope['return_path'] ) ? sanitize_text_field( (string) $envelope['return_path'] ) : '';

		$content_hash            = isset( $content_meta['hash'] ) ? sanitize_text_field( (string) $content_meta['hash'] ) : '';
		$content_preview         = isset( $content_meta['preview'] ) ? sanitize_textarea_field( (string) $content_meta['preview'] ) : '';
		$transport_debug_display = self::format_transport_debug( $transport_debug );
		$environment_display     = self::format_environment_meta( $environment );
		
		$attachment_count = isset( $envelope['attachments'] ) && is_array( $envelope['attachments'] ) ? count( $envelope['attachments'] ) : 0;
		$mailer_name      = '';
		$message_id       = '';
		
		if ( isset( $payload['headers'] ) && is_array( $payload['headers'] ) ) {
			$raw_headers = array_change_key_case( $payload['headers'], CASE_LOWER );
			if ( isset( $raw_headers['x-mailer'][0] ) ) {
				$mailer_name = sanitize_text_field( $raw_headers['x-mailer'][0] );
			}
			if ( isset( $raw_headers['message-id'][0] ) ) {
				$message_id = sanitize_text_field( $raw_headers['message-id'][0] );
			}
		}

		$modal = array(
			'status'          => $status_label,
			'event'           => isset( $row['event'] ) ? sanitize_text_field( (string) $row['event'] ) : '',
			'email'           => $recipient_display ?: __( 'N/A', 'mail-doctor' ),
			'subject'         => $subject ?: __( 'N/A', 'mail-doctor' ),
			'provider'        => $provider ?: __( 'N/A', 'mail-doctor' ),
			'attempts'        => number_format_i18n( $attempts ),
			'latency'         => $latency ? sprintf(
				/* translators: %s: duration in milliseconds. */
				__( '%s ms', 'mail-doctor' ),
				number_format_i18n( $latency )
			) : __( 'N/A', 'mail-doctor' ),
			'headers'         => $headers,
			'error'           => $error ?: __( 'N/A', 'mail-doctor' ),
			'transport'       => $transport_display,
			'profile'         => $profile_display,
			'from'            => $from ?: __( 'N/A', 'mail-doctor' ),
			'replyTo'         => $reply_to ?: __( 'N/A', 'mail-doctor' ),
			'returnPath'      => $return_path ?: __( 'N/A', 'mail-doctor' ),
			'nextRetry'       => $next_retry ?: __( 'Not scheduled', 'mail-doctor' ),
			'history'         => $history ?: __( 'No attempts recorded yet.', 'mail-doctor' ),
			'transportDebug'  => $transport_debug_display ?: __( 'Not recorded', 'mail-doctor' ),
			'contentHash'     => $content_hash ?: __( 'N/A', 'mail-doctor' ),
			'bodyPreview'     => $content_preview ?: __( 'Not recorded', 'mail-doctor' ),
			'environment'     => $environment_display ?: __( 'N/A', 'mail-doctor' ),
			'logId'           => $id,
			'attachmentCount' => $attachment_count,
			'mailerName'      => $mailer_name ?: __( 'WordPress / MailDoctor', 'mail-doctor' ),
			'messageId'       => $message_id ?: __( 'N/A', 'mail-doctor' ),
		);

		return array(
			'id'                 => $id,
			'time'               => $created,
			'time_display'       => $time_display,
			'time_export'        => $time_display ? $time_display : $created,
			'event'              => isset( $row['event'] ) ? sanitize_text_field( (string) $row['event'] ) : '',
			'recipient'          => $recipient_display,
			'subject'            => $subject,
			'status'             => $status,
			'status_label'       => $status_label,
			'latency'            => $latency,
			'attempts'           => $attempts,
			'provider_code'      => $provider,
			'error'              => $error,
			'transport'          => $transport_key,
			'transport_label'    => $transport_display,
			'smtp_profile'       => $profile_slug,
			'smtp_profile_label' => $profile_label,
			'smtp_host'          => $smtp_host,
			'modal'              => $modal,
		);
	}

	/**
	 * Resolve header string for modal display.
	 *
	 * @param array<string,mixed> $row     Raw database row.
	 * @param array<string,mixed> $payload Decoded payload metadata.
	 * @param bool                $obfuscate Whether sensitive data should be hidden.
	 * @return string
	 */
	protected static function format_headers_for_modal( array $row, array $payload, bool $obfuscate ): string {
		if ( $obfuscate ) {
			return __( '[redacted]', 'mail-doctor' );
		}

		$headers = '';

		if ( ! empty( $row['headers'] ) ) {
			$headers = (string) $row['headers'];
		} elseif ( isset( $payload['mail_args']['headers'] ) ) {
			$raw_headers = $payload['mail_args']['headers'];
			if ( is_array( $raw_headers ) ) {
				$headers = implode( "\n", array_map( 'sanitize_text_field', $raw_headers ) );
			} else {
				$headers = sanitize_textarea_field( (string) $raw_headers );
			}
		}

		$headers = trim( $headers );

		return '' === $headers ? __( 'Not recorded', 'mail-doctor' ) : $headers;
	}

	/**
	 * Human readable attempt history for modal display.
	 *
	 * @param array<int,array<string,mixed>> $history Attempt history entries.
	 * @return string
	 */
	protected static function format_attempt_history( array $history ): string {
		if ( empty( $history ) ) {
			return '';
		}

		usort(
			$history,
			static function ( $a, $b ) {
				return strcmp( (string) ( $a['occurred_at_gmt'] ?? '' ), (string) ( $b['occurred_at_gmt'] ?? '' ) );
			}
		);

		$lines = array();

		foreach ( $history as $entry ) {
			$attempt       = isset( $entry['attempt'] ) ? (int) $entry['attempt'] : 0;
			$status        = isset( $entry['status'] ) ? ucfirst( sanitize_text_field( (string) $entry['status'] ) ) : '';
			$when          = isset( $entry['occurred_at_gmt'] ) ? Formatter::local_time( (string) $entry['occurred_at_gmt'] ) : '';
			$duration      = isset( $entry['duration_ms'] ) && null !== $entry['duration_ms']
				? Formatter::duration_ms( (int) $entry['duration_ms'] )
				: '';
			$provider_code = isset( $entry['provider_code'] ) && $entry['provider_code']
				/* translators: %s: Provider error code */
				? sprintf( __( 'Code: %s', 'mail-doctor' ), sanitize_text_field( (string) $entry['provider_code'] ) )
				: '';
			$error = isset( $entry['error'] ) ? sanitize_textarea_field( (string) $entry['error'] ) : '';

			$parts = array_filter( array( $status, $when, $duration, $provider_code, $error ) );

			$lines[] = sprintf(
				/* translators: 1: attempt number, 2: details */
				__( 'Attempt %1$d — %2$s', 'mail-doctor' ),
				$attempt,
				implode( ' • ', $parts )
			);
		}

		return implode( "\n", $lines );
	}

	/**
	 * Flatten transport debug metadata into readable text.
	 *
	 * @param array<string,mixed> $debug Debug metadata.
	 * @return string
	 */
	protected static function format_transport_debug( array $debug ): string {
		if ( empty( $debug ) ) {
			return '';
		}

		$parts = array();

		if ( isset( $debug['status'] ) && $debug['status'] ) {
			/* translators: %s: HTTP status code */
			$parts[] = sprintf( __( 'HTTP %s', 'mail-doctor' ), sanitize_text_field( (string) $debug['status'] ) );
		}

		if ( isset( $debug['provider_message'] ) && $debug['provider_message'] ) {
			$parts[] = sprintf(
				/* translators: %s: Provider message ID */
				__( 'Provider id: %s', 'mail-doctor' ),
				sanitize_text_field( (string) $debug['provider_message'] )
			);
		}

		if ( isset( $debug['error_code'] ) && $debug['error_code'] ) {
			/* translators: %s: Error code */
			$parts[] = sprintf( __( 'Error: %s', 'mail-doctor' ), sanitize_text_field( (string) $debug['error_code'] ) );
		}

		if ( isset( $debug['response_digest'] ) && $debug['response_digest'] ) {
			$size = '';
			if ( isset( $debug['response_bytes'] ) ) {
				$size = sprintf(
					/* translators: %d: byte count. */
					__( ' (%d bytes)', 'mail-doctor' ),
					(int) $debug['response_bytes']
				);
			}
			$parts[] = sprintf(
				/* translators: 1: hash, 2: size info */
				__( 'Body hash %1$s%2$s', 'mail-doctor' ),
				sanitize_text_field( (string) $debug['response_digest'] ),
				$size
			);
		} elseif ( isset( $debug['response_bytes'] ) && $debug['response_bytes'] ) {
			$parts[] = sprintf(
				/* translators: %d: byte count. */
				__( 'Response size: %d bytes', 'mail-doctor' ),
				(int) $debug['response_bytes']
			);
		}

		if ( isset( $debug['response_excerpt'] ) && $debug['response_excerpt'] ) {
			$parts[] = sanitize_textarea_field( (string) $debug['response_excerpt'] );
		}

		return implode( "\n", array_filter( $parts ) );
	}

	/**
	 * Summarize environment metadata.
	 *
	 * @param array<string,mixed> $environment Environment meta.
	 * @return string
	 */
	protected static function format_environment_meta( array $environment ): string {
		if ( empty( $environment ) ) {
			return '';
		}

		$parts = array();

		if ( ! empty( $environment['plugin_version'] ) ) {
			$parts[] = sprintf(
				/* translators: %s: Plugin version */
				__( 'Plugin %s', 'mail-doctor' ),
				sanitize_text_field( (string) $environment['plugin_version'] )
			);
		}

		if ( ! empty( $environment['wp_version'] ) ) {
			$parts[] = sprintf(
				/* translators: %s: WordPress version */
				__( 'WP %s', 'mail-doctor' ),
				sanitize_text_field( (string) $environment['wp_version'] )
			);
		}

		if ( ! empty( $environment['php_version'] ) ) {
			$parts[] = sprintf(
				/* translators: %s: PHP version */
				__( 'PHP %s', 'mail-doctor' ),
				sanitize_text_field( (string) $environment['php_version'] )
			);
		}

		if ( ! empty( $environment['async_mode'] ) ) {
			$parts[] = sprintf(
				/* translators: %s: Async mode status */
				__( 'Async: %s', 'mail-doctor' ),
				sanitize_text_field( (string) $environment['async_mode'] )
			);
		}

		return implode( ' • ', array_filter( $parts ) );
	}

	/**
	 * Extract recipient list based on meta and stored values.
	 *
	 * @param array<string,mixed> $meta      Log metadata.
	 * @param mixed               $raw       Stored recipients column.
	 * @param bool                $obfuscate Whether privacy mode is active.
	 * @return array<int, string>
	 */
	protected static function resolve_recipients( array $meta, $raw, bool $obfuscate ): array {
		if ( ! $obfuscate && ! empty( $meta['recipients_plain'] ) && is_array( $meta['recipients_plain'] ) ) {
			$list = array();
			foreach ( $meta['recipients_plain'] as $value ) {
				$email = sanitize_email( (string) $value );
				if ( $email ) {
					$list[] = $email;
				}
			}

			if ( ! empty( $list ) ) {
				return array_values( array_unique( $list ) );
			}
		}

		if ( ! empty( $meta['recipients_hash'] ) && is_array( $meta['recipients_hash'] ) ) {
			$list = array();
			foreach ( $meta['recipients_hash'] as $value ) {
				$hash = sanitize_text_field( (string) $value );
				if ( $hash ) {
					$list[] = $hash;
				}
			}

			if ( ! empty( $list ) ) {
				return array_values( array_unique( $list ) );
			}
		}

		if ( is_array( $raw ) ) {
			$list = array();
			foreach ( $raw as $value ) {
				$email = sanitize_email( (string) $value );
				if ( $email ) {
					$list[] = $email;
				}
			}

			if ( ! empty( $list ) ) {
				return array_values( array_unique( $list ) );
			}
		}

		$email = sanitize_email( (string) $raw );

		return $email ? array( $email ) : array();
	}

	/**
	 * AJAX handler: queue retry for specific log entry.
	 *
	 * @return void
	 */
	public static function ajax_retry_email(): void {
check_ajax_referer( 'mail_doctor_retry_nonce' );
		self::assert_capability();

		$log_id = isset( $_POST['log_id'] ) ? absint( wp_unslash( $_POST['log_id'] ) ) : 0;

		if ( ! $log_id ) {
			wp_send_json_error(
				array(
					'message' => esc_html__( 'Missing log entry identifier.', 'mail-doctor' ),
				),
				400
			);
		}

		$result = Retry::enqueue( $log_id, 0 );

		if ( is_wp_error( $result ) ) {
			wp_send_json_error(
				array(
					'message' => $result->get_error_message(),
				),
				400
			);
		}

		wp_send_json_success(
			array(
				'message' => esc_html__( 'Retry request queued.', 'mail-doctor' ),
			)
		);
	}

	/**
	 * AJAX handler: resend an email log.
	 *
	 * @return void
	 */
	public static function ajax_resend_email(): void {
check_ajax_referer( 'mail_doctor_resend_nonce' );
		self::assert_capability();

		$log_id = isset( $_POST['log_id'] ) ? absint( wp_unslash( $_POST['log_id'] ) ) : 0;
		$recipient = isset( $_POST['recipient'] ) ? sanitize_text_field( wp_unslash( $_POST['recipient'] ) ) : '';

		if ( ! $log_id ) {
			wp_send_json_error(
				array(
					'message' => esc_html__( 'Missing log entry identifier.', 'mail-doctor' ),
				),
				400
			);
		}

		if ( '' === trim( $recipient ) ) {
			$result = Retry::manual_retry( $log_id );
		} else {
			$result = Retry::manual_resend_to( $log_id, $recipient );
		}

		if ( is_wp_error( $result ) ) {
			wp_send_json_error(
				array(
					'message' => $result->get_error_message(),
				),
				400
			);
		}

		$message = '' === trim( $recipient )
			? esc_html__( 'Resend queued to the original recipient.', 'mail-doctor' )
			: esc_html__( 'Manual resend queued.', 'mail-doctor' );

		wp_send_json_success(
			array(
				'message' => $message,
			)
		);
	}





	/**
	 * Parse recipients into an array.
	 *
	 * @param mixed $raw Raw recipient value.
	 * @return array<int,string>
	 */
	protected static function parse_recipient_list( $raw ): array {
		if ( empty( $raw ) ) {
			return array();
		}

		if ( is_array( $raw ) ) {
			$list = $raw;
		} else {
			$list = explode( ',', (string) $raw );
		}

		$recipients = array();

		foreach ( $list as $item ) {
			$email = sanitize_email( trim( (string) $item ) );
			if ( $email && is_email( $email ) ) {
				$recipients[] = $email;
			}
		}

		return array_values( array_unique( $recipients ) );
	}

	/**
	 * Normalize header data into name/value map.
	 *
	 * @param mixed  $headers  Stored header data.
	 * @param string $fallback Fallback serialized headers.
	 * @return array<string, array<int,string>>
	 */
	protected static function normalize_header_map( $headers, string $fallback ): array {
		$lines = array();

		if ( is_string( $headers ) && '' !== trim( $headers ) ) {
			$lines = array_map( 'trim', preg_split( '/\r\n|\r|\n/', $headers ) ?: array() );
		} elseif ( is_array( $headers ) ) {
			foreach ( $headers as $key => $value ) {
				if ( is_string( $key ) ) {
					$lines[] = trim( $key . ': ' . $value );
				} elseif ( is_string( $value ) ) {
					$lines[] = trim( $value );
				}
			}
		}

		if ( empty( $lines ) && '' !== trim( $fallback ) ) {
			$lines = array_map( 'trim', preg_split( '/\r\n|\r|\n/', $fallback ) ?: array() );
		}

		$map = array();

		foreach ( $lines as $line ) {
			if ( '' === $line || false === strpos( $line, ':' ) ) {
				continue;
			}

			list($name, $value) = array_map( 'trim', explode( ':', $line, 2 ) );

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

			$lower = strtolower( $name );

			if ( ! isset( $map[ $lower ] ) ) {
				$map[ $lower ] = array();
			}

			$map[ $lower ][] = $value;
		}

		return $map;
	}

	/**
	 * Extract sender information from headers.
	 *
	 * @param array<string,array<int,string>> $headers Normalized headers.
	 * @return array{email:string|null,name:string|null}
	 */
	protected static function extract_from_header( array $headers ): array {
		$email = null;
		$name  = null;

		if ( isset( $headers['from'][0] ) ) {
			$raw = $headers['from'][0];
			if ( preg_match( '/(.*)<(.+?)>/', $raw, $matches ) ) {
				$name  = trim( $matches[1], "\" \t" );
				$email = sanitize_email( $matches[2] );
			} else {
				$email = sanitize_email( $raw );
			}
		}

		if ( $name && '' !== $name ) {
			$name = wp_specialchars_decode( $name, ENT_QUOTES );
		} else {
			$name = null;
		}

		if ( ! $email || ! is_email( $email ) ) {
			$email = null;
		}

		return array(
			'email' => $email,
			'name'  => $name,
		);
	}

	/**
	 * Determine content type and charset for the export.
	 *
	 * @param array<string,array<int,string>> $headers Normalized headers.
	 * @param string                          $message Message body.
	 * @return array{type:string,charset:string}
	 */
	protected static function detect_content_type( array $headers, string $message ): array {
		$type    = 'text/plain';
		$charset = 'UTF-8';

		if ( isset( $headers['content-type'][0] ) ) {
			$parts = explode( ';', $headers['content-type'][0] );
			if ( ! empty( $parts[0] ) ) {
				$type = strtolower( trim( $parts[0] ) );
			}

			foreach ( $parts as $part ) {
				$part = strtolower( trim( $part ) );
				if ( str_starts_with( $part, 'charset=' ) ) {
					$charset = strtoupper( trim( substr( $part, 8 ) ) );
					break;
				}
			}
		} elseif ( false !== stripos( $message, '<html' ) || false !== stripos( $message, '<p' ) ) {
			$type = 'text/html';
		}

		return array(
			'type'    => $type,
			'charset' => $charset,
		);
	}

	/**
	 * Apply custom headers to the PHPMailer instance.
	 *
	 * @param \PHPMailer\PHPMailer\PHPMailer  $mailer   Mailer instance.
	 * @param array<string,array<int,string>> $headers Header map.
	 * @param array<int,string>               $skip    List of header names to skip (lowercase).
	 * @return void
	 */
	protected static function apply_custom_headers( \PHPMailer\PHPMailer\PHPMailer $mailer, array $headers, array $skip ): void {
		foreach ( $headers as $name => $values ) {
			if ( in_array( $name, $skip, true ) ) {
				continue;
			}

			foreach ( $values as $value ) {
				$mailer->addCustomHeader( $name, $value );
			}
		}
	}

	/**
	 * Attach files to PHPMailer instance if available.
	 *
	 * @param \PHPMailer\PHPMailer\PHPMailer $mailer      Mailer instance.
	 * @param mixed                          $attachments Stored attachments.
	 * @return void
	 */
	protected static function attach_files( \PHPMailer\PHPMailer\PHPMailer $mailer, $attachments ): void {
		if ( empty( $attachments ) ) {
			return;
		}

		$list = is_array( $attachments ) ? $attachments : array( $attachments );

		foreach ( $list as $attachment ) {
			$path = '';

			if ( is_array( $attachment ) && isset( $attachment['file'] ) ) {
				$path = (string) $attachment['file'];
			} elseif ( is_string( $attachment ) ) {
				$path = $attachment;
			}

			if ( ! $path ) {
				continue;
			}

			$path = wp_normalize_path( $path );

			if ( file_exists( $path ) && is_readable( $path ) ) {
				try {
					$mailer->addAttachment( $path, basename( $path ) );
				} catch ( \Exception $exception ) {
					$mailer->addCustomHeader( 'X-MailDoctor-Attachment-Notice', sprintf( 'Unable to attach %s: %s', basename( $path ), $exception->getMessage() ) );
				}
			} else {
				$mailer->addCustomHeader( 'X-MailDoctor-Attachment-Notice', sprintf( 'Attachment missing: %s', basename( $path ) ) );
			}
		}
	}
}
