package imap

import (
	"bytes"
	"encoding/base64"
	"io"
	"mime"
	"mime/multipart"
	"mime/quotedprintable"
	"net/mail"
	"strings"

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

// AttachmentPart is a decoded MIME body part stored as an attachment or inline resource.
type AttachmentPart struct {
	Filename    string
	ContentType string
	ContentID   string
	IsInline    bool
	Data        []byte
}

// ExtractAttachments parses raw RFC 822 message bytes and returns attachment parts.
// Body text/plain and text/html parts are skipped. Non-text parts are collected when
// Content-Disposition is attachment, inline (with filename or Content-ID), or when a
// related inline part has Content-ID (typical cid: image references). Parts exceeding
// limits.MaxAttachmentBytes are skipped; collection stops at limits.MaxAttachmentsPerMessage.
func ExtractAttachments(raw []byte) ([]AttachmentPart, error) {
	if len(raw) == 0 {
		return nil, nil
	}

	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return nil, err
	}

	contentType := msg.Header.Get("Content-Type")
	if contentType == "" {
		return nil, nil
	}

	mediaType, params, err := mime.ParseMediaType(contentType)
	if err != nil || !strings.HasPrefix(mediaType, "multipart/") {
		return nil, nil
	}

	return extractAttachmentsFromMultipart(msg.Body, params["boundary"]), nil
}

func extractAttachmentsFromMultipart(r io.Reader, boundary string) []AttachmentPart {
	var attachments []AttachmentPart

	mr := multipart.NewReader(r, boundary)
	for {
		part, err := mr.NextPart()
		if err != nil {
			break
		}
		if len(attachments) >= limits.MaxAttachmentsPerMessage {
			break
		}

		partType := part.Header.Get("Content-Type")
		mediaType, params, _ := mime.ParseMediaType(partType)
		if mediaType == "" {
			mediaType = "application/octet-stream"
		}

		switch {
		case mediaType == "text/plain", mediaType == "text/html":
			continue
		case strings.HasPrefix(mediaType, "multipart/"):
			nested := extractAttachmentsFromMultipart(part, params["boundary"])
			for _, att := range nested {
				if len(attachments) >= limits.MaxAttachmentsPerMessage {
					break
				}
				attachments = append(attachments, att)
			}
		default:
			if att, ok := partToAttachment(part, mediaType, params); ok {
				attachments = append(attachments, att)
			}
		}
	}

	return attachments
}

func partToAttachment(part *multipart.Part, mediaType string, typeParams map[string]string) (AttachmentPart, bool) {
	if strings.HasPrefix(mediaType, "text/") {
		return AttachmentPart{}, false
	}

	disposition, dispParams, _ := mime.ParseMediaType(part.Header.Get("Content-Disposition"))
	filename := dispParams["filename"]
	if filename == "" {
		filename = typeParams["name"]
	}

	contentID := normalizeContentID(part.Header.Get("Content-ID"))
	isInline := strings.EqualFold(disposition, "inline")
	isAttachment := strings.EqualFold(disposition, "attachment")

	switch {
	case isAttachment:
		// always store
	case isInline && (filename != "" || contentID != ""):
		// inline image / resource referenced via cid:
	case contentID != "":
		// multipart/related parts often omit Content-Disposition
		isInline = true
	default:
		return AttachmentPart{}, false
	}

	if filename == "" {
		filename = inlineAttachmentFilename(contentID, mediaType)
	}

	data, err := io.ReadAll(part)
	if err != nil {
		return AttachmentPart{}, false
	}

	data, err = decodePartBody(part.Header.Get("Content-Transfer-Encoding"), data)
	if err != nil || len(data) > limits.MaxAttachmentBytes {
		return AttachmentPart{}, false
	}

	return AttachmentPart{
		Filename:    filename,
		ContentType: mediaType,
		ContentID:   contentID,
		IsInline:    isInline,
		Data:        data,
	}, true
}

func normalizeContentID(raw string) string {
	return strings.Trim(raw, "<> \t")
}

func inlineAttachmentFilename(contentID, mediaType string) string {
	base := "inline"
	if contentID != "" {
		base = strings.Map(func(r rune) rune {
			switch r {
			case '<', '>', '/', '\\', ':', '"', '\'', '?', '*':
				return '_'
			default:
				return r
			}
		}, contentID)
	}
	ext := extensionFromMediaType(mediaType)
	if ext != "" && !strings.HasSuffix(strings.ToLower(base), ext) {
		return base + ext
	}
	return base
}

func extensionFromMediaType(mediaType string) string {
	switch strings.ToLower(strings.TrimSpace(strings.Split(mediaType, ";")[0])) {
	case "image/jpeg", "image/jpg":
		return ".jpg"
	case "image/png":
		return ".png"
	case "image/gif":
		return ".gif"
	case "image/webp":
		return ".webp"
	case "image/svg+xml":
		return ".svg"
	default:
		return ""
	}
}

func decodePartBody(transferEncoding string, data []byte) ([]byte, error) {
	switch strings.ToLower(strings.TrimSpace(transferEncoding)) {
	case "base64":
		return decodeBase64Body(data)
	case "quoted-printable":
		return io.ReadAll(quotedprintable.NewReader(bytes.NewReader(data)))
	default:
		return data, nil
	}
}

func decodeBase64Body(data []byte) ([]byte, error) {
	clean := bytes.Map(func(r rune) rune {
		switch r {
		case '\r', '\n', ' ', '\t':
			return -1
		default:
			return r
		}
	}, data)
	decoded := make([]byte, base64.StdEncoding.DecodedLen(len(clean)))
	n, err := base64.StdEncoding.Decode(decoded, clean)
	if err != nil {
		return nil, err
	}
	return decoded[:n], nil
}
