package twitchclient

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"io/ioutil"
	"math/rand"
	"net/http"
	"net/url"
	"strings"
	"time"
)

func init() {
	rand.Seed(time.Now().UnixNano())
}

// NewClient is a constructor to make a new Client.
// Please avoid using this, we recommend using NewHTTPClient,
// that returns a standard *http.Client instead.
func NewClient(conf ClientConf) (Client, error) {
	hostURL, err := sanitizeHostURL(conf.Host)
	if err != nil {
		return nil, err
	}

	httpClient := NewHTTPClient(conf)

	return &clientImpl{
		hostURL:    hostURL,
		httpClient: httpClient,
	}, nil
}

// Client is almost like an *http.Client but with a few differences.
// Please use NewHTTPClient instead.
type Client interface {
	NewRequest(method string, path string, body io.Reader) (*http.Request, error)
	Do(context.Context, *http.Request, ReqOpts) (*http.Response, error)
	DoNoContent(context.Context, *http.Request, ReqOpts) (*http.Response, error)
	DoJSON(context.Context, interface{}, *http.Request, ReqOpts) (*http.Response, error)
}

type clientImpl struct {
	hostURL    *url.URL
	httpClient *http.Client
}

// NewRequest creates an *http.Request using the configured hostURL as the base for the path.
func (c *clientImpl) 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.hostURL.ResolveReference(u).String(), body)
}

// Do is similar to http.Client.Do, but accepts twitchclient.ReqOpts as parameter.
// The ctx must be the req.Context(), it is left for backwards compatibility (Go 1.7 didn't have req.Context()).
// If the ctx already has ReqOpts, the reqOpts passed will be merged, taking precedence
func (c *clientImpl) Do(ctx context.Context, req *http.Request, reqOpts ReqOpts) (*http.Response, error) {
	ctx = WithReqOpts(ctx, reqOpts)
	req = req.WithContext(ctx)
	return c.httpClient.Do(req)
}

// DoNoContent is meant to be used when the caller is not interested in reading the response body.
func (c *clientImpl) DoNoContent(ctx context.Context, req *http.Request, opts ReqOpts) (*http.Response, error) {
	resp, err := c.Do(ctx, req, opts)
	if err != nil {
		return nil, err
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	if resp.StatusCode >= 500 {
		return resp, plainErrorFrom5xx(resp)
	}
	if resp.StatusCode >= 400 {
		return resp, ErrorFromFailedResponse(resp)
	}

	return resp, nil
}

// DoJSON executes a request, then deserializes the response data.
func (c *clientImpl) DoJSON(ctx context.Context, data interface{}, req *http.Request, opts ReqOpts) (*http.Response, error) {
	resp, err := c.Do(ctx, req, opts)
	if err != nil {
		return nil, err
	}
	defer func() {
		_ = resp.Body.Close()
	}()

	if resp.StatusCode >= 500 {
		return resp, plainErrorFrom5xx(resp)
	}
	if resp.StatusCode >= 400 {
		return resp, ErrorFromFailedResponse(resp)
	}
	if resp.StatusCode == http.StatusNoContent {
		return resp, nil
	}

	// Decode response into data
	err = json.NewDecoder(resp.Body).Decode(data)
	if err != nil {
		return resp, fmt.Errorf("Unable to read response body: %s", err)
	}
	return resp, nil
}

// plainErrorFrom5xx returns an error using the body as plain-text for 5xx errors.
func plainErrorFrom5xx(resp *http.Response) error {
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return errors.New(resp.Status + ": unable to read response body for more error information")
	}
	return errors.New(resp.Status + ": " + string(body))
}

func sanitizeHostURL(host string) (*url.URL, error) {
	if host == "" {
		return nil, errors.New("Host cannot be blank")
	}
	if !strings.HasPrefix(host, "http") {
		host = fmt.Sprintf("http://%v", host)
	}
	hostURL, err := url.Parse(host)
	if err != nil {
		return nil, err
	}
	return hostURL, nil
}
