package imap

import (
	"bytes"
	"encoding/json"
	"net/mail"
	"regexp"
	"strings"

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

// MessageAuthInfo is persisted in messages.auth_info (JSON).
type MessageAuthInfo struct {
	MailedBy         string `json:"mailed_by,omitempty"`
	SignedBy         string `json:"signed_by,omitempty"`
	DKIMPass         *bool  `json:"dkim_pass,omitempty"`
	TLS              bool   `json:"tls,omitempty"`
	ListUnsubscribe  string `json:"list_unsubscribe,omitempty"`
}

var (
	dkimDomainRe  = regexp.MustCompile(`(?i)header\.d=([^\s;]+)`)
	dkimSigDRe    = regexp.MustCompile(`(?i)\bd=([^;\s]+)`)
	returnPathRe  = regexp.MustCompile(`(?i)<([^>]+)>`)
	receivedFromRe = regexp.MustCompile(`(?i)from\s+([^\s;(\[]+)`)
)

func parseMessageMeta(raw []byte, envelope *imap.Envelope) (replyToJSON, authJSON []byte) {
	auth := MessageAuthInfo{}
	replyTo := replyAddresses(envelope, raw)

	if len(raw) > 0 {
		msg, err := mail.ReadMessage(bytes.NewReader(raw))
		if err == nil {
			if len(replyTo) == 0 {
				replyTo = parseAddressListHeader(msg.Header.Get("Reply-To"))
			}
			auth.ListUnsubscribe = strings.TrimSpace(msg.Header.Get("List-Unsubscribe"))
			mergeAuthFromHeaders(&auth, msg)
		}
	}

	if auth.MailedBy == "" && len(envelope.From) > 0 {
		auth.MailedBy = domainFromAddr(envelope.From[0].Addr())
	}
	if auth.SignedBy == "" && auth.MailedBy != "" {
		auth.SignedBy = auth.MailedBy
	}

	authJSON, _ = json.Marshal(auth)
	replyToJSON, _ = json.Marshal(replyTo)
	return replyToJSON, authJSON
}

func replyAddresses(envelope *imap.Envelope, raw []byte) []EmailAddress {
	if len(envelope.ReplyTo) > 0 {
		return imapAddressesToEmail(envelope.ReplyTo)
	}
	if len(raw) == 0 {
		return nil
	}
	msg, err := mail.ReadMessage(bytes.NewReader(raw))
	if err != nil {
		return nil
	}
	return parseAddressListHeader(msg.Header.Get("Reply-To"))
}

func imapAddressesToEmail(addrs []imap.Address) []EmailAddress {
	out := make([]EmailAddress, 0, len(addrs))
	for _, a := range addrs {
		out = append(out, EmailAddress{Name: a.Name, Address: a.Addr()})
	}
	return out
}

func parseAddressListHeader(header string) []EmailAddress {
	header = strings.TrimSpace(header)
	if header == "" {
		return nil
	}
	parsed, err := mail.ParseAddressList(header)
	if err != nil {
		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 headerValues(h mail.Header, key string) []string {
	return h[key]
}

func mergeAuthFromHeaders(auth *MessageAuthInfo, msg *mail.Message) {
	for _, line := range headerValues(msg.Header, "Authentication-Results") {
		lower := strings.ToLower(line)
		if strings.Contains(lower, "dkim=pass") {
			pass := true
			auth.DKIMPass = &pass
			if m := dkimDomainRe.FindStringSubmatch(line); len(m) > 1 && auth.SignedBy == "" {
				auth.SignedBy = strings.Trim(m[1], `"'`)
			}
		} else if strings.Contains(lower, "dkim=fail") {
			fail := false
			auth.DKIMPass = &fail
		}
		if strings.Contains(lower, "tls=1") || strings.Contains(lower, "version=tls") {
			auth.TLS = true
		}
	}
	if auth.SignedBy == "" {
		if sig := msg.Header.Get("DKIM-Signature"); sig != "" {
			if m := dkimSigDRe.FindStringSubmatch(sig); len(m) > 1 {
				auth.SignedBy = strings.Trim(m[1], `"'`)
			}
		}
	}
	if rp := msg.Header.Get("Return-Path"); rp != "" {
		if m := returnPathRe.FindStringSubmatch(rp); len(m) > 1 {
			auth.MailedBy = domainFromAddr(m[1])
		}
	}
	for _, recv := range headerValues(msg.Header, "Received") {
		lower := strings.ToLower(recv)
		if strings.Contains(lower, "esmtps") || strings.Contains(lower, "tls") {
			auth.TLS = true
		}
		if auth.MailedBy == "" {
			if m := receivedFromRe.FindStringSubmatch(recv); len(m) > 1 {
				auth.MailedBy = domainFromAddr(m[1])
			}
		}
	}
}

func domainFromAddr(addr string) string {
	addr = strings.Trim(addr, "<>")
	if i := strings.LastIndex(addr, "@"); i >= 0 && i < len(addr)-1 {
		return strings.ToLower(addr[i+1:])
	}
	host := strings.TrimSpace(addr)
	if strings.Contains(host, ".") {
		return strings.ToLower(host)
	}
	return ""
}
