package twitchclient

import (
	"context"
	"crypto/tls"
	"net"
	"net/http"
	"strings"
	"time"
)

const (
	defaultIdleConnTimeout       = 55 * time.Second // set lower than the ALB/ELB idle timeout (60s).
	defaultDialTimeout           = 30 * time.Second
	defaultDialKeepAlive         = 30 * time.Second
	defaultExpectContinueTimeout = 30 * time.Second
	defaultTLSHandshakeTimeout   = 5 * time.Second
)

// ClientConf provides the configuration for a new Client
type ClientConf struct {

	// Configuration for the HTTP default transport.
	Transport TransportConf

	// Please leave this unset, unless you want to provide a mock RoundTripper for tests.
	// Defaults to the *http.Transport generated by twitchclient using the provided TransportConf.
	BaseTransport http.RoundTripper

	// Add extra RoundTrippers to wrap the BaseTransport.
	// NOTE: The BaseTransport is already wrapped with xray, wrappers and then twitchclientRT.
	// Wrappers are applied in order: RTW[2](RTW[1](RTW[0](BaseTransport)))
	RoundTripperWrappers []func(http.RoundTripper) http.RoundTripper

	// CheckRedirect specifies the policy for handling redirects.
	CheckRedirect func(req *http.Request, via []*http.Request) error

	// Enables tracking of DNS and request timings.
	Stats Statter

	// Specify a custom stat prefix for DNS timing stats, defaults to "dns"
	// The sample rate depends on the sample rate in ReqOpts.
	DNSStatsPrefix string

	// Used to prefix StatName is requests. One usage of this parameter is to prefix
	// all client stats with your service, e.g. "service.users_service". There
	// shouldn't be a leading period on StatName or a trailing period on StatNamePrefix.
	StatNamePrefix string

	// Avoid sending the Twitch-Repository header (which is otherwise automatically included).
	// Please set to true when calling 3rd party clients (rollbar, facebook, etc)
	SuppressRepositoryHeader bool

	// The code.justin.tv/chat/timing sub-transaction name, defaults to "twitchhttp"
	TimingXactName string

	// Optional TLS config for making secure requests
	TLSClientConfig *tls.Config

	// An optional logger. The default implementation prints normal logs to stdout,
	// discards debug logs, and does not log any context dimensions like request ID.
	Logger Logger

	// Host configures the client to connect to a specific URI host.
	// If not specified, URIs created by the client will default to use "http://".
	// This setting is ignored on NewHTTPClient(clientConf).
	Host string
}

// TransportConf provides configuration options for the HTTP transport
type TransportConf struct {
	// MaxIdleConnsPerHost controls the maximum number of idle TCP connections that can exist in the
	// connection pool for a specific host.
	// Defaults to `http.DefaultMaxIdleConnsPerHost` (2).
	MaxIdleConnsPerHost int

	// IdleConnTimeout is the maximum amount of time an idle (keep-alive) connection will remain idle
	// before closing itself.
	// If zero, defaults to `defaultIdleconnTimeout`.
	// To disable timeout, set to a negative value.
	IdleConnTimeout time.Duration

	// TLSHandshakeTimeout specifies the maximum amount of time to wait for a TLS handshake.
	// If zero, defaults to `defaultTLSHandshakeTimeout`.
	// To disable timeout, set to a negative value.
	TLSHandshakeTimeout time.Duration

	// ExpectContinueTimeout, if non-zero, specifies the amount of time to wait for a server's first
	// response headers after fully writing the request headers if the request has an
	// "Expect: 100-continue" header.
	//
	// This time does not include the time to send the request header.
	//
	// If zero, defaults to `defaultExpectContinueTimeout`.
	// To disable timeout, set to a negative value.
	//
	// If timeout is disabled, this causes the body to by sent immediately, without waiting for the
	// server to approve.
	ExpectContinueTimeout time.Duration

	// TLSNextProto specifies how the Transport switches to an alternate protocol (such as HTTP/2)
	// after a TLS NPN/ALPN protocol negotiation.
	//
	// Leaving this value as the default of nil will enable HTTP/2 automatically.
	// Setting this value to a non-nil empty map will disable HTTP/2.
	TLSNextProto map[string]func(string, *tls.Conn) http.RoundTripper

	// DialContext specifies the dial function for creating unencrypted TCP connections.
	// If DialContext is nil, then the transport dials using package net.
	// The default dial timeout is `defaultDialTimeout`. The default dial keep-alive is `defaultDialKeepAlive`.
	//
	// An example that allows you to specify your own dial timeout and keep-alive:
	// (&net.Dialer{
	//	Timeout:   DialTimeout,
	//	KeepAlive: DialKeepAlive,
	// }).DialContext
	//
	// DialTimeout is the maximum amount of time a dial will wait for a connect to complete.
	//
	// When using TCP and dialing a host name with multiple IP addresses, the timeout may be divided
	// between them.
	//
	// With or without a timeout, the operating system may impose its own earlier timeout.
	// For instance, TCP timeouts are often around 3 minutes.
	//
	// To disable timeout, do not set this value (or set it to 0).
	//
	// DialKeepAlive specifies the keep-alive period for an active network connection.
	// Network protocols that do not support keep-alives ignore this field.
	//
	// To disable keep-alive, do not set this value (or set it to 0).
	DialContext func(ctx context.Context, network, addr string) (net.Conn, error)
}

// Logger is a logging interface which offers the ability to log, debug log, and do so with
// dimensions attached to the context, like request ID.
type Logger interface {
	DebugCtx(ctx context.Context, params ...interface{})
	Debug(params ...interface{})
	LogCtx(ctx context.Context, params ...interface{})
	Log(params ...interface{})
}

// applyDefaults changes zero-values for its defaults
func applyDefaults(conf *ClientConf) {
	if conf.Stats == nil {
		conf.Stats = noopStatter{}
	}
	if conf.Logger == nil {
		conf.Logger = &defaultLogger{}
	}
	if conf.TimingXactName == "" {
		conf.TimingXactName = "twitchhttp"
	}
	if conf.DNSStatsPrefix == "" {
		conf.DNSStatsPrefix = "dns"
	}

	// make sure stat prefixes don't have a trailing dot
	conf.DNSStatsPrefix = strings.TrimSuffix(conf.DNSStatsPrefix, ".")
	conf.StatNamePrefix = strings.TrimSuffix(conf.StatNamePrefix, ".")

	transport := &conf.Transport
	transport.ExpectContinueTimeout = applyDefaultTimeout(transport.ExpectContinueTimeout, defaultExpectContinueTimeout)
	transport.IdleConnTimeout = applyDefaultTimeout(transport.IdleConnTimeout, defaultIdleConnTimeout)
	transport.TLSHandshakeTimeout = applyDefaultTimeout(transport.TLSHandshakeTimeout, defaultTLSHandshakeTimeout)

	// default dialer
	if conf.Transport.DialContext == nil {
		conf.Transport.DialContext = (&net.Dialer{
			Timeout:   defaultDialTimeout,
			KeepAlive: defaultDialKeepAlive,
		}).DialContext
	}
}

// applyDefaultTimeout returns default for zero (unset) or zero for negative (disabled).
// For transport timeout values, the standard library considers the zero-value
// a "no limit", but in twitchclient we want the zero-value to have a sensitive default.
// We use negative values (-1) to disable the timeout (which is a 0 for the standard library).
func applyDefaultTimeout(val, defaultVal time.Duration) time.Duration {
	switch {
	case val < 0:
		return time.Duration(0)
	case val == 0:
		return defaultVal
	default:
		return val
	}
}
