package cn.hutool.extra.mail;

import cn.hutool.core.builder.Builder;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.io.IORuntimeException;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;

import jakarta.activation.DataHandler;
import jakarta.activation.DataSource;
import jakarta.activation.FileDataSource;
import jakarta.activation.FileTypeMap;
import jakarta.mail.*;
import jakarta.mail.internet.MimeBodyPart;
import jakarta.mail.internet.MimeMessage;
import jakarta.mail.internet.MimeMultipart;
import jakarta.mail.internet.MimeUtility;
import jakarta.mail.util.ByteArrayDataSource;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.util.Date;

/**
 * 邮件发送客户端
 *
 * @author looly
 * @since 3.2.0
 */
public class JakartaMail implements Builder<MimeMessage> {
	private static final long serialVersionUID = 1L;

	/**
	 * 邮箱帐户信息以及一些客户端配置信息
	 */
	private final MailAccount mailAccount;
	/**
	 * 收件人列表
	 */
	private String[] tos;
	/**
	 * 抄送人列表（carbon copy）
	 */
	private String[] ccs;
	/**
	 * 密送人列表（blind carbon copy）
	 */
	private String[] bccs;
	/**
	 * 回复地址(reply-to)
	 */
	private String[] reply;
	/**
	 * 标题
	 */
	private String title;
	/**
	 * 内容
	 */
	private String content;
	/**
	 * 是否为HTML
	 */
	private boolean isHtml;
	/**
	 * 正文、附件和图片的混合部分
	 */
	private final Multipart multipart = new MimeMultipart();
	/**
	 * 是否使用全局会话，默认为false
	 */
	private boolean useGlobalSession = false;

	/**
	 * debug输出位置，可以自定义debug日志
	 */
	private PrintStream debugOutput;

	/**
	 * 创建邮件客户端
	 *
	 * @param mailAccount 邮件帐号
	 * @return Mail
	 */
	public static JakartaMail create(MailAccount mailAccount) {
		return new JakartaMail(mailAccount);
	}

	/**
	 * 创建邮件客户端，使用全局邮件帐户
	 *
	 * @return Mail
	 */
	public static JakartaMail create() {
		return new JakartaMail();
	}

	// --------------------------------------------------------------- Constructor start

	/**
	 * 构造，使用全局邮件帐户
	 */
	public JakartaMail() {
		this(GlobalMailAccount.INSTANCE.getAccount());
	}

	/**
	 * 构造
	 *
	 * @param mailAccount 邮件帐户，如果为null使用默认配置文件的全局邮件配置
	 */
	public JakartaMail(MailAccount mailAccount) {
		mailAccount = (null != mailAccount) ? mailAccount : GlobalMailAccount.INSTANCE.getAccount();
		this.mailAccount = mailAccount.defaultIfEmpty();
	}
	// --------------------------------------------------------------- Constructor end

	// --------------------------------------------------------------- Getters and Setters start

	/**
	 * 设置收件人
	 *
	 * @param tos 收件人列表
	 * @return this
	 * @see #setTos(String...)
	 */
	public JakartaMail to(String... tos) {
		return setTos(tos);
	}

	/**
	 * 设置多个收件人
	 *
	 * @param tos 收件人列表
	 * @return this
	 */
	public JakartaMail setTos(String... tos) {
		this.tos = tos;
		return this;
	}

	/**
	 * 设置多个抄送人（carbon copy）
	 *
	 * @param ccs 抄送人列表
	 * @return this
	 * @since 4.0.3
	 */
	public JakartaMail setCcs(String... ccs) {
		this.ccs = ccs;
		return this;
	}

	/**
	 * 设置多个密送人（blind carbon copy）
	 *
	 * @param bccs 密送人列表
	 * @return this
	 * @since 4.0.3
	 */
	public JakartaMail setBccs(String... bccs) {
		this.bccs = bccs;
		return this;
	}

	/**
	 * 设置多个回复地址(reply-to)
	 *
	 * @param reply 回复地址(reply-to)列表
	 * @return this
	 * @since 4.6.0
	 */
	public JakartaMail setReply(String... reply) {
		this.reply = reply;
		return this;
	}

	/**
	 * 设置标题
	 *
	 * @param title 标题
	 * @return this
	 */
	public JakartaMail setTitle(String title) {
		this.title = title;
		return this;
	}

	/**
	 * 设置正文<br>
	 * 正文可以是普通文本也可以是HTML（默认普通文本），可以通过调用{@link #setHtml(boolean)} 设置是否为HTML
	 *
	 * @param content 正文
	 * @return this
	 */
	public JakartaMail setContent(String content) {
		this.content = content;
		return this;
	}

	/**
	 * 设置是否是HTML
	 *
	 * @param isHtml 是否为HTML
	 * @return this
	 */
	public JakartaMail setHtml(boolean isHtml) {
		this.isHtml = isHtml;
		return this;
	}

	/**
	 * 设置正文
	 *
	 * @param content 正文内容
	 * @param isHtml  是否为HTML
	 * @return this
	 */
	public JakartaMail setContent(String content, boolean isHtml) {
		setContent(content);
		return setHtml(isHtml);
	}

	/**
	 * 设置文件类型附件，文件可以是图片文件，此时自动设置cid（正文中引用图片），默认cid为文件名
	 *
	 * @param files 附件文件列表
	 * @return this
	 */
	public JakartaMail setFiles(File... files) {
		if (ArrayUtil.isEmpty(files)) {
			return this;
		}

		final DataSource[] attachments = new DataSource[files.length];
		for (int i = 0; i < files.length; i++) {
			attachments[i] = new FileDataSource(files[i]);
		}
		return setAttachments(attachments);
	}

	/**
	 * 增加附件或图片，附件使用{@link DataSource} 形式表示，可以使用{@link FileDataSource}包装文件表示文件附件
	 *
	 * @param attachments 附件列表
	 * @return this
	 * @since 4.0.9
	 */
	public JakartaMail setAttachments(DataSource... attachments) {
		if (ArrayUtil.isNotEmpty(attachments)) {
			final Charset charset = this.mailAccount.getCharset();
			MimeBodyPart bodyPart;
			String nameEncoded;
			try {
				for (DataSource attachment : attachments) {
					bodyPart = new MimeBodyPart();
					bodyPart.setDataHandler(new DataHandler(attachment));
					nameEncoded = attachment.getName();
					if (this.mailAccount.isEncodefilename()) {
						nameEncoded = JakartaInternalMailUtil.encodeText(nameEncoded, charset);
					}
					// 普通附件文件名
					bodyPart.setFileName(nameEncoded);
					if (StrUtil.startWith(attachment.getContentType(), "image/")) {
						// 图片附件，用于正文中引用图片
						bodyPart.setContentID(nameEncoded);
						// 图片附件设置内联,否则无法正常引用图片
						bodyPart.setDisposition(MimeBodyPart.INLINE);
					}
					this.multipart.addBodyPart(bodyPart);
				}
			} catch (MessagingException e) {
				throw new MailException(e);
			}
		}
		return this;
	}

	/**
	 * 增加图片，图片的键对应到邮件模板中的占位字符串，图片类型默认为"image/jpeg"
	 *
	 * @param cid         图片与占位符，占位符格式为cid:${cid}
	 * @param imageStream 图片文件
	 * @return this
	 * @since 4.6.3
	 */
	public JakartaMail addImage(String cid, InputStream imageStream) {
		return addImage(cid, imageStream, null);
	}

	/**
	 * 增加图片，图片的键对应到邮件模板中的占位字符串
	 *
	 * @param cid         图片与占位符，占位符格式为cid:${cid}
	 * @param imageStream 图片流，不关闭
	 * @param contentType 图片类型，null赋值默认的"image/jpeg"
	 * @return this
	 * @since 4.6.3
	 */
	public JakartaMail addImage(String cid, InputStream imageStream, String contentType) {
		ByteArrayDataSource imgSource;
		try {
			imgSource = new ByteArrayDataSource(imageStream, ObjectUtil.defaultIfNull(contentType, "image/jpeg"));
		} catch (IOException e) {
			throw new IORuntimeException(e);
		}
		imgSource.setName(cid);
		return setAttachments(imgSource);
	}

	/**
	 * 增加图片，图片的键对应到邮件模板中的占位字符串
	 *
	 * @param cid       图片与占位符，占位符格式为cid:${cid}
	 * @param imageFile 图片文件
	 * @return this
	 * @since 4.6.3
	 */
	public JakartaMail addImage(String cid, File imageFile) {
		InputStream in = null;
		try {
			in = FileUtil.getInputStream(imageFile);
			return addImage(cid, in, FileTypeMap.getDefaultFileTypeMap().getContentType(imageFile));
		} finally {
			IoUtil.close(in);
		}
	}

	/**
	 * 设置字符集编码
	 *
	 * @param charset 字符集编码
	 * @return this
	 * @see MailAccount#setCharset(Charset)
	 */
	public JakartaMail setCharset(Charset charset) {
		this.mailAccount.setCharset(charset);
		return this;
	}

	/**
	 * 设置是否使用全局会话，默认为true
	 *
	 * @param isUseGlobalSession 是否使用全局会话，默认为true
	 * @return this
	 * @since 4.0.2
	 */
	public JakartaMail setUseGlobalSession(boolean isUseGlobalSession) {
		this.useGlobalSession = isUseGlobalSession;
		return this;
	}

	/**
	 * 设置debug输出位置，可以自定义debug日志
	 *
	 * @param debugOutput debug输出位置
	 * @return this
	 * @since 5.5.6
	 */
	public JakartaMail setDebugOutput(PrintStream debugOutput) {
		this.debugOutput = debugOutput;
		return this;
	}
	// --------------------------------------------------------------- Getters and Setters end

	@Override
	public MimeMessage build() {
		try {
			return buildMsg();
		} catch (MessagingException e) {
			throw new MailException(e);
		}
	}

	/**
	 * 发送
	 *
	 * @return message-id
	 * @throws MailException 邮件发送异常
	 */
	public String send() throws MailException {
		try {
			return doSend();
		} catch (MessagingException e) {
			if (e instanceof SendFailedException) {
				// 当地址无效时，显示更加详细的无效地址信息
				final Address[] invalidAddresses = ((SendFailedException) e).getInvalidAddresses();
				final String msg = StrUtil.format("Invalid Addresses: {}", ArrayUtil.toString(invalidAddresses));
				throw new MailException(msg, e);
			}
			throw new MailException(e);
		}
	}

	// --------------------------------------------------------------- Private method start

	/**
	 * 执行发送
	 *
	 * @return message-id
	 * @throws MessagingException 发送异常
	 */
	private String doSend() throws MessagingException {
		final MimeMessage mimeMessage = buildMsg();
		Transport.send(mimeMessage);
		return mimeMessage.getMessageID();
	}

	/**
	 * 构建消息
	 *
	 * @return {@link MimeMessage}消息
	 * @throws MessagingException 消息异常
	 */
	private MimeMessage buildMsg() throws MessagingException {
		final Charset charset = this.mailAccount.getCharset();
		final MimeMessage msg = new MimeMessage(getSession());
		// 发件人
		final String from = this.mailAccount.getFrom();
		if (StrUtil.isEmpty(from)) {
			// 用户未提供发送方，则从Session中自动获取
			msg.setFrom();
		} else {
			msg.setFrom(JakartaInternalMailUtil.parseFirstAddress(from, charset));
		}
		// 标题
		msg.setSubject(this.title, (null == charset) ? null : charset.name());
		// 发送时间
		msg.setSentDate(new Date());
		// 内容和附件
		msg.setContent(buildContent(charset));
		// 收件人
		msg.setRecipients(MimeMessage.RecipientType.TO, JakartaInternalMailUtil.parseAddressFromStrs(this.tos, charset));
		// 抄送人
		if (ArrayUtil.isNotEmpty(this.ccs)) {
			msg.setRecipients(MimeMessage.RecipientType.CC, JakartaInternalMailUtil.parseAddressFromStrs(this.ccs, charset));
		}
		// 密送人
		if (ArrayUtil.isNotEmpty(this.bccs)) {
			msg.setRecipients(MimeMessage.RecipientType.BCC, JakartaInternalMailUtil.parseAddressFromStrs(this.bccs, charset));
		}
		// 回复地址(reply-to)
		if (ArrayUtil.isNotEmpty(this.reply)) {
			msg.setReplyTo(JakartaInternalMailUtil.parseAddressFromStrs(this.reply, charset));
		}

		return msg;
	}

	/**
	 * 构建邮件信息主体
	 *
	 * @param charset 编码，{@code null}则使用{@link MimeUtility#getDefaultJavaCharset()}
	 * @return 邮件信息主体
	 * @throws MessagingException 消息异常
	 */
	private Multipart buildContent(Charset charset) throws MessagingException {
		final String charsetStr = null != charset ? charset.name() : MimeUtility.getDefaultJavaCharset();
		// 正文
		final MimeBodyPart body = new MimeBodyPart();
		body.setContent(content, StrUtil.format("text/{}; charset={}", isHtml ? "html" : "plain", charsetStr));
		this.multipart.addBodyPart(body);

		return this.multipart;
	}

	/**
	 * 获取默认邮件会话<br>
	 * 如果为全局单例的会话，则全局只允许一个邮件帐号，否则每次发送邮件会新建一个新的会话
	 *
	 * @return 邮件会话 {@link Session}
	 */
	private Session getSession() {
		final Session session = JakartaMailUtil.getSession(this.mailAccount, this.useGlobalSession);

		if (null != this.debugOutput) {
			session.setDebugOut(debugOutput);
		}

		return session;
	}
	// --------------------------------------------------------------- Private method end
}
