package resolver

import (
	"context"
	"net"
	"strings"
	"time"

	"github.com/0xERR0R/blocky/cache"
	"github.com/0xERR0R/blocky/config"
	"github.com/0xERR0R/blocky/log"
	"github.com/0xERR0R/blocky/model"
	"github.com/0xERR0R/blocky/util"

	expirationcache "github.com/0xERR0R/expiration-cache"
	"github.com/miekg/dns"
	"github.com/sirupsen/logrus"
)

// ClientNamesResolver tries to determine client name by asking responsible DNS server via rDNS (reverse lookup)
type ClientNamesResolver struct {
	configurable[*config.ClientLookup]
	NextResolver
	typed

	cache            cache.ExpiringCache[[]string]
	externalResolver Resolver
}

// NewClientNamesResolver creates new resolver instance
func NewClientNamesResolver(ctx context.Context,
	cfg config.ClientLookup, upstreamsCfg config.Upstreams, bootstrap *Bootstrap,
) (cr *ClientNamesResolver, err error) {
	var r Resolver
	if !cfg.Upstream.IsDefault() {
		r, err = NewUpstreamResolver(ctx, newUpstreamConfig(cfg.Upstream, upstreamsCfg), bootstrap)
		if err != nil {
			return nil, err
		}
	}

	cr = &ClientNamesResolver{
		configurable: withConfig(&cfg),
		typed:        withType("client_names"),

		cache: expirationcache.NewCache[[]string](ctx, expirationcache.Options{
			CleanupInterval: time.Hour,
		}),
		externalResolver: r,
	}

	return
}

// LogConfig implements `config.Configurable`.
func (r *ClientNamesResolver) LogConfig(logger *logrus.Entry) {
	r.cfg.LogConfig(logger)

	logger.Infof("cache entries = %d", r.cache.TotalCount())
}

// Resolve tries to resolve the client name from the ip address
func (r *ClientNamesResolver) Resolve(ctx context.Context, request *model.Request) (*model.Response, error) {
	clientNames := r.getClientNames(ctx, request)

	request.ClientNames = clientNames
	ctx, _ = log.CtxWithFields(ctx, logrus.Fields{"client_names": strings.Join(clientNames, "; ")})

	return r.next.Resolve(ctx, request)
}

// returns names of client
func (r *ClientNamesResolver) getClientNames(ctx context.Context, request *model.Request) []string {
	if request.RequestClientID != "" {
		return []string{request.RequestClientID}
	}

	ip := request.ClientIP
	if ip == nil {
		return []string{}
	}

	c, _ := r.cache.Get(ip.String())
	if c != nil {
		// return copy here, since we can't control all usages here
		cpy := make([]string, len(*c))
		copy(cpy, *c)

		return cpy
	}

	names := r.resolveClientNames(ctx, ip)

	r.cache.Put(ip.String(), &names, time.Hour)

	return names
}

func extractClientNamesFromAnswer(answer []dns.RR, fallbackIP net.IP) (clientNames []string) {
	for _, answer := range answer {
		if t, ok := answer.(*dns.PTR); ok {
			hostName := strings.TrimSuffix(t.Ptr, ".")
			clientNames = append(clientNames, hostName)
		}
	}

	if len(clientNames) == 0 {
		clientNames = []string{fallbackIP.String()}
	}

	return
}

// tries to resolve client name from mapping, performs reverse DNS lookup otherwise
func (r *ClientNamesResolver) resolveClientNames(ctx context.Context, ip net.IP) (result []string) {
	ctx, logger := r.log(ctx)

	// try client mapping first
	result = r.getNameFromIPMapping(ip, result)
	if len(result) > 0 {
		return result
	}

	if r.externalResolver == nil {
		return []string{ip.String()}
	}

	reverse, _ := dns.ReverseAddr(ip.String())

	resp, err := r.externalResolver.Resolve(ctx, &model.Request{
		Req: util.NewMsgWithQuestion(reverse, dns.Type(dns.TypePTR)),
	})
	if err != nil {
		logger.Error("can't resolve client name: ", err)

		return []string{ip.String()}
	}

	clientNames := extractClientNamesFromAnswer(resp.Res.Answer, ip)

	// optional: if singleNameOrder is set, use only one name in the defined order
	if len(r.cfg.SingleNameOrder) > 0 {
		for _, i := range r.cfg.SingleNameOrder {
			if i > 0 && int(i) <= len(clientNames) {
				result = []string{clientNames[i-1]}

				break
			}
		}
	} else {
		result = clientNames
	}

	logger.WithField("client_names", strings.Join(result, "; ")).Debug("resolved client name(s) from external resolver")

	return result
}

func (r *ClientNamesResolver) getNameFromIPMapping(ip net.IP, result []string) []string {
	for name, ips := range r.cfg.ClientnameIPMapping {
		for _, i := range ips {
			if ip.String() == i.String() {
				result = append(result, name)
			}
		}
	}

	return result
}

// FlushCache reset client name cache
func (r *ClientNamesResolver) FlushCache() {
	r.cache.Clear()
}
