package http

import (
	"crypto/tls"
	"net"
	"net/http"
	"net/url"
	"time"

	"github.com/influxdata/influxdb/v2/dbrp"
	"github.com/influxdata/influxdb/v2/kit/tracing"
	"github.com/influxdata/influxdb/v2/pkg/httpc"
)

// NewHTTPClient creates a new httpc.Client type. This call sets all
// the options that are important to the http pkg on the httpc client.
// The default status fn and so forth will all be set for the caller.
// In addition, some options can be specified. Those will be added to the defaults.
func NewHTTPClient(addr, token string, insecureSkipVerify bool, opts ...httpc.ClientOptFn) (*httpc.Client, error) {
	u, err := url.Parse(addr)
	if err != nil {
		return nil, err
	}

	defaultOpts := []httpc.ClientOptFn{
		httpc.WithAddr(addr),
		httpc.WithContentType("application/json"),
		httpc.WithHTTPClient(NewClient(u.Scheme, insecureSkipVerify)),
		httpc.WithInsecureSkipVerify(insecureSkipVerify),
		httpc.WithStatusFn(CheckError),
	}
	if token != "" {
		defaultOpts = append(defaultOpts, httpc.WithAuthToken(token))
	}
	opts = append(defaultOpts, opts...)
	return httpc.New(opts...)
}

// Service connects to an InfluxDB via HTTP.
type Service struct {
	Addr               string
	Token              string
	InsecureSkipVerify bool

	*TaskService
	*NotificationRuleService
	*VariableService
	*WriteService
	*CheckService
	*NotificationEndpointService
	*TelegrafService
	*LabelService
	DBRPMappingService *dbrp.Client
}

// NewService returns a service that is an HTTP client to a remote.
// Address and token are needed for those services that do not use httpc.Client,
// but use those for configuring.
// Usually one would do:
//
// ```
// c := NewHTTPClient(addr, token, insecureSkipVerify)
// s := NewService(c, addr token)
// ```
//
// So one should provide the same `addr` and `token` to both calls to ensure consistency
// in the behavior of the returned service.
func NewService(httpClient *httpc.Client, addr, token string) (*Service, error) {
	return &Service{
		Addr:                    addr,
		Token:                   token,
		TaskService:             &TaskService{Client: httpClient},
		NotificationRuleService: &NotificationRuleService{Client: httpClient},
		VariableService:         &VariableService{Client: httpClient},
		WriteService: &WriteService{
			Addr:  addr,
			Token: token,
		},
		CheckService:                &CheckService{Client: httpClient},
		NotificationEndpointService: &NotificationEndpointService{Client: httpClient},
		TelegrafService:             NewTelegrafService(httpClient),
		LabelService:                &LabelService{Client: httpClient},
		DBRPMappingService:          dbrp.NewClient(httpClient),
	}, nil
}

// NewURL concats addr and path.
func NewURL(addr, path string) (*url.URL, error) {
	u, err := url.Parse(addr)
	if err != nil {
		return nil, err
	}
	u.Path = path
	return u, nil
}

// NewClient returns an http.Client that pools connections and injects a span.
func NewClient(scheme string, insecure bool) *http.Client {
	return httpClient(scheme, insecure)
}

// SpanTransport injects the http.RoundTripper.RoundTrip() request
// with a span.
type SpanTransport struct {
	base http.RoundTripper
}

// RoundTrip implements the http.RoundTripper, intercepting the base
// round trippers call and injecting a span.
func (s *SpanTransport) RoundTrip(r *http.Request) (*http.Response, error) {
	span, _ := tracing.StartSpanFromContext(r.Context())
	defer span.Finish()
	tracing.InjectToHTTPRequest(span, r)
	return s.base.RoundTrip(r)
}

// DefaultTransport wraps http.DefaultTransport in SpanTransport to inject
// tracing headers into all outgoing requests.
var DefaultTransport http.RoundTripper = &SpanTransport{base: http.DefaultTransport}

// DefaultTransportInsecure is identical to DefaultTransport, with
// the exception that tls.Config is configured with InsecureSkipVerify
// set to true.
var DefaultTransportInsecure http.RoundTripper = &SpanTransport{
	base: &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		DialContext: (&net.Dialer{
			Timeout:   30 * time.Second,
			KeepAlive: 30 * time.Second,
			DualStack: true,
		}).DialContext,
		ForceAttemptHTTP2:     true,
		MaxIdleConns:          100,
		IdleConnTimeout:       90 * time.Second,
		TLSHandshakeTimeout:   10 * time.Second,
		ExpectContinueTimeout: 1 * time.Second,
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true,
		},
	},
}

func httpClient(scheme string, insecure bool) *http.Client {
	if scheme == "https" && insecure {
		return &http.Client{Transport: DefaultTransportInsecure}
	}
	return &http.Client{Transport: DefaultTransport}
}
