package me.qwq.doghouse.service;

import java.io.IOException;
import java.io.InputStream;
import java.net.Proxy;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Collectors;

import javax.mail.Authenticator;
import javax.mail.Message.RecipientType;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.text.StringEscapeUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.Context;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.StringTemplateResolver;

import lombok.extern.slf4j.Slf4j;
import me.qwq.doghouse.entity.Comment;
import me.qwq.doghouse.entity.MailTask;
import me.qwq.doghouse.entity.Post;
import me.qwq.doghouse.entity.config.MailConfig;
import me.qwq.doghouse.entity.config.NetworkConfig;
import me.qwq.doghouse.entity.config.SiteConfig;
import me.qwq.doghouse.enums.CommentStatusEnum;
import me.qwq.doghouse.enums.MailTaskTypeEnum;
import me.qwq.doghouse.service.post.PostService;
import me.qwq.doghouse.util.MarkDownUtils;

@Service
@Slf4j
public class MailService {

    static final String CONTENT_FORMAT = "text/html;charset=UTF-8";
    static final String REPLY_SUBSCRIPTION_TEMPLATE = "system/mail/reply-subscription.html";
    static final TemplateEngine MAIL_TEMPLATE_ENGINE = createMailTemplateEngine();

    public enum MailTaskResult {
        SENT,
        CANCELLED
    }

    @Autowired
    MailConfig mailConfig;

    @Autowired
    NetworkConfig networkConfig;

    @Autowired
    SiteConfig siteConfig;

    @Autowired
    CommentService commentService;

    @Autowired
    PostService postService;

    @Autowired
    MailTaskService mailTaskService;

    /**
     * 创建用于渲染邮件正文的 Thymeleaf 字符串模板引擎。
     * @return 邮件模板引擎
     */
    private static TemplateEngine createMailTemplateEngine() {
        StringTemplateResolver templateResolver = new StringTemplateResolver();
        templateResolver.setTemplateMode(TemplateMode.HTML);
        templateResolver.setCacheable(false);

        TemplateEngine templateEngine = new TemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    /**
     * @param debug 是否开启调试, 开启后会在输出打印邮件状态
     * @return 邮件会话
     */
    public Session getMailSession(boolean debug) {

        String username = mailConfig.getUsername();
        String password = mailConfig.getPassword();

        Properties props = new Properties();

        props.put("mail.smtp.starttls.enable", mailConfig.isTlsEnable());
        props.put("mail.smtp.host", mailConfig.getSmtpHost());
        props.put("mail.smtp.port", mailConfig.getSmtpPort());
        props.put("mail.smtp.auth", true);

        if (networkConfig.getProxyType() == Proxy.Type.SOCKS) {
            props.put("mail.smtp.socks.host", networkConfig.getProxyHost());
            props.put("mail.smtp.socks.port", networkConfig.getProxyPort());
        }
        else if (networkConfig.getProxyType() == Proxy.Type.HTTP) {
            props.put("mail.smtp.proxy.host", networkConfig.getProxyHost());
            props.put("mail.smtp.proxy.port", networkConfig.getProxyPort());
        }

        Session session = Session.getInstance(props, new Authenticator() {
            @Override
            protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(username, password);
            }
        });

        session.setDebug(debug);
        return session;
    }

    /**
     * 在邮件间隔设置变更后，重设邮件任务延时
     */
    public void resetTaskDelay() {
        mailTaskService.resetReplyTaskDelay(mailConfig.getMailDuration());
    }

    /**
     * 取消可编辑评论的待处理邮件任务
     * @param comment
     * @return 是否存在待处理任务并已将其取消
     */
    public boolean clearTaskDelay(Comment comment) {
        if (comment == null || comment.getId() == null) {
            return false;
        }
        return mailTaskService.cancelOutstandingTasks(comment.getId(), MailTaskTypeEnum.REPLY_SUBSCRIPTION);
    }

    /**
     * 添加或更新邮件推送任务
     * @param comment 需要处理的评论
     */
    public void addOrUpdateSendQueue(Comment comment) {
        if (comment == null || comment.getId() == null) {
            return;
        }

        if (comment.isMailSent()) {
            log.info("ID 为 {} 的评论通知已发送，不会再次发送", comment.getId());
            return;
        }

        if (comment.getIsDeleted() || comment.getCommentStatus() != CommentStatusEnum.RELEASED) {
            log.info("ID 为 {} 的评论已删除或未发布，取消其回复订阅通知任务", comment.getId());
            mailTaskService.cancelOutstandingTasks(comment.getId(), MailTaskTypeEnum.REPLY_SUBSCRIPTION);
            return;
        }

        Set<String> recipients = getReplySubscriptionRecipients(comment);
        if (recipients.isEmpty()) {
            mailTaskService.cancelOutstandingTasks(comment.getId(), MailTaskTypeEnum.REPLY_SUBSCRIPTION);
            log.debug("ID 为 {} 的评论未找到回复订阅通知收件人", comment.getId());
            return;
        }

        LocalDateTime sendAfter = getSendAfter(comment);
        mailTaskService.replaceReplyTasks(comment.getId(), recipients, sendAfter);
        log.info("ID 为 {} 的评论回复订阅通知任务已提交，收件人数 {}", comment.getId(), recipients.size());
    }

    /**
     * 为新评论发送管理员通知
     * @param comment
     */
    public void sendAdminNotification(Comment comment) {
        if (comment == null || comment.getId() == null) {
            return;
        }
        if (!mailConfig.isMailOn()) {
            log.debug("邮件功能未开启，评论 ID 为 {} 的管理员邮件不会入队", comment.getId());
            return;
        }
        mailTaskService.enqueueAdminTask(comment.getId(), mailConfig.getAdminAddress());
        log.debug("ID 为 {} 的评论的管理员邮件任务已提交", comment.getId());
    }

    /**
     * 执行指定邮件任务，并根据任务类型分派到对应发送逻辑。
     * @param task 待执行的邮件任务
     * @return 邮件任务执行结果
     * @throws MessagingException 邮件构造或发送失败时抛出
     */
    public MailTaskResult executeMailTask(MailTask task) throws MessagingException {
        if (task == null || task.getTaskType() == null) {
            return MailTaskResult.CANCELLED;
        }
        if (!mailConfig.isMailOn()) {
            log.debug("邮件功能未开启，邮件任务 {} 取消", task.getId());
            return MailTaskResult.CANCELLED;
        }

        if (task.getTaskType() == MailTaskTypeEnum.ADMIN_NOTIFICATION) {
            return sendAdminNotificationTask(task);
        }
        if (task.getTaskType() == MailTaskTypeEnum.REPLY_SUBSCRIPTION) {
            return sendReplySubscriptionTask(task);
        }
        return MailTaskResult.CANCELLED;
    }

    /**
     * 发送评论回复订阅通知邮件。
     * @param task 回复订阅邮件任务
     * @return 邮件任务执行结果
     * @throws MessagingException 邮件构造或发送失败时抛出
     */
    private MailTaskResult sendReplySubscriptionTask(MailTask task) throws MessagingException {
        Comment comment = commentService.getById(task.getCommentId());
        if (comment == null || comment.getIsDeleted() || comment.getCommentStatus() != CommentStatusEnum.RELEASED) {
            return MailTaskResult.CANCELLED;
        }
        if (comment.isMailSent()) {
            return MailTaskResult.CANCELLED;
        }

        String recipient = StringUtils.lowerCase(task.getRecipient(), Locale.ENGLISH);
        if (!isReplySubscriptionRecipientStillValid(comment, recipient)) {
            log.info("评论 ID 为 {} 的收件人 {} 已不再订阅该回复，任务取消", comment.getId(), recipient);
            return MailTaskResult.CANCELLED;
        }

        Post post = postService.getByIdOrName(comment.getPostId(), comment.getPostPageName(), false);
        if (post == null) {
            return MailTaskResult.CANCELLED;
        }

        Comment subscriptionComment = getReplySubscriptionComment(comment, recipient);
        if (subscriptionComment == null) {
            return MailTaskResult.CANCELLED;
        }

        String contentTemplate = getResourceText(REPLY_SUBSCRIPTION_TEMPLATE);
        if (contentTemplate == null) {
            throw new MessagingException("回复订阅通知模板无法读取: " + REPLY_SUBSCRIPTION_TEMPLATE);
        }

        String token = commentService.refreshUnsubscribeToken(subscriptionComment.getId());
        if (StringUtils.isBlank(token)) {
            throw new MessagingException("无法生成邮件退订令牌");
        }

        MimeMessage message = new MimeMessage(getMailSession(false));
        message.setSubject(String.format("您在 %s 的 %s 文章评论有新回复",
                siteConfig.getName(), post.getTitle()));
        setMessageFrom(message);
        message.setRecipient(RecipientType.TO, new InternetAddress(recipient));
        message.setContent(renderReplySubscriptionContent(
                contentTemplate, siteConfig, post, comment, token), CONTENT_FORMAT);

        Transport.send(message);

        log.info("评论 ID 为 {} 的回复订阅通知已发送至 {}", comment.getId(), recipient);
        return MailTaskResult.SENT;
    }

    /**
     * 发送新评论管理员通知邮件。
     * @param task 管理员通知邮件任务
     * @return 邮件任务执行结果
     * @throws MessagingException 邮件构造或发送失败时抛出
     */
    private MailTaskResult sendAdminNotificationTask(MailTask task) throws MessagingException {
        Comment comment = commentService.getById(task.getCommentId());
        if (comment == null || comment.getIsDeleted()) {
            return MailTaskResult.CANCELLED;
        }

        Post post = postService.getById(comment.getPostId());
        if (post == null) {
            return MailTaskResult.CANCELLED;
        }

        MimeMessage message = new MimeMessage(getMailSession(false));
        message.setSubject(String.format("您在 %s 的 %s 文章有新回复",
                siteConfig.getName(), post.getTitle()));
        setMessageFrom(message);
        message.setRecipient(RecipientType.TO, new InternetAddress(task.getRecipient()));
        message.setContent(String.format(
                "<p><b>%s</b>：%s<br>"
                + "<b>评论状态</b>：%s<br>"
                + "<a href=\"%s%s\" rel=\"noopener noreferrer\" target=\"_blank\">查看详情</a></p>"
                + "<p><i style=\"color: red\">该邮件由系统自动发出，请勿直接回复该邮件</i></p>",
                comment.getAuthor(),
                comment.getContent(),
                comment.getCommentStatusNote(),
                siteConfig.getAddress(),
                comment.getCommentStatus() == CommentStatusEnum.RELEASED ?
                        commentService.getJumpToUrl(comment.getId()) :
                        "/admin/v2"), CONTENT_FORMAT);

        Transport.send(message);
        log.info("评论 ID 为 {} 的管理员邮件已发送至 {}", comment.getId(), task.getRecipient());
        return MailTaskResult.SENT;
    }

    /**
     * 从指定评论中获取开启了订阅的收件人
     * @param comment 关联评论
     * @return 收件人邮件地址 Set
     */
    private Set<String> getReplySubscriptionRecipients(Comment comment) {
        return commentService.listFromChildToParent(comment.getId(), false)
                .stream()
                .filter(c -> c.isSubscribeReply())
                .map(Comment::getEmail)
                .filter(StringUtils::isNotBlank)
                .map(email -> email.toLowerCase(Locale.ENGLISH).trim())
                .filter(email -> !email.equalsIgnoreCase(comment.getEmail()))
                .collect(Collectors.toSet());
    }

    /**
     * @param comment 指定评论
     * @param recipient 指定收件人
     * @return 指定收件人是否存在于某评论的订阅中
     */
    private boolean isReplySubscriptionRecipientStillValid(Comment comment, String recipient) {
        return getReplySubscriptionComment(comment, recipient) != null;
    }

    private Comment getReplySubscriptionComment(Comment comment, String recipient) {
        if (StringUtils.isBlank(recipient)) {
            return null;
        }
        String normalizedRecipient = recipient.toLowerCase(Locale.ENGLISH).trim();
        return commentService.listFromChildToParent(comment.getId(), false)
                .stream()
                .filter(Comment::isSubscribeReply)
                .filter(c -> StringUtils.isNotBlank(c.getEmail()))
                .filter(c -> normalizedRecipient.equals(c.getEmail().toLowerCase(Locale.ENGLISH).trim()))
                .filter(c -> !normalizedRecipient.equalsIgnoreCase(comment.getEmail()))
                .findFirst()
                .orElse(null);
    }

    /**
     * 计算评论回复订阅通知的最早发送时间。
     * @param comment 关联评论
     * @return 最早发送时间
     */
    private LocalDateTime getSendAfter(Comment comment) {
        LocalDateTime baseTime = comment.getCreateTime() == null ? LocalDateTime.now() : comment.getCreateTime();
        Duration delay = mailConfig.getMailDuration();
        LocalDateTime sendAfter = baseTime.plus(delay);
        LocalDateTime minSendAfter = LocalDateTime.now().plusSeconds(1);
        return sendAfter.isBefore(minSendAfter) ? minSendAfter : sendAfter;
    }

    /**
     * 根据邮件配置设置发件人信息。
     * @param message 待设置的邮件消息
     * @throws MessagingException 发件人设置失败时抛出
     */
    private void setMessageFrom(MimeMessage message) throws MessagingException {
        if (StringUtils.isEmpty(mailConfig.getFromUser())) {
            message.setFrom(mailConfig.getFromAddress());
        }
        else {
            message.setFrom(String.format("%s<%s>",
                    mailConfig.getFromUser(),
                    mailConfig.getFromAddress()));
        }
    }
    
    /**
     * 渲染回复订阅内容
     * @param template 模板源文本
     * @param siteConfig 站点配置, 用以获取博客名、博客地址
     * @param post 对应文章, 用以获取文章标题
     * @param comment 订阅者发布的本条评论, 用以获取订阅者称呼、当前评论内容和评论 id, 其中评论 id 用作链接跳转
     * @param token 对应评论发布者邮箱退订用令牌，用以鉴别发布者身份
     * @return 渲染完毕后的邮件内容(html)
     */
    private String renderReplySubscriptionContent(
            String template, SiteConfig siteConfig, Post post, Comment comment, String token) {
        Context context = new Context(Locale.getDefault());
        context.setVariable("blogName", siteConfig.getName());
        context.setVariable("postName", post.getPostTitle());
        context.setVariable("author", comment.getAuthor());
        context.setVariable("contentHtml", renderSafeCommentContent(comment));
        context.setVariable("detailUrl", siteConfig.getAddress() + "/comments/go/" + comment.getId());
        context.setVariable("unsubscribeUrl", siteConfig.getAddress() + "/system/unsubscribe-mail?token=" + token);

        return MAIL_TEMPLATE_ENGINE.process(template, context);
    }

    /**
     * 将评论内容转换为可安全放入邮件正文的 HTML。
     * @param comment 关联评论
     * @return 可直接作为 HTML 输出的评论内容
     */
    private String renderSafeCommentContent(Comment comment) {
        if (comment.isMarkdown()) {
            return MarkDownUtils.commentToHtml(comment.getContent());
        }
        return StringEscapeUtils.escapeHtml4(comment.getContent());
    }

    /**
     * 读取 classpath 下的文本资源内容。
     * @param fileName 资源文件路径
     * @return 资源文本内容，读取失败时返回 null
     */
    String getResourceText(String fileName) {
        ClassPathResource resource = new ClassPathResource(fileName);
        try (InputStream inputStream = resource.getInputStream()) {
            return new String(inputStream.readAllBytes(), StandardCharsets.UTF_8);
        }
        catch (IOException e) {
            log.error("读取 resource 文件 " + fileName + " 时发生错误", e);
            return null;
        }
    }
}
