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

import (
	"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"
)

// Client ensures that a Trace-compatible http.Client is used.
type Client interface {
	Do(context.Context, *http.Request, ReqOpts) (*http.Response, error)
	NewRequest(method string, path string, body io.Reader) (*http.Request, 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

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

// 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,
		},
		stats:    stats,
		xactName: xactName,
	}, nil
}

type client struct {
	host      *url.URL
	transport http.RoundTripper
	stats     statsd.Statter
	xactName  string
}

var _ Client = (*client)(nil)

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

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

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

func (c *client) resolveHTTPClient(ctx context.Context) (*http.Client, error) {
	rt, err := chitin.RoundTripper(ctx, c.transport)
	if err != nil {
		return nil, err
	}
	return &http.Client{
		Transport: rt,
	}, nil
}
