package connectedmailhttp

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"mime"
	"net/http"
	"strconv"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	connector_mail "github.com/pingcap-inc/nexuscrm-connectors/features/connector/mail"
	outboundintentv1 "github.com/pingcap-inc/nexuscrm-contract/pkg/runtime/outboundintentv1"
)

type MailConfig struct {
	// Endpoint is the credential-bearing provider send endpoint. RequestField
	// and ResponseIDField select the provider's bounded JSON grammar.
	Endpoint         string
	RequestField     string
	ResponseIDField  string
	TerminationBound time.Duration
}

// MailAdapter implements the provider-neutral mail effect with one HTTP
// transaction. The injected RoundTripper has the same dial-time address and
// cancellation requirements as OAuthAdapter.
type MailAdapter struct {
	config    MailConfig
	transport http.RoundTripper
	urls      URLAllowlist
}

func NewMailAdapter(config MailConfig, transport http.RoundTripper, urls URLAllowlist) (*MailAdapter, error) {
	if transport == nil || urls == nil || config.TerminationBound <= 0 || config.TerminationBound > maxEffectTermination ||
		!validEndpoint(config.Endpoint) || !fieldName(config.RequestField) || !fieldName(config.ResponseIDField) {
		return nil, connector_mail.ErrInvalidInput
	}
	if urls.EnsureAllowed(config.Endpoint) != nil {
		return nil, connector_mail.ErrInvalidInput
	}
	return &MailAdapter{config: config, transport: transport, urls: urls}, nil
}

func (adapter *MailAdapter) EffectTerminationBound() time.Duration {
	if adapter == nil {
		return 0
	}
	return adapter.config.TerminationBound
}

func (adapter *MailAdapter) Dispatch(ctx context.Context, accessToken string, message connector_mail.Message) (connector_mail.DispatchResult, error) {
	if adapter == nil || ctx == nil || !headerValue(accessToken, 64*1024) || !validMessage(message) {
		return connector_mail.DispatchResult{}, mailFailure(connector_mail.ProviderRejected)
	}
	return adapter.dispatchMIME(ctx, accessToken, buildPlainTextMIME(message))
}

func (adapter *MailAdapter) DispatchV2(ctx context.Context, accessToken string, message connector_mail.MessageV2) (connector_mail.DispatchResult, error) {
	if adapter == nil || ctx == nil || !headerValue(accessToken, 64*1024) || !validMessageV2(message) {
		return connector_mail.DispatchResult{}, mailFailure(connector_mail.ProviderRejected)
	}
	return adapter.dispatchMIME(ctx, accessToken, buildMailV2MIME(message))
}

func (adapter *MailAdapter) DispatchV3(ctx context.Context, accessToken string, message connector_mail.MessageV3) (connector_mail.DispatchResult, error) {
	if adapter == nil || ctx == nil || !headerValue(accessToken, 64*1024) || !validMessageV3(message) {
		return connector_mail.DispatchResult{}, mailFailure(connector_mail.ProviderRejected)
	}
	raw, ok := buildMailV3MIME(message)
	if !ok {
		return connector_mail.DispatchResult{}, mailFailure(connector_mail.ProviderRejected)
	}
	return adapter.dispatchMIME(ctx, accessToken, raw)
}

func (adapter *MailAdapter) dispatchMIME(ctx context.Context, accessToken string, mimeMessage []byte) (connector_mail.DispatchResult, error) {
	encoded := make([]byte, base64.RawURLEncoding.EncodedLen(len(mimeMessage)))
	base64.RawURLEncoding.Encode(encoded, mimeMessage)
	clearBytes(mimeMessage)
	payload := make([]byte, 0, len(adapter.config.RequestField)+len(encoded)+7)
	payload = append(payload, '{', '"')
	payload = append(payload, adapter.config.RequestField...)
	payload = append(payload, '"', ':', '"')
	payload = append(payload, encoded...)
	payload = append(payload, '"', '}')
	clearBytes(encoded)
	request, err := http.NewRequestWithContext(ctx, http.MethodPost, adapter.config.Endpoint, bytes.NewReader(payload))
	if err != nil {
		clearBytes(payload)
		return connector_mail.DispatchResult{}, mailFailure(connector_mail.ProviderRejected)
	}
	request.Body = &clearingRequestBody{ReadCloser: request.Body, payload: payload}
	request.GetBody = nil
	request.Header.Set("Authorization", "Bearer "+accessToken)
	request.Header.Set("Content-Type", "application/json")
	response, err := adapter.doMail(request)
	if err != nil {
		return connector_mail.DispatchResult{}, err
	}
	defer response.Body.Close()
	status := safeHTTPStatus(response.StatusCode)
	result := connector_mail.DispatchResult{ResponseStatus: status}
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return result, mailStatusFailure(response.StatusCode)
	}
	values := map[string]json.RawMessage{}
	if decodeLimited(response.Body, &values) != nil {
		return result, mailFailure(connector_mail.ProviderUncertain)
	}
	rawMessageID, exists := values[adapter.config.ResponseIDField]
	var remoteMessageID string
	if !exists || !utf8.Valid(rawMessageID) || json.Unmarshal(rawMessageID, &remoteMessageID) != nil ||
		!validRemoteMessageID(remoteMessageID) {
		return result, mailFailure(connector_mail.ProviderUncertain)
	}
	result.RemoteMessageID = remoteMessageID
	return result, nil
}

func (adapter *MailAdapter) doMail(request *http.Request) (*http.Response, error) {
	if adapter == nil || adapter.transport == nil || adapter.urls == nil || request == nil || request.URL == nil {
		closeRequestBody(request)
		return nil, mailFailure(connector_mail.ProviderRejected)
	}
	if request.Context().Err() != nil {
		closeRequestBody(request)
		return nil, mailFailure(connector_mail.ProviderNotDispatched)
	}
	if adapter.urls.EnsureAllowed(request.URL.String()) != nil {
		closeRequestBody(request)
		return nil, mailFailure(connector_mail.ProviderRejected)
	}
	response, err := adapter.transport.RoundTrip(request)
	if err != nil || response == nil || response.Body == nil {
		if response != nil && response.Body != nil {
			response.Body.Close()
		}
		return nil, mailFailure(connector_mail.ProviderUncertain)
	}
	return response, nil
}

func mailStatusFailure(status int) error {
	switch {
	case status == http.StatusUnauthorized || status == http.StatusForbidden:
		return mailFailure(connector_mail.ProviderAuthorization)
	case status < 200,
		status >= 300 && status < 400,
		status == http.StatusRequestTimeout,
		status == http.StatusTooEarly,
		status == http.StatusTooManyRequests,
		status >= 500:
		return mailFailure(connector_mail.ProviderUncertain)
	default:
		return mailFailure(connector_mail.ProviderRejected)
	}
}

func mailFailure(kind connector_mail.ProviderErrorKind) error {
	return &connector_mail.ProviderError{Kind: kind}
}

func safeHTTPStatus(status int) int {
	if status < 100 || status > 599 {
		return 0
	}
	return status
}

func validMessage(message connector_mail.Message) bool {
	if !validMailAddress(message.FromAddress) || len(message.ToAddresses) == 0 ||
		len(message.ToAddresses) > outboundintentv1.MaxMailRecipients ||
		!boundedMailText(message.Subject, outboundintentv1.MaxMailSubjectBytes, false) ||
		message.TextBody == "" || !boundedMailText(message.TextBody, outboundintentv1.MaxMailTextBodyBytes, true) {
		return false
	}
	seen := make(map[string]struct{}, len(message.ToAddresses))
	for _, address := range message.ToAddresses {
		if !validMailAddress(address) {
			return false
		}
		key := strings.ToLower(address)
		if _, duplicate := seen[key]; duplicate {
			return false
		}
		seen[key] = struct{}{}
	}
	return true
}

func validMessageV2(message connector_mail.MessageV2) bool {
	if !validMessage(message.Message) ||
		len(message.Message.ToAddresses)+len(message.CcAddresses)+len(message.BccAddresses) > outboundintentv1.MaxMailRecipients ||
		(message.HTMLBody != "" && !boundedMailText(message.HTMLBody, outboundintentv1.MaxMailHTMLBodyBytes, true)) {
		return false
	}
	seen := make(map[string]struct{}, len(message.Message.ToAddresses)+len(message.CcAddresses)+len(message.BccAddresses))
	for _, addresses := range [][]string{message.Message.ToAddresses, message.CcAddresses, message.BccAddresses} {
		for _, address := range addresses {
			if !validMailAddress(address) {
				return false
			}
			key := strings.ToLower(address)
			if _, duplicate := seen[key]; duplicate {
				return false
			}
			seen[key] = struct{}{}
		}
	}
	return true
}

func validMessageV3(message connector_mail.MessageV3) bool {
	if !validMessageV2(message.MessageV2) || len(message.Attachments) == 0 ||
		len(message.Attachments) > outboundintentv1.MaxMailAttachments {
		return false
	}
	for _, attachment := range message.Attachments {
		if len(attachment.Data) == 0 || len(attachment.Data) > outboundintentv1.MaxMailAttachmentBytes ||
			!boundedMailText(attachment.FileName, outboundintentv1.MaxMailAttachmentNameBytes, false) ||
			attachment.FileName == "" || !boundedMailText(attachment.ContentType, outboundintentv1.MaxMailContentTypeBytes, false) {
			return false
		}
		mediaType, _, err := mime.ParseMediaType(attachment.ContentType)
		if err != nil || !strings.Contains(mediaType, "/") {
			return false
		}
	}
	return true
}

func validMailAddress(value string) bool {
	return boundedMailText(value, outboundintentv1.MaxMailAddressBytes, false) &&
		strings.Contains(value, "@") && bareAddress(value)
}

func validRemoteMessageID(value string) bool {
	return value != "" && value == strings.TrimSpace(value) &&
		boundedMailText(value, outboundintentv1.MaxIdentifierBytes, false)
}

func boundedMailText(value string, maximum int, allowLineBreaks bool) bool {
	if len(value) > maximum || !utf8.ValidString(value) {
		return false
	}
	for _, current := range value {
		if current == 0 || !allowLineBreaks && unicode.IsControl(current) ||
			allowLineBreaks && unicode.IsControl(current) && current != '\n' && current != '\r' && current != '\t' {
			return false
		}
	}
	return true
}

func buildPlainTextMIME(message connector_mail.Message) []byte {
	body := strings.ReplaceAll(strings.ReplaceAll(message.TextBody, "\r\n", "\n"), "\r", "\n")
	body = strings.ReplaceAll(body, "\n", "\r\n")
	var builder strings.Builder
	builder.Grow(base64.StdEncoding.EncodedLen(len(body)) + len(message.Subject) + len(message.FromAddress) + len(message.ToAddresses)*64 + 256)
	builder.WriteString("From: ")
	builder.WriteString(message.FromAddress)
	builder.WriteString("\r\n")
	writeAddressHeader(&builder, "To", message.ToAddresses)
	writeSubjectHeader(&builder, message.Subject)
	builder.WriteString("MIME-Version: 1.0\r\n")
	builder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
	builder.WriteString("Content-Transfer-Encoding: base64\r\n\r\n")
	bodyBytes := []byte(body)
	writeBase64Body(&builder, bodyBytes)
	clearBytes(bodyBytes)
	return []byte(builder.String())
}

func buildMailV2MIME(message connector_mail.MessageV2) []byte {
	if message.HTMLBody == "" {
		return buildSinglePartMailMIME(message.Message, message.CcAddresses, message.BccAddresses)
	}
	textBody := canonicalMailLineBreaks(message.Message.TextBody)
	htmlBody := canonicalMailLineBreaks(message.HTMLBody)
	digest := sha256.Sum256([]byte(textBody + "\x00" + htmlBody))
	boundary := "nexuscrm-" + base64.RawURLEncoding.EncodeToString(digest[:18])
	var builder strings.Builder
	builder.Grow(base64.StdEncoding.EncodedLen(len(textBody)+len(htmlBody)) + len(message.Message.Subject) + 1024)
	writeMailHeaders(&builder, message.Message, message.CcAddresses, message.BccAddresses)
	builder.WriteString("MIME-Version: 1.0\r\n")
	builder.WriteString("Content-Type: multipart/alternative; boundary=\"")
	builder.WriteString(boundary)
	builder.WriteString("\"\r\n\r\n")
	writeMailAlternativePart(&builder, boundary, "text/plain", []byte(textBody))
	writeMailAlternativePart(&builder, boundary, "text/html", []byte(htmlBody))
	builder.WriteString("--")
	builder.WriteString(boundary)
	builder.WriteString("--\r\n")
	return []byte(builder.String())
}

type mailMIMEWriter interface {
	Write([]byte) (int, error)
	WriteString(string) (int, error)
}

type boundedMailMIMECountWriter struct {
	size     int
	limit    int
	exceeded bool
}

func (writer *boundedMailMIMECountWriter) Write(value []byte) (int, error) {
	return writer.add(len(value)), nil
}

func (writer *boundedMailMIMECountWriter) WriteString(value string) (int, error) {
	return writer.add(len(value)), nil
}

func (writer *boundedMailMIMECountWriter) add(size int) int {
	if !writer.exceeded {
		if size > writer.limit-writer.size {
			writer.exceeded = true
		} else {
			writer.size += size
		}
	}
	return size
}

func buildMailV3MIME(message connector_mail.MessageV3) ([]byte, bool) {
	digest := sha256.New()
	_, _ = digest.Write([]byte(message.Message.Subject + "\x00" + message.Message.TextBody + "\x00" + message.HTMLBody))
	for _, attachment := range message.Attachments {
		sum := sha256.Sum256(attachment.Data)
		_, _ = digest.Write(sum[:])
	}
	mixedBoundary := "nexuscrm-mixed-" + base64.RawURLEncoding.EncodeToString(digest.Sum(nil)[:18])
	alternativeSum := sha256.Sum256([]byte(message.Message.TextBody + "\x00" + message.HTMLBody))
	alternativeBoundary := "nexuscrm-alt-" + base64.RawURLEncoding.EncodeToString(alternativeSum[:18])
	counter := &boundedMailMIMECountWriter{limit: outboundintentv1.MaxMailRawMessageBytes}
	writeMailV3MIME(counter, message, mixedBoundary, alternativeBoundary)
	if counter.exceeded {
		return nil, false
	}
	var builder bytes.Buffer
	builder.Grow(counter.size)
	writeMailV3MIME(&builder, message, mixedBoundary, alternativeBoundary)
	return builder.Bytes(), true
}

func writeMailV3MIME(builder mailMIMEWriter, message connector_mail.MessageV3, mixedBoundary, alternativeBoundary string) {
	writeMailHeaders(builder, message.Message, message.CcAddresses, message.BccAddresses)
	builder.WriteString("MIME-Version: 1.0\r\nContent-Type: multipart/mixed; boundary=\"")
	builder.WriteString(mixedBoundary)
	builder.WriteString("\"\r\n\r\n--")
	builder.WriteString(mixedBoundary)
	builder.WriteString("\r\n")
	if message.HTMLBody != "" {
		builder.WriteString("Content-Type: multipart/alternative; boundary=\"")
		builder.WriteString(alternativeBoundary)
		builder.WriteString("\"\r\n\r\n")
		writeMailAlternativePart(builder, alternativeBoundary, "text/plain", []byte(canonicalMailLineBreaks(message.Message.TextBody)))
		writeMailAlternativePart(builder, alternativeBoundary, "text/html", []byte(canonicalMailLineBreaks(message.HTMLBody)))
		builder.WriteString("--")
		builder.WriteString(alternativeBoundary)
		builder.WriteString("--\r\n")
	} else {
		builder.WriteString("Content-Type: text/plain; charset=UTF-8\r\nContent-Transfer-Encoding: base64\r\n\r\n")
		writeBase64Body(builder, []byte(canonicalMailLineBreaks(message.Message.TextBody)))
	}
	for _, attachment := range message.Attachments {
		builder.WriteString("--")
		builder.WriteString(mixedBoundary)
		builder.WriteString("\r\nContent-Type: ")
		builder.WriteString(attachment.ContentType)
		builder.WriteString("\r\nContent-Transfer-Encoding: base64\r\n")
		writeAttachmentDisposition(builder, attachment.FileName)
		builder.WriteString("\r\n")
		writeBase64Body(builder, attachment.Data)
	}
	builder.WriteString("--")
	builder.WriteString(mixedBoundary)
	builder.WriteString("--\r\n")
}

func writeAttachmentDisposition(builder mailMIMEWriter, fileName string) {
	encoded := rfc2231Value(fileName)
	builder.WriteString("Content-Disposition: attachment; filename=\"attachment\"")
	for index := 0; len(encoded) > 0; index++ {
		length := min(45, len(encoded))
		for length > 0 && (encoded[length-1] == '%' || length >= 2 && encoded[length-2] == '%') {
			length--
		}
		if length == 0 {
			length = min(3, len(encoded))
		}
		builder.WriteString(";\r\n filename*")
		builder.WriteString(strconv.Itoa(index))
		builder.WriteString("*=")
		if index == 0 {
			builder.WriteString("UTF-8''")
		}
		builder.WriteString(encoded[:length])
		encoded = encoded[length:]
	}
	builder.WriteString("\r\n")
}

func rfc2231Value(value string) string {
	const hexDigits = "0123456789ABCDEF"
	var builder strings.Builder
	for _, current := range []byte(value) {
		if current >= 'a' && current <= 'z' || current >= 'A' && current <= 'Z' ||
			current >= '0' && current <= '9' || strings.ContainsRune("!#$&+-.^_`|~", rune(current)) {
			builder.WriteByte(current)
		} else {
			builder.WriteByte('%')
			builder.WriteByte(hexDigits[current>>4])
			builder.WriteByte(hexDigits[current&15])
		}
	}
	return builder.String()
}

func buildSinglePartMailMIME(message connector_mail.Message, cc, bcc []string) []byte {
	body := canonicalMailLineBreaks(message.TextBody)
	var builder strings.Builder
	builder.Grow(base64.StdEncoding.EncodedLen(len(body)) + len(message.Subject) + 512)
	writeMailHeaders(&builder, message, cc, bcc)
	builder.WriteString("MIME-Version: 1.0\r\n")
	builder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
	builder.WriteString("Content-Transfer-Encoding: base64\r\n\r\n")
	bodyBytes := []byte(body)
	writeBase64Body(&builder, bodyBytes)
	clearBytes(bodyBytes)
	return []byte(builder.String())
}

func writeMailHeaders(builder mailMIMEWriter, message connector_mail.Message, cc, bcc []string) {
	builder.WriteString("From: ")
	builder.WriteString(message.FromAddress)
	builder.WriteString("\r\n")
	writeAddressHeader(builder, "To", message.ToAddresses)
	if len(cc) != 0 {
		writeAddressHeader(builder, "Cc", cc)
	}
	if len(bcc) != 0 {
		writeAddressHeader(builder, "Bcc", bcc)
	}
	writeSubjectHeader(builder, message.Subject)
}

func writeMailAlternativePart(builder mailMIMEWriter, boundary, mediaType string, body []byte) {
	builder.WriteString("--")
	builder.WriteString(boundary)
	builder.WriteString("\r\nContent-Type: ")
	builder.WriteString(mediaType)
	builder.WriteString("; charset=UTF-8\r\nContent-Transfer-Encoding: base64\r\n\r\n")
	writeBase64Body(builder, body)
}

func canonicalMailLineBreaks(value string) string {
	value = strings.ReplaceAll(value, "\r\n", "\n")
	value = strings.ReplaceAll(value, "\r", "\n")
	return strings.ReplaceAll(value, "\n", "\r\n")
}

func writeBase64Body(builder mailMIMEWriter, body []byte) {
	var encoded [76]byte
	for len(body) > 0 {
		length := min(len(body), 57)
		encodedLength := base64.StdEncoding.EncodedLen(length)
		base64.StdEncoding.Encode(encoded[:encodedLength], body[:length])
		builder.Write(encoded[:encodedLength])
		builder.WriteString("\r\n")
		clearBytes(encoded[:encodedLength])
		body = body[length:]
	}
}

func writeAddressHeader(builder mailMIMEWriter, name string, addresses []string) {
	builder.WriteString(name)
	builder.WriteString(": ")
	column := len(name) + 2
	for index, address := range addresses {
		if index > 0 {
			if column+2+len(address) > 78 {
				builder.WriteString(",\r\n ")
				column = 1
			} else {
				builder.WriteString(", ")
				column += 2
			}
		}
		builder.WriteString(address)
		column += len(address)
	}
	builder.WriteString("\r\n")
}

func writeSubjectHeader(builder mailMIMEWriter, subject string) {
	builder.WriteString("Subject:")
	for _, word := range encodedSubjectWords(subject) {
		builder.WriteString("\r\n ")
		builder.WriteString(word)
	}
	builder.WriteString("\r\n")
}

func encodedSubjectWords(subject string) []string {
	if subject == "" {
		return nil
	}
	words := make([]string, 0, len(subject)/39+1)
	for len(subject) > 0 {
		length := min(len(subject), 39)
		for length < len(subject) && !utf8.RuneStart(subject[length]) {
			length--
		}
		chunk := subject[:length]
		words = append(words, "=?UTF-8?B?"+base64.StdEncoding.EncodeToString([]byte(chunk))+"?=")
		subject = subject[length:]
	}
	return words
}

var _ connector_mail.Provider = (*MailAdapter)(nil)
var _ connector_mail.ProviderV2 = (*MailAdapter)(nil)
var _ connector_mail.ProviderV3 = (*MailAdapter)(nil)
