package email_sender

import (
	"context"
	"crypto/rand"
	"crypto/tls"
	"errors"
	"fmt"
	"mime"
	"net"
	"net/mail"
	"net/smtp"
	"strings"
	"time"
)

type sender struct {
	smtpHost     string
	smtpPort     string
	smtpUser     string
	smtpPassword string
	from         string
	fromName     string
	replyTo      string
	timeout      time.Duration
	tlsConfig    *tls.Config
}

func NewSender(
	smtpHost string,
	smtpPort string,
	smtpUser string,
	smtpPassword string,
	fromName string,
	replyTo string,
	timeout time.Duration,
) IEmailSender {
	// From заголовка письма по умолчанию совпадает с учётной записью SMTP.
	from := smtpUser
	if timeout <= 0 {
		timeout = 10 * time.Second
	}
	return &sender{
		smtpHost:     smtpHost,
		smtpPort:     smtpPort,
		smtpUser:     smtpUser,
		smtpPassword: smtpPassword,
		from:         from,
		fromName:     sanitizeHeader(fromName),
		replyTo:      sanitizeHeader(replyTo),
		timeout:      timeout,
		// Проверка имени сервера включена всегда; поле переопределяется
		// только в тестах (свой RootCAs для самоподписанного сертификата).
		tlsConfig: &tls.Config{ServerName: smtpHost},
	}
}

// Send доставляет письмо через SMTP (implicit TLS) с учётом контекста и
// таймаута: соединение устанавливается через DialContext, а все фазы SMTP-
// диалога ограничены общим deadline, производным от ctx.
func (s *sender) Send(ctx context.Context, message Message) error {
	// Санитизация пользовательского ввода: CR/LF/NUL в заголовках ломают
	// формат письма и позволяют инъекцию произвольных заголовков (Bcc и т.п.).
	to := sanitizeHeader(message.To)
	subject := sanitizeHeader(message.Subject)
	if err := validateMessage(to, subject); err != nil {
		return err
	}
	body := buildMessage(s.from, s.fromName, to, subject, s.replyTo, message.Body, message.HTML)

	ctx, cancel := context.WithTimeout(ctx, s.timeout)
	defer cancel()

	addr := net.JoinHostPort(s.smtpHost, s.smtpPort)
	rawConn, err := (&net.Dialer{}).DialContext(ctx, "tcp", addr)
	if err != nil {
		return fmt.Errorf("email: smtp dial %s: %w", addr, err)
	}
	defer rawConn.Close()

	conn := tls.Client(rawConn, s.tlsConfig)
	if deadline, ok := ctx.Deadline(); ok {
		// Единый бюджет времени на весь SMTP-диалог: зависший сервер не
		// должен держать запрос дольше таймаута.
		if err = conn.SetDeadline(deadline); err != nil {
			return fmt.Errorf("email: set deadline: %w", err)
		}
	}

	client, err := smtp.NewClient(conn, s.smtpHost)
	if err != nil {
		return fmt.Errorf("email: smtp greeting: %w", err)
	}
	// Close гарантированно закрывает соединение при раннем выходе; в конце
	// штатного пути вместо него вызывается Quit.
	defer client.Close() //nolint:errcheck

	auth := smtp.PlainAuth("", s.smtpUser, s.smtpPassword, s.smtpHost)
	if err = client.Auth(auth); err != nil {
		return fmt.Errorf("email: smtp auth: %w", err)
	}
	if err = client.Mail(s.from); err != nil {
		return fmt.Errorf("email: smtp mail from: %w", err)
	}
	if err = client.Rcpt(to); err != nil {
		return fmt.Errorf("email: smtp rcpt to: %w", err)
	}

	w, err := client.Data()
	if err != nil {
		return fmt.Errorf("email: smtp data: %w", err)
	}
	if _, err = w.Write(body); err != nil {
		return fmt.Errorf("email: smtp write: %w", err)
	}
	if err = w.Close(); err != nil {
		return fmt.Errorf("email: smtp data close: %w", err)
	}
	if err = client.Quit(); err != nil {
		return fmt.Errorf("email: smtp quit: %w", err)
	}
	return nil
}

// buildMessage собирает тело письма. Заголовки обязаны быть ASCII, поэтому
// не-ASCII значения (имя отправителя, тема) кодируются по RFC 2047, иначе
// кириллица может быть испорчена промежуточными серверами. Заголовки
// Date и Message-ID добавляются явно: их отсутствие — типичный признак
// спама для почтовых фильтров (включая Gmail). Если задана HTML-версия,
// письмо собирается как multipart/alternative (text/plain + text/html).
func buildMessage(from, fromName, to, subject, replyTo, body, html string) []byte {
	var b strings.Builder
	b.Grow(len(from) + len(fromName) + len(to) + len(subject) + len(body) + len(html) + 512)
	b.WriteString("From: ")
	if fromName != "" {
		b.WriteString(mime.QEncoding.Encode("utf-8", fromName))
		b.WriteString(" <")
		b.WriteString(from)
		b.WriteString(">\r\n")
	} else {
		b.WriteString(from)
		b.WriteString("\r\n")
	}
	b.WriteString("To: ")
	b.WriteString(to)
	b.WriteString("\r\n")
	b.WriteString("Subject: ")
	b.WriteString(mime.QEncoding.Encode("utf-8", subject))
	b.WriteString("\r\n")
	b.WriteString("Date: ")
	b.WriteString(time.Now().UTC().Format(time.RFC1123Z))
	b.WriteString("\r\n")
	b.WriteString("Message-ID: ")
	b.WriteString(messageID(from))
	b.WriteString("\r\n")
	if replyTo != "" {
		b.WriteString("Reply-To: ")
		b.WriteString(replyTo)
		b.WriteString("\r\n")
	}
	b.WriteString("MIME-Version: 1.0\r\n")
	if html == "" {
		b.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
		b.WriteString("\r\n")
		b.WriteString(body)
		b.WriteString("\r\n")
		return []byte(b.String())
	}

	boundary := fmt.Sprintf("----=_evening_detective_%s", randomHex(10))
	b.WriteString("Content-Type: multipart/alternative; boundary=\"")
	b.WriteString(boundary)
	b.WriteString("\"\r\n\r\n")
	b.WriteString("--")
	b.WriteString(boundary)
	b.WriteString("\r\n")
	b.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
	b.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
	b.WriteString(body)
	b.WriteString("\r\n--")
	b.WriteString(boundary)
	b.WriteString("\r\n")
	b.WriteString("Content-Type: text/html; charset=utf-8\r\n")
	b.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
	b.WriteString(html)
	b.WriteString("\r\n--")
	b.WriteString(boundary)
	b.WriteString("--\r\n")
	return []byte(b.String())
}

// messageID генерирует уникальный Message-ID в домене отправителя.
func messageID(from string) string {
	domain := ""
	if i := strings.LastIndex(from, "@"); i >= 0 && i < len(from)-1 {
		domain = from[i+1:]
	}
	if domain == "" {
		domain = "localhost"
	}
	return "<" + randomHex(12) + "@" + domain + ">"
}

// randomHex возвращает случайную hex-строку (криптостойкий генератор;
// при сбое — значение из таймера, чтобы отправка не падала).
func randomHex(n int) string {
	b := make([]byte, n)
	if _, err := rand.Read(b); err != nil {
		return fmt.Sprintf("%x", time.Now().UnixNano())
	}
	return fmt.Sprintf("%x", b)
}

// sanitizeHeader удаляет символы, ломающие структуру заголовков письма
// (CRLF-инъекция заголовков, NUL).
func sanitizeHeader(s string) string {
	r := strings.NewReplacer("\r", "", "\n", "", "\x00", "")
	return r.Replace(s)
}

// validateMessage проверяет адрес получателя и наличие темы до обращения к
// SMTP-серверу.
func validateMessage(to, subject string) error {
	if _, err := mail.ParseAddress(to); err != nil {
		return fmt.Errorf("email: некорректный адрес получателя: %w", err)
	}
	if subject == "" {
		return errors.New("email: пустая тема письма")
	}
	return nil
}
