package twitchhttp

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

	"code.justin.tv/chat/timing"
	"code.justin.tv/common/golibs/pkgpath"
	"code.justin.tv/feeds/ctxlog"
	"github.com/cactus/go-statsd-client/statsd"
)

const (
	TwitchAuthorizationHeader = "Twitch-Authorization" // header used when forwarding Authorization tokens.
	TwitchClientRowIDHeader   = "Twitch-Client-Row-ID" // header used when passing client row IDs.
)

type contextKey int // unexported to prevent collisions with other packages

var headersAuthorizationKey = new(contextKey)
var headersClientRowIDKey = new(contextKey)
var timingStatsKey = new(contextKey)

type timingStatsConf struct {
	name       string
	sampleRate float32
}

// WrapWithTwitchHTTPRoundTripper adds a twitchhttp RoundTripper
// that handles stats, auth headers and xact.
func wrapWithTwitchHTTPRoundTripper(baseRT http.RoundTripper, stats statsd.Statter, xactName string, suppressRepositoryHeader bool, ctxLog *ctxlog.Ctxlog) http.RoundTripper {
	if xactName == "" {
		xactName = "twitchhttp"
	}
	return &twitchHTTPRoundTripper{BaseRT: baseRT, Stats: stats, XactName: xactName, SuppressRepositoryHeader: suppressRepositoryHeader, ctxlog: ctxLog}
}

// Adds twitchhttp specific functionality
type twitchHTTPRoundTripper struct {
	BaseRT http.RoundTripper

	Stats                    statsd.Statter
	XactName                 string
	SuppressRepositoryHeader bool
	ctxlog                   *ctxlog.Ctxlog
}

func (rt *twitchHTTPRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
	start := time.Now()

	// NewHTTPRequest is idempotent
	req = ctxlog.NewHTTPRequest(rt.ctxlog, req)
	ctx := req.Context()

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

	// Set headers if needed
	if clientRowID := twitchClientRowIDFromContext(ctx); clientRowID != "" {
		req.Header.Set(TwitchClientRowIDHeader, clientRowID)
	}
	if authHeader := twitchAuthorizationFromContext(ctx); authHeader != "" {
		req.Header.Set(TwitchAuthorizationHeader, authHeader)
	}
	if repo, ok := pkgpath.Main(); ok && !rt.SuppressRepositoryHeader {
		req.Header.Set("Twitch-Repository", repo)
	}

	// Call baseRT
	resp, err := rt.BaseRT.RoundTrip(req)

	// Timing Stats
	statName, sampleRate := timingStatsFromContext(ctx)
	if rt.Stats != nil && statName != "" && sampleRate != 0 {
		duration := time.Since(start)

		statusCode := 0
		if err == nil && resp != nil {
			statusCode = resp.StatusCode
		}

		stat := fmt.Sprintf("%s.%d", statName, statusCode)
		_ = rt.Stats.TimingDuration(stat, duration, sampleRate)
	}

	// 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
}

// WithReqOpts is a convenience function to annotate the context with the available ReqOpts
func WithReqOpts(ctx context.Context, opts ReqOpts) context.Context {
	if opts.StatName != "" {
		ctx = WithTimingStats(ctx, opts.StatName, opts.StatSampleRate)
	}
	if opts.AuthorizationToken != "" {
		ctx = WithTwitchAuthorization(ctx, opts.AuthorizationToken)
	}
	if opts.ClientRowID != "" {
		ctx = WithTwitchClientRowID(ctx, opts.ClientRowID)
	}
	return ctx
}

// WithTwitchAuthorization annotates the context to add the auth header
func WithTwitchAuthorization(ctx context.Context, authorizationToken string) context.Context {
	return context.WithValue(ctx, headersAuthorizationKey, authorizationToken)
}

// WithTwitchClientRowID annotates the context to add the client id header
func WithTwitchClientRowID(ctx context.Context, clientRowID string) context.Context {
	return context.WithValue(ctx, headersClientRowIDKey, clientRowID)
}

// WithStats annotates the context with a stat name and sample rate to allow collecting timing stats.
// i.e. ctx = WithStats(ctx, "my_cool_stat", 0.1)
func WithTimingStats(ctx context.Context, statName string, statSampleRate float32) context.Context {
	return context.WithValue(ctx, timingStatsKey, timingStatsConf{statName, statSampleRate})
}

func twitchAuthorizationFromContext(ctx context.Context) string {
	if val, ok := ctx.Value(headersAuthorizationKey).(string); ok {
		return val
	}
	return ""
}

func twitchClientRowIDFromContext(ctx context.Context) string {
	if val, ok := ctx.Value(headersClientRowIDKey).(string); ok {
		return val
	}
	return ""
}

func timingStatsFromContext(ctx context.Context) (string, float32) {
	if val, ok := ctx.Value(timingStatsKey).(timingStatsConf); ok {
		return val.name, val.sampleRate
	}
	return "", 0
}
