package appclient

import (
	"net"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/deepfence/ThreatMapper/deepfence_utils/log"
	"github.com/miekg/dns"
)

const (
	dnsPollInterval = 5 * time.Minute
)

// fastStartTicker is a ticker that 'ramps up' from 1 sec to duration.
func fastStartTicker(duration time.Duration) <-chan time.Time {
	c := make(chan time.Time, 1)
	go func() {
		d := 1 * time.Second
		for {
			time.Sleep(d)
			d = d * 2
			if d > duration {
				d = duration
			}

			select {
			case c <- time.Now():
			default:
			}
		}
	}()
	return c
}

// Resolver is a thing that can be stopped...
type Resolver interface {
	Stop()
}

type staticResolver struct {
	ResolverConfig

	failedResolutions map[string]struct{}
	quit              chan struct{}
}

// LookupIP type is used for looking up IPs.
type LookupIP func(host string) (ips []net.IP, err error)

// Target is a parsed representation of the app location.
type Target struct {
	original string   // the original url string
	url      *url.URL // the parsed url
	hostname string   // the hostname (without port) from the url
	port     int      // the port, or a sensible default
}

func (t Target) String() string {
	return net.JoinHostPort(t.hostname, strconv.Itoa(t.port))
}

// ResolverConfig is the config for a resolver.
type ResolverConfig struct {
	Targets       []Target
	Set           func(string, []url.URL)
	ResolveDomain bool // Weather to resolve domain into ip address or not

	// Optional
	Lookup LookupIP
	Ticker func(time.Duration) <-chan time.Time
}

// NewResolver periodically resolves the targets, and calls the set
// function with all the resolved IPs. It explictiy supports targets which
// resolve to multiple IPs.  It uses the supplied DNS server name.
func NewResolver(config ResolverConfig) (Resolver, error) {
	if config.Lookup == nil {
		config.Lookup = net.LookupIP
	}
	if config.Ticker == nil {
		config.Ticker = fastStartTicker
	}
	r := staticResolver{
		ResolverConfig:    config,
		failedResolutions: map[string]struct{}{},
		quit:              make(chan struct{}),
	}
	go r.loop()
	return r, nil
}

// LookupUsing produces a LookupIP function for the given DNS server.
func LookupUsing(dnsServer string) func(host string) (ips []net.IP, err error) {
	client := dns.Client{
		Net: "tcp",
	}
	return func(host string) (ips []net.IP, err error) {
		m := &dns.Msg{}
		m.SetQuestion(dns.Fqdn(host), dns.TypeA)
		in, _, err := client.Exchange(m, dnsServer)
		if err != nil {
			return nil, err
		}
		result := []net.IP{}
		for _, answer := range in.Answer {
			if a, ok := answer.(*dns.A); ok {
				result = append(result, a.A)
			}
		}
		return result, nil
	}
}

func (r staticResolver) loop() {
	r.resolve()
	t := r.Ticker(dnsPollInterval)
	for {
		select {
		case <-t:
			r.resolve()
		case <-r.quit:
			return
		}
	}
}

func (r staticResolver) Stop() {
	close(r.quit)
}

// ParseTargets deals with missing information in the targets string, defaulting
// the scheme, port etc.
func ParseTargets(urls []string) ([]Target, error) {
	var targets []Target
	for _, u := range urls {
		// naked hostnames (such as "localhost") are interpreted as relative URLs
		// so we add a scheme if u doesn't have one.
		prefixAdded := false
		if !strings.Contains(u, "://") {
			prefixAdded = true
			if strings.HasSuffix(u, ":443") {
				u = "https://" + u
			} else {
				u = "http://" + u
			}
		}
		parsed, err := url.Parse(u)
		if err != nil {
			return nil, err
		}

		var hostname string
		var port int
		if strings.Contains(parsed.Host, ":") {
			var portStr string
			hostname, portStr, err = net.SplitHostPort(parsed.Host)
			if err != nil {
				return nil, err
			}
			port, err = strconv.Atoi(portStr)
			if err != nil {
				return nil, err
			}
		} else {
			if prefixAdded {
				port = 80
			} else if strings.HasPrefix(u, "https://") {
				port = 443
			} else {
				port = 80
			}
			hostname = parsed.Host
		}
		targets = append(targets, Target{
			original: u,
			url:      parsed,
			hostname: hostname,
			port:     port,
		})
	}
	return targets, nil
}

func (r staticResolver) resolve() {
	for _, t := range r.Targets {
		ips := r.resolveOne(t)
		urls := makeURLs(t, ips)
		r.Set(t.hostname, urls)
	}
}

func makeURLs(t Target, ips []string) []url.URL {
	result := []url.URL{}
	for _, ip := range ips {
		u := *t.url
		u.Host = net.JoinHostPort(ip, strconv.Itoa(t.port))
		result = append(result, u)
	}
	return result
}

func (r staticResolver) resolveOne(t Target) []string {
	var addrs []net.IP
	if addr := net.ParseIP(t.hostname); addr != nil {
		addrs = []net.IP{addr}
	} else {
		var err error
		if r.ResolveDomain == false {
			return []string{t.hostname}
		}
		addrs, err = r.Lookup(t.hostname)
		if err != nil {
			if _, ok := r.failedResolutions[t.hostname]; !ok {
				log.Warn().Msgf("Cannot resolve '%s': %v", t.hostname, err)
				// Only log the error once
				r.failedResolutions[t.hostname] = struct{}{}
			}
			return []string{}
		}
		// Allow logging errors in future resolutions
		delete(r.failedResolutions, t.hostname)
	}
	endpoints := make([]string, 0, len(addrs))
	for _, addr := range addrs {
		// For now, ignore IPv6
		if addr.To4() == nil {
			continue
		}
		endpoints = append(endpoints, addr.String())
	}
	return endpoints
}
