package httpclient

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

	"github.com/pingcap/dbaas/billing2/billing_errors"
	"github.com/pingcap/errors"
)

type AuthFunc func(req *http.Request) error
type HTTPClient interface {
	Request(method, apiPath string, query map[string]string, input, output interface{}) error
	RequestRaw(method, apiPath string, query map[string]string, input io.Reader) ([]byte, error)
	Get(apiPath string, query map[string]string, output interface{}) error
	GetByArray(apiPath string, query map[string]string, queryArray map[string][]string, output interface{}) error
}

type HTTPClientImpl struct {
	globalClient *http.Client
	AuthFunc     AuthFunc
	URLPrefix    string
	Host         string
}

func HTTPCall(method, url string, input io.Reader, authFunc AuthFunc, client *http.Client) ([]byte, error) {
	req, err := http.NewRequest(method, url, input)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	if authFunc != nil {
		err = authFunc(req)
		if err != nil {
			return nil, err
		}
	}

	resp, err := client.Do(req)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	defer resp.Body.Close() // nolint: errcheck

	bodyBytes, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, billing_errors.NewSystemError(err)
	}

	if resp.StatusCode >= 400 {
		return nil, billing_errors.NewSystemError(fmt.Errorf("http client resp code err %d, resp: %s", resp.StatusCode, string(bodyBytes)))
	}
	return bodyBytes, nil
}

func (httpClient *HTTPClientImpl) RequestRaw(method, apiPath string, query map[string]string, input io.Reader) ([]byte, error) {
	values := &url.Values{}

	host := strings.TrimRight(httpClient.Host, "/")
	path := fmt.Sprintf("%s/%s", strings.Trim(httpClient.URLPrefix, "/"), strings.TrimLeft(apiPath, "/"))
	url := host + "/" + strings.TrimLeft(path, "/")
	if query != nil {
		for k, v := range query {
			values.Set(k, v)
		}
		rawQuery := values.Encode()
		url = url + "?" + rawQuery
	}
	return HTTPCall(method, url, input, httpClient.AuthFunc, httpClient.globalClient)
}

func (httpClient *HTTPClientImpl) Request(method, apiPath string, query map[string]string, input, output interface{}) error {
	var inputMarshalled io.Reader
	if input == nil {
		inputMarshalled = nil
	} else {
		inputBytes, err := json.Marshal(input)
		if err != nil {
			return errors.Trace(err)
		}
		inputMarshalled = bytes.NewReader(inputBytes)
	}

	bodyBytes, err := httpClient.RequestRaw(method, apiPath, query, inputMarshalled)
	if err != nil {
		return err
	}
	if output != nil {
		err = json.Unmarshal(bodyBytes, output)
		if err != nil {
			return billing_errors.NewSystemError(err)
		}
	}
	return nil
}

func (httpClient *HTTPClientImpl) Get(apiPath string, query map[string]string, output interface{}) error {
	return httpClient.Request("GET", apiPath, query, nil, output)
}

func NewClient(authFunc AuthFunc, host, prefix string) HTTPClient {
	return &HTTPClientImpl{
		globalClient: &http.Client{
			Timeout: 60 * time.Second,
		},
		Host:      host,
		AuthFunc:  authFunc,
		URLPrefix: prefix,
	}
}

func (httpClient *HTTPClientImpl) GetByArray(apiPath string, query map[string]string, queryArray map[string][]string, output interface{}) error {
	return httpClient.RequestByArray("GET", apiPath, query, queryArray, nil, output)
}

func (httpClient *HTTPClientImpl) RequestByArray(method, apiPath string, query map[string]string, queryArray map[string][]string, input, output interface{}) error {
	var inputMarshalled io.Reader
	if input == nil {
		inputMarshalled = nil
	} else {
		inputBytes, err := json.Marshal(input)
		if err != nil {
			return errors.Trace(err)
		}
		inputMarshalled = bytes.NewReader(inputBytes)
	}

	bodyBytes, err := httpClient.RequestRawByArray(method, apiPath, query, queryArray, inputMarshalled)
	if err != nil {
		return err
	}
	if output != nil {
		err = json.Unmarshal(bodyBytes, output)
		if err != nil {
			return billing_errors.NewSystemError(err)
		}
	}
	return nil
}

func (httpClient *HTTPClientImpl) RequestRawByArray(method, apiPath string, query map[string]string, queryArray map[string][]string, input io.Reader) ([]byte, error) {
	values := &url.Values{}

	host := strings.TrimRight(httpClient.Host, "/")
	path := fmt.Sprintf("%s/%s", strings.Trim(httpClient.URLPrefix, "/"), strings.TrimLeft(apiPath, "/"))
	url := host + "/" + strings.TrimLeft(path, "/")
	for k, v := range query {
		values.Set(k, v)
	}
	for k, v := range queryArray {
		for _, value := range v {
			values.Add(k, value)
		}
	}

	if len(*values) > 0 {
		rawQuery := values.Encode()
		url = url + "?" + rawQuery
	}
	return HTTPCall(method, url, input, httpClient.AuthFunc, httpClient.globalClient)
}
