package mail

import (
	"encoding/json"
	"fmt"
	"io"
	"regexp"
	"strings"
	"unicode"
	"unicode/utf8"
)

const (
	TLSModeImplicit = "implicit"
	TLSModeStartTLS = "starttls"

	maxExtensionNameBytes = 255
	maxDisplayNameBytes   = 255
	maxHostBytes          = 253
	maxUsernameBytes      = 512
	maxPasswordBytes      = 4096
)

var extensionNamePattern = regexp.MustCompile(`^[a-z0-9]+(?:-[a-z0-9]+)*$`)

type Auth struct {
	Username string `json:"username"`
	Password string `json:"password"`
}

type IMAPSettings struct {
	Host    string `json:"host"`
	Port    int    `json:"port"`
	TLSMode string `json:"tlsMode"`
	Auth    Auth   `json:"auth"`
}

type SMTPSettings struct {
	Host    string `json:"host"`
	Port    int    `json:"port"`
	TLSMode string `json:"tlsMode"`
	Auth    *Auth  `json:"auth,omitempty"`
}

type InstallRequest struct {
	ExtensionName  string       `json:"extensionName"`
	DisplayName    string       `json:"displayName"`
	SenderAddress  string       `json:"senderAddress"`
	IMAP           IMAPSettings `json:"imap"`
	SMTPSameAsIMAP bool         `json:"smtpSameAsImap"`
	SMTP           SMTPSettings `json:"smtp"`
}

type ReconnectRequest struct {
	IMAP           IMAPSettings `json:"imap"`
	SMTPSameAsIMAP bool         `json:"smtpSameAsImap"`
	SMTP           SMTPSettings `json:"smtp"`
}

func DecodeInstallRequest(reader io.Reader) (InstallRequest, error) {
	var request InstallRequest
	if err := decodeStrictJSON(reader, &request); err != nil {
		return InstallRequest{}, err
	}
	return NormalizeInstallRequest(request)
}

func DecodeReconnectRequest(reader io.Reader) (ReconnectRequest, error) {
	var request ReconnectRequest
	if err := decodeStrictJSON(reader, &request); err != nil {
		return ReconnectRequest{}, err
	}
	return NormalizeReconnectRequest(request)
}

func ValidateInstallRequest(request InstallRequest) error {
	_, err := NormalizeInstallRequest(request)
	return err
}

func ValidateReconnectRequest(request ReconnectRequest) error {
	_, err := NormalizeReconnectRequest(request)
	return err
}

func NormalizeInstallRequest(request InstallRequest) (InstallRequest, error) {
	request.ExtensionName = strings.TrimSpace(request.ExtensionName)
	request.DisplayName = strings.TrimSpace(request.DisplayName)
	var err error
	request.SenderAddress, err = CanonicalSenderAddress(
		request.SenderAddress,
	)
	if err != nil {
		return InstallRequest{}, err
	}
	request.IMAP, request.SMTP, err = normalizeProtocolSettings(
		request.IMAP,
		request.SMTP,
		request.SMTPSameAsIMAP,
	)
	if err != nil {
		return InstallRequest{}, err
	}
	if request.ExtensionName == "" ||
		len(request.ExtensionName) > maxExtensionNameBytes ||
		!extensionNamePattern.MatchString(request.ExtensionName) ||
		request.DisplayName == "" ||
		len(request.DisplayName) > maxDisplayNameBytes ||
		!utf8.ValidString(request.DisplayName) ||
		strings.IndexFunc(request.DisplayName, unicode.IsControl) >= 0 {
		return InstallRequest{},
			fmt.Errorf("Email Installation name is invalid")
	}
	return request, nil
}

func NormalizeReconnectRequest(
	request ReconnectRequest,
) (ReconnectRequest, error) {
	var err error
	request.IMAP, request.SMTP, err = normalizeProtocolSettings(
		request.IMAP,
		request.SMTP,
		request.SMTPSameAsIMAP,
	)
	if err != nil {
		return ReconnectRequest{}, err
	}
	return request, nil
}

func normalizeProtocolSettings(
	imap IMAPSettings,
	smtp SMTPSettings,
	smtpSameAsIMAP bool,
) (IMAPSettings, SMTPSettings, error) {
	imap.Host = strings.ToLower(
		strings.TrimSuffix(strings.TrimSpace(imap.Host), "."),
	)
	imap.TLSMode = strings.ToLower(strings.TrimSpace(imap.TLSMode))
	imap.Auth = normalizeAuth(imap.Auth)
	smtp.Host = strings.ToLower(
		strings.TrimSuffix(strings.TrimSpace(smtp.Host), "."),
	)
	smtp.TLSMode = strings.ToLower(strings.TrimSpace(smtp.TLSMode))
	if smtp.Auth != nil {
		normalized := normalizeAuth(*smtp.Auth)
		smtp.Auth = &normalized
	}
	if err := validateEndpoint(
		"IMAP",
		imap.Host,
		imap.Port,
		imap.TLSMode,
		993,
		143,
	); err != nil {
		return IMAPSettings{}, SMTPSettings{}, err
	}
	if err := validateEndpoint(
		"SMTP",
		smtp.Host,
		smtp.Port,
		smtp.TLSMode,
		465,
		587,
	); err != nil {
		return IMAPSettings{}, SMTPSettings{}, err
	}
	if err := validateAuth("IMAP", imap.Auth); err != nil {
		return IMAPSettings{}, SMTPSettings{}, err
	}
	switch {
	case smtpSameAsIMAP && smtp.Auth != nil:
		return IMAPSettings{}, SMTPSettings{},
			fmt.Errorf("smtp.auth is forbidden when smtpSameAsImap is true")
	case smtpSameAsIMAP:
		copied := imap.Auth
		smtp.Auth = &copied
	case !smtpSameAsIMAP && smtp.Auth == nil:
		return IMAPSettings{}, SMTPSettings{},
			fmt.Errorf("smtp.auth is required when smtpSameAsImap is false")
	default:
		if err := validateAuth("SMTP", *smtp.Auth); err != nil {
			return IMAPSettings{}, SMTPSettings{}, err
		}
	}
	return imap, smtp, nil
}

func normalizeAuth(value Auth) Auth {
	value.Username = strings.TrimSpace(value.Username)
	return value
}

func validateEndpoint(
	protocol string,
	host string,
	port int,
	tlsMode string,
	implicitPort int,
	startTLSPort int,
) error {
	if host == "" || len(host) > maxHostBytes {
		return fmt.Errorf("%s host is invalid", protocol)
	}
	valid := port == implicitPort && tlsMode == TLSModeImplicit ||
		port == startTLSPort && tlsMode == TLSModeStartTLS
	if !valid {
		return fmt.Errorf("%s port and TLS mode are not allowed", protocol)
	}
	return nil
}

func validateAuth(protocol string, auth Auth) error {
	if auth.Username == "" || len(auth.Username) > maxUsernameBytes ||
		auth.Password == "" || len(auth.Password) > maxPasswordBytes {
		return fmt.Errorf("%s authentication is invalid", protocol)
	}
	return nil
}

func decodeStrictJSON(reader io.Reader, value any) error {
	if reader == nil {
		return fmt.Errorf("Email request body is required")
	}
	decoder := json.NewDecoder(reader)
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(value); err != nil {
		return fmt.Errorf("decode Email request: %w", err)
	}
	if err := decoder.Decode(&struct{}{}); err != io.EOF {
		if err == nil {
			return fmt.Errorf("Email request must contain one JSON object")
		}
		return fmt.Errorf("decode Email request trailer: %w", err)
	}
	return nil
}
