// Package smtpclient sends classic RFC 5322 messages to mail2news gateways.
package smtpclient

import (
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/mail"
	"net/smtp"
	"strings"
	"time"

	"golang.org/x/net/proxy"
)

type Config struct {
	Host               string
	Port               string
	Mode               string
	Username           string
	Password           string
	InsecureSkipVerify bool
	ProxyType          string
	ProxyAddress       string
	Timeout            time.Duration
}

func Send(cfg Config, from string, recipients []string, message []byte) error {
	if err := validate(cfg, from, recipients); err != nil {
		return err
	}
	timeout := cfg.Timeout
	if timeout <= 0 {
		timeout = 30 * time.Second
	}
	target := net.JoinHostPort(cfg.Host, cfg.Port)
	dialer := &net.Dialer{Timeout: timeout, KeepAlive: 30 * time.Second}
	var conn net.Conn
	var err error
	if strings.EqualFold(cfg.ProxyType, "SOCKS5") {
		socks, proxyErr := proxy.SOCKS5("tcp", cfg.ProxyAddress, nil, dialer)
		if proxyErr != nil {
			return fmt.Errorf("configure SMTP SOCKS5 proxy: %w", proxyErr)
		}
		conn, err = socks.Dial("tcp", target)
	} else {
		conn, err = dialer.Dial("tcp", target)
	}
	if err != nil {
		return fmt.Errorf("connect SMTP server: %w", err)
	}
	defer conn.Close()
	tlsConfig := &tls.Config{ServerName: cfg.Host, MinVersion: tls.VersionTLS12, InsecureSkipVerify: cfg.InsecureSkipVerify} // #nosec G402, explicit user opt-in
	if strings.EqualFold(cfg.Mode, "TLS") {
		tlsConn := tls.Client(conn, tlsConfig)
		if err := tlsConn.Handshake(); err != nil {
			return fmt.Errorf("SMTP TLS handshake: %w", err)
		}
		conn = tlsConn
	}
	client, err := smtp.NewClient(conn, cfg.Host)
	if err != nil {
		return fmt.Errorf("initialize SMTP client: %w", err)
	}
	defer client.Close()
	if strings.EqualFold(cfg.Mode, "STARTTLS") {
		if ok, _ := client.Extension("STARTTLS"); !ok {
			return errors.New("SMTP server does not advertise STARTTLS")
		}
		if err := client.StartTLS(tlsConfig); err != nil {
			return fmt.Errorf("SMTP STARTTLS: %w", err)
		}
	}
	if cfg.Username != "" {
		state, ok := client.TLSConnectionState()
		if !ok || !state.HandshakeComplete {
			return errors.New("SMTP authentication requires TLS")
		}
		if err := client.Auth(smtp.PlainAuth("", cfg.Username, cfg.Password, cfg.Host)); err != nil {
			return fmt.Errorf("SMTP authentication: %w", err)
		}
	}
	if err := client.Mail(from); err != nil {
		return fmt.Errorf("SMTP MAIL FROM: %w", err)
	}
	for _, recipient := range recipients {
		if err := client.Rcpt(recipient); err != nil {
			return fmt.Errorf("SMTP RCPT TO: %w", err)
		}
	}
	writer, err := client.Data()
	if err != nil {
		return fmt.Errorf("SMTP DATA: %w", err)
	}
	if _, err := writer.Write(message); err != nil {
		_ = writer.Close()
		return fmt.Errorf("write SMTP message: %w", err)
	}
	if err := writer.Close(); err != nil {
		return fmt.Errorf("finish SMTP message: %w", err)
	}
	return client.Quit()
}

func validate(cfg Config, from string, recipients []string) error {
	if strings.TrimSpace(cfg.Host) == "" || strings.TrimSpace(cfg.Port) == "" {
		return errors.New("SMTP host and port are required")
	}
	if cfg.Mode != "" && cfg.Mode != "TLS" && cfg.Mode != "STARTTLS" {
		return errors.New("SMTP mode must be cleartext, TLS or STARTTLS")
	}
	if cfg.ProxyType != "DIRECT" && cfg.ProxyType != "SOCKS5" {
		return errors.New("SMTP proxy type must be DIRECT or SOCKS5")
	}
	if strings.HasSuffix(strings.ToLower(cfg.Host), ".onion") && cfg.ProxyType != "SOCKS5" {
		return errors.New(".onion SMTP server requires SOCKS5")
	}
	if cfg.ProxyType == "SOCKS5" {
		if _, _, err := net.SplitHostPort(cfg.ProxyAddress); err != nil {
			return fmt.Errorf("invalid SMTP SOCKS5 address: %w", err)
		}
	}
	if cfg.Username != "" && cfg.Mode == "" {
		return errors.New("SMTP authentication requires TLS or STARTTLS")
	}
	if _, err := mail.ParseAddress(from); err != nil {
		return fmt.Errorf("invalid SMTP sender: %w", err)
	}
	if len(recipients) == 0 {
		return errors.New("at least one SMTP recipient is required")
	}
	for _, recipient := range recipients {
		if _, err := mail.ParseAddress(recipient); err != nil {
			return fmt.Errorf("invalid SMTP recipient: %w", err)
		}
	}
	return nil
}
