package util

import (
	"bytes"
	jsonlib "encoding/json"
	"fmt"
	"io"
	"net/http"
	urllib "net/url"
	"strconv"
	"time"

	"github.com/fatih/color"
	"github.com/pkg/errors"
)

const (
	defaultTimeout = 10 * time.Second
	requestIDKey   = "X-Request-ID"
)

func req2str(req http.Request) string {
	var content []byte
	if req.GetBody != nil {
		body, err := req.GetBody()
		if err != nil {
			return err.Error()
		}
		content, err = io.ReadAll(body)
		if err != nil {
			return err.Error()
		}
	}
	return fmt.Sprintf("%s %s %s", req.Method, req.URL.String(), string(content))
}

func genURL(prefix string, params map[string]string) string {
	base, err := urllib.Parse(prefix)
	if err != nil {
		panic(err)
	}

	// Query params
	query := base.Query()
	for key, val := range params {
		query.Add(key, val)
	}
	base.RawQuery = query.Encode()
	return base.String()
}

func json2body(json interface{}) *bytes.Buffer {
	if json == nil {
		return nil
	}
	var buffer = &bytes.Buffer{}
	encoder := jsonlib.NewEncoder(buffer)
	err := encoder.Encode(json)
	if err != nil {
		panic(err)
	}
	return buffer
}

func Req() *req {
	return &req{
		header: map[string]string{},
	}
}

type req struct {
	client *http.Client
	method string
	header map[string]string
	url    string
	body   io.Reader
	into   interface{}
}

func (r *req) Host(host string) *req {
	r.url = host
	return r
}

func (r *req) RequestID(id string) *req {
	r.header[requestIDKey] = id
	return r
}

func (r *req) Client(client *http.Client) *req {
	r.client = client
	return r
}

func (r *req) Url(url string) *req {
	r.url = url
	return r
}

func (r *req) Path(path string) *req {
	r.url = r.url + "/" + path
	return r
}

func (r *req) Json(b interface{}) *req {
	r.body = json2body(b)
	r.header["Content-Type"] = "application/json"
	return r
}

func (r *req) DryRun(dryRun bool) *req {
	return r.Param(map[string]string{"dryrun": strconv.FormatBool(dryRun)})
}

func (r *req) Param(p map[string]string) *req {
	r.url = genURL(r.url, p)
	return r
}

func (r *req) Header(key, val string) *req {
	r.header[key] = val
	return r
}

func (r *req) Bearer(token string) *req {
	return r.Header("Authorization", "Bearer "+token)
}

func (r *req) Into(result interface{}) *req {
	r.into = result
	return r
}

func (r req) Build() (*http.Request, error) {
	req, err := http.NewRequest(r.method, r.url, r.body)
	if err != nil {
		return nil, err
	}
	for key, val := range r.header {
		req.Header.Set(key, val)
	}
	return req, nil
}

func (r *req) doRequest(method string) (*http.Response, error) {
	r.method = method
	if _, requestIDExist := r.header[requestIDKey]; !requestIDExist {
		r = r.RequestID("gen-" + RandomString(16))
	}
	req, err := r.Build()
	if err != nil {
		return nil, err
	}

	LogDebugf("%s: %s", color.GreenString("request-ID %s", r.header[requestIDKey]), req2str(*req))
	if r.client == nil {
		r.client = &http.Client{
			Timeout: defaultTimeout,
		}
	}

	resp, err := r.client.Do(req)
	return resp, errors.WithMessagef(err, "requestID: %s", r.header[requestIDKey])
}

func (r *req) requestInternal(method string) (*http.Response, error) {
	resp, err := r.doRequest(method)
	if err != nil {
		return nil, err
	}
	if r.into != nil {
		err := decodeBody(resp, r.into)
		if err != nil {
			return resp, err
		}
		return resp, nil
	} else {
		return resp, nil
	}
}

func (r *req) Request(method string, result ...interface{}) (*http.Response, error) {
	if len(result) == 1 {
		r.Into(result[0])
	} else if len(result) > 1 {
		panic("undefined multiple request result param")
	}
	return r.requestInternal(method)
}

func decodeBody(resp *http.Response, rt interface{}) error {
	if resp.StatusCode != 200 {
		content, err := io.ReadAll(resp.Body)
		if err != nil {
			return err
		}
		LogDebugf("http fail: %s", content)
		return HttpError{Code: resp.StatusCode, Url: resp.Request.URL.String(), Content: string(content)}
	}
	decoder := jsonlib.NewDecoder(resp.Body)
	return decoder.Decode(rt)
}

func (r *req) Post(result ...interface{}) (*http.Response, error) {
	return r.Request(http.MethodPost, result...)
}

func (r *req) Get(result ...interface{}) (*http.Response, error) {
	return r.Request(http.MethodGet, result...)
}

func (r *req) Put(result ...interface{}) (*http.Response, error) {
	return r.Request(http.MethodPut, result...)
}

func (r *req) Delete(result ...interface{}) (*http.Response, error) {
	return r.Request(http.MethodDelete, result...)
}
