package account

import (
	"context"
	"fmt"
	"net"
	"net/smtp"

	"go.uber.org/zap"
)

// Mailer delivers a transactional email. It is the seam behind which the email
// confirm-code flow sends codes, so the relay is swappable and unit tests use a
// fixture (see docs/TESTING.md: no real network in tests). The context is offered
// for cancellation; the standard-library SMTP implementation sends synchronously
// and ignores it.
type Mailer interface {
	Send(ctx context.Context, to, subject, body string) error
}

// SMTPConfig configures the SMTP relay. An empty Host selects the LogMailer
// instead, so a deployment without a relay still runs (the code lands in the log).
type SMTPConfig struct {
	Host     string
	Port     string
	Username string
	Password string
	From     string
}

// SMTPMailer sends mail through an SMTP relay using the standard library. When a
// username is set it authenticates with PLAIN; otherwise it relays unauthenticated.
type SMTPMailer struct {
	cfg SMTPConfig
}

// NewSMTPMailer constructs an SMTPMailer for cfg.
func NewSMTPMailer(cfg SMTPConfig) SMTPMailer {
	return SMTPMailer{cfg: cfg}
}

// Send delivers a plain-text UTF-8 message to to via the configured relay.
func (m SMTPMailer) Send(_ context.Context, to, subject, body string) error {
	addr := net.JoinHostPort(m.cfg.Host, m.cfg.Port)
	var auth smtp.Auth
	if m.cfg.Username != "" {
		auth = smtp.PlainAuth("", m.cfg.Username, m.cfg.Password, m.cfg.Host)
	}
	if err := smtp.SendMail(addr, auth, m.cfg.From, []string{to}, message(m.cfg.From, to, subject, body)); err != nil {
		return fmt.Errorf("account: send mail to %s: %w", to, err)
	}
	return nil
}

// message renders a minimal RFC 5322 plain-text email.
func message(from, to, subject, body string) []byte {
	return []byte("From: " + from + "\r\n" +
		"To: " + to + "\r\n" +
		"Subject: " + subject + "\r\n" +
		"MIME-Version: 1.0\r\n" +
		"Content-Type: text/plain; charset=UTF-8\r\n" +
		"\r\n" + body + "\r\n")
}

// LogMailer logs the message instead of sending it. It is the default when no
// SMTP relay is configured and is intended for development only: it logs the body,
// which carries the confirm-code, so it must not be used in production.
type LogMailer struct {
	log *zap.Logger
}

// NewLogMailer constructs a LogMailer that logs through log.
func NewLogMailer(log *zap.Logger) LogMailer {
	return LogMailer{log: log}
}

// Send logs the message at info level and reports success.
func (m LogMailer) Send(_ context.Context, to, subject, body string) error {
	if m.log != nil {
		m.log.Info("email not sent (log mailer)",
			zap.String("to", to), zap.String("subject", subject), zap.String("body", body))
	}
	return nil
}
