package transport

import (
	"context"
	"crypto/tls"
	"fmt"
	"net"
	"strconv"
	"time"
)

const (
	ProtocolIMAP = "imap"
	ProtocolSMTP = "smtp"

	TLSModeImplicit = "implicit"
	TLSModeStartTLS = "starttls"

	defaultTotalTimeout = 15 * time.Second
	maxDialAttempts     = 4
)

type Target struct {
	Protocol string
	Host     string
	Port     int
	TLSMode  string
}

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

type Dialer struct {
	Resolver    Resolver
	DialContext DialContextFunc
	// TotalTimeout bounds DNS resolution and all pinned-address attempts.
	// Non-positive values use defaultTotalTimeout.
	TotalTimeout time.Duration
}

type PinnedConnection struct {
	Conn      net.Conn
	Host      string
	Address   string
	TLSConfig *tls.Config
	TLSMode   string
}

func (dialer *Dialer) ResolveAndDial(
	ctx context.Context,
	target Target,
) (PinnedConnection, error) {
	if err := ValidateTarget(target); err != nil {
		return PinnedConnection{}, err
	}
	totalTimeout := dialer.TotalTimeout
	if totalTimeout <= 0 {
		totalTimeout = defaultTotalTimeout
	}
	totalCtx, cancel := context.WithTimeout(ctx, totalTimeout)
	defer cancel()
	host, addresses, err := ResolveAllowed(
		totalCtx,
		dialer.Resolver,
		target.Host,
	)
	if err != nil {
		return PinnedConnection{}, err
	}
	dial := dialer.DialContext
	if dial == nil {
		netDialer := &net.Dialer{}
		dial = netDialer.DialContext
	}
	var lastErr error
	attempts := len(addresses)
	if attempts > maxDialAttempts {
		attempts = maxDialAttempts
	}
	for _, address := range addresses[:attempts] {
		if totalErr := totalCtx.Err(); totalErr != nil {
			lastErr = totalErr
			break
		}
		pinnedAddress := net.JoinHostPort(
			address.String(),
			strconv.Itoa(target.Port),
		)
		connection, dialErr := dial(totalCtx, "tcp", pinnedAddress)
		if dialErr != nil {
			lastErr = dialErr
			if totalErr := totalCtx.Err(); totalErr != nil {
				lastErr = totalErr
				break
			}
			continue
		}
		if totalErr := totalCtx.Err(); totalErr != nil {
			_ = connection.Close()
			lastErr = totalErr
			break
		}
		return PinnedConnection{
			Conn:    connection,
			Host:    host,
			Address: pinnedAddress,
			TLSConfig: &tls.Config{
				MinVersion: tls.VersionTLS12,
				ServerName: host,
			},
			TLSMode: target.TLSMode,
		}, nil
	}
	if lastErr == nil {
		lastErr = fmt.Errorf("mail endpoint has no dialable address")
	}
	return PinnedConnection{}, fmt.Errorf(
		"dial pinned mail endpoint: %w",
		lastErr,
	)
}

func ValidateTarget(target Target) error {
	valid := false
	switch target.Protocol {
	case ProtocolIMAP:
		valid = target.Port == 993 &&
			target.TLSMode == TLSModeImplicit ||
			target.Port == 143 &&
				target.TLSMode == TLSModeStartTLS
	case ProtocolSMTP:
		valid = target.Port == 465 &&
			target.TLSMode == TLSModeImplicit ||
			target.Port == 587 &&
				target.TLSMode == TLSModeStartTLS
	}
	if !valid {
		return fmt.Errorf("mail endpoint protocol, port, or TLS mode is invalid")
	}
	return nil
}
