<?php
/**
 * Normalized outbound email message.
 *
 * @package EmailSmtp
 * @since 1.1.0
 */

namespace EmailSmtp\Includes;

use WP_PowerSuite\Email\MailHeaders;

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

final class EmailMessage {

	/** @var list<string> */
	private array $to = [];

	/** @var list<string> */
	private array $cc = [];

	/** @var list<string> */
	private array $bcc = [];

	private string $subject = '';

	private string $body = '';

	private string $alt_body = '';

	private bool $is_html = false;

	private string $from_email = '';

	private string $from_name = '';

	private string $reply_to_email = '';

	private string $reply_to_name = '';

	private string $return_path = '';

	private string $content_type = 'text/plain';

	private string $charset = 'UTF-8';

	/** @var list<string> */
	private array $attachments = [];

	/** @var array<string, string> */
	private array $custom_headers = [];

	/** @var array<string, mixed> */
	private array $meta = [];

	/**
	 * Build from wp_mail() arguments.
	 *
	 * @param array<string, mixed> $args
	 */
	public static function from_wp_mail_args( array $args ): self {
		$message = new self();
		$message->to      = self::normalize_address_list( $args['to'] ?? '' );
		$message->subject = isset( $args['subject'] ) ? (string) $args['subject'] : '';
		$message->body    = isset( $args['message'] ) ? (string) $args['message'] : '';

		$headers = MailHeaders::normalizeLines( $args['headers'] ?? [] );
		$parsed  = self::parse_headers( $headers );

		$message->cc             = $parsed['cc'];
		$message->bcc            = $parsed['bcc'];
		$message->from_email     = $parsed['from_email'];
		$message->from_name      = $parsed['from_name'];
		$message->reply_to_email = $parsed['reply_to_email'];
		$message->reply_to_name  = $parsed['reply_to_name'];
		$message->content_type   = $parsed['content_type'];
		$message->charset        = $parsed['charset'];
		$message->custom_headers = $parsed['custom'];
		$message->is_html        = stripos( $message->content_type, 'text/html' ) !== false;

		$attachments = $args['attachments'] ?? [];
		if ( is_string( $attachments ) && $attachments !== '' ) {
			$attachments = [ $attachments ];
		}
		if ( is_array( $attachments ) ) {
			foreach ( $attachments as $path ) {
				$path = (string) $path;
				if ( $path !== '' ) {
					$message->attachments[] = $path;
				}
			}
		}

		return $message;
	}

	/**
	 * Normalize wp_mail-style recipients to bare email addresses.
	 *
	 * Important: never run sanitize_email() on "Name <email>" — WordPress strips
	 * spaces and angle brackets and can produce a mangled address like
	 * creativebirdforumhostingtech@gmail.com from "Creative Bird <forumhostingtech@gmail.com>".
	 *
	 * @param mixed $value
	 * @return list<string>
	 */
	public static function normalize_address_list( $value ): array {
		return MailHeaders::parseAddresses( $value );
	}

	/**
	 * @param list<string> $headers
	 * @return array{
	 *   cc: list<string>,
	 *   bcc: list<string>,
	 *   from_email: string,
	 *   from_name: string,
	 *   reply_to_email: string,
	 *   reply_to_name: string,
	 *   content_type: string,
	 *   charset: string,
	 *   custom: array<string, string>
	 * }
	 */
	private static function parse_headers( array $headers ): array {
		$result = [
			'cc'             => [],
			'bcc'            => [],
			'from_email'     => '',
			'from_name'      => '',
			'reply_to_email' => '',
			'reply_to_name'  => '',
			'content_type'   => 'text/plain',
			'charset'        => 'UTF-8',
			'custom'         => [],
		];

		foreach ( $headers as $header ) {
			if ( ! is_string( $header ) || strpos( $header, ':' ) === false ) {
				continue;
			}
			[ $name, $value ] = array_map( 'trim', explode( ':', $header, 2 ) );
			$key              = strtolower( $name );

			switch ( $key ) {
				case 'cc':
					$result['cc'] = array_merge( $result['cc'], self::normalize_address_list( $value ) );
					break;
				case 'bcc':
					$result['bcc'] = array_merge( $result['bcc'], self::normalize_address_list( $value ) );
					break;
				case 'from':
					$parsed = self::parse_name_email( $value );
					$result['from_email'] = $parsed['email'];
					$result['from_name']  = $parsed['name'];
					break;
				case 'reply-to':
					$parsed = self::parse_name_email( $value );
					$result['reply_to_email'] = $parsed['email'];
					$result['reply_to_name']  = $parsed['name'];
					break;
				case 'content-type':
					if ( preg_match( '/([^;]+)/', $value, $m ) ) {
						$result['content_type'] = strtolower( trim( $m[1] ) );
					}
					if ( preg_match( '/charset\s*=\s*["\']?([^"\';\s]+)/i', $value, $m ) ) {
						$result['charset'] = trim( $m[1] );
					}
					break;
				default:
					$result['custom'][ $name ] = $value;
					break;
			}
		}

		$result['cc']  = array_values( array_unique( $result['cc'] ) );
		$result['bcc'] = array_values( array_unique( $result['bcc'] ) );

		return $result;
	}

	/**
	 * @return array{email: string, name: string}
	 */
	private static function parse_name_email( string $value ): array {
		$email = '';
		$name  = '';
		if ( preg_match( '/^(.*)<([^>]+)>$/', trim( $value ), $m ) ) {
			$name  = trim( $m[1], " \t\"'" );
			$email = sanitize_email( trim( $m[2] ) );
		} else {
			$email = MailHeaders::extractAddress( $value );
			if ( $email !== '' && strcasecmp( trim( $value ), $email ) !== 0 ) {
				// Strip the bare email token to recover a display name when present.
				$name = trim( str_ireplace( $email, '', $value ), " \t\"'<>" );
			}
		}

		return [
			'email' => is_email( $email ) ? $email : '',
			'name'  => sanitize_text_field( $name ),
		];
	}

	/** @return list<string> */
	public function get_to(): array {
		return $this->to;
	}

	/** @return list<string> */
	public function get_cc(): array {
		return $this->cc;
	}

	/** @return list<string> */
	public function get_bcc(): array {
		return $this->bcc;
	}

	public function get_subject(): string {
		return $this->subject;
	}

	public function get_body(): string {
		return $this->body;
	}

	public function get_alt_body(): string {
		return $this->alt_body;
	}

	public function is_html(): bool {
		return $this->is_html;
	}

	public function get_from_email(): string {
		return $this->from_email;
	}

	public function get_from_name(): string {
		return $this->from_name;
	}

	public function get_reply_to_email(): string {
		return $this->reply_to_email;
	}

	public function get_reply_to_name(): string {
		return $this->reply_to_name;
	}

	public function get_return_path(): string {
		return $this->return_path;
	}

	public function get_content_type(): string {
		return $this->content_type;
	}

	public function get_charset(): string {
		return $this->charset;
	}

	/** @return list<string> */
	public function get_attachments(): array {
		return $this->attachments;
	}

	/** @return array<string, string> */
	public function get_custom_headers(): array {
		return $this->custom_headers;
	}

	/** @return array<string, mixed> */
	public function get_meta(): array {
		return $this->meta;
	}

	public function with_from( string $email, string $name = '' ): self {
		$clone             = clone $this;
		$clone->from_email = sanitize_email( $email );
		$clone->from_name  = sanitize_text_field( $name );
		return $clone;
	}

	public function with_reply_to( string $email, string $name = '' ): self {
		$clone                  = clone $this;
		$clone->reply_to_email  = sanitize_email( $email );
		$clone->reply_to_name   = sanitize_text_field( $name );
		return $clone;
	}

	public function with_return_path( string $email ): self {
		$clone              = clone $this;
		$clone->return_path = sanitize_email( $email );
		return $clone;
	}

	public function with_alt_body( string $alt ): self {
		$clone           = clone $this;
		$clone->alt_body = $alt;
		return $clone;
	}

	public function with_html( bool $is_html ): self {
		$clone          = clone $this;
		$clone->is_html = $is_html;
		$clone->content_type = $is_html ? 'text/html' : 'text/plain';
		return $clone;
	}

	/**
	 * @param array<string, mixed> $meta
	 */
	public function with_meta( array $meta ): self {
		$clone       = clone $this;
		$clone->meta = array_merge( $this->meta, $meta );
		return $clone;
	}

	/**
	 * Apply shared sender settings (force From / default Reply-To / return path).
	 *
	 * @param array<string, mixed> $sender
	 */
	public function apply_sender_settings( array $sender ): self {
		$clone = clone $this;

		$force_email = ! empty( $sender['force_from_email'] ) && (string) $sender['force_from_email'] !== '0';
		$force_name  = ! empty( $sender['force_from_name'] ) && (string) $sender['force_from_name'] !== '0';
		// Legacy override_from forces both.
		if ( ! empty( $sender['override_from'] ) && (string) $sender['override_from'] !== '0' ) {
			$force_email = true;
			$force_name  = true;
		}

		$configured_email = sanitize_email( (string) ( $sender['from_email'] ?? '' ) );
		$configured_name  = sanitize_text_field( (string) ( $sender['from_name'] ?? '' ) );

		if ( $force_email && is_email( $configured_email ) ) {
			$clone->from_email = $configured_email;
		} elseif ( $clone->from_email === '' && is_email( $configured_email ) ) {
			$clone->from_email = $configured_email;
		}

		if ( $force_name && $configured_name !== '' ) {
			$clone->from_name = $configured_name;
		} elseif ( $clone->from_name === '' && $configured_name !== '' ) {
			$clone->from_name = $configured_name;
		}

		$reply_email = sanitize_email( (string) ( $sender['reply_to_email'] ?? '' ) );
		if ( $clone->reply_to_email === '' && is_email( $reply_email ) ) {
			$clone->reply_to_email = $reply_email;
			$clone->reply_to_name  = sanitize_text_field( (string) ( $sender['reply_to_name'] ?? '' ) );
		}

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

		$content_type = sanitize_text_field( (string) ( $sender['default_content_type'] ?? '' ) );
		if ( $clone->content_type === 'text/plain' && in_array( $content_type, [ 'text/plain', 'text/html' ], true ) ) {
			$clone->content_type = $content_type;
			$clone->is_html      = $content_type === 'text/html';
		}

		$charset = sanitize_text_field( (string) ( $sender['default_charset'] ?? '' ) );
		if ( $charset !== '' ) {
			$clone->charset = $charset;
		}

		return $clone;
	}
}
