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

import (
	"crypto/tls"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
	"time"

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

	"code.justin.tv/chat/timing"
	"code.justin.tv/common/chitin"
	"code.justin.tv/common/config"
)

// TwitchAuthorizationHeader is the key we use when forwarding Authorization tokens
const TwitchAuthorizationHeader = "Twitch-Authorization"

// Client ensures that a Trace-compatible http.Client is used.
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)
}

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

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

	// 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 RoundTripper used for requests.
	// MUST use the RoundTripper passed in.
	// Wrappers are applied in order: RTW[2](RTW[1](RTW[0](baseRT)))
	RoundTripperWrappers []func(http.RoundTripper) http.RoundTripper
}

// NewClient allocates and returns a new Client.
func NewClient(conf ClientConf) (Client, error) {
	if conf.Host == "" {
		return nil, fmt.Errorf("Host cannot be blank")
	}

	host := conf.Host
	if !strings.HasPrefix(host, "http") {
		host = fmt.Sprintf("http://%v", host)
	}

	u, err := url.Parse(host)
	if err != nil {
		return nil, err
	}

	stats := config.Statsd()
	if conf.Stats != nil {
		stats = conf.Stats
	}

	xactName := conf.TimingXactName
	if xactName == "" {
		xactName = "twitchhttp"
	}

	return &client{
		host: u,
		transport: &http.Transport{
			Proxy:               http.ProxyFromEnvironment,
			MaxIdleConnsPerHost: conf.Transport.MaxIdleConnsPerHost,
			Dial:                newDialer(stats, conf.DNSStatsPrefix).Dial,
			TLSClientConfig:     conf.TLSClientConfig,
		},
		stats:         stats,
		xactName:      xactName,
		wrappers:      conf.RoundTripperWrappers,
		checkRedirect: conf.CheckRedirect,
	}, nil
}

type client struct {
	host          *url.URL
	transport     http.RoundTripper
	stats         statsd.Statter
	xactName      string
	checkRedirect func(*http.Request, []*http.Request) error
	wrappers      []func(http.RoundTripper) http.RoundTripper
}

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) {
	var trackFns []func(*http.Response, error)

	start := time.Now()
	if c.stats != nil && opts.StatName != "" {
		trackFns = append(trackFns, func(resp *http.Response, err error) {
			duration := time.Now().Sub(start)
			sampleRate := opts.StatSampleRate
			if sampleRate == 0 {
				sampleRate = 0.1
			}
			result := 0
			if err == nil && resp != nil {
				result = resp.StatusCode
			}
			stat := fmt.Sprintf("%s.%d", opts.StatName, result)
			c.stats.TimingDuration(stat, duration, sampleRate)
		})
	}

	if xact, ok := timing.XactFromContext(ctx); ok {
		sub := xact.Sub(c.xactName)
		sub.Start()
		trackFns = append(trackFns, func(resp *http.Response, err error) {
			sub.End()
		})
	}

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

	httpClient, err := c.resolveHTTPClient(ctx)
	if err != nil {
		return nil, err
	}
	resp, err := httpClient.Do(req)
	for _, track := range trackFns {
		track(resp, err)
	}
	if resp != nil {
		resp.Body = &readAllOnCloseBody{body: resp.Body}
	}
	return resp, err
}

// 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 {
		return resp, errors.New(resp.Status)
	}

	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 (c *client) resolveHTTPClient(ctx context.Context) (*http.Client, error) {
	rt, err := chitin.RoundTripper(ctx, c.transport)
	if err != nil {
		return nil, err
	}
	for _, w := range c.wrappers {
		rt = w(rt)
	}
	return &http.Client{
		Transport:     rt,
		CheckRedirect: c.checkRedirect,
	}, nil
}
