package spflib

import (
	"encoding/json"
	"fmt"
	"net"
	"os"
	"strings"
)

// Resolver looks up spf txt records associated with a FQDN.
type Resolver interface {
	GetSPF(string) (string, error)
}

// LiveResolver simply queries DNS to resolve SPF records.
type LiveResolver struct{}

// GetSPF looks up the SPF record named "name".
func (l LiveResolver) GetSPF(name string) (string, error) {
	vals, err := net.LookupTXT(name)
	if err != nil {
		return "", err
	}
	spf := ""
	for _, v := range vals {
		if strings.HasPrefix(v, "v=spf1") {
			if spf != "" {
				return "", fmt.Errorf("%s has multiple SPF records", name)
			}
			spf = v
		}
	}
	if spf == "" {
		return "", fmt.Errorf("%s has no SPF record", name)
	}
	return spf, nil
}

// CachingResolver wraps a live resolver and adds caching to it.
// GetSPF will always return the cached value, if present.
// It will also query the inner resolver and compare results.
// If a given lookup has inconsistencies between cache and live,
// GetSPF will return the cached result.
// All records queries will be stored for the lifetime of the resolver,
// and can be flushed to disk at the end.
// All resolution errors from the inner resolver will be saved and can be retreived later.
type CachingResolver interface {
	Resolver
	ChangedRecords() []string
	ResolveErrors() []error
	Save(filename string) error
	IsCachePreserved() bool // Return true if cache preservation mode is enabled.
}

type cacheEntry struct {
	SPF string

	// value we have looked up this run
	resolvedSPF  string
	resolveError error
}

type cache struct {
	records map[string]*cacheEntry

	inner          Resolver
	cachePreserved bool // Set to true if cache preservation mode is enabled.
}

// NewCache creates a new cache file named filename.
func NewCache(filename string) (CachingResolver, error) {
	dat, err := os.ReadFile(filename)
	if err != nil {
		if os.IsNotExist(err) {
			return &cache{
				records:        map[string]*cacheEntry{},
				inner:          LiveResolver{},
				cachePreserved: false, // Disable cache preservation mode.
			}, nil
		}
		return nil, err // Otherwise, return the error.
	}
	recs := map[string]*cacheEntry{}
	if len(dat) == 0 {
		// json.Unmarshal considers empty input invalid. Use empty object instead.
		dat = []byte("{}")
	}
	if err := json.Unmarshal(dat, &recs); err != nil {
		return nil, err
	}
	return &cache{
		records:        recs,
		inner:          LiveResolver{},
		cachePreserved: true, // Enable cache preservation mode.
	}, nil
}

// IsCachePreserved returns true if cache preservation mode is enabled.
// In this mode, warnings are issued if the cache is out of date.
// This mode is enabled by the existance of the cache file "spfcache.json"
func (c *cache) IsCachePreserved() bool {
	return c.cachePreserved
}

func (c *cache) GetSPF(name string) (string, error) {
	entry, ok := c.records[name]
	if !ok {
		entry = &cacheEntry{}
		c.records[name] = entry
	}
	if entry.resolvedSPF == "" && entry.resolveError == nil {
		entry.resolvedSPF, entry.resolveError = c.inner.GetSPF(name)
	}
	// return cached value
	if entry.SPF != "" {
		return entry.SPF, nil
	}
	// if not cached, return results of inner resolver
	return entry.resolvedSPF, entry.resolveError
}

func (c *cache) ChangedRecords() []string {
	names := []string{}
	for name, entry := range c.records {
		if entry.resolvedSPF != entry.SPF {
			names = append(names, name)
		}
	}
	return names
}

func (c *cache) ResolveErrors() (errs []error) {
	for _, entry := range c.records {
		if entry.resolveError != nil {
			errs = append(errs, entry.resolveError)
		}
	}
	return
}

func (c *cache) Save(filename string) error {
	outRecs := make(map[string]*cacheEntry, len(c.records))
	for k, entry := range c.records {
		// move resolved data into cached field
		// only take those we actually resolved
		if entry.resolvedSPF != "" {
			entry.SPF = entry.resolvedSPF
			outRecs[k] = entry
		}
	}
	dat, _ := json.MarshalIndent(outRecs, "", "  ")
	return os.WriteFile(filename, dat, 0o644)
}
