package me.qwq.doghouse.service;

import java.io.IOException;
import java.io.InputStream;
import java.net.Proxy;
import java.security.SecureRandom;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Properties;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

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

import org.apache.commons.lang3.StringUtils;
import org.redisson.api.ExpiredObjectListener;
import org.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Service;
import org.sqids.Sqids;

import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;

import lombok.extern.slf4j.Slf4j;
import me.qwq.doghouse.component.Rk;
import me.qwq.doghouse.entity.Comment;
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.entity.Post;
import me.qwq.doghouse.enums.CommentStatusEnum;
import me.qwq.doghouse.service.post.PostService;

/**
 * 邮件服务
 * @author Doghole
 *
 */
@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";

    @Autowired
    MailConfig mailConfig;
    
    @Autowired
    NetworkConfig networkConfig;
    
    @Autowired
    SiteConfig siteConfig;
    
    @Autowired
    CommentService commentService;
    
    @Autowired
    PostService postService;
    
    @Autowired
    RedissonClient redis;
    
    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() {
        
        for (String key : Rk.Mail.getAllExpirationTaskKeys()) {
            if (!key.endsWith("-comment")) {
                RBucket<Object> task = redis.getBucket(key);
                if (mailConfig.getSendingDelay() == 0L) {
                    task.set(null, Duration.ofSeconds(1L));
                }
                else {
                    task.set(null, Duration.ofMinutes(mailConfig.getSendingDelay()));
                }
            }
        }
    }
    
    /**
     * 清除评论对应的延迟
     * @param comment
     * @return
     */
    public boolean clearTaskDelay(Comment comment) {
        RBucket<Object> taskBucket = Rk.Mail.getTaskBucket(comment.getId());
        return taskBucket.clearExpire() || taskBucket.isExists();
    }
    
    /**
     * 添加或更新发送队列
     * @param comment
     */
    public void addOrUpdateSendQueue(Comment comment) {
        
        if (comment.isMailSent()) {
            log.info("评论 ID 为 {} 的通知已发送，不会再次发送", comment.getId());
            return;
        }
        
        RBucket<Object> taskBucket = Rk.Mail.getTaskBucket(comment.getId());
        RBucket<Comment> commentCache = Rk.Mail.getTaskCommentBucket(comment.getId());
        
        if (comment.getIsDeleted()) {
            // 已删除，需要删除其任务
            log.info("评论 ID 为 {} 的评论已删除，清除可能存在的通知任务", comment.getId());
            taskBucket.deleteAsync();
            commentCache.deleteAsync();
            return;
        }
        
        if (taskBucket.isExists()
                && comment.getCommentStatus() != CommentStatusEnum.RELEASED) {
            // 已存在，但变更了评论状态，不为发布状态，需要删除其任务
            log.info("评论 ID 为 {} 的评论状态修改为 {}，清除其通知任务",
                    comment.getId(), comment.getCommentStatusNote());
            taskBucket.deleteAsync();
            commentCache.deleteAsync();
            return;
        }
        
        if (taskBucket.isExists()) {
            // 已存在，只更新 commentCache 和时间
            
            if (taskBucket.clearExpire()) {
                // 有 TTL，重设时间
                log.info("评论 ID 为 {} 的评论内容已变更，重设推送时间",
                        comment.getId());
                commentCache.set(comment);
                taskBucket.expire(mailConfig.getMailDuration());
                return;
            }
            else {
                // 没 TTL, 继续时间
                LocalDateTime now = LocalDateTime.now();
                LocalDateTime commentDate = comment.getCreateTime();
                
                Duration delay = Duration.ofMinutes(mailConfig.getSendingDelay());
                Temporal sendTime = delay.addTo(commentDate);
                
                Duration newDelay = Duration.between(now, sendTime);
                if (newDelay.isNegative() || newDelay.isZero()) {
                    newDelay = Duration.ofSeconds(1L);
                }
                commentCache.set(comment);
                taskBucket.expire(newDelay);
                return;
            }
            
        }
        
        commentCache.set(comment);

        
        taskBucket.addListener(new ExpiredObjectListener() {
            @Override
            public void onExpired(String name) {
                // 寻找本级至顶级的所有回复
                Comment comment = commentCache.getAndDelete();
                
                if (comment == null) {
                    log.info("评论 ID 为 {} 的评论无法获取推送缓存，可能状态已变更，推送不会进行");
                    return;
                }
                if (!mailConfig.isMailOn()) {
                    log.debug("邮件功能未开启，评论 ID 为 {} 的推送任务将不会进行被发送", comment.getId());
                    return;
                }
                
                log.info("触发评论 ID 为 {} 的推送任务...", comment.getId());
                Post post = postService.getById(comment.getPostId());
                if (post == null) {
                    log.info("评论 ID 为 {} 的评论所属 Post Id = {} 不存在，可能状态已变更，推送不会进行");
                    return;
                }
                List<Comment> parentComments = commentService.listFromChildToParent(comment.getId(), false);
                Set<String> mailSet = parentComments.stream()
                        .filter(c -> c.isSubscribeReply() && !c.getEmail().equalsIgnoreCase(comment.getEmail()))
                        .map(c -> c.getEmail().toLowerCase(Locale.ENGLISH))
                        .collect(Collectors.toSet());
                if (!mailSet.isEmpty()) {
                    MimeMessage message = new MimeMessage(getMailSession(false));
                    try {
                        message.setSubject(
                                String.format("您在 %s 的 %s 文章评论有新回复",
                                        siteConfig.getName(),
                                        post.getTitle()));
                        
                        if (StringUtils.isEmpty(mailConfig.getFromUser())) {
                            message.setFrom(mailConfig.getFromAddress());
                        }
                        else {
                            message.setFrom(
                                String.format("%s<%s>",
                                mailConfig.getFromUser(),
                                mailConfig.getFromAddress()));
                        }
                        
                        String contentTemplate = getResourceText(REPLY_SUBSCRIPTION_TEMPLATE);
                        if (contentTemplate == null) {
                            log.warn("回复订阅通知模板文件路径 {} 无法找到, 通知将不会被发送",
                                    REPLY_SUBSCRIPTION_TEMPLATE);
                            return;
                        }
                        
                        mailSet.forEach(mail -> {
                            try {
                                log.info("评论 ID 为 {} 的通知欲发送至 {}", 
                                        comment.getId(), mail);
                                RBucket<String> mgrBucket = Rk.Mail.getMgrBucket(mail);
                                String uuid = null;
                                if (mgrBucket.isExists()) {
                                    uuid = mgrBucket.get();
                                    log.debug("邮箱 {} 存在管理 UUID {}", mail, uuid);
                                }
                                else {
                                    uuid = UUID.randomUUID().toString();
                                    log.debug("邮箱 {} 不存在管理 UUID，生成 {}", mail, uuid);
                                }
                                
                                String content = renderReplySubscriptionContent(
                                        contentTemplate, siteConfig, post, comment, uuid);
                                message.setContent(content, CONTENT_FORMAT);
                                
                                message.setRecipient(RecipientType.TO,
                                        new InternetAddress(mail));
                                Transport.send(message);
                                
                                // 发送成功了再存 :P
                                RBucket<String> uuidBucket = Rk.Mail.getMgrUuidBucket(uuid);
                                uuidBucket.setAsync(mail, 86400, TimeUnit.SECONDS);
                                mgrBucket.setAsync(uuid, 86400, TimeUnit.SECONDS);
                                
                                log.debug("已为邮箱 {} 设置 UUID {}", mail, uuid);
                                
                                log.info("评论 ID 为 {} 的通知已发送至 {}", comment.getId(), mail);
                            }
                            catch (MessagingException e) {
                                log.error("发送评论 ID 为 " + comment.getId() + " 的通知至 " + mail + " 时发生错误", e);
                            }
                        });
                        // 更新已发送状态
                        commentService.update(new LambdaUpdateWrapper<Comment>()
                                .eq(Comment::getId, comment.getId())
                                .set(Comment::isMailSent, true));
                    }
                    catch (MessagingException e) {
                        log.error("初始化评论 ID 为 {} 的通知失败", comment.getId());
                    }
                }
                else {
                    // 本级到顶级没有找到能接收通知的邮箱，或找到的邮箱是回复者本人的邮箱
                    log.debug("评论 ID 为 {} 的任务未找到可执行通知的邮箱，"
                            + "或找到的邮箱为回复者本人邮箱，邮件不会发送。"
                            + "若非您预期的结果，请检查：被回复的评论及上级"
                            + "评论是否开启了邮件订阅？", 
                            comment.getId());
                }
            }
        });
        Long sendingDelay = mailConfig.getSendingDelay();
        if (sendingDelay == 0L) {
            taskBucket.set(0, Duration.ofSeconds(1L));
            log.debug("评论 ID 为 {} 的任务将在 1 秒后立即执行", comment.getId());
        }
        else {
            taskBucket.set(0, Duration.ofMinutes(sendingDelay));
            log.debug("评论 ID 为 {} 的任务将在 {} 分钟后执行", comment.getId(), sendingDelay);
        }
        log.info("评论 ID 为 {} 的通知任务已提交，等待执行", comment.getId());
    }

    /**
     * 发送新评论通知到管理员邮箱
     * @param comment
     */
    public void sendAdminNotification(Comment comment) {
        
        if (!mailConfig.isMailOn()) {
            log.debug("邮件功能未开启，评论 ID 为 {} 的管理邮件将不会被发送", comment.getId());
            return;
        }
        
        Post post = postService.getByIdOrName(comment.getPostId(), comment.getPostPageName());
        
        RBucket<Object> expiredKey = Rk.Mail.getRandomExpirationBucket();
        expiredKey.addListener(new ExpiredObjectListener() {
            @Override
            public void onExpired(String name) {
                log.info("评论 ID 为 {} 的管理邮件任务开始", comment.getId());
                try {
                    MimeMessage message = new MimeMessage(getMailSession(false));
                    message.setSubject(
                            String.format("您在 %s 的 %s 文章有新回复",
                            siteConfig.getName(),
                            post.getTitle()));
                    String content = 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");
                    message.setContent(content, CONTENT_FORMAT);
                    
                    if (StringUtils.isEmpty(mailConfig.getFromUser())) {
                        message.setFrom(mailConfig.getFromAddress());
                    }
                    else {
                        message.setFrom(
                            String.format("%s<%s>",
                            mailConfig.getFromUser(),
                            mailConfig.getFromAddress()));
                    }
                    message.setRecipient(RecipientType.TO,
                            new InternetAddress(mailConfig.getAdminAddress()));
                    Transport.send(message);
                    log.info("评论 ID 为 {} 的管理邮件发送成功", comment.getId());
                }
                catch (Exception e) {
                    log.error("评论 ID 为 " + comment.getId() + " 的管理邮件发送失败", e);
                }
            }
        });
        expiredKey.set(1, Duration.ofSeconds(1L));
        log.debug("评论 ID 为 {} 的管理邮件任务将在 1 秒后立即执行", comment.getId());
    }
    
    String renderReplySubscriptionContent(
            String template, SiteConfig siteConfig, Post post, Comment comment, String uuid) {
        String blogName = siteConfig.getName();
        String postName = post.getPostTitle();
        String author = comment.getAuthor();
        String content = comment.getContent();
        String detailUrl = siteConfig.getAddress() + "/comments/go/" + comment.getId();
        String unsubscribeUrl = siteConfig.getAddress() + "/system/unsubscribe-mail?token=" + uuid;
        
        List<Long> list = new ArrayList<>();
        SecureRandom random = new SecureRandom();
        list.add(Math.abs(random.nextLong()));
        list.add(Math.abs(random.nextLong()));
        String mess = Sqids.builder().build().encode(list);

        String blogNameReplacement = "{{blogName" + mess + "}}";
        String postNameReplacement = "{{postName" + mess + "}}";
        String authorReplacement = "{{author" + mess + "}}";
        String contentReplacement = "{{content" + mess + "}}";
        String detailUrlReplacement = "{{detailUrl" + mess + "}}";
        String unsubscribeUrlReplacement = "{{unsubscribeUrl" + mess + "}}";
        
        
        return template
                .replace("{{blogName}}", blogNameReplacement)
                .replace("{{postName}}", postNameReplacement)
                .replace("{{author}}", authorReplacement)
                .replace("{{content}}", contentReplacement)
                .replace("{{detailUrl}}", detailUrlReplacement)
                .replace("{{unsubscribeUrl}}", unsubscribeUrlReplacement)
                .replace(blogNameReplacement, blogName)
                .replace(postNameReplacement, postName)
                .replace(authorReplacement, author)
                .replace(contentReplacement, content)
                .replace(detailUrlReplacement, detailUrl)
                .replace(unsubscribeUrlReplacement, unsubscribeUrl);
    }
    
    /**
     * 读取资源文件
     * @param fileName
     * @return
     */
    String getResourceText(String fileName) {
        ClassPathResource resource = new ClassPathResource(fileName);
        String content = null;
        try (InputStream inputStream = resource.getInputStream()) {
            content = new String(inputStream.readAllBytes());
        }
        catch (IOException e) {
            log.error("读取 resource 文件 " + fileName + " 时发生错误", e);
        }
        return content;
    }

}
