<?php
/**
 * Helpers for parsing and validating wp_mail headers.
 *
 * @package WP_PowerSuite\Email
 */

namespace WP_PowerSuite\Email;

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

final class MailHeaders {

	/**
	 * @param string|array<int, string> $headers
	 * @return list<string>
	 */
	public static function normalizeLines( $headers ): array {
		if ( is_string( $headers ) ) {
			$headers = preg_split( '/\r\n|\r|\n/', $headers ) ?: [];
		}

		if ( ! is_array( $headers ) ) {
			return [];
		}

		$lines = [];
		foreach ( $headers as $header ) {
			$header = trim( (string) $header );
			if ( $header !== '' ) {
				$lines[] = $header;
			}
		}

		return $lines;
	}

	/**
	 * Store headers without strip_tags(), which removes addresses in angle brackets.
	 */
	public static function sanitizeForStorage( string $headers ): string {
		$headers = str_replace( [ "\0", "\r" ], '', $headers );
		$headers = preg_replace( "/\n{3,}/", "\n\n", $headers ) ?? $headers;

		return trim( $headers );
	}

	/**
	 * Parse wp_mail-style recipients into validated email addresses.
	 *
	 * @param mixed $addresses
	 * @return list<string>
	 */
	public static function parseAddresses( $addresses ): array {
		$emails = [];

		if ( is_array( $addresses ) ) {
			foreach ( $addresses as $key => $entry ) {
				if ( is_array( $entry ) ) {
					$entry = $entry[0] ?? '';
				}

				if ( is_string( $key ) && is_email( $key ) ) {
					$emails[] = sanitize_email( $key );
					continue;
				}

				if ( is_string( $entry ) && is_email( $entry ) ) {
					$emails[] = sanitize_email( $entry );
					continue;
				}

				if ( is_string( $entry ) && $entry !== '' ) {
					$emails = array_merge( $emails, self::parseAddresses( $entry ) );
				}
			}

			return array_values( array_unique( array_filter( $emails ) ) );
		}

		$parts = preg_split( '/[,;]\s*/', (string) $addresses ) ?: [];
		foreach ( $parts as $part ) {
			$part = trim( (string) $part );
			if ( $part === '' ) {
				continue;
			}

			$email = self::extractAddress( $part );
			if ( $email !== '' ) {
				$emails[] = $email;
			}
		}

		return array_values( array_unique( $emails ) );
	}

	public static function extractAddress( string $address ): string {
		$address = trim( $address );
		if ( $address === '' ) {
			return '';
		}

		// RFC-style "Display Name <email@domain>" (also handles quotes).
		if ( preg_match( '/<([^<>]+)>/', $address, $matches ) ) {
			$candidate = trim( $matches[1], " \t\n\r\0\x0B\"'" );
			$candidate = sanitize_email( $candidate );
			return is_email( $candidate ) ? $candidate : '';
		}

		// Bare email — only sanitize when there is no display name noise.
		$trimmed = trim( $address, " \t\n\r\0\x0B\"'" );
		if ( is_email( $trimmed ) ) {
			return sanitize_email( $trimmed );
		}

		// "Display Name email@domain" without angle brackets (common in some plugins).
		// Never sanitize_email() the full string first — it concatenates name+local-part.
		if ( preg_match( '/([a-z0-9._%+\-]+@[a-z0-9.\-]+\.[a-z]{2,})/i', $trimmed, $matches ) ) {
			$candidate = sanitize_email( $matches[1] );
			return is_email( $candidate ) ? $candidate : '';
		}

		return '';
	}

	/**
	 * @param list<string> $headers
	 * @return list<string>
	 */
	public static function dropInvalidAddressHeaders( array $headers ): array {
		$clean = [];

		foreach ( $headers as $header ) {
			if ( self::isValidHeaderLine( (string) $header ) ) {
				$clean[] = $header;
			}
		}

		return $clean;
	}

	public static function isValidHeaderLine( string $header ): bool {
		$header = trim( $header );
		if ( $header === '' || ! str_contains( $header, ':' ) ) {
			return false;
		}

		[ $name, $value ] = array_pad( explode( ':', $header, 2 ), 2, '' );
		$name              = strtolower( trim( $name ) );

		if ( ! in_array( $name, [ 'from', 'reply-to', 'cc', 'bcc' ], true ) ) {
			return true;
		}

		return self::parseAddresses( trim( $value ) ) !== [];
	}

	/**
	 * Apply configured sender headers and drop malformed address headers first.
	 *
	 * @param array<int, string> $headers
	 * @return list<string>
	 */
	public static function applyIdentityHeaders( array $headers ): array {
		$headers = self::dropInvalidAddressHeaders( $headers );

		if ( class_exists( '\EmailSmtp\Includes\SmtpSettingsHelper' ) ) {
			return \EmailSmtp\Includes\SmtpSettingsHelper::prepareOutgoingMailHeaders( $headers );
		}

		if ( class_exists( '\CustomWpEmails\Includes\SystemEmailService' ) ) {
			return \CustomWpEmails\Includes\SystemEmailService::build_mail_headers( $headers );
		}

		return $headers;
	}

	/**
	 * Parse header lines into a case-insensitive name => value map (first wins).
	 *
	 * @param string|array<int, string> $headers
	 * @return array<string, string> Lowercase header name => value.
	 */
	public static function toMap( $headers ): array {
		$map = [];
		foreach ( self::normalizeLines( $headers ) as $line ) {
			if ( ! str_contains( $line, ':' ) ) {
				continue;
			}
			[ $name, $value ] = array_pad( explode( ':', $line, 2 ), 2, '' );
			$key              = strtolower( trim( $name ) );
			if ( $key === '' || isset( $map[ $key ] ) ) {
				continue;
			}
			$map[ $key ] = trim( $value );
		}

		return $map;
	}

	/**
	 * @param string|array<int, string> $headers
	 */
	public static function getHeader( $headers, string $name ): string {
		$map = self::toMap( $headers );
		$key = strtolower( trim( $name ) );

		return $map[ $key ] ?? '';
	}

	/**
	 * Merge header lines. Later lines replace earlier ones for the same header name
	 * (except unknown custom headers, which are appended).
	 *
	 * @param string|array<int, string> $base
	 * @param string|array<int, string> $overlay
	 * @return list<string>
	 */
	public static function mergeLines( $base, $overlay ): array {
		$replaceable = [ 'from', 'reply-to', 'cc', 'bcc', 'content-type', 'content-transfer-encoding', 'mime-version', 'message-id', 'x-mailer', 'return-path' ];
		$out         = [];
		$seen        = [];

		foreach ( self::normalizeLines( $base ) as $line ) {
			if ( ! str_contains( $line, ':' ) ) {
				continue;
			}
			[ $name ] = explode( ':', $line, 2 );
			$key      = strtolower( trim( $name ) );
			$out[]    = $line;
			$seen[ $key ] = count( $out ) - 1;
		}

		foreach ( self::normalizeLines( $overlay ) as $line ) {
			if ( ! str_contains( $line, ':' ) ) {
				continue;
			}
			[ $name ] = explode( ':', $line, 2 );
			$key      = strtolower( trim( $name ) );
			if ( in_array( $key, $replaceable, true ) && isset( $seen[ $key ] ) ) {
				$out[ $seen[ $key ] ] = $line;
				continue;
			}
			if ( isset( $seen[ $key ] ) && in_array( $key, $replaceable, true ) ) {
				continue;
			}
			$out[]        = $line;
			$seen[ $key ] = count( $out ) - 1;
		}

		return array_values( $out );
	}

	/**
	 * Snapshot identity / transport headers from a PHPMailer instance (no secrets).
	 *
	 * @param object $phpmailer PHPMailer instance.
	 * @return array{
	 *   lines:list<string>,
	 *   from_email:string,
	 *   envelope_sender:string,
	 *   message_id:string,
	 *   transport:string,
	 *   provider:string,
	 *   host:string
	 * }
	 */
	public static function snapshotFromPhpMailer( object $phpmailer ): array {
		$lines = [];

		$from_email = '';
		$from_raw   = isset( $phpmailer->From ) ? (string) $phpmailer->From : '';
		if ( is_email( $from_raw ) ) {
			$from_email = sanitize_email( $from_raw );
			$from_name  = isset( $phpmailer->FromName ) ? trim( (string) $phpmailer->FromName ) : '';
			$lines[]    = $from_name !== ''
				? sprintf( 'From: %s <%s>', $from_name, $from_email )
				: sprintf( 'From: %s', $from_email );
		}

		$envelope = '';
		$sender   = isset( $phpmailer->Sender ) ? (string) $phpmailer->Sender : '';
		if ( is_email( $sender ) ) {
			$envelope = sanitize_email( $sender );
		}

		$address_groups = [
			'Reply-To' => self::phpmailerAddressList( $phpmailer, 'getReplyToAddresses', 'ReplyTo' ),
			'Cc'       => self::phpmailerAddressList( $phpmailer, 'getCcAddresses', 'cc' ),
			'Bcc'      => self::phpmailerAddressList( $phpmailer, 'getBccAddresses', 'bcc' ),
		];
		foreach ( $address_groups as $label => $entries ) {
			$parts = [];
			foreach ( $entries as $entry ) {
				$email = (string) ( $entry[0] ?? '' );
				$name  = trim( (string) ( $entry[1] ?? '' ) );
				if ( ! is_email( $email ) ) {
					continue;
				}
				$email   = sanitize_email( $email );
				$parts[] = $name !== ''
					? sprintf( '%s <%s>', $name, $email )
					: $email;
			}
			if ( $parts !== [] ) {
				// One line per header so mergeLines does not keep only the last address.
				$lines[] = $label . ': ' . implode( ', ', $parts );
			}
		}

		$content_type = isset( $phpmailer->ContentType ) ? trim( (string) $phpmailer->ContentType ) : '';
		$charset      = isset( $phpmailer->CharSet ) ? trim( (string) $phpmailer->CharSet ) : '';
		if ( $content_type !== '' ) {
			$lines[] = $charset !== ''
				? sprintf( 'Content-Type: %s; charset=%s', $content_type, $charset )
				: sprintf( 'Content-Type: %s', $content_type );
		}

		$message_id = '';
		if ( isset( $phpmailer->MessageID ) && is_string( $phpmailer->MessageID ) ) {
			$message_id = trim( $phpmailer->MessageID );
			if ( $message_id !== '' ) {
				$lines[] = 'Message-ID: ' . $message_id;
			}
		}

		$custom = [];
		if ( method_exists( $phpmailer, 'getCustomHeaders' ) ) {
			$custom = $phpmailer->getCustomHeaders();
		} elseif ( isset( $phpmailer->CustomHeader ) && is_array( $phpmailer->CustomHeader ) ) {
			$custom = $phpmailer->CustomHeader;
		}
		if ( is_array( $custom ) ) {
			foreach ( $custom as $header ) {
				if ( ! is_array( $header ) || count( $header ) < 2 ) {
					continue;
				}
				$name  = trim( (string) $header[0] );
				$value = trim( (string) $header[1] );
				if ( $name === '' || $value === '' ) {
					continue;
				}
				// Never persist credential-like custom headers.
				if ( preg_match( '/^(authorization|x-api-key|api-?key|x-auth)/i', $name ) ) {
					continue;
				}
				$lines[] = $name . ': ' . $value;
			}
		}

		$mailer = isset( $phpmailer->Mailer ) ? strtolower( trim( (string) $phpmailer->Mailer ) ) : '';
		$host   = isset( $phpmailer->Host ) ? trim( (string) $phpmailer->Host ) : '';
		// Strip credentials accidentally embedded in host (user:pass@host).
		if ( $host !== '' && str_contains( $host, '@' ) ) {
			$host = (string) preg_replace( '#^[^@]+@#', '', $host );
		}
		$host = preg_replace( '/[;\s].*$/', '', $host ) ?? $host;

		$transport = match ( $mailer ) {
			'smtp'     => 'smtp',
			'sendmail' => 'sendmail',
			'qmail'    => 'qmail',
			'mail'     => 'php_mail',
			default    => $mailer !== '' ? sanitize_key( $mailer ) : '',
		};

		$provider = '';
		if ( $transport === 'smtp' && $host !== '' ) {
			$provider = 'external-smtp';
		} elseif ( in_array( $transport, [ 'php_mail', 'sendmail', 'qmail' ], true ) ) {
			$provider = 'php_mail';
		}

		return [
			'lines'           => $lines,
			'from_email'      => $from_email,
			'envelope_sender' => $envelope,
			'message_id'      => $message_id,
			'transport'       => $transport,
			'provider'        => $provider,
			'host'            => substr( sanitize_text_field( $host ), 0, 191 ),
		];
	}

	/**
	 * @return list<array{0:string,1:string}>
	 */
	private static function phpmailerAddressList( object $phpmailer, string $getter, string $property ): array {
		$list = [];
		if ( method_exists( $phpmailer, $getter ) ) {
			$raw = $phpmailer->{$getter}();
			if ( is_array( $raw ) ) {
				$list = $raw;
			}
		} elseif ( isset( $phpmailer->{$property} ) && is_array( $phpmailer->{$property} ) ) {
			$list = $phpmailer->{$property};
		}

		$out = [];
		foreach ( $list as $entry ) {
			if ( ! is_array( $entry ) ) {
				continue;
			}
			$out[] = [
				(string) ( $entry[0] ?? '' ),
				(string) ( $entry[1] ?? '' ),
			];
		}

		return $out;
	}
}
