package smtpserver

import (
	"bytes"
	"encoding/base64"
	"io"
	"mime"
	"mime/multipart"
	"net/mail"
	"path/filepath"
	"strings"
	"time"
)

// attachmentStoragePath mirrors smtp_handler.get_attachment_storage_path:
// {base}/{safe_domain}/{username_or_ip}/{YYYY-DD-MMM}/
func attachmentStoragePath(base, domain, usernameOrIP string, now time.Time) string {
	safeDomain := sanitizePathSegment(domain, "/\\")
	dateFolder := now.Format("2006-02-Jan")
	parts := []string{base, safeDomain}
	if usernameOrIP != "" {
		parts = append(parts, usernameOrIP)
	}
	parts = append(parts, dateFolder)
	return filepath.Join(parts...)
}

func sanitizePathSegment(s string, chars string) string {
	for _, c := range chars {
		s = strings.ReplaceAll(s, string(c), "_")
	}
	return s
}

// cleanMessageIDPrefix strips everything from "@" onward, mirroring the
// clean_message_id computation used to build attachment filenames.
func cleanMessageIDPrefix(messageID string) string {
	if i := strings.Index(messageID, "@"); i >= 0 {
		return messageID[:i]
	}
	return messageID
}

type attachmentPart struct {
	Filename    string
	ContentType string
	Data        []byte
}

type parsedMessage struct {
	HeaderLines []string // "Name: value" per header, in order
	BodyText    string   // concatenated text/* parts
	Attachments []attachmentPart
}

// parseMessage mirrors the repeated BytesParser(policy=policy.default) passes in
// handle_DATA: it extracts header lines for logging, concatenated text body, and any
// attachment parts (Content-Disposition: attachment with a filename).
func parseMessage(raw []byte) (*parsedMessage, error) {
	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return nil, err
	}
	out := &parsedMessage{}
	for k, vs := range msg.Header {
		for _, v := range vs {
			out.HeaderLines = append(out.HeaderLines, k+": "+v)
		}
	}

	contentType := msg.Header.Get("Content-Type")
	mediaType, params, err := mime.ParseMediaType(contentType)
	if err != nil {
		mediaType = "text/plain"
	}

	if strings.HasPrefix(mediaType, "multipart/") {
		mr := multipart.NewReader(msg.Body, params["boundary"])
		for {
			part, err := mr.NextPart()
			if err == io.EOF {
				break
			}
			if err != nil {
				break
			}
			data, _ := io.ReadAll(part)
			// multipart.Reader auto-decodes quoted-printable transparently during Read,
			// but not base64 (see the mime/multipart docs) — without this, a base64
			// attachment/body part is stored/relayed-for-display as raw base64 text
			// instead of its actual decoded bytes.
			data = decodeContentTransferEncoding(part.Header.Get("Content-Transfer-Encoding"), data)
			disp, dispParams, _ := mime.ParseMediaType(part.Header.Get("Content-Disposition"))
			partCT := part.Header.Get("Content-Type")
			partMediaType, _, _ := mime.ParseMediaType(partCT)

			if disp == "attachment" && dispParams["filename"] != "" {
				out.Attachments = append(out.Attachments, attachmentPart{
					Filename:    dispParams["filename"],
					ContentType: getContentType(partMediaType, dispParams["filename"]),
					Data:        data,
				})
				continue
			}
			if strings.HasPrefix(partMediaType, "text/") && disp != "attachment" {
				out.BodyText += string(data) + "\n"
			}
		}
	} else if strings.HasPrefix(mediaType, "text/") {
		data, _ := io.ReadAll(msg.Body)
		out.BodyText = string(data)
	}
	out.BodyText = strings.TrimSpace(out.BodyText)
	return out, nil
}

// decodeContentTransferEncoding decodes a MIME part's body per its
// Content-Transfer-Encoding when that isn't already handled transparently by
// multipart.Reader (which only auto-decodes quoted-printable). base64 bodies are
// wrapped at a fixed line length, so whitespace/newlines are stripped before
// decoding. Falls back to the raw bytes on a decode error or any other encoding
// (7bit/8bit/binary need no transform).
func decodeContentTransferEncoding(cte string, data []byte) []byte {
	if !strings.EqualFold(strings.TrimSpace(cte), "base64") {
		return data
	}
	cleaned := make([]byte, 0, len(data))
	for _, b := range data {
		switch b {
		case ' ', '\t', '\r', '\n':
			continue
		default:
			cleaned = append(cleaned, b)
		}
	}
	decoded, err := base64.StdEncoding.DecodeString(string(cleaned))
	if err != nil {
		return data
	}
	return decoded
}
