package protocol

import (
	"context"
	"errors"
	"fmt"
	"net"
	netmail "net/mail"
	"net/smtp"
	"net/textproto"
	"strings"

	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/transport"
)

const (
	maxSMTPRecipients   = 100
	maxSMTPMessageBytes = 30 << 20
)

type SMTPClient struct {
	options ClientOptions
}

func NewSMTPClient(options ClientOptions) *SMTPClient {
	return &SMTPClient{options: options}
}

func (client *SMTPClient) ProbeSender(
	ctx context.Context,
	endpoint Endpoint,
	senderAddress string,
) (Capabilities, error) {
	if err := validateSMTPAddress(senderAddress); err != nil {
		return Capabilities{}, err
	}
	session, connection, err := client.open(ctx, endpoint)
	if err != nil {
		return Capabilities{}, classifyReadError("open SMTP session", err)
	}
	defer connection.Close()
	defer session.Close()

	capabilities := smtpCapabilities(session)
	if err := session.Mail(senderAddress); err != nil {
		return Capabilities{}, &ProtocolError{
			Class:     ErrorFailed,
			Operation: "probe SMTP sender",
			Cause:     err,
		}
	}
	if err := session.Reset(); err != nil {
		return Capabilities{}, &ProtocolError{
			Class:     ErrorFailed,
			Operation: "reset SMTP sender probe",
			Cause:     err,
		}
	}
	_ = session.Quit()
	return Capabilities{Values: capabilities}, nil
}

func (client *SMTPClient) Send(
	ctx context.Context,
	endpoint Endpoint,
	message FrozenMessage,
) (SendResult, error) {
	rawMessage, recipients, err := buildSMTPMessage(message)
	if err != nil ||
		len(rawMessage) == 0 ||
		len(rawMessage) > maxSMTPMessageBytes ||
		strings.TrimSpace(message.SenderAddress) == "" ||
		len(recipients) == 0 ||
		len(recipients) > maxSMTPRecipients {
		return SendResult{Status: ResultFailed},
			&ProtocolError{
				Class:     ErrorInvalid,
				Operation: "validate SMTP send",
				Cause:     err,
			}
	}
	if err := validateSMTPAddress(message.SenderAddress); err != nil {
		return SendResult{Status: ResultFailed}, err
	}
	for _, recipient := range recipients {
		if err := validateSMTPAddress(recipient); err != nil {
			return SendResult{Status: ResultFailed}, err
		}
	}
	session, connection, err := client.openBudgeted(
		ctx,
		endpoint,
		smtpSendBudget(client.options),
	)
	if err != nil {
		return SendResult{Status: ResultFailed},
			classifyReadError("open SMTP session", err)
	}
	defer connection.Close()
	defer session.Close()

	if err := session.Mail(message.SenderAddress); err != nil {
		return SendResult{Status: ResultFailed}, &ProtocolError{
			Class:     ErrorFailed,
			Operation: "begin SMTP envelope",
			Cause:     err,
		}
	}
	for _, recipient := range recipients {
		if err := session.Rcpt(recipient); err != nil {
			return SendResult{Status: ResultFailed}, &ProtocolError{
				Class:     ErrorFailed,
				Operation: "submit SMTP recipient",
				Cause:     err,
			}
		}
	}
	writer, err := session.Data()
	if err != nil {
		return SendResult{Status: ResultFailed}, &ProtocolError{
			Class:     ErrorFailed,
			Operation: "begin SMTP message data",
			Cause:     err,
		}
	}
	if _, err := writer.Write(rawMessage); err != nil {
		_ = writer.Close()
		return SendResult{Status: ResultUnknown},
			mutationUnknown("write SMTP message after dispatch", err)
	}
	if err := writer.Close(); err != nil {
		var responseError *textproto.Error
		if errors.As(err, &responseError) {
			return SendResult{Status: ResultFailed}, &ProtocolError{
				Class:     ErrorFailed,
				Operation: "complete SMTP message",
				Cause:     err,
			}
		}
		return SendResult{Status: ResultUnknown},
			mutationUnknown("complete SMTP message after dispatch", err)
	}
	_ = session.Quit()
	return SendResult{Status: ResultAccepted}, nil
}

func (client *SMTPClient) open(
	ctx context.Context,
	endpoint Endpoint,
) (*smtp.Client, net.Conn, error) {
	return client.openBudgeted(
		ctx,
		endpoint,
		defaultProtocolBudget(client.options),
	)
}

func (client *SMTPClient) openBudgeted(
	ctx context.Context,
	endpoint Endpoint,
	budget protocolBudget,
) (*smtp.Client, net.Conn, error) {
	pinned, err := openPinned(
		ctx,
		client.options,
		transport.ProtocolSMTP,
		endpoint,
		budget,
	)
	if err != nil {
		return nil, nil, err
	}
	connection := pinned.Conn
	if pinned.TLSMode == transport.TLSModeImplicit {
		connection, err = implicitTLS(ctx, pinned)
		if err != nil {
			return nil, nil, err
		}
	}
	session, err := smtp.NewClient(connection, pinned.Host)
	if err != nil {
		_ = connection.Close()
		return nil, nil, fmt.Errorf("open SMTP session: %w", err)
	}
	if err := session.Hello("agentteam.invalid"); err != nil {
		_ = session.Close()
		_ = connection.Close()
		return nil, nil, fmt.Errorf("send SMTP EHLO: %w", err)
	}
	if pinned.TLSMode == transport.TLSModeStartTLS {
		if pinned.TLSConfig == nil {
			_ = session.Close()
			_ = connection.Close()
			return nil, nil,
				fmt.Errorf("SMTP TLS configuration is unavailable")
		}
		supported, _ := session.Extension("STARTTLS")
		if !supported {
			_ = session.Close()
			_ = connection.Close()
			return nil, nil, fmt.Errorf("SMTP STARTTLS is required")
		}
		if err := session.StartTLS(pinned.TLSConfig.Clone()); err != nil {
			_ = session.Close()
			_ = connection.Close()
			return nil, nil, fmt.Errorf("verify SMTP STARTTLS: %w", err)
		}
	}
	if err := session.Auth(smtp.PlainAuth(
		"",
		endpoint.Username,
		endpoint.Password,
		pinned.Host,
	)); err != nil {
		_ = session.Close()
		_ = connection.Close()
		return nil, nil, fmt.Errorf("authenticate SMTP session: %w", err)
	}
	return session, connection, nil
}

func smtpCapabilities(client *smtp.Client) map[string]bool {
	result := make(map[string]bool)
	for _, name := range []string{
		"8BITMIME",
		"SMTPUTF8",
		"SIZE",
	} {
		if supported, _ := client.Extension(name); supported {
			result[name] = true
		}
	}
	return result
}

func validateSMTPAddress(value string) error {
	trimmed := strings.TrimSpace(value)
	if trimmed == "" ||
		len(trimmed) > maxAddressBytes ||
		strings.ContainsAny(trimmed, "\r\n\x00") {
		return &ProtocolError{
			Class:     ErrorInvalid,
			Operation: "validate SMTP address",
		}
	}
	address, err := netmail.ParseAddress(trimmed)
	if err != nil || address.Address != trimmed {
		return &ProtocolError{
			Class:     ErrorInvalid,
			Operation: "validate SMTP address",
			Cause:     err,
		}
	}
	return nil
}
