package httpc

import (
	"errors"
	"io"
	"net/http"
	"net/url"
	"path"

	"github.com/influxdata/influxdb/v2"
)

type (
	// WriteCloserFn is a write closer wrapper than indicates the type of writer by
	// returning the header and header value associated with the writer closer.
	//	i.e. GZIP writer returns header Content-Encoding with value gzip alongside
	//	the writer.
	WriteCloserFn func(closer io.WriteCloser) (string, string, io.WriteCloser)

	// doer provides an abstraction around the actual http client behavior. The doer
	// can be faked out in tests or another http client provided in its place.
	doer interface {
		Do(*http.Request) (*http.Response, error)
	}
)

// Client is a basic http client that can make cReqs with out having to juggle
// the token and so forth. It provides sane defaults for checking response
// statuses, sets auth token when provided, and sets the content type to
// application/json for each request. The token, response checker, and
// content type can be overridden on the Req as well.
type Client struct {
	addr           url.URL
	doer           doer
	defaultHeaders http.Header

	writerFns []WriteCloserFn

	authFn   func(*http.Request)
	respFn   func(*http.Response) error
	statusFn func(*http.Response) error
}

// New creates a new httpc client.
func New(opts ...ClientOptFn) (*Client, error) {
	opt := clientOpt{
		authFn: func(*http.Request) {},
	}
	for _, o := range opts {
		if err := o(&opt); err != nil {
			return nil, err
		}
	}

	if opt.addr == "" {
		return nil, errors.New("must provide a non empty host address")
	}

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

	if opt.doer == nil {
		opt.doer = defaultHTTPClient(u.Scheme, opt.insecureSkipVerify)
	}

	return &Client{
		addr:           *u,
		doer:           opt.doer,
		defaultHeaders: opt.headers,
		authFn:         opt.authFn,
		statusFn:       opt.statusFn,
		writerFns:      opt.writerFns,
	}, nil
}

// Delete generates a DELETE request.
func (c *Client) Delete(urlPath ...string) *Req {
	return c.Req(http.MethodDelete, nil, urlPath...)
}

// Get generates a GET request.
func (c *Client) Get(urlPath ...string) *Req {
	return c.Req(http.MethodGet, nil, urlPath...)
}

// Patch generates a PATCH request.
func (c *Client) Patch(bFn BodyFn, urlPath ...string) *Req {
	return c.Req(http.MethodPatch, bFn, urlPath...)
}

// PatchJSON generates a PATCH request. This is to be used with value or pointer to value type.
// Providing a stream/reader will result in disappointment.
func (c *Client) PatchJSON(v interface{}, urlPath ...string) *Req {
	return c.Patch(BodyJSON(v), urlPath...)
}

// Post generates a POST request.
func (c *Client) Post(bFn BodyFn, urlPath ...string) *Req {
	return c.Req(http.MethodPost, bFn, urlPath...)
}

// PostJSON generates a POST request and json encodes the body. This is to be
// used with value or pointer to value type. Providing a stream/reader will result
// in disappointment.
func (c *Client) PostJSON(v interface{}, urlPath ...string) *Req {
	return c.Post(BodyJSON(v), urlPath...)
}

// Put generates a PUT request.
func (c *Client) Put(bFn BodyFn, urlPath ...string) *Req {
	return c.Req(http.MethodPut, bFn, urlPath...)
}

// PutJSON generates a PUT request. This is to be used with value or pointer to value type.
// Providing a stream/reader will result in disappointment.
func (c *Client) PutJSON(v interface{}, urlPath ...string) *Req {
	return c.Put(BodyJSON(v), urlPath...)
}

// Req constructs a request.
func (c *Client) Req(method string, bFn BodyFn, urlPath ...string) *Req {
	bodyF := BodyEmpty
	if bFn != nil {
		bodyF = bFn
	}

	headers := make(http.Header, len(c.defaultHeaders))
	for header, vals := range c.defaultHeaders {
		for _, v := range vals {
			headers.Add(header, v)
		}
	}
	var buf nopBufCloser
	var w io.WriteCloser = &buf
	for _, writerFn := range c.writerFns {
		header, headerVal, ww := writerFn(w)
		w = ww
		headers.Add(header, headerVal)
	}

	header, headerVal, err := bodyF(w)
	if err != nil {
		// TODO(@jsteenb2): add a inspection for an OK() or Valid() method, then enforce
		//  that across all consumers? Same for all bodyFns for that matter.
		return &Req{
			err: &influxdb.Error{
				Code: influxdb.EInvalid,
				Err:  err,
			},
		}
	}
	if header != "" {
		headers.Set(header, headerVal)
	}
	// w.Close here is necessary since we have to close any gzip writer
	// or other writer that requires closing.
	if err := w.Close(); err != nil {
		return &Req{err: err}
	}

	var body io.Reader
	if buf.Len() > 0 {
		body = &buf
	}

	req, err := http.NewRequest(method, c.buildURL(urlPath...), body)
	if err != nil {
		return &Req{err: err}
	}

	cr := &Req{
		client:   c.doer,
		req:      req,
		authFn:   c.authFn,
		respFn:   c.respFn,
		statusFn: c.statusFn,
	}
	return cr.Headers(headers)
}

// Clone creates a new *Client type from an existing client. This may be
// useful if you want to have a shared base client, then take a specific
// client from that base and tack on some extra goodies like specific headers
// and whatever else that suits you.
// Note: a new net.http.Client type will not be created. It will share the existing
// http.Client from the parent httpc.Client. Same connection pool, different specifics.
func (c *Client) Clone(opts ...ClientOptFn) (*Client, error) {
	existingOpts := []ClientOptFn{
		WithAuth(c.authFn),
		withDoer(c.doer),
		WithRespFn(c.respFn),
		WithStatusFn(c.statusFn),
	}
	for h, vals := range c.defaultHeaders {
		for _, v := range vals {
			existingOpts = append(existingOpts, WithHeader(h, v))
		}
	}
	for _, fn := range c.writerFns {
		existingOpts = append(existingOpts, WithWriterFn(fn))
	}

	return New(append(existingOpts, opts...)...)
}

func (c *Client) buildURL(urlPath ...string) string {
	u := c.addr
	if len(urlPath) > 0 {
		u.Path = path.Join(u.Path, path.Join(urlPath...))
	}
	return u.String()
}
