package geolocate

import (
	"context"
	"errors"
	"fmt"
	"math/rand"
	"net"
	"net/http"
	"time"

	"github.com/ooni/probe-cli/v3/internal/engine/netx"
	"github.com/ooni/probe-cli/v3/internal/model"
	"github.com/ooni/probe-cli/v3/internal/multierror"
)

var (
	// ErrAllIPLookuppersFailed indicates that we failed with looking
	// up the probe IP for with all the lookuppers that we tried.
	ErrAllIPLookuppersFailed = errors.New("all IP lookuppers failed")

	// ErrInvalidIPAddress indicates that the code returned to us a
	// string that actually isn't a valid IP address.
	ErrInvalidIPAddress = errors.New("lookupper did not return a valid IP")
)

type lookupFunc func(
	ctx context.Context, client *http.Client,
	logger model.Logger, userAgent string,
) (string, error)

type method struct {
	name string
	fn   lookupFunc
}

var (
	methods = []method{
		{
			name: "avast",
			fn:   avastIPLookup,
		},
		{
			name: "cloudflare",
			fn:   cloudflareIPLookup,
		},
		{
			name: "ipconfig",
			fn:   ipConfigIPLookup,
		},
		{
			name: "ipinfo",
			fn:   ipInfoIPLookup,
		},
		{
			name: "stun_ekiga",
			fn:   stunEkigaIPLookup,
		},
		{
			name: "stun_google",
			fn:   stunGoogleIPLookup,
		},
		{
			name: "ubuntu",
			fn:   ubuntuIPLookup,
		},
	}
)

type ipLookupClient struct {
	// Resolver is the resolver to use for HTTP.
	Resolver model.Resolver

	// Logger is the logger to use
	Logger model.Logger

	// UserAgent is the user agent to use
	UserAgent string
}

func makeSlice() []method {
	r := rand.New(rand.NewSource(time.Now().UnixNano()))
	ret := make([]method, len(methods))
	perm := r.Perm(len(methods))
	for idx, randIdx := range perm {
		ret[idx] = methods[randIdx]
	}
	return ret
}

func (c ipLookupClient) doWithCustomFunc(
	ctx context.Context, fn lookupFunc,
) (string, error) {
	// Reliability fix: let these mechanisms timeout earlier.
	const timeout = 7 * time.Second
	ctx, cancel := context.WithTimeout(ctx, timeout)
	defer cancel()
	// Implementation note: we MUST use an HTTP client that we're
	// sure IS NOT using any proxy. To this end, we construct a
	// client ourself that we know is not proxied.
	clnt := &http.Client{Transport: netx.NewHTTPTransport(netx.Config{
		Logger:       c.Logger,
		FullResolver: c.Resolver,
	})}
	defer clnt.CloseIdleConnections()
	ip, err := fn(ctx, clnt, c.Logger, c.UserAgent)
	if err != nil {
		return DefaultProbeIP, err
	}
	if net.ParseIP(ip) == nil {
		return DefaultProbeIP, fmt.Errorf("%w: %s", ErrInvalidIPAddress, ip)
	}
	c.Logger.Debugf("iplookup: IP: %s", ip)
	return ip, nil
}

func (c ipLookupClient) LookupProbeIP(ctx context.Context) (string, error) {
	union := multierror.New(ErrAllIPLookuppersFailed)
	for _, method := range makeSlice() {
		c.Logger.Infof("iplookup: using %s", method.name)
		ip, err := c.doWithCustomFunc(ctx, method.fn)
		if err == nil {
			return ip, nil
		}
		union.Add(err)
	}
	return DefaultProbeIP, union
}