package manager

import (
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"sync"
	"time"

	"code.justin.tv/systems/sandstorm/logging"
	"code.justin.tv/systems/sandstorm/pkg/atomic"
)

// ProxyStatter API for reporting proxy usage stats.
// its a subset statsd.Statter interface
type ProxyStatter interface {

	// Inc increments a count type.
	// stat is a string name for the metric.
	// value is the integer value
	// rate is the sample rate (0.0 to 1.0)
	Inc(string, int64, float32) error

	// Timing submits a timing type.
	// stat is a string name for the metric.
	// delta is the time duration value in milliseconds
	// rate is the sample rate (0.0 to 1.0).
	Timing(string, int64, float32) error
}

// NoopProxyStatter is a client that does nothing.
type NoopProxyStatter struct {
}

// Inc does nothing
func (s *NoopProxyStatter) Inc(stat string, value int64, rate float32) error {
	return nil
}

// Timing does nothing
func (s *NoopProxyStatter) Timing(stat string, delta int64, rate float32) error {
	return nil
}

const (
	proxyDuration            = 5 * time.Minute
	proxyTimeout             = 30 * time.Second
	proxyKeepAlive           = 30 * time.Second
	proxyMaxIdleConns        = 100
	proxyIdleTimeout         = 90 * time.Second
	proxyTLSHandshakeTimeout = 10 * time.Second
	proxyContinueTimeout     = 1 * time.Second
)

const (
	defaultStatsdHost = "statsd.internal.justin.tv:8125"
)

type wrappedRoundTripper struct {
	defaultRT http.RoundTripper
	proxyRT   http.RoundTripper

	// init proxy only once.
	initSync sync.Once

	ProxyURL   string
	proxyInUse atomic.Bool

	proxyDuration time.Duration

	Pop     string
	Logger  logging.Logger
	Statter ProxyStatter
}

func (wrt *wrappedRoundTripper) roundTripper() http.RoundTripper {
	if wrt.proxyInUse.IsSet() {
		return wrt.proxyRT
	}
	return wrt.defaultRT
}

func (wrt *wrappedRoundTripper) init() (err error) {
	// only try to parse ProxyURL once, if its not set correctly, ignore proxy settings and log error.
	wrt.initSync.Do(func() {
		if wrt.ProxyURL == "" {
			err = errors.New("proxyURL not provided")
			return
		}
		var proxy *url.URL
		proxy, err = url.Parse(wrt.ProxyURL)
		if err != nil {
			return
		}
		wrt.proxyRT = &http.Transport{
			Proxy: http.ProxyURL(proxy),
			DialContext: (&net.Dialer{
				Timeout:   proxyTimeout,
				KeepAlive: proxyKeepAlive,
			}).DialContext,
			MaxIdleConns:          proxyMaxIdleConns,
			IdleConnTimeout:       proxyIdleTimeout,
			TLSHandshakeTimeout:   proxyTLSHandshakeTimeout,
			ExpectContinueTimeout: proxyContinueTimeout,
		}

		wrt.defaultRT = http.DefaultTransport

		if wrt.proxyDuration == 0*time.Second {
			wrt.proxyDuration = proxyDuration
		}

		if wrt.Logger == nil {
			wrt.Logger = &logging.NoopLogger{}
		}

		if wrt.Pop == "" {
			wrt.Pop = "unknownPop"
		}

		if wrt.Statter == nil {
			wrt.Statter = &NoopProxyStatter{}
		}
	})
	return err
}

func (wrt *wrappedRoundTripper) switchToProxy() {
	wrt.Logger.Infof("manager: switching to use proxy http client for %v", proxyDuration)
	time.AfterFunc(wrt.proxyDuration, func() {
		wrt.Logger.Info("manager: switching back to use default http client")
		wrt.proxyInUse.Unset()
	})
	wrt.proxyInUse.Set()
}

func (wrt *wrappedRoundTripper) RoundTrip(request *http.Request) (resp *http.Response, err error) {

	err = wrt.init()
	if err != nil {
		return
	}

	currentTime := time.Now()
	resp, err = wrt.roundTripper().RoundTrip(request)
	if err != nil {
		if _, ok := err.(net.Error); ok && !wrt.proxyInUse.IsSet() {
			wrt.switchToProxy()
		}
		return
	}

	if wrt.proxyInUse.IsSet() {
		metricsErr := wrt.Statter.Inc(fmt.Sprintf("%s.proxy.requests", wrt.Pop), 1, 1.0)
		if metricsErr != nil {
			wrt.Logger.Warnf("failed to send stats: %s", err.Error())
		}

		metricsErr = wrt.Statter.Timing(fmt.Sprintf("%s.proxy.requestTime", wrt.Pop), int64(time.Since(currentTime).Seconds()*1000), 1.0)
		if metricsErr != nil {
			wrt.Logger.Warnf("failed to send stats: %s", err.Error())
		}
	}
	return
}
