package http_wrapper

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

// N.B. Can be removed after feat-fi-central-o11y merged into master

// DefaultClient use 3 seconds as the default request timeout.
var DefaultClient, _ = New(TimeoutOption(1000 * time.Millisecond))

type Client struct {
	*http.Client
}

func New(opts ...Option) (*Client, error) {
	cli := Client{Client: &http.Client{Transport: http.DefaultTransport}}
	for _, opt := range opts {
		if err := opt(&cli); err != nil {
			return nil, err
		}
	}
	return &cli, nil
}

func (r Client) DoRequestWithForm(
	ctx context.Context, method, uri string, data map[string][]string,
) (resp *http.Response, err error) {
	msg := url.Values(data).Encode()
	if method == "GET" || method == "HEAD" || method == "DELETE" {
		if strings.ContainsRune(uri, '?') {
			uri += "&"
		} else {
			uri += "?"
		}
		return r.DoRequest(ctx, method, uri+msg)
	}
	return r.DoRequestWith(
		ctx, method, uri, "application/x-www-form-urlencoded", strings.NewReader(msg), int64(len(msg)))
}

func (r Client) DoRequestWithFormHeader(
	ctx context.Context, method, uri string, data map[string][]string, h map[string]string,
) (resp *http.Response, err error) {
	msg := url.Values(data).Encode()
	if method == "GET" || method == "HEAD" || method == "DELETE" {
		if strings.ContainsRune(uri, '?') {
			uri += "&"
		} else {
			uri += "?"
		}
		return r.DoRequestWithHeader(ctx, method, uri+msg, "application/x-www-form-urlencoded", nil, 0, h)
	}
	return r.DoRequestWithHeader(
		ctx, method, uri, "application/x-www-form-urlencoded", strings.NewReader(msg), int64(len(msg)), h)
}

func (r Client) DoRequestWithJSON(
	ctx context.Context, method, uri string, data any,
) (resp *http.Response, err error) {
	msg, err := json.Marshal(data)
	if err != nil {
		return
	}
	return r.DoRequestWith(
		ctx, method, uri, "application/json", bytes.NewReader(msg), int64(len(msg)))
}

func (r Client) DoRequestWithJSONHeader(
	ctx context.Context, method, uri string, data any, h map[string]string,
) (resp *http.Response, err error) {
	msg, err := json.Marshal(data)
	if err != nil {
		return
	}
	return r.DoRequestWithHeader(
		ctx, method, uri, "application/json", bytes.NewReader(msg), int64(len(msg)), h)
}

func (r Client) DoRequest(ctx context.Context, method, uri string) (resp *http.Response, err error) {
	req, err := http.NewRequest(method, uri, nil)
	if err != nil {
		return
	}
	return r.Do(ctx, req)
}

func (r Client) DoRequestWithHeader(
	ctx context.Context, method, uri string,
	bodyType string, body io.Reader, bodyLength int64, m map[string]string,
) (resp *http.Response, err error) {
	req, err := http.NewRequest(method, uri, body)
	if err != nil {
		return
	}
	req.Header.Set("Content-Type", bodyType)
	for key, value := range m {
		req.Header.Set(key, value)
	}
	req.ContentLength = bodyLength
	return r.Do(ctx, req)
}

func (r Client) DoRequestWith(
	ctx context.Context, method, uri string,
	bodyType string, body io.Reader, bodyLength int64,
) (resp *http.Response, err error) {
	req, err := http.NewRequest(method, uri, body)
	if err != nil {
		return
	}
	req.Header.Set("Content-Type", bodyType)
	req.ContentLength = bodyLength
	return r.Do(ctx, req)
}

func (r Client) Do(ctx context.Context, req *http.Request) (resp *http.Response, err error) {
	// ineffectual assignment to ctx
	// if ctx == nil {
	// 	ctx = context.Background()
	// }

	return r.Client.Do(req)
}

func (r Client) CallWithForm(
	ctx context.Context, ret any, method, url1 string, param map[string][]string,
) (err error) {
	resp, err := r.DoRequestWithForm(ctx, method, url1, param)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return r.callRet(ctx, ret, resp)
}

func (r Client) CallWithFormHeader(
	ctx context.Context, ret any, method, url1 string, param map[string][]string, h map[string]string,
) (err error) {
	resp, err := r.DoRequestWithFormHeader(ctx, method, url1, param, h)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return r.callRet(ctx, ret, resp)
}

func (r Client) CallWithJSON(
	ctx context.Context, ret any, method, url1 string, param any,
) (err error) {
	resp, err := r.DoRequestWithJSON(ctx, method, url1, param)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return r.callRet(ctx, ret, resp)
}

func (r Client) CallWithJSONHeader(
	ctx context.Context, ret any, method, url1 string, param any, h map[string]string,
) (err error) {
	resp, err := r.DoRequestWithJSONHeader(ctx, method, url1, param, h)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	return r.callRet(ctx, ret, resp)
}

func (r Client) callRet(_ context.Context, ret any, resp *http.Response) (err error) {
	defer func() {
		_, _ = io.Copy(io.Discard, resp.Body)
		_ = resp.Body.Close() // must close http body
	}()

	// check status code before process resp.Body.
	// the assumption is status code larger than 300 is abnormal.
	if resp.StatusCode >= 300 {
		return fmt.Errorf("got abnormal status code: %d", resp.StatusCode)
	}

	if ret == nil {
		return
	}
	return json.NewDecoder(resp.Body).Decode(ret)
}
