package usecase

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"net/smtp"
	"strings"
	"time"

	"apps/backend/internal/module/notification/domain"
)

// SMTPConfig for generic SMTP (Mailgun, SES SMTP, Mailpit, etc.).
// Mailpit local: Host=127.0.0.1 Port=1025 User/Password empty (or any if MP_SMTP_AUTH_ACCEPT_ANY).
// Mailgun: Host=smtp.mailgun.org Port=587 User=postmaster@domain Password=key.
type SMTPConfig struct {
	Host     string
	Port     int
	User     string
	Password string
}

// SMTPDelivery sends HTML mail via SMTP (optional AUTH).
type SMTPDelivery struct {
	cfg SMTPConfig
}

func NewSMTPDelivery(cfg SMTPConfig) *SMTPDelivery {
	if cfg.Port <= 0 {
		// Mailpit default is 1025; production SMTP usually 587
		cfg.Port = 587
	}
	return &SMTPDelivery{cfg: cfg}
}

func (d *SMTPDelivery) Name() string { return "smtp" }

func (d *SMTPDelivery) SendEmail(ctx context.Context, req domain.EmailRequest) error {
	if d.cfg.Host == "" {
		return fmt.Errorf("smtp: empty host")
	}
	fromAddr := extractEmail(req.SenderEmail)
	if fromAddr == "" {
		return fmt.Errorf("smtp: invalid sender")
	}
	to := strings.TrimSpace(req.RecipientEmail)
	if to == "" {
		return fmt.Errorf("smtp: empty recipient")
	}

	msg := buildMIME(req.SenderEmail, to, req.Subject, req.HTMLBody)
	addr := fmt.Sprintf("%s:%d", d.cfg.Host, d.cfg.Port)

	// Prefer STARTTLS on 587; plain TLS on 465.
	dialer := &net.Dialer{Timeout: 15 * time.Second}
	conn, err := dialer.DialContext(ctx, "tcp", addr)
	if err != nil {
		return fmt.Errorf("smtp dial: %w", err)
	}

	var client *smtp.Client
	if d.cfg.Port == 465 {
		tlsConn := tls.Client(conn, &tls.Config{ServerName: d.cfg.Host, MinVersion: tls.VersionTLS12})
		client, err = smtp.NewClient(tlsConn, d.cfg.Host)
		if err != nil {
			_ = conn.Close()
			return fmt.Errorf("smtp tls client: %w", err)
		}
	} else {
		client, err = smtp.NewClient(conn, d.cfg.Host)
		if err != nil {
			_ = conn.Close()
			return fmt.Errorf("smtp client: %w", err)
		}
		if ok, _ := client.Extension("STARTTLS"); ok {
			if err := client.StartTLS(&tls.Config{ServerName: d.cfg.Host, MinVersion: tls.VersionTLS12}); err != nil {
				_ = client.Close()
				return fmt.Errorf("smtp starttls: %w", err)
			}
		}
	}
	defer client.Close()

	if d.cfg.User != "" {
		auth := smtp.PlainAuth("", d.cfg.User, d.cfg.Password, d.cfg.Host)
		if err := client.Auth(auth); err != nil {
			return fmt.Errorf("smtp auth: %w", err)
		}
	}
	if err := client.Mail(fromAddr); err != nil {
		return fmt.Errorf("smtp mail: %w", err)
	}
	if err := client.Rcpt(to); err != nil {
		return fmt.Errorf("smtp rcpt: %w", err)
	}
	w, err := client.Data()
	if err != nil {
		return fmt.Errorf("smtp data: %w", err)
	}
	if _, err := w.Write(msg); err != nil {
		_ = w.Close()
		return fmt.Errorf("smtp write: %w", err)
	}
	if err := w.Close(); err != nil {
		return fmt.Errorf("smtp close: %w", err)
	}
	return client.Quit()
}

func extractEmail(from string) string {
	from = strings.TrimSpace(from)
	if i := strings.LastIndex(from, "<"); i >= 0 {
		j := strings.LastIndex(from, ">")
		if j > i {
			return strings.TrimSpace(from[i+1 : j])
		}
	}
	return from
}

func buildMIME(from, to, subject, html string) []byte {
	var b strings.Builder
	b.WriteString("From: " + from + "\r\n")
	b.WriteString("To: " + to + "\r\n")
	b.WriteString("Subject: " + encodeSubject(subject) + "\r\n")
	b.WriteString("MIME-Version: 1.0\r\n")
	b.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
	b.WriteString("\r\n")
	b.WriteString(html)
	return []byte(b.String())
}

// encodeSubject keeps ASCII subjects as-is; non-ASCII uses a simple UTF-8 Q-encoding header.
func encodeSubject(s string) string {
	for _, r := range s {
		if r > 127 {
			// RFC 2047
			return "=?UTF-8?B?" + b64(s) + "?="
		}
	}
	return s
}

func b64(s string) string {
	const table = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
	src := []byte(s)
	var out strings.Builder
	for i := 0; i < len(src); i += 3 {
		var n uint32
		remain := len(src) - i
		switch {
		case remain >= 3:
			n = uint32(src[i])<<16 | uint32(src[i+1])<<8 | uint32(src[i+2])
			out.WriteByte(table[(n>>18)&63])
			out.WriteByte(table[(n>>12)&63])
			out.WriteByte(table[(n>>6)&63])
			out.WriteByte(table[n&63])
		case remain == 2:
			n = uint32(src[i])<<16 | uint32(src[i+1])<<8
			out.WriteByte(table[(n>>18)&63])
			out.WriteByte(table[(n>>12)&63])
			out.WriteByte(table[(n>>6)&63])
			out.WriteByte('=')
		case remain == 1:
			n = uint32(src[i]) << 16
			out.WriteByte(table[(n>>18)&63])
			out.WriteByte(table[(n>>12)&63])
			out.WriteByte('=')
			out.WriteByte('=')
		}
	}
	return out.String()
}
