package mailruntime

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	stdhtml "html"
	"io"
	"net/mail"
	"strings"
	"unicode/utf8"

	"github.com/tidbcloud/AgentTeam/internal/extension/egress"
	"github.com/tidbcloud/AgentTeam/internal/extension/providers/mail/protocol"
	xhtml "golang.org/x/net/html"
)

// Leave room for the generic Extension result envelope while enforcing a
// stricter ceiling than its hard 64 KiB final response budget.
const maxMailResultBytes = egress.MaxFinalResponseBytes - 1024

var errMailResultTooLarge = errors.New(
	"mail result exceeded the response budget",
)

func marshalBounded(value any) (json.RawMessage, error) {
	encoded, err := json.Marshal(value)
	if err != nil {
		return nil, mailError(
			ErrorInternal,
			"mail result could not be encoded",
			err,
		)
	}
	if len(encoded) > maxMailResultBytes {
		return nil, errMailResultTooLarge
	}
	return encoded, nil
}

func decodeStrictJSON(payload []byte, destination any) error {
	decoder := json.NewDecoder(bytes.NewReader(payload))
	decoder.DisallowUnknownFields()
	if err := decoder.Decode(destination); err != nil {
		return err
	}
	var trailing json.RawMessage
	if err := decoder.Decode(&trailing); err != io.EOF {
		if err == nil {
			return fmt.Errorf("JSON must contain one document")
		}
		return fmt.Errorf("JSON has trailing data")
	}
	return nil
}

func normalizeReadBody(body protocol.Body) (protocol.Body, error) {
	switch body.Format {
	case "text":
		body.Content = string(bytes.ToValidUTF8(
			[]byte(body.Content),
			[]byte("\uFFFD"),
		))
		return body, nil
	case "sanitized_html":
		text, err := htmlToPlainText(body.Content)
		if err != nil {
			return protocol.Body{}, fmt.Errorf(
				"sanitize mailbox HTML: %w",
				err,
			)
		}
		body.Content = "<pre>" + stdhtml.EscapeString(text) + "</pre>"
		return body, nil
	default:
		return protocol.Body{}, fmt.Errorf(
			"mailbox body format is invalid",
		)
	}
}

func validateMessageSummary(
	message protocol.MessageSummary,
) error {
	if message.Ref.Mailbox == "" ||
		message.Ref.UIDValidity == 0 ||
		message.Ref.UID == 0 ||
		message.From == "" ||
		message.SentAt.IsZero() ||
		len(message.Subject) > 998 {
		return fmt.Errorf("message summary is incomplete")
	}
	if err := validateAddress(message.From); err != nil {
		return err
	}
	if len(message.To) > 50 || len(message.CC) > 50 {
		return fmt.Errorf("message address list exceeded limit")
	}
	for _, values := range [][]string{message.To, message.CC} {
		for _, value := range values {
			if err := validateAddress(value); err != nil {
				return err
			}
		}
	}
	return nil
}

func validateMessage(message protocol.Message) error {
	if err := validateMessageSummary(protocol.MessageSummary{
		Ref:     message.Ref,
		From:    message.From,
		To:      message.To,
		CC:      message.CC,
		Subject: message.Subject,
		SentAt:  message.SentAt,
	}); err != nil {
		return err
	}
	if len(message.BCC) > 50 {
		return fmt.Errorf("message address list exceeded limit")
	}
	for _, value := range message.BCC {
		if err := validateAddress(value); err != nil {
			return err
		}
	}
	return nil
}

func validateQuotedMessage(message protocol.Message) error {
	if err := validateMessage(message); err != nil {
		return err
	}
	if message.Truncated || message.Body.Truncated {
		return fmt.Errorf("message content is truncated")
	}
	return nil
}

func htmlToPlainText(raw string) (string, error) {
	tokenizer := xhtml.NewTokenizer(strings.NewReader(raw))
	var output strings.Builder
	for {
		tokenType := tokenizer.Next()
		switch tokenType {
		case xhtml.ErrorToken:
			if errors.Is(tokenizer.Err(), io.EOF) {
				return strings.TrimSpace(output.String()), nil
			}
			return "", tokenizer.Err()
		case xhtml.TextToken:
			output.Write(tokenizer.Text())
		case xhtml.StartTagToken, xhtml.EndTagToken,
			xhtml.SelfClosingTagToken:
			name, _ := tokenizer.TagName()
			switch string(name) {
			case "br", "p", "div", "li", "tr", "blockquote", "pre":
				output.WriteByte('\n')
			}
		}
	}
}

func validateSearchFilters(filters SearchFilters) error {
	count := 0
	if filters.Text != "" {
		count++
		if len(filters.Text) > 1024 ||
			strings.TrimSpace(filters.Text) == "" ||
			strings.ContainsAny(filters.Text, "\r\n\x00") {
			return fmt.Errorf("search text is invalid")
		}
	}
	if filters.Folder != "" {
		count++
		switch filters.Folder {
		case protocol.FolderInbox,
			protocol.FolderSent,
			protocol.FolderDrafts,
			protocol.FolderArchive,
			protocol.FolderTrash,
			"spam",
			protocol.FolderAll:
		default:
			return fmt.Errorf("search folder is invalid")
		}
	}
	for _, addresses := range [][]string{filters.From, filters.To} {
		if len(addresses) > 0 {
			count++
		}
		if len(addresses) > 10 {
			return fmt.Errorf("search address filter exceeded limit")
		}
		for _, address := range addresses {
			if err := validateAddress(address); err != nil {
				return err
			}
		}
	}
	if filters.ReceivedAfter != nil {
		count++
	}
	if filters.ReceivedBefore != nil {
		count++
	}
	if filters.ReceivedAfter != nil &&
		filters.ReceivedBefore != nil &&
		!filters.ReceivedAfter.Before(*filters.ReceivedBefore) {
		return fmt.Errorf("search time range is invalid")
	}
	if filters.Unread != nil {
		count++
	}
	if filters.HasAttachments != nil {
		count++
	}
	if count == 0 {
		return fmt.Errorf("at least one search filter is required")
	}
	return nil
}

func validateDraftContent(content DraftContent) error {
	if len(content.To)+len(content.CC)+len(content.BCC) == 0 ||
		len(content.To)+len(content.CC)+len(content.BCC) > 50 ||
		len(content.Subject) > 998 ||
		strings.ContainsAny(content.Subject, "\r\n") {
		return fmt.Errorf("draft envelope is invalid")
	}
	if err := validateBody(content.Body); err != nil {
		return err
	}
	seen := make(map[string]struct{})
	for _, addresses := range [][]string{
		content.To,
		content.CC,
		content.BCC,
	} {
		for _, address := range addresses {
			if err := validateAddress(address); err != nil {
				return err
			}
			key, err := addressIdentity(address)
			if err != nil {
				return err
			}
			if _, duplicate := seen[key]; duplicate {
				return fmt.Errorf("draft address is duplicated")
			}
			seen[key] = struct{}{}
		}
	}
	return nil
}

func validateBody(body Body) error {
	if body.Format != "text" && body.Format != "html" {
		return fmt.Errorf("draft body format is invalid")
	}
	if len(body.Content) > 512*1024 {
		return fmt.Errorf("draft body is too large")
	}
	return nil
}

func validateAddress(value string) error {
	if value == "" ||
		len(value) > 320 ||
		strings.ContainsAny(value, "\r\n\x00") {
		return fmt.Errorf("mail address is invalid")
	}
	parsed, err := mail.ParseAddress(value)
	if err != nil || parsed.Address != value {
		return fmt.Errorf("mail address is invalid")
	}
	return nil
}

func addressIdentity(value string) (string, error) {
	parsed, err := mail.ParseAddress(value)
	if err != nil || parsed.Address != value {
		return "", fmt.Errorf("mail address is invalid")
	}
	at := strings.LastIndexByte(parsed.Address, '@')
	if at <= 0 || at == len(parsed.Address)-1 {
		return "", fmt.Errorf("mail address is invalid")
	}
	return parsed.Address[:at] + "@" +
		strings.ToLower(parsed.Address[at+1:]), nil
}

func toProtocolDraft(
	sender string,
	content DraftContent,
	attachments []protocol.DraftAttachment,
) protocol.DraftContent {
	return protocol.DraftContent{
		From:        sender,
		To:          append([]string(nil), content.To...),
		CC:          append([]string(nil), content.CC...),
		BCC:         append([]string(nil), content.BCC...),
		Subject:     content.Subject,
		BodyFormat:  content.Body.Format,
		Body:        content.Body.Content,
		Attachments: attachments,
	}
}

func boundedUTF8(value string, maxBytes int) string {
	if maxBytes <= 0 {
		return ""
	}
	if len(value) <= maxBytes {
		return value
	}
	value = value[:maxBytes]
	for len(value) > 0 && !utf8.ValidString(value) {
		value = value[:len(value)-1]
	}
	return value
}

func sha256JSON(value any) (string, error) {
	encoded, err := json.Marshal(value)
	if err != nil {
		return "", err
	}
	digest := sha256.Sum256(encoded)
	return "sha256:" + hex.EncodeToString(digest[:]), nil
}
