package main

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

// Client is the default http client instance used by the following methods.
var Client = http.DefaultClient

// RequestOption is a function which can be used to modify
// a request instance before Do.
type RequestOption func(*http.Request) error

// WithAccessToken sets the given "token" to the authorization request header.
func WithAccessToken(token []byte) RequestOption {
	bearer := "Bearer " + string(token)
	return func(req *http.Request) error {
		req.Header.Add("Authorization", bearer)
		return nil
	}
}

// WithContentType sets the content-type request header.
func WithContentType(cType string) RequestOption {
	return func(req *http.Request) error {
		req.Header.Set("Content-Type", cType)
		return nil
	}
}

// WithContentLength sets the content-length request header.
func WithContentLength(length int) RequestOption {
	return func(req *http.Request) error {
		req.Header.Set("Content-Length", strconv.Itoa(length))
		return nil
	}
}

// Do fires a request to the server.
func Do(method, url string, body io.Reader, opts ...RequestOption) (*http.Response, error) {
	req, err := http.NewRequest(method, url, body)
	if err != nil {
		return nil, err
	}

	for _, opt := range opts {
		if err = opt(req); err != nil {
			return nil, err
		}
	}

	return Client.Do(req)
}

// JSON fires a request with "v" as client json data.
func JSON(method, url string, v interface{}, opts ...RequestOption) (*http.Response, error) {
	buf := new(bytes.Buffer)
	err := json.NewEncoder(buf).Encode(v)
	if err != nil {
		return nil, err
	}

	opts = append(opts, WithContentType("application/json; charset=utf-8"))
	return Do(method, url, buf, opts...)
}

// Form fires a request with "formData" as client form data.
func Form(method, url string, formData url.Values, opts ...RequestOption) (*http.Response, error) {
	encoded := formData.Encode()
	body := strings.NewReader(encoded)

	opts = append([]RequestOption{
		WithContentType("application/x-www-form-urlencoded"),
		WithContentLength(len(encoded)),
	}, opts...)

	return Do(method, url, body, opts...)
}

// BindResponse binds a response body to the "dest" pointer and closes the body.
func BindResponse(resp *http.Response, dest interface{}) error {
	contentType := resp.Header.Get("Content-Type")
	if idx := strings.IndexRune(contentType, ';'); idx > 0 {
		contentType = contentType[0:idx]
	}

	switch contentType {
	case "application/json":
		defer resp.Body.Close()
		return json.NewDecoder(resp.Body).Decode(dest)
	default:
		return fmt.Errorf("unsupported content type: %s", contentType)
	}
}

// RawResponse simply returns the raw response body.
func RawResponse(resp *http.Response) ([]byte, error) {
	defer resp.Body.Close()

	return io.ReadAll(resp.Body)
}