package smtpclient

import (
	"bufio"
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"net"
	"net/textproto"
	"strings"
	"time"
)

type DialContextFunc func(ctx context.Context, network, address string) (net.Conn, error)

type Config struct {
	Host          string
	Port          int
	Recipient     string
	EnvelopeFrom  string
	HELO          string
	TLSServerName string
	RequireTLS    bool
	ImplicitTLS   bool
	Timeout       time.Duration
	DryRun        bool
}

type Client struct {
	cfg  Config
	dial DialContextFunc
}

type Message struct {
	EnvelopeFrom string
	Raw          string
}

func New(cfg Config, dial DialContextFunc) *Client {
	return &Client{cfg: cfg, dial: dial}
}

func (c *Client) Send(ctx context.Context, msg Message) error {
	if c.cfg.DryRun {
		return nil
	}
	if c.dial == nil {
		c.dial = (&net.Dialer{}).DialContext
	}
	timeout := c.cfg.Timeout
	if timeout <= 0 {
		timeout = 90 * time.Second
	}
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()

	addr := net.JoinHostPort(c.cfg.Host, fmt.Sprintf("%d", c.cfg.Port))
	conn, err := c.dial(ctx, "tcp", addr)
	if err != nil {
		return fmt.Errorf("dial smtp: %w", err)
	}
	defer conn.Close()
	if deadline, ok := ctx.Deadline(); ok {
		_ = conn.SetDeadline(deadline)
	}
	if c.cfg.ImplicitTLS {
		tlsConn := tls.Client(conn, &tls.Config{
			ServerName: c.cfg.TLSServerName,
			MinVersion: tls.VersionTLS12,
		})
		if err := tlsConn.HandshakeContext(ctx); err != nil {
			return fmt.Errorf("implicit tls handshake: %w", err)
		}
		conn = tlsConn
	}

	session := newSession(conn)
	if _, _, err := session.read(220); err != nil {
		return fmt.Errorf("smtp greeting: %w", err)
	}

	if err := session.ehlo(c.cfg.HELO); err != nil {
		return err
	}

	if c.cfg.RequireTLS && !c.cfg.ImplicitTLS {
		if _, _, err := session.cmd(220, "STARTTLS\r\n"); err != nil {
			return fmt.Errorf("starttls: %w", err)
		}
		tlsConn := tls.Client(conn, &tls.Config{
			ServerName: c.cfg.TLSServerName,
			MinVersion: tls.VersionTLS12,
		})
		if err := tlsConn.HandshakeContext(ctx); err != nil {
			return fmt.Errorf("tls handshake: %w", err)
		}
		session = newSession(tlsConn)
		if err := session.ehlo(c.cfg.HELO); err != nil {
			return err
		}
	}

	envelopeFrom := sanitizeEnvelope(c.cfg.EnvelopeFrom)
	if envelopeFrom == "" {
		envelopeFrom = sanitizeEnvelope(msg.EnvelopeFrom)
	}
	if _, _, err := session.cmd(250, "MAIL FROM:<%s>\r\n", envelopeFrom); err != nil {
		return fmt.Errorf("mail from rejected: %w", err)
	}
	if _, _, err := session.cmd(250, "RCPT TO:<%s>\r\n", sanitizeEnvelope(c.cfg.Recipient)); err != nil {
		return fmt.Errorf("rcpt to rejected: %w", err)
	}
	if _, _, err := session.cmd(354, "DATA\r\n"); err != nil {
		return fmt.Errorf("data rejected: %w", err)
	}
	if err := writeSMTPData(session.w, msg.Raw); err != nil {
		return err
	}
	if _, _, err := session.read(250); err != nil {
		return fmt.Errorf("message rejected: %w", err)
	}
	_, _, _ = session.cmd(221, "QUIT\r\n")
	return nil
}

type session struct {
	conn net.Conn
	tp   *textproto.Reader
	w    *bufio.Writer
}

func newSession(conn net.Conn) *session {
	reader := bufio.NewReader(conn)
	return &session{
		conn: conn,
		tp:   textproto.NewReader(reader),
		w:    bufio.NewWriter(conn),
	}
}

func (s *session) ehlo(helo string) error {
	if helo == "" {
		helo = "n2usenet.local"
	}
	if _, _, err := s.cmd(250, "EHLO %s\r\n", sanitizeAtom(helo)); err != nil {
		if _, _, heloErr := s.cmd(250, "HELO %s\r\n", sanitizeAtom(helo)); heloErr != nil {
			return fmt.Errorf("ehlo failed: %w", err)
		}
	}
	return nil
}

func (s *session) cmd(expect int, format string, args ...any) (int, string, error) {
	if _, err := fmt.Fprintf(s.w, format, args...); err != nil {
		return 0, "", err
	}
	if err := s.w.Flush(); err != nil {
		return 0, "", err
	}
	return s.read(expect)
}

func (s *session) read(expect int) (int, string, error) {
	code, msg, err := s.tp.ReadResponse(expect)
	if err != nil {
		return code, msg, err
	}
	return code, msg, nil
}

func writeSMTPData(w *bufio.Writer, raw string) error {
	raw = strings.ReplaceAll(raw, "\r\n", "\n")
	raw = strings.ReplaceAll(raw, "\r", "\n")
	for _, line := range strings.Split(raw, "\n") {
		if strings.HasPrefix(line, ".") {
			line = "." + line
		}
		if _, err := io.WriteString(w, line+"\r\n"); err != nil {
			return fmt.Errorf("write smtp data: %w", err)
		}
	}
	if _, err := io.WriteString(w, ".\r\n"); err != nil {
		return fmt.Errorf("write smtp terminator: %w", err)
	}
	if err := w.Flush(); err != nil {
		return fmt.Errorf("flush smtp data: %w", err)
	}
	return nil
}

func sanitizeEnvelope(v string) string {
	v = strings.TrimSpace(v)
	v = strings.ReplaceAll(v, "\r", "")
	v = strings.ReplaceAll(v, "\n", "")
	v = strings.Trim(v, "<>")
	return v
}

func sanitizeAtom(v string) string {
	v = strings.TrimSpace(v)
	v = strings.ReplaceAll(v, "\r", "")
	v = strings.ReplaceAll(v, "\n", "")
	if v == "" {
		return "n2usenet.local"
	}
	return v
}
