package notification

import (
	"context"
	"fmt"
	"log/slog"
	"strings"

	"github.com/pkg/errors"

	"github.com/usememos/memos/internal/email"
	"github.com/usememos/memos/internal/profile"
	storepb "github.com/usememos/memos/proto/gen/store"
	"github.com/usememos/memos/store"
)

// EmailSender sends a prepared email message with the given SMTP configuration.
type EmailSender func(*email.Config, *email.Message)

// EmailDispatcher dispatches notification emails for inbox events.
type EmailDispatcher struct {
	profile *profile.Profile
	store   *store.Store
	sender  EmailSender
}

// NewEmailDispatcher creates a notification email dispatcher.
func NewEmailDispatcher(profile *profile.Profile, store *store.Store, sender EmailSender) *EmailDispatcher {
	if sender == nil {
		sender = email.SendAsync
	}
	return &EmailDispatcher{
		profile: profile,
		store:   store,
		sender:  sender,
	}
}

// DispatchInboxEmail sends the email notification for an inbox entry when configured.
func (d *EmailDispatcher) DispatchInboxEmail(ctx context.Context, inbox *store.Inbox) error {
	if inbox == nil || inbox.Message == nil {
		return nil
	}

	setting, err := d.store.GetInstanceNotificationSetting(ctx)
	if err != nil {
		return errors.Wrap(err, "failed to get notification setting")
	}
	emailSetting := setting.GetEmail()
	if emailSetting == nil || !emailSetting.Enabled {
		return nil
	}
	if d.baseURL() == "" {
		slog.Warn("Skipping inbox email notification because instance URL is required",
			slog.Int64("inbox_id", int64(inbox.ID)),
			slog.Int64("receiver_id", int64(inbox.ReceiverID)))
		return nil
	}

	receiver, err := d.store.GetUser(ctx, &store.FindUser{ID: &inbox.ReceiverID})
	if err != nil {
		return errors.Wrap(err, "failed to get notification receiver")
	}
	if receiver == nil || strings.TrimSpace(receiver.Email) == "" {
		return nil
	}

	sender, err := d.store.GetUser(ctx, &store.FindUser{ID: &inbox.SenderID})
	if err != nil {
		return errors.Wrap(err, "failed to get notification sender")
	}
	if sender == nil {
		return nil
	}

	memosByID, err := d.listMemosByID(ctx, collectInboxMemoIDs([]*store.Inbox{inbox}))
	if err != nil {
		return errors.Wrap(err, "failed to get notification memos")
	}

	message, err := d.buildInboxEmailMessage(inbox, receiver, sender, memosByID)
	if err != nil {
		return err
	}
	if message == nil {
		return nil
	}
	message.ReplyTo = emailSetting.ReplyTo

	config := EmailConfigFromInstanceSetting(emailSetting)
	if err := config.Validate(); err != nil {
		return errors.Wrap(err, "invalid notification email setting")
	}

	d.sender(config, message)
	return nil
}

// EmailConfigFromInstanceSetting converts persisted notification settings into SMTP config.
func EmailConfigFromInstanceSetting(setting *storepb.InstanceNotificationSetting_EmailSetting) *email.Config {
	if setting == nil {
		return &email.Config{}
	}
	return &email.Config{
		SMTPHost:     setting.SmtpHost,
		SMTPPort:     int(setting.SmtpPort),
		SMTPUsername: setting.SmtpUsername,
		SMTPPassword: setting.SmtpPassword,
		FromEmail:    setting.FromEmail,
		FromName:     setting.FromName,
		UseTLS:       setting.UseTls,
		UseSSL:       setting.UseSsl,
	}
}

// NewTestEmailMessage builds the plain-text test email for notification settings.
func NewTestEmailMessage(recipientEmail, replyTo string) *email.Message {
	return &email.Message{
		To:      []string{recipientEmail},
		Subject: "[阿年的备忘录] 测试邮件",
		Body:    "这是一封测试邮件，说明通知邮件设置已正确配置。",
		ReplyTo: replyTo,
	}
}

// ValidateEmailSetting validates notification email SMTP settings.
func ValidateEmailSetting(setting *storepb.InstanceNotificationSetting_EmailSetting) error {
	return EmailConfigFromInstanceSetting(setting).Validate()
}

// SendTestEmail sends a plain-text test email using notification email settings.
func SendTestEmail(setting *storepb.InstanceNotificationSetting_EmailSetting, recipientEmail string) error {
	return email.Send(EmailConfigFromInstanceSetting(setting), NewTestEmailMessage(recipientEmail, setting.GetReplyTo()))
}

func (d *EmailDispatcher) buildInboxEmailMessage(inbox *store.Inbox, receiver *store.User, sender *store.User, memosByID map[int32]*store.Memo) (*email.Message, error) {
	senderName := displayNameForEmail(sender)
	switch inbox.Message.Type {
	case storepb.InboxMessage_MEMO_COMMENT:
		return d.buildMemoCommentEmailMessage(inbox.Message, receiver, senderName, memosByID)
	case storepb.InboxMessage_MEMO_MENTION:
		return d.buildMemoMentionEmailMessage(inbox.Message, receiver, senderName, memosByID)
	default:
		return nil, nil
	}
}

func (d *EmailDispatcher) buildMemoCommentEmailMessage(message *storepb.InboxMessage, receiver *store.User, senderName string, memosByID map[int32]*store.Memo) (*email.Message, error) {
	payload := message.GetMemoComment()
	if payload == nil {
		return nil, nil
	}
	commentMemo := memosByID[payload.MemoId]
	relatedMemo := memosByID[payload.RelatedMemoId]
	if !canViewerAccessMemo(receiver, commentMemo) || !canViewerAccessMemo(receiver, relatedMemo) {
		return nil, nil
	}
	url := d.memoCommentURL(relatedMemo, commentMemo)
	if url == "" {
		return nil, nil
	}

	body := []string{
		fmt.Sprintf("%s，你好：", displayNameForEmail(receiver)),
		"",
		fmt.Sprintf("%s 评论了你的备忘录。", senderName),
		"",
		"前往查看:",
		url,
	}

	return &email.Message{
		To:      []string{receiver.Email},
		Subject: fmt.Sprintf("[阿年的备忘录] %s评论了你的备忘录", senderName),
		Body:    strings.Join(body, "\n"),
	}, nil
}

func (d *EmailDispatcher) buildMemoMentionEmailMessage(message *storepb.InboxMessage, receiver *store.User, senderName string, memosByID map[int32]*store.Memo) (*email.Message, error) {
	payload := message.GetMemoMention()
	if payload == nil {
		return nil, nil
	}
	memo := memosByID[payload.MemoId]
	if !canViewerAccessMemo(receiver, memo) {
		return nil, nil
	}
	url := d.memoURL(memo)
	if url == "" {
		return nil, nil
	}

	body := []string{
		fmt.Sprintf("%s，你好：", displayNameForEmail(receiver)),
		"",
		fmt.Sprintf("%s 在备忘录中提到了你。", senderName),
		"",
		"前往查看:",
		url,
	}

	return &email.Message{
		To:      []string{receiver.Email},
		Subject: fmt.Sprintf("[阿年的备忘录] %s在备忘录中提到了你", senderName),
		Body:    strings.Join(body, "\n"),
	}, nil
}

func (d *EmailDispatcher) listMemosByID(ctx context.Context, memoIDs []int32) (map[int32]*store.Memo, error) {
	if len(memoIDs) == 0 {
		return map[int32]*store.Memo{}, nil
	}

	uniqueMemoIDs := make([]int32, 0, len(memoIDs))
	seenMemoIDs := make(map[int32]struct{}, len(memoIDs))
	for _, memoID := range memoIDs {
		if memoID == 0 {
			continue
		}
		if _, seen := seenMemoIDs[memoID]; seen {
			continue
		}
		seenMemoIDs[memoID] = struct{}{}
		uniqueMemoIDs = append(uniqueMemoIDs, memoID)
	}
	if len(uniqueMemoIDs) == 0 {
		return map[int32]*store.Memo{}, nil
	}

	memos, err := d.store.ListMemos(ctx, &store.FindMemo{IDList: uniqueMemoIDs})
	if err != nil {
		return nil, err
	}

	memosByID := make(map[int32]*store.Memo, len(memos))
	for _, memo := range memos {
		memosByID[memo.ID] = memo
	}
	return memosByID, nil
}

func collectInboxMemoIDs(inboxes []*store.Inbox) []int32 {
	memoIDs := make([]int32, 0, len(inboxes)*2)
	for _, inbox := range inboxes {
		if inbox == nil || inbox.Message == nil {
			continue
		}
		switch inbox.Message.Type {
		case storepb.InboxMessage_MEMO_COMMENT:
			payload := inbox.Message.GetMemoComment()
			if payload != nil {
				memoIDs = append(memoIDs, payload.MemoId, payload.RelatedMemoId)
			}
		case storepb.InboxMessage_MEMO_MENTION:
			payload := inbox.Message.GetMemoMention()
			if payload != nil {
				memoIDs = append(memoIDs, payload.MemoId, payload.RelatedMemoId)
			}
		default:
			// Ignore notification types without memo references.
		}
	}
	return memoIDs
}

func displayNameForEmail(user *store.User) string {
	if user == nil {
		return "there"
	}
	if strings.TrimSpace(user.Nickname) != "" {
		return user.Nickname
	}
	if strings.TrimSpace(user.Username) != "" {
		return user.Username
	}
	return "there"
}

func (d *EmailDispatcher) baseURL() string {
	if d.profile == nil || strings.TrimSpace(d.profile.InstanceURL) == "" {
		return ""
	}
	return strings.TrimRight(strings.TrimSpace(d.profile.InstanceURL), "/")
}

func (d *EmailDispatcher) memoURL(memo *store.Memo) string {
	baseURL := d.baseURL()
	if memo == nil || memo.UID == "" || baseURL == "" {
		return ""
	}
	return fmt.Sprintf("%s/memos/%s", baseURL, memo.UID)
}

func (d *EmailDispatcher) memoCommentURL(relatedMemo *store.Memo, commentMemo *store.Memo) string {
	baseURL := d.baseURL()
	if relatedMemo == nil || relatedMemo.UID == "" || commentMemo == nil || commentMemo.UID == "" || baseURL == "" {
		return ""
	}
	return fmt.Sprintf("%s/memos/%s#%s", baseURL, relatedMemo.UID, commentMemo.UID)
}

func canViewerAccessMemo(viewer *store.User, memo *store.Memo) bool {
	if memo == nil {
		return false
	}
	if viewer != nil && viewer.Role == store.RoleAdmin {
		return true
	}
	if memo.Visibility == store.Private {
		return viewer != nil && viewer.ID == memo.CreatorID
	}
	if memo.Visibility == store.Protected {
		return viewer != nil
	}
	return true
}
