package mailstore

import (
	"context"
	"net"
	"strings"
)

// CheckSPF is a minimal, single-level SPF check (v=spf1 ip4:/a/mx/include:, no
// recursive include/redirect, no macro expansion) against the sender domain's TXT
// record — one signal feeding the spam heuristic in spam.go, not an authoritative
// pass/fail gate.
// ponytail: not full RFC 7208 (no multi-level includes, no redirect, no macros) —
// good enough as a signal, revisit if a real sender's SPF record depends on it.
func CheckSPF(mailFrom, peerIP string) bool {
	domain := domainOf(mailFrom)
	if domain == "" {
		return true
	}
	ip := net.ParseIP(peerIP)
	if ip == nil {
		return true
	}
	record, ok := lookupSPFRecord(domain)
	if !ok {
		return true // no SPF record published: neutral, not a penalty
	}
	return evalSPF(record, ip, domain, 0)
}

func domainOf(address string) string {
	i := strings.LastIndex(address, "@")
	if i < 0 {
		return ""
	}
	return strings.ToLower(address[i+1:])
}

func lookupSPFRecord(domain string) (string, bool) {
	txts, err := net.DefaultResolver.LookupTXT(context.Background(), domain)
	if err != nil {
		return "", false
	}
	for _, t := range txts {
		if strings.HasPrefix(strings.ToLower(t), "v=spf1") {
			return t, true
		}
	}
	return "", false
}

// evalSPF walks mechanisms left to right; depth caps includes at one level.
func evalSPF(record string, ip net.IP, domain string, depth int) bool {
	fields := strings.Fields(record)
	for _, f := range fields[1:] { // skip "v=spf1"
		qualifier := byte('+')
		mech := f
		if len(f) > 0 && strings.ContainsRune("+-~?", rune(f[0])) {
			qualifier = f[0]
			mech = f[1:]
		}
		switch {
		case mech == "all":
			return qualifier != '-'
		case strings.HasPrefix(mech, "ip4:"):
			if matchIP4(mech[4:], ip) {
				return qualifier != '-'
			}
		case mech == "a":
			if matchA(domain, ip) {
				return qualifier != '-'
			}
		case strings.HasPrefix(mech, "a:"):
			if matchA(mech[2:], ip) {
				return qualifier != '-'
			}
		case mech == "mx":
			if matchMX(domain, ip) {
				return qualifier != '-'
			}
		case strings.HasPrefix(mech, "mx:"):
			if matchMX(mech[3:], ip) {
				return qualifier != '-'
			}
		case strings.HasPrefix(mech, "include:") && depth == 0:
			sub, ok := lookupSPFRecord(mech[len("include:"):])
			if ok && evalSPF(sub, ip, mech[len("include:"):], depth+1) {
				return true
			}
		}
	}
	return true // no matching mechanism and no explicit "all": neutral
}

func matchIP4(cidr string, ip net.IP) bool {
	if !strings.Contains(cidr, "/") {
		cidr += "/32"
	}
	_, network, err := net.ParseCIDR(cidr)
	if err != nil {
		return false
	}
	return network.Contains(ip)
}

func matchA(host string, ip net.IP) bool {
	ips, err := net.DefaultResolver.LookupIP(context.Background(), "ip4", host)
	if err != nil {
		return false
	}
	for _, a := range ips {
		if a.Equal(ip) {
			return true
		}
	}
	return false
}

func matchMX(domain string, ip net.IP) bool {
	mxs, err := net.LookupMX(domain)
	if err != nil {
		return false
	}
	for _, mx := range mxs {
		if matchA(strings.TrimSuffix(mx.Host, "."), ip) {
			return true
		}
	}
	return false
}
