package mail

import (
	"context"
	"errors"
	"fmt"

	"galaxy/backend/internal/config"

	gomail "github.com/wneessen/go-mail"
	"go.uber.org/zap"
)

// SMTPClient is the abstraction surface over `wneessen/go-mail` so
// tests can stub the wire layer without dialling. Production wires
// realSMTPClient.
type SMTPClient interface {
	DialAndSendWithContext(ctx context.Context, msg *gomail.Msg) error
}

// realSMTPClient adapts *gomail.Client to SMTPClient. The variadic
// nature of DialAndSendWithContext is hidden because the worker only
// ever sends one message per call.
type realSMTPClient struct {
	inner *gomail.Client
}

func (c *realSMTPClient) DialAndSendWithContext(ctx context.Context, msg *gomail.Msg) error {
	return c.inner.DialAndSendWithContext(ctx, msg)
}

// smtpSender implements SMTPSender on top of an SMTPClient. The
// `from` address is captured at construction time from
// `BACKEND_SMTP_FROM`.
type smtpSender struct {
	client SMTPClient
	from   string
	logger *zap.Logger
}

// NewSMTPSender constructs the production sender bound to the SMTP
// relay configured in cfg. The TLS-mode mapping is:
//
//   - "none"     → plain TCP, no TLS;
//   - "starttls" → STARTTLS required (TLSMandatory);
//   - "tls"      → implicit TLS at the configured port (WithSSL).
//
// PLAIN authentication is enabled when both Username and Password are
// non-empty.
func NewSMTPSender(cfg config.SMTPConfig, logger *zap.Logger) (SMTPSender, error) {
	if logger == nil {
		logger = zap.NewNop()
	}
	logger = logger.Named("mail.smtp")

	opts := []gomail.Option{gomail.WithPort(cfg.Port)}
	switch cfg.TLSMode {
	case "none":
		opts = append(opts, gomail.WithTLSPolicy(gomail.NoTLS))
	case "starttls":
		opts = append(opts, gomail.WithTLSPolicy(gomail.TLSMandatory))
	case "tls":
		opts = append(opts, gomail.WithSSL())
	default:
		return nil, fmt.Errorf("mail: unsupported SMTP TLS mode %q", cfg.TLSMode)
	}
	if cfg.Username != "" && cfg.Password != "" {
		opts = append(opts,
			gomail.WithSMTPAuth(gomail.SMTPAuthPlain),
			gomail.WithUsername(cfg.Username),
			gomail.WithPassword(cfg.Password),
		)
	}

	cli, err := gomail.NewClient(cfg.Host, opts...)
	if err != nil {
		return nil, fmt.Errorf("mail: build smtp client: %w", err)
	}
	return &smtpSender{
		client: &realSMTPClient{inner: cli},
		from:   cfg.From,
		logger: logger,
	}, nil
}

// Send renders the OutboundMessage as a *gomail.Msg and dispatches it
// through the SMTP client. Address validation is intentional: a
// malformed To here means the producer slipped past
// normaliseRecipient, which is a programming error and gets wrapped
// as Permanent so the worker dead-letters immediately.
func (s *smtpSender) Send(ctx context.Context, msg OutboundMessage) error {
	if len(msg.To) == 0 {
		return &SendError{Err: errors.New("mail: outbound message has no recipients"), Permanent: true}
	}
	m := gomail.NewMsg()
	if err := m.From(s.from); err != nil {
		return &SendError{Err: fmt.Errorf("set FROM: %w", err), Permanent: true}
	}
	for _, addr := range msg.To {
		if err := m.AddTo(addr); err != nil {
			return &SendError{Err: fmt.Errorf("add TO %q: %w", addr, err), Permanent: true}
		}
	}
	m.Subject(msg.Subject)
	contentType := gomail.ContentType(msg.ContentType)
	if msg.ContentType == "" {
		contentType = gomail.TypeTextPlain
	}
	m.SetBodyString(contentType, string(msg.Body))

	if err := s.client.DialAndSendWithContext(ctx, m); err != nil {
		permanent := classifySMTPError(err)
		return &SendError{Err: err, Permanent: permanent}
	}
	return nil
}

// classifySMTPError decides whether err is permanent. A *gomail.SendError
// reports its permanence through IsTemp; everything else (dial
// failures, context errors, generic I/O) is treated as transient so the
// worker retries until MaxAttempts.
func classifySMTPError(err error) bool {
	if err == nil {
		return false
	}
	var sendErr *gomail.SendError
	if errors.As(err, &sendErr) && sendErr != nil {
		return !sendErr.IsTemp()
	}
	return false
}
