/*
Package twitchhttp enables quicker creation of production-ready HTTP clients and servers.
*/
package twitchhttp

import (
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"net/url"
	"strings"

	"github.com/cactus/go-statsd-client/statsd"
	"golang.org/x/net/context"

	"code.justin.tv/common/chitin"
	"code.justin.tv/common/config"
	"code.justin.tv/feeds/ctxlog"
	"code.justin.tv/foundation/xray"
)

type Client interface {
	NewRequest(method string, path string, body io.Reader) (*http.Request, error)
	Do(context.Context, *http.Request, ReqOpts) (*http.Response, error)
	DoJSON(context.Context, interface{}, *http.Request, ReqOpts) (*http.Response, error)
}

// ClientArgs provides the configuration for a new Client
type ClientConf struct {
	// Host (required) configures the client to connect to a specific URI host.
	// If not specified, URIs created by the client will default to use "http://".
	Host string

	// Transport supplies configuration for the client's HTTP transport.
	Transport TransportConf

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

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

	// Specify a custom stat prefix for DNS timing stats, defaults to "dns."
	DNSStatsPrefix string

	// Enable extra DNS stats, specifically how many IP's are resolved and which is selected
	EnableExtraDNSStats bool

	// Doesn't send the Twitch-Repository header. Set to TRUE for 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

	// Used to modify the BaseRoundTripper used for requests.
	// Wrappers are applied in order: RTW[2](RTW[1](RTW[0](baseRT)))
	RoundTripperWrappers []func(http.RoundTripper) http.RoundTripper

	// Base RoundTripper that makes the http request.
	// By default it is an *http.Transport provided by twitchhttp with the current config,
	// but it could be overridden to create mock RoundTripper for tests or similar.
	BaseRoundTripper http.RoundTripper

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

	// ctxlog will append logging dimensions to the context using this key. A logger may
	// want to log those context values.
	DimensionKey interface{}
}

// 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.Transport's default value.
	MaxIdleConnsPerHost int
}

// NewHTTPClient builds a new http.Client using ClientConf,
// with stats, xact, headers and chitin roundtrippers.
// The option ClientConf.Host is ignored (http.Client has no host field).
//
// Use request context to activate stats and add auth headers, i.e.:
//     client := twithhttp.NewHTTPClient(ClientConf{Stats: stats})
//
//     ctx := context.Background()
//     ctx = twithhttp.WithTimingStats(ctx, "statname", 0.2) // track timing stats on "statname.status"
//     ctx = twithhttp.WithTwitchAuthorization(ctx, opts.AuthorizationToken) // add "Twitch-Authorization" header
//     ctx = twithhttp.WithTwitchClientRowID(ctx, opts.ClientRowID) // add "Twitch-Client-Row-ID" header
//     req, _ := http.NewRequest("GET", host+"/path", body)
//     req = req.WithContext(ctx)
//
//     response, err := client.Do(req)
//
func NewHTTPClient(conf ClientConf) *http.Client {
	fillConfigDefaults(&conf)
	ctxLog := &ctxlog.Ctxlog{
		CtxDims:       &ctxDimensions{conf.DimensionKey},
		StartingIndex: rand.Int63(),
	}
	return resolveHTTPClient(conf, context.Background(), ctxLog)
}

func resolveHTTPTransport(conf ClientConf) *http.Transport {
	return &http.Transport{
		Proxy:               http.ProxyFromEnvironment,
		MaxIdleConnsPerHost: conf.Transport.MaxIdleConnsPerHost,
		Dial:                newDialer(conf.Stats, conf.DNSStatsPrefix, conf.EnableExtraDNSStats).Dial,
		TLSClientConfig:     conf.TLSClientConfig,
	}
}

func resolveHTTPClient(conf ClientConf, chitinCtx context.Context, ctxLog *ctxlog.Ctxlog) *http.Client {
	// Chitin RoundTripper
	rt, err := chitin.RoundTripper(chitinCtx, conf.BaseRoundTripper) // with ctx.Background() will use req.Context()
	if err != nil {                                                  // new versions of chitin never return an error here
		panic("You are using a very old version of chitin, please upgrade")
	}

	rt = xray.RoundTripper(rt)

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

	// Twitch specific RoundTripper (stats, xact and headers)
	rt = wrapWithTwitchHTTPRoundTripper(rt, conf.Stats, conf.TimingXactName, conf.SuppressRepositoryHeader, ctxLog)

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

func fillConfigDefaults(conf *ClientConf) {
	if conf.Stats == nil {
		conf.Stats = config.Statsd()
	}
	if conf.BaseRoundTripper == nil {
		conf.BaseRoundTripper = resolveHTTPTransport(*conf)
	}
	if conf.Logger == nil {
		conf.Logger = &defaultLogger{}
	}
	if conf.DimensionKey == nil {
		conf.DimensionKey = new(int)
	}
}

// NewClient builds a new twitchhttp.Client using ClientConf,
// with stats, xact, headers and chitin roundtrippers.
func NewClient(conf ClientConf) (Client, error) {
	fillConfigDefaults(&conf)
	hostURL, err := sanitizeHostURL(conf.Host)
	if err != nil {
		return nil, err
	}
	ctxLog := &ctxlog.Ctxlog{
		CtxDims:       &ctxDimensions{conf.DimensionKey},
		StartingIndex: rand.Int63(),
	}

	return &client{
		host:   hostURL,
		conf:   &conf,
		logger: conf.Logger,
		ctxLog: ctxLog,
	}, nil
}

type client struct {
	host   *url.URL
	conf   *ClientConf
	logger Logger
	ctxLog *ctxlog.Ctxlog
}

var _ Client = (*client)(nil)

// NewRequest creates an *http.Request using the configured host as the base for the path.
func (c *client) NewRequest(method string, path string, body io.Reader) (*http.Request, error) {
	u, err := url.Parse(path)
	if err != nil {
		return nil, err
	}

	return http.NewRequest(method, c.host.ResolveReference(u).String(), body)
}

// Do executes a requests using the given Context for Trace support
func (c *client) Do(ctx context.Context, req *http.Request, opts ReqOpts) (*http.Response, error) {

	// Default ReqOpt StatSampleRate is 0.1 if StatName was specified
	if opts.StatName != "" && opts.StatSampleRate == 0 {
		opts.StatSampleRate = 0.1
	}

	// Annotate ctx with opts (used by twitchHTTPRoundTripper)
	ctx = WithReqOpts(ctx, opts)

	// Use provided ctx if req.Context() was not set
	if req.Context() == context.Background() {
		req = req.WithContext(ctx)
	}

	// Make new httpClient using ctx for chitin.RoundTripper
	httpClient := resolveHTTPClient(*c.conf, ctx, c.ctxLog)

	// Make request
	return httpClient.Do(req)
}

// DoJSON executes a request, then deserializes the response.
// A *twitchhttp.Error is returned if it could contact the service, but got a 4xx.
// An error is returned if the service did not return, returned a 5xx, or returned a malformed payload.
func (c *client) DoJSON(ctx context.Context, data interface{}, req *http.Request, opts ReqOpts) (*http.Response, error) {
	resp, err := c.Do(ctx, req, opts)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	if resp.StatusCode >= 500 {
		body, readAllErr := ioutil.ReadAll(resp.Body)
		if readAllErr != nil {
			return resp, errors.New(resp.Status + ": unable to read response body for more error information")
		}

		return resp, errors.New(resp.Status + ": " + string(body))
	}

	if resp.StatusCode >= 400 {
		return resp, HandleFailedResponse(resp)
	}

	if resp.StatusCode != http.StatusNoContent {
		err = json.NewDecoder(resp.Body).Decode(data)
		if err != nil {
			return resp, fmt.Errorf("Unable to read response body: %s", err)
		}
	}

	return resp, nil
}

func sanitizeHostURL(host string) (*url.URL, error) {
	if host == "" {
		return nil, fmt.Errorf("Host cannot be blank")
	}
	if !strings.HasPrefix(host, "http") {
		host = fmt.Sprintf("http://%v", host)
	}
	hostURL, err := url.Parse(host)
	if err != nil {
		return nil, err
	}
	return hostURL, nil
}
