package imap

import (
	"bytes"
	"encoding/json"
	"io"
	"mime"
	"mime/multipart"
	"net/mail"
	"regexp"
	"strings"

	imapTypes "github.com/emersion/go-imap/v2"

	"github.com/ultisuite/ulti-backend/internal/mail/threading"
)

type EmailAddress struct {
	Name    string `json:"name"`
	Address string `json:"address"`
}

var mimeBoundaryParamRE = regexp.MustCompile(`(?i)boundary\s*=\s*"?([^";\s]+)"?`)

func addressesToJSON(addrs []imapTypes.Address) []byte {
	result := make([]EmailAddress, 0, len(addrs))
	for _, a := range addrs {
		result = append(result, EmailAddress{
			Name:    a.Name,
			Address: a.Addr(),
		})
	}
	b, _ := json.Marshal(result)
	return b
}

func parseBody(raw []byte) (text string, html string) {
	if len(raw) == 0 {
		return "", ""
	}

	text, html = parseBodyFromRFC822(raw)
	if text != "" || html != "" {
		if !looksLikeRawMIME(text) && !looksLikeRawMIME(html) {
			return finalizeDecodedBody(text), finalizeDecodedBody(html)
		}
	}

	if t, h, ok := parseEmbeddedMIME(raw); ok {
		return finalizeDecodedBody(t), finalizeDecodedBody(h)
	}

	if text != "" || html != "" {
		return finalizeDecodedBody(text), finalizeDecodedBody(html)
	}
	fallback := repairRawBytesToUTF8(raw)
	return finalizeDecodedBody(fallback), ""
}

func finalizeDecodedBody(s string) string {
	s = decodeBareQuotedPrintableIfNeeded(s)
	return decodeBareBase64IfNeeded(s)
}

func parseBodyFromRFC822(raw []byte) (text string, html string) {
	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return "", ""
	}

	contentType := msg.Header.Get("Content-Type")
	if contentType == "" {
		contentType = "text/plain"
	}

	mediaType, params, err := mime.ParseMediaType(contentType)
	if err != nil {
		body, _ := readDecodedBody(msg.Body, contentType, msg.Header.Get("Content-Transfer-Encoding"))
		return body, ""
	}

	if strings.HasPrefix(mediaType, "multipart/") {
		return parseMultipart(msg.Body, params["boundary"])
	}

	body, _ := readDecodedBody(msg.Body, contentType, msg.Header.Get("Content-Transfer-Encoding"))
	if mediaType == "text/html" {
		return "", body
	}
	outText := body
	if looksLikeEmbeddedMIME(raw) {
		if t, h, ok := parseEmbeddedMIME(raw); ok {
			return t, h
		}
	}
	return outText, ""
}

func parseMultipart(r io.Reader, boundary string) (text string, html string) {
	if boundary == "" {
		return "", ""
	}
	mr := multipart.NewReader(r, boundary)
	for {
		part, err := mr.NextPart()
		if err != nil {
			break
		}

		partType := part.Header.Get("Content-Type")
		mediaType, params, _ := mime.ParseMediaType(partType)

		switch {
		case mediaType == "text/plain":
			body, _ := readDecodedBody(part, partType, part.Header.Get("Content-Transfer-Encoding"))
			if text == "" {
				text = body
			}
		case mediaType == "text/html", mediaType == "text/x-html", mediaType == "application/xhtml+xml":
			body, _ := readDecodedBody(part, partType, part.Header.Get("Content-Transfer-Encoding"))
			if len(body) > len(html) {
				html = body
			}
		case strings.HasPrefix(mediaType, "multipart/"):
			t, h := parseMultipart(part, params["boundary"])
			if text == "" {
				text = t
			}
			if html == "" && h != "" {
				html = h
			}
		}
	}
	return text, html
}

func readDecodedBody(r io.Reader, contentType, transferEncoding string) (string, error) {
	data, err := io.ReadAll(r)
	if err != nil {
		return "", err
	}
	data, err = decodePartBody(transferEncoding, data)
	if err != nil {
		return "", err
	}
	return decodeBodyBytesToUTF8(data, contentType), nil
}

func parseEmbeddedMIME(raw []byte) (text string, html string, ok bool) {
	if !looksLikeEmbeddedMIME(raw) {
		return "", "", false
	}
	boundary := boundaryFromMIMEBytes(raw)
	if boundary == "" {
		return "", "", false
	}
	text, html = parseMultipart(bytes.NewReader(raw), boundary)
	if text == "" && html == "" {
		return "", "", false
	}
	if looksLikeRawMIME(text) || looksLikeRawMIME(html) {
		return "", "", false
	}
	return text, html, true
}

func looksLikeEmbeddedMIME(raw []byte) bool {
	s := string(raw)
	if !strings.Contains(s, "Content-Type:") {
		return false
	}
	return strings.Contains(s, "Content-Transfer-Encoding:") ||
		strings.Contains(strings.ToLower(s), "multipart/") ||
		strings.Contains(s, "This is a multi-part message in MIME format")
}

func looksLikeRawMIME(s string) bool {
	if s == "" {
		return false
	}
	if !strings.Contains(s, "Content-Type:") {
		return false
	}
	return strings.Contains(s, "Content-Transfer-Encoding:") ||
		strings.Contains(s, "--") && strings.Contains(strings.ToLower(s), "multipart")
}

func boundaryFromMIMEBytes(raw []byte) string {
	if m := mimeBoundaryParamRE.FindSubmatch(raw); len(m) >= 3 {
		return strings.Trim(string(m[2]), `"`)
	}
	return detectBoundaryDelimiter(raw)
}

func detectBoundaryDelimiter(raw []byte) string {
	for _, line := range bytes.Split(raw, []byte("\n")) {
		line = bytes.TrimSpace(line)
		if len(line) < 4 || line[0] != '-' || line[1] != '-' {
			continue
		}
		if line[len(line)-1] == '-' && line[len(line)-2] == '-' {
			continue
		}
		b := strings.TrimPrefix(string(line), "--")
		b = strings.TrimSpace(b)
		if b != "" && !strings.Contains(b, " ") {
			return b
		}
	}
	return ""
}

func parseFromHeader(raw []byte) []EmailAddress {
	if len(raw) == 0 {
		return nil
	}
	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return nil
	}
	fromHdr := strings.TrimSpace(msg.Header.Get("From"))
	if fromHdr == "" {
		return nil
	}
	parsed, err := mail.ParseAddressList(fromHdr)
	if err != nil || len(parsed) == 0 {
		if id := threading.NormalizeMessageID(fromHdr); strings.Contains(fromHdr, "@") {
			addr := strings.Trim(id, "<>")
			return []EmailAddress{{Address: addr}}
		}
		return nil
	}
	out := make([]EmailAddress, 0, len(parsed))
	for _, a := range parsed {
		out = append(out, EmailAddress{Name: a.Name, Address: a.Address})
	}
	return out
}

func parseThreadHeaders(raw []byte) (references []string, inReplyTo string) {
	if len(raw) == 0 {
		return nil, ""
	}
	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return nil, ""
	}
	refs := msg.Header.Get("References")
	irt := strings.TrimSpace(msg.Header.Get("In-Reply-To"))
	return threading.ParseMessageIDs(refs), threading.NormalizeMessageID(irt)
}

func toValidUTF8(s string) string {
	s = repairLegacyCharsetString(s)
	return strings.ToValidUTF8(s, "")
}
