package twitchclient

import (
	"fmt"
	"net/http"
	"time"

	"code.justin.tv/chat/timing"
	"code.justin.tv/common/golibs/pkgpath"
	"code.justin.tv/foundation/xray"
)

const (
	// TwitchAuthorizationHeader is the header used when forwarding Authorization tokens.
	TwitchAuthorizationHeader = "Twitch-Authorization"
	// TwitchClientRowIDHeader is the header used when passing client row IDs.
	TwitchClientRowIDHeader = "Twitch-Client-Row-ID"
	// TwitchClientIDHeader is the header used when passing client row IDs.
	TwitchClientIDHeader = "Twitch-Client-ID"
	// TwitchRepositoryHeader is a header with the name of the repository that is using the client.
	TwitchRepositoryHeader = "Twitch-Repository"
)

// NewHTTPClient builds a new http.Client using ClientConf, default timeouts,
// roundtrippers for stats, xact and twitch headers.
// Note: ClientConf.Host is ignored.
//
// Use request context to activate stats and add auth headers, for example:
//
//     httpClient := twitchclient.NewHTTPClient(twitchclient.ClientConf{Stats: stats})
//
//     // Make request
//     ctx = twitchclient.WithReqOpts(context.Background(), twitchclient.ReqOpts{
//         StatName: "statname",
//         StatSampleRate: 0.2,
//     })
//     req, _ := http.NewRequest("GET", host+"/path", body)
//     req = req.WithContext(ctx)
//
//     response, err := httpClient.Do(req)
//
func NewHTTPClient(conf ClientConf) *http.Client {
	applyDefaults(&conf)

	t := conf.BaseTransport
	if t == nil {
		t = newHTTPTransport(conf)
	}

	t = xray.RoundTripper(t)

	for _, wrap := range conf.RoundTripperWrappers {
		t = wrap(t) // user provided wrappers (i.e. hystrix support)
	}

	t = &twitchRoundTripper{base: t, conf: conf}

	return &http.Client{
		Transport:     t,
		CheckRedirect: conf.CheckRedirect,
	}
}

// newHTTPTransport returns the default HTTP transport used by the http.Client
func newHTTPTransport(conf ClientConf) *http.Transport {
	return &http.Transport{
		DialContext:           conf.Transport.DialContext,
		ExpectContinueTimeout: conf.Transport.ExpectContinueTimeout,
		IdleConnTimeout:       conf.Transport.IdleConnTimeout,
		Proxy:                 http.ProxyFromEnvironment,
		MaxIdleConnsPerHost:   conf.Transport.MaxIdleConnsPerHost,
		TLSClientConfig:       conf.TLSClientConfig,
		TLSHandshakeTimeout:   conf.Transport.TLSHandshakeTimeout,
		TLSNextProto:          conf.Transport.TLSNextProto,
	}
}

// twitchRoundTripper wraps a base RoundTripper to add
// twitchclient features like stats, xact, headers, etc.
type twitchRoundTripper struct {
	base http.RoundTripper
	conf ClientConf
}

func (rt *twitchRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	startTime := time.Now()
	ctx := req.Context()

	reqOpts := GetReqOpts(ctx)
	if reqOpts.StatSampleRate == 0 {
		reqOpts.StatSampleRate = 0.1
	}

	// Client Tracer stats
	if rt.conf.Stats != nil {
		ctx = withStats(ctx, rt.conf.Stats, rt.conf.DNSStatsPrefix, rt.conf.Logger, reqOpts.StatSampleRate)
		req = req.WithContext(ctx)
	}

	// Xact (track if enabled in the ctx)
	xact, xactEnabled := timing.XactFromContext(ctx)
	var sub *timing.SubXact
	if xactEnabled {
		sub = xact.Sub(rt.conf.TimingXactName)
		sub.Start()
	}

	// Set headers if needed
	rt.setHeaders(req)

	// Call base roundTripper
	resp, err := rt.base.RoundTrip(req)
	statusCode := 0
	if err == nil && resp != nil {
		statusCode = resp.StatusCode
	}

	// Request timing stats
	if rt.conf.Stats != nil && reqOpts.StatName != "" {
		stat := timingStatKey(rt.conf.StatNamePrefix, reqOpts.StatName, statusCode)
		_ = rt.conf.Stats.TimingDuration(stat, time.Since(startTime), reqOpts.StatSampleRate)
	}

	// Xact End
	if xactEnabled {
		sub.End()
	}

	// Make sure to discard the remaining http body upon calling Close()
	// NOTE: Re-evaluate if this behavior is still needed on Go1.7
	if resp != nil {
		resp.Body = &readAllOnCloseBody{body: resp.Body}
	}

	return resp, err
}

// setHeaders adds client-Row, auth and repo HTTP headers to the request if needed.
func (rt *twitchRoundTripper) setHeaders(req *http.Request) {
	reqOpts := GetReqOpts(req.Context())

	if reqOpts.ClientID != "" {
		req.Header.Set(TwitchClientIDHeader, reqOpts.ClientID)
	}

	if reqOpts.ClientRowID != "" {
		req.Header.Set(TwitchClientRowIDHeader, reqOpts.ClientRowID)
	}

	if reqOpts.AuthorizationToken != "" {
		req.Header.Set(TwitchAuthorizationHeader, reqOpts.AuthorizationToken)
	}

	if repo, ok := pkgpath.Main(); ok && !rt.conf.SuppressRepositoryHeader {
		req.Header.Set(TwitchRepositoryHeader, repo)
	}
}

func timingStatKey(statNamePrefix string, statName string, statusCode int) string {
	if statNamePrefix == "" {
		return fmt.Sprintf("%s.%d", statName, statusCode)
	}
	return fmt.Sprintf("%s.%s.%d", statNamePrefix, statName, statusCode)
}
