package twitchclient

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"strings"
)

// JSONClient makes it easy and safe to do JSON requests to REST services.
type JSONClient struct {
	baseURL    string
	httpClient httpDoer
}

// httpDoer is the interface used by JSONClient to send HTTP requests.
// It is fulfilled by *(net/http).Client, which is what should be used most of the time.
type httpDoer interface {
	Do(req *http.Request) (*http.Response, error)
}

// NewJSONClient builds a new *JSONClient.
// The baseURL is the host and path to the API.
// The httpClient is normally the *http.Client returned by NewHTTPClient.
// Example:
//   httpCli := twitchclient.NewHTTPClient(clientConf)
//   jsonCLi := twitchclient.NewJSONClient("http://localhost:8000", httpCli)
func NewJSONClient(baseURL string, httpClient httpDoer) *JSONClient {
	return &JSONClient{
		baseURL:    strings.TrimRight(baseURL, "/"),
		httpClient: httpClient,
	}
}

// Pathf formats and sanitizes a relative path to be used on a JSONClient request. It does not include query params, just the path.
// Example: twitchclient.Pathf("foo/bar/%s/report", "an id") //=> "foo/bar/an%20id/report"
func Pathf(pathFmt string, params ...interface{}) string {
	// Escape params
	for i, param := range params {
		if paramStr, ok := param.(string); ok {
			params[i] = url.PathEscape(paramStr)
		}
	}
	return fmt.Sprintf(pathFmt, params...)
}

// Get makes a GET JSON request usgin the query struct as query parameters.
func (c *JSONClient) Get(ctx context.Context, path string, query url.Values, output interface{}) error {
	_, err := c.Do(ctx, JSONRequest{
		Method: http.MethodGet,
		Path:   path,
		Query:  query,
	}, output)
	return err
}

// Post makes a POST JSON request using the body struct as JSON body.
func (c *JSONClient) Post(ctx context.Context, path string, body interface{}, output interface{}) error {
	_, err := c.Do(ctx, JSONRequest{
		Method: http.MethodPost,
		Path:   path,
		Body:   body,
	}, output)
	return err
}

// Put makes a PUT JSON request using the body struct as JSON body.
func (c *JSONClient) Put(ctx context.Context, path string, body interface{}, output interface{}) error {
	_, err := c.Do(ctx, JSONRequest{
		Method: http.MethodPut,
		Path:   path,
		Body:   body,
	}, output)
	return err
}

// Delete makes a DELETE request to the given path with no request parameters and ignoring the response body.
func (c *JSONClient) Delete(ctx context.Context, path string) error {
	_, err := c.Do(ctx, JSONRequest{
		Method: http.MethodDelete,
		Path:   path,
	}, nil)
	return err
}

// JSONRequest represents the different types of JSON requests that can be done with the *JSONClient.
type JSONRequest struct {
	Method string      // e.g. "GET", "POST", "PUT", "DELETE".
	Path   string      // e.g. twitchclient.Pathf("/users/%s", userID).
	Query  url.Values  // will be url-encoded and added to the path.
	Body   interface{} // either a struct or a map[string]interface{}, will be serialized into JSON.
	Header http.Header // other request headers beside "Content-Type: application/json" (added by default).
}

// Do makes a request with a generic JSONRequest. It is useful when you need to set or read headers.
func (c *JSONClient) Do(ctx context.Context, jsonReq JSONRequest, output interface{}) (httpResp *http.Response, err error) {
	url := c.urlStr(jsonReq.Path, jsonReq.Query)
	reqBody, err := serializeJSON(jsonReq.Body)
	if err != nil {
		return nil, err
	}

	httpReq, err := http.NewRequest(jsonReq.Method, url, reqBody)
	if err != nil {
		return nil, err
	}
	httpReq = httpReq.WithContext(ctx)
	addJSONHeader(httpReq, jsonReq.Header)

	httpResp, err = c.httpClient.Do(httpReq)
	if err != nil {
		return nil, err
	}
	defer func() {
		e := httpResp.Body.Close()
		if e != nil {
			err = e
		}
	}()

	if httpResp.StatusCode >= 400 {
		return httpResp, ErrorFromFailedResponse(httpResp)
	}

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

	return httpResp, nil
}

func (c *JSONClient) urlStr(path string, query url.Values) string {
	sep := ""
	if path[0:1] != "/" {
		sep = "/"
	}
	url := c.baseURL + sep + path

	if len(query) > 0 {
		url += "?" + query.Encode()
	}

	return url
}

func serializeJSON(data interface{}) (io.Reader, error) {
	if data == nil {
		return nil, nil
	}

	jsonBytes, err := json.Marshal(data)
	if err != nil {
		return nil, err
	}

	return bytes.NewBuffer(jsonBytes), nil
}

func addJSONHeader(req *http.Request, header http.Header) {
	if header == nil {
		req.Header = http.Header{"Content-Type": {"application/json"}}
		return
	}

	if header.Get("Content-Type") == "" {
		header.Set("Content-Type", "application/json")
	}
	req.Header = header
}
