package chitin

import (
	"fmt"
	"io"
	"net"
	"net/http"
	"regexp"
	"runtime/pprof"
	"sync"
	"time"

	"code.justin.tv/common/chitin/internal/trace"
	"golang.org/x/net/context"
)

var (
	baseTransport *http.Transport

	httpClientProfile *pprof.Profile
)

func init() {
	baseTransport = &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		Dial: (&net.Dialer{
			Timeout:   30 * time.Second,
			KeepAlive: 30 * time.Second,

			DualStack: false,
		}).Dial,
		TLSHandshakeTimeout: 10 * time.Second,

		TLSClientConfig:       nil,
		DisableKeepAlives:     false,
		DisableCompression:    false,
		MaxIdleConnsPerHost:   0,
		ResponseHeaderTimeout: 0,
	}

	pkgname := "code.justin.tv/common/chitin"

	// The pprof tool expects profiles to match the following regexp:
	//
	//     `\A(\w+) profile: total \d+\n\z`
	//
	// This means that the profile name can only contain the characters [0-9A-
	// Za-z_], contrary to the runtime/pprof package docs, which say:
	//
	//     The convention is to use a 'import/path.' prefix to create separate
	//     name spaces for each package.
	//
	// See https://golang.org/issue/13195
	cmdPprofName := regexp.MustCompile("[^0-9A-Za-z_]")

	httpClientProfile = pprof.NewProfile(cmdPprofName.ReplaceAllString(pkgname, "_") + "_httpclient")
}

// Client returns a Context-enhanced *http.Client.  Outbound requests will
// include the headers needed to follow user requests via Trace as they
// traverse binaries and hosts.  The Trace transaction id will be included in
// the outbound headers, and a new Trace span id will be allocated and
// included in the outbound headers as well.
//
// When the Context times out or is canceled, any in-progress http requests
// using the returned *http.Client will be canceled.
//
// If called with context.Background, the resulting http.Client will use the
// Context value attached to the http.Request (in Go 1.7 and later), allowing
// the http.Client to be reused for multiple independent requests.
func Client(ctx context.Context) *http.Client {
	t, err := RoundTripper(ctx, baseTransport)
	if err != nil {
		panic(fmt.Errorf("RoundTripper must always accept a *http.Transport, and yet: %v", err))
	}

	c := &http.Client{
		Transport: t,
	}
	return c
}

// RoundTripper returns a Context-enhanced http.RoundTripper.  Outbound
// requests will include the headers needed to follow user requests via Trace
// as they traverse binaries and hosts.  The Trace transaction id will be
// included in the outbound headers, and a new Trace span id will be allocated
// and included in the outbound headers as well.
//
// When the Context times out or is canceled, any in-progress http requests
// using the returned http.RoundTripper will be canceled.
//
// If called with context.Background, the resulting http.RoundTripper will use
// the Context value attached to the http.Request (in Go 1.7 and later),
// allowing the http.RoundTripper to be reused for multiple independent
// requests.
func RoundTripper(ctx context.Context, rt http.RoundTripper) (http.RoundTripper, error) {
	if rt == nil {
		rt = baseTransport
	}

	t := &transport{
		parent: ctx,
		base:   rt,
	}
	return t, nil
}

type transport struct {
	parent context.Context
	base   http.RoundTripper
}

func filterOutboundRequest(ctx context.Context, src *http.Request, onClose func()) (*http.Request, error) {
	dst, ctx := requestWithContext(ctx, src)

	// Copy *net/http.Request fields:

	// We scrub Method

	dst.URL = src.URL

	// Proto, ProtoMajor, and ProtoMinor don't apply to outbound requests.

	// We scrub Header

	// We replace Body

	dst.ContentLength = src.ContentLength

	dst.TransferEncoding = src.TransferEncoding

	// We may want to muck with Close to encourage connection churn.
	dst.Close = src.Close

	dst.Host = src.Host

	// Form, PostForm, and MultipartForm are populated by user calls

	// We don't support Trailer

	// RemoteAddr is meaningless for outbound requests.

	// RequestURI must not be set for outbound requests.

	// TLS is ignored for outbound requests.

	switch src.Method {
	default:
		return nil, fmt.Errorf("bad method: %q", src.Method)
	case "GET", "POST", "PUT", "HEAD", "DELETE", "PATCH", "OPTIONS", "":
		dst.Method = src.Method
	}

	dst.Header = make(http.Header, len(src.Header))
	for k, v := range src.Header {
		k = http.CanonicalHeaderKey(k)
		switch {
		default:
			dst.Header[k] = v
		case reservedHeader(k):
			printf(ctx, "reserved-header=%q", k)
		}
	}

	// TODO: add a test for sendfile support
	dst.Body = &clientBody{
		rc:      src.Body,
		onClose: onClose,
	}

	return dst, nil
}

func filterInboundResponse(ctx context.Context, dst, src *http.Response, onClose func()) {
	*dst = http.Response{
		Status:     src.Status,
		StatusCode: src.StatusCode,
		Proto:      src.Proto,
		ProtoMajor: src.ProtoMajor,
		ProtoMinor: src.ProtoMinor,

		// We scrub Header

		// TODO: instrument byte count, duration, etc.
		// We replace Body

		ContentLength: src.ContentLength,

		TransferEncoding: src.TransferEncoding,

		Close: src.Close,

		// We don't support Trailer.

		// The Request must be set to the original user-provided request.

		TLS: src.TLS,
	}

	dst.Header = make(http.Header, len(src.Header))
	for k, v := range src.Header {
		k = http.CanonicalHeaderKey(k)
		switch {
		default:
			dst.Header[k] = v
		case reservedHeader(k):
			printf(ctx, "reserved-header=%q", k)
		}
	}

	dst.Body = &clientBody{
		rc:      src.Body,
		onClose: onClose,
	}
}

func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
	ctx := t.parent
	if ctx == context.Background() {
		ctx = contextOfRequest(req)
	}
	ctx = trace.NewSpan(ctx)
	ctx, cancel := context.WithCancel(ctx)
	cancelOnError := cancel
	defer func() {
		// Be sure to call the cancel func when RoundTrip returns with an
		// error. If the request has succeeded and we're returning the
		// response (with body) to the user, we'll overwrite the cancelOnError
		// function and make a note to call it later.
		cancelOnError()
	}()

	if req.Cancel != nil {
		go func() {
			select {
			case <-ctx.Done():
			case <-req.Cancel:
				cancel()
			}
		}()
	}

	// The context is ready. DO NOT add more values for internal use, some
	// consumers will miss them.

	blessedReq, err := filterOutboundRequest(ctx, req, func() {
		trace.SendRequestBodySent(ctx)
	})
	if err != nil {
		return nil, err
	}

	trace.AugmentHeader(ctx, blessedReq.Header)

	defer func() {
		// Mark the original body as consumed ... when the time is right.
		req.Body = blessedReq.Body
	}()

	pprofKey := new(byte) // This needs to have a unique address, so it must not be size zero
	httpClientProfile.Add(pprofKey, 1)
	// It's happening!
	trace.SendRequestHeadPrepared(ctx, blessedReq)
	resp, err := t.base.RoundTrip(blessedReq)
	trace.SendResponseHeadReceived(ctx, resp, err)
	if err != nil {
		httpClientProfile.Remove(pprofKey)
		return resp, err
	}

	blessedResp := &http.Response{}
	filterInboundResponse(ctx, blessedResp, resp, func() {
		trace.SendResponseBodyReceived(ctx)
		httpClientProfile.Remove(pprofKey)
		cancel()
	})
	blessedResp.Request = req
	cancelOnError = func() {}

	return blessedResp, nil
}

// CancelRequest attempts to cancel and in-flight request by calling the
// CancelRequest method of the underlying net/http.RoundTripper.
//
// Deprecated: Use net/http.Request.WithContext to attach a context to the
// request, which will allow the request to be canceled.
func (t *transport) CancelRequest(req *http.Request) {
	type i interface {
		CancelRequest(*http.Request)
	}
	var _ i = (*http.Transport)(nil)

	if rt, ok := t.base.(i); ok {
		rt.CancelRequest(req)
	}
}

func (t *transport) CloseIdleConnections() {
	type i interface {
		CloseIdleConnections()
	}
	var _ i = (*http.Transport)(nil)

	if rt, ok := t.base.(i); ok {
		rt.CloseIdleConnections()
	}
}

type clientBody struct {
	rc io.ReadCloser

	onCloseMu sync.Mutex
	onClose   func()
}

func (b *clientBody) Read(p []byte) (int, error) {
	var n int
	var err error = io.EOF
	if b.rc != nil {
		n, err = b.rc.Read(p)
	}
	if err == io.EOF {
		b.onCloseMu.Lock()
		defer b.onCloseMu.Unlock()
		if b.onClose != nil {
			// trigger only once
			onClose := b.onClose
			b.onClose = nil
			// defer here to match the Close method
			defer onClose()
		}
	}
	return n, err
}

func (b *clientBody) Close() error {
	b.onCloseMu.Lock()
	defer b.onCloseMu.Unlock()
	if b.onClose != nil {
		// trigger only once
		onClose := b.onClose
		b.onClose = nil
		// trigger after we've signalled the underlying io.Closer
		defer onClose()
	}
	if b.rc == nil {
		return nil
	}
	err := b.rc.Close()
	return err
}
