package mailer

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	netmail "net/mail"
	"net/smtp"
	"strconv"
	"strings"
)

const (
	smtpSecuritySSL      = "ssl"
	smtpSecurityStartTLS = "starttls"
)

type smtpTransportConfig struct {
	host     string
	port     int
	username string
	password string
	security string
}

type smtpSendFunc func(
	ctx context.Context,
	transport smtpTransportConfig,
	fromAddress string,
	toAddress string,
	subject string,
	body string,
) error

type smtpMailer struct {
	transport     smtpTransportConfig
	fromAddress   string
	publicBaseURL string
	send          smtpSendFunc
}

// NewSMTP constructs an SMTP-backed mailer.
func NewSMTP(publicBaseURL string, fromAddress string, host string, port int, username string, password string, security string) (Mailer, error) {
	trimmedFromAddress := strings.TrimSpace(fromAddress)
	if trimmedFromAddress == "" {
		return nil, fmt.Errorf("mail from address is required")
	}
	parsedFrom, err := netmail.ParseAddress(trimmedFromAddress)
	if err != nil {
		return nil, fmt.Errorf("mail from address must be a valid email address: %w", err)
	}
	if parsedFrom.Address != trimmedFromAddress {
		return nil, fmt.Errorf("mail from address must be a plain email address (e.g. noreply@example.com): %q", trimmedFromAddress)
	}

	trimmedHost := strings.TrimSpace(host)
	if trimmedHost == "" {
		return nil, fmt.Errorf("mail SMTP host is required")
	}
	if port <= 0 || port > 65535 {
		return nil, fmt.Errorf("mail SMTP port must be between 1 and 65535")
	}
	trimmedUsername := strings.TrimSpace(username)
	if trimmedUsername == "" {
		return nil, fmt.Errorf("mail SMTP username is required")
	}
	trimmedPassword := strings.TrimSpace(password)
	if trimmedPassword == "" {
		return nil, fmt.Errorf("mail SMTP password is required")
	}
	trimmedSecurity := strings.ToLower(strings.TrimSpace(security))
	switch trimmedSecurity {
	case smtpSecuritySSL, smtpSecurityStartTLS:
	default:
		return nil, fmt.Errorf("mail SMTP security must be %q or %q", smtpSecuritySSL, smtpSecurityStartTLS)
	}

	return newSMTPMailer(
		publicBaseURL,
		trimmedFromAddress,
		smtpTransportConfig{
			host:     trimmedHost,
			port:     port,
			username: trimmedUsername,
			password: trimmedPassword,
			security: trimmedSecurity,
		},
		sendSMTPEmail,
	), nil
}

func newSMTPMailer(publicBaseURL string, fromAddress string, transport smtpTransportConfig, send smtpSendFunc) *smtpMailer {
	return &smtpMailer{
		transport:     transport,
		fromAddress:   fromAddress,
		publicBaseURL: publicBaseURL,
		send:          send,
	}
}

func (mailer *smtpMailer) SendVerificationEmail(ctx context.Context, email string, token string) error {
	body := strings.Join([]string{
		"Welcome to AgentTeam.",
		"Use the verification token below to finish verifying your email:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "verify-email", token),
		"If you did not request this email, you can safely ignore it.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Verify your AgentTeam email", body)
}

func (mailer *smtpMailer) SendPasswordResetEmail(ctx context.Context, email string, token string) error {
	body := strings.Join([]string{
		"A password reset was requested for your AgentTeam account.",
		"Use the reset token below to set a new password:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "reset-password", token),
		"If you did not request a password reset, you can safely ignore this email.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Reset your AgentTeam password", body)
}

func (mailer *smtpMailer) SendWorkspaceInviteEmail(ctx context.Context, email string, workspaceName string, token string) error {
	body := strings.Join([]string{
		fmt.Sprintf("You've been invited to join %s on AgentTeam.", strings.TrimSpace(workspaceName)),
		"Use the invite token below after you register and verify your email:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "accept-invite", token),
		"If you were not expecting this invite, you can safely ignore it.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Your AgentTeam workspace invite", body)
}

func (mailer *smtpMailer) SendOrganizationInviteEmail(ctx context.Context, email string, organizationName string, token string) error {
	body := strings.Join([]string{
		fmt.Sprintf("You've been invited to join %s on AgentTeam.", strings.TrimSpace(organizationName)),
		"Use the invite token below after you register and verify your email:",
		token,
		"You can also open this link:",
		tokenLink(mailer.publicBaseURL, "accept-invite", token),
		"If you were not expecting this invite, you can safely ignore it.",
	}, "\n\n")
	return mailer.sendPlainTextEmail(ctx, email, "Your AgentTeam organization invite", body)
}

func (mailer *smtpMailer) sendPlainTextEmail(ctx context.Context, email string, subject string, body string) error {
	trimmedEmail := strings.TrimSpace(email)
	if trimmedEmail == "" {
		return fmt.Errorf("email is required")
	}
	if err := mailer.send(ctx, mailer.transport, mailer.fromAddress, trimmedEmail, subject, body); err != nil {
		return fmt.Errorf("send email via SMTP server %s:%d: %w", mailer.transport.host, mailer.transport.port, err)
	}
	return nil
}

func sendSMTPEmail(
	ctx context.Context,
	transport smtpTransportConfig,
	fromAddress string,
	toAddress string,
	subject string,
	body string,
) error {
	serverAddress := net.JoinHostPort(transport.host, strconv.Itoa(transport.port))

	dialer := &net.Dialer{}
	connection, err := dialer.DialContext(ctx, "tcp", serverAddress)
	if err != nil {
		return fmt.Errorf("dial server %s: %w", serverAddress, err)
	}

	if deadline, ok := ctx.Deadline(); ok {
		_ = connection.SetDeadline(deadline)
	}

	tlsConfig := &tls.Config{
		ServerName: transport.host,
		MinVersion: tls.VersionTLS12,
	}

	if transport.security == smtpSecuritySSL {
		tlsConnection := tls.Client(connection, tlsConfig)
		if err := tlsConnection.HandshakeContext(ctx); err != nil {
			_ = connection.Close()
			return fmt.Errorf("start SSL session with server %s: %w", serverAddress, err)
		}
		connection = tlsConnection
	}

	client, err := smtp.NewClient(connection, transport.host)
	if err != nil {
		_ = connection.Close()
		return fmt.Errorf("create client for server %s: %w", serverAddress, err)
	}
	defer client.Close()

	if transport.security == smtpSecurityStartTLS {
		supportsStartTLS, _ := client.Extension("STARTTLS")
		if !supportsStartTLS {
			return fmt.Errorf("SMTP server %s does not support STARTTLS", serverAddress)
		}
		if err := client.StartTLS(tlsConfig); err != nil {
			return fmt.Errorf("start TLS with server %s: %w", serverAddress, err)
		}
	}

	supportsAuth, _ := client.Extension("AUTH")
	if !supportsAuth {
		return fmt.Errorf("SMTP server %s does not support AUTH", serverAddress)
	}
	if err := client.Auth(smtp.PlainAuth("", transport.username, transport.password, transport.host)); err != nil {
		return fmt.Errorf("authenticate SMTP session with server %s: %w", serverAddress, err)
	}

	if err := client.Mail(fromAddress); err != nil {
		return fmt.Errorf("set SMTP sender %s: %w", fromAddress, err)
	}
	if err := client.Rcpt(toAddress); err != nil {
		return fmt.Errorf("set SMTP recipient %s: %w", toAddress, err)
	}

	writer, err := client.Data()
	if err != nil {
		return fmt.Errorf("open SMTP DATA stream: %w", err)
	}
	if _, err := writer.Write(encodeSMTPPlainTextMessage(fromAddress, toAddress, subject, body)); err != nil {
		_ = writer.Close()
		return fmt.Errorf("write SMTP message body: %w", err)
	}
	if err := writer.Close(); err != nil {
		return fmt.Errorf("close SMTP DATA stream: %w", err)
	}
	if err := client.Quit(); err != nil {
		return fmt.Errorf("quit SMTP session: %w", err)
	}
	return nil
}

func encodeSMTPPlainTextMessage(fromAddress string, toAddress string, subject string, body string) []byte {
	normalizedSubject := strings.NewReplacer("\r", " ", "\n", " ").Replace(subject)
	normalizedBody := strings.NewReplacer("\r\n", "\n", "\r", "\n").Replace(body)
	normalizedBody = strings.ReplaceAll(normalizedBody, "\n", "\r\n")

	payload := strings.Join([]string{
		"From: " + fromAddress,
		"To: " + toAddress,
		"Subject: " + normalizedSubject,
		"MIME-Version: 1.0",
		`Content-Type: text/plain; charset="UTF-8"`,
		"",
		normalizedBody,
		"",
	}, "\r\n")
	return []byte(payload)
}
