package mail

import (
	"fmt"
	"mime"
	"net"
	"net/smtp"
	"strings"
	"sync"
	"time"

	"heckel.io/ntfy/v2/log"
	"heckel.io/ntfy/v2/model"
)

const (
	tagMail = "mail"

	emailVerificationSubject = "Verify your email for ntfy"
	passwordResetSubject     = "Reset your ntfy password"
)

// Config holds the SMTP configuration for the mail sender
type Config struct {
	BaseURL  string // ntfy base URL, used to build topic URLs in notification emails
	SMTPAddr string // SMTP server address (host:port)
	SMTPUser string // SMTP auth username
	SMTPPass string // SMTP auth password
	From     string // Sender email address
}

// Sender sends all of ntfy's outgoing email: notification emails (the email-on-publish feature)
// as well as the magic-link emails for email verification and password reset. realSender is the
// SMTP-backed implementation; tests inject a fake.
type Sender interface {
	SendNotification(to string, m *model.Message, senderIP string) error
	NotificationCounts() (total int64, success int64, failure int64)
	SendEmailVerification(to, link string) error
	SendPasswordReset(to, link string) error
}

// realSender is the SMTP-backed implementation of Sender. Pending verification/reset state lives
// in the database (see user.Manager), not in this struct.
type realSender struct {
	config  *Config
	success int64
	failure int64
	mu      sync.Mutex
}

// NewSender creates a new mail Sender with the given SMTP config
func NewSender(config *Config) Sender {
	return &realSender{config: config}
}

// SendNotification formats a ntfy message into a notification email and sends it via SMTP. It
// tracks success/failure counts, exposed via Counts (used for the server stats).
func (s *realSender) SendNotification(to string, m *model.Message, senderIP string) error {
	message, err := formatMail(s.config.BaseURL, senderIP, s.config.From, to, m)
	if err != nil {
		s.count(false)
		return err
	}
	log.Tag(tagMail).Field("email_to", to).Debug("Sending notification email")
	err = s.sendRaw(to, []byte(message))
	s.count(err == nil)
	return err
}

// NotificationCounts returns the number of notification emails sent, broken down into total, success and failure
func (s *realSender) NotificationCounts() (total int64, success int64, failure int64) {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.success + s.failure, s.success, s.failure
}

// SendEmailVerification sends an email containing a magic link to verify ownership of the
// recipient address. The link carries a one-time token validated against the database.
func (s *realSender) SendEmailVerification(to, link string) error {
	body := fmt.Sprintf(`Click the link below to verify this email address for your ntfy account:

%s

This link expires in 24 hours. If you did not request this, you can safely ignore this email.`, link)
	return s.send(to, emailVerificationSubject, body)
}

// SendPasswordReset sends an email containing a magic link to set a new password. The link
// carries a one-time token validated against the database.
func (s *realSender) SendPasswordReset(to, link string) error {
	body := fmt.Sprintf(`Click the link below to set a new password for your ntfy account:

%s

This link expires in 1 hour. If you did not request this, you can safely ignore this email -- your password will not change.`, link)
	return s.send(to, passwordResetSubject, body)
}

// send sends a plain text email via SMTP
func (s *realSender) send(to, subject, body string) error {
	date := time.Now().UTC().Format(time.RFC1123Z)
	encodedSubject := mime.BEncoding.Encode("utf-8", subject)
	message := `From: ntfy <{from}>
To: {to}
Date: {date}
Subject: {subject}
Content-Type: text/plain; charset="utf-8"

{body}`
	message = strings.ReplaceAll(message, "{from}", s.config.From)
	message = strings.ReplaceAll(message, "{to}", to)
	message = strings.ReplaceAll(message, "{date}", date)
	message = strings.ReplaceAll(message, "{subject}", encodedSubject)
	message = strings.ReplaceAll(message, "{body}", body)
	log.Tag(tagMail).Field("email_to", to).Debug("Sending email")
	return s.sendRaw(to, []byte(message))
}

// sendRaw sends a raw email message via SMTP
func (s *realSender) sendRaw(to string, message []byte) error {
	host, _, err := net.SplitHostPort(s.config.SMTPAddr)
	if err != nil {
		return err
	}
	var auth smtp.Auth
	if s.config.SMTPUser != "" {
		auth = smtp.PlainAuth("", s.config.SMTPUser, s.config.SMTPPass, host)
	}
	return smtp.SendMail(s.config.SMTPAddr, auth, s.config.From, []string{to}, message)
}

func (s *realSender) count(ok bool) {
	s.mu.Lock()
	defer s.mu.Unlock()
	if ok {
		s.success++
	} else {
		s.failure++
	}
}
