package xarth

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

	"code.justin.tv/rhys/nursery/xarth/internal/trace"

	"golang.org/x/net/context"
)

var (
	baseTransport *http.Transport
)

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

// 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 cancelled, any in-progress http requests
// using the returned *http.Client will be cancelled.
func Client(ctx context.Context) *http.Client {
	t := &transport{
		parent: ctx,
		base:   baseTransport,
	}
	c := &http.Client{
		Transport: t,
	}
	return c
}

type goodRoundTripper interface {
	http.RoundTripper
	CancelRequest(*http.Request)
	CloseIdleConnections()
}

type transport struct {
	parent context.Context
	base   goodRoundTripper
}

func filterOutboundRequest(ctx context.Context, dst, src *http.Request) error {
	*dst = http.Request{
		// We scrub Method

		URL: src.URL,

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

		// We scrub Header

		// We replace Body

		ContentLength: src.ContentLength,

		TransferEncoding: src.TransferEncoding,

		// We may want to muck with Close to encourage connection churn.
		Close: src.Close,

		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 fmt.Errorf("bad method: %q", src.Method)
	case "GET", "POST", "PUT", "HEAD", "DELETE", "":
		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: instrument byte count, duration, etc while trying not to break
	// sendfile.
	dst.Body = src.Body

	return nil
}

func filterInboundResponse(ctx context.Context, dst, src *http.Response) {
	*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.
		Body: src.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,
	}

	// TODO: scrub Header
	dst.Header = make(http.Header, len(src.Header))
}

func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) {
	ctx := trace.NewSpan(t.parent)

	blessedReq := &http.Request{}
	err := filterOutboundRequest(ctx, blessedReq, req)
	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
	}()

	// TODO: test cancellations
	done := make(chan struct{})
	defer close(done)
	go func() {
		select {
		case <-done:
		case <-ctx.Done():
			t.base.CancelRequest(blessedReq)
		}
	}()

	// It's happening!
	resp, err := t.base.RoundTrip(blessedReq)
	if err != nil {
		return resp, err
	}

	blessedResp := &http.Response{}
	filterInboundResponse(ctx, blessedResp, resp)
	blessedResp.Request = req

	return blessedResp, nil
}

func (t *transport) CancelRequest(req *http.Request) {
	t.base.CancelRequest(req)
}

func (t *transport) CloseIdleConnections() {
	t.base.CloseIdleConnections()
}
