package util

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

	"github.com/gorilla/schema"
)

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

var defaultHttpClient = &http.Client{
	Timeout: defaultTimeout,
}

var encoder = schema.NewEncoder()

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, vals := range params {
		for _, val := range vals {
			query.Add(key, val)
		}
	}
	base.RawQuery = query.Encode()
	return base.String()
}

func NewRequest() *Request {
	return &Request{
		header: map[string]string{},
	}
}

type Request struct {
	client *http.Client
	method string
	header map[string]string
	url    urllib.URL
	body   io.Reader
	into   interface{}
	err    error
	ctx    context.Context
}

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

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

func (r *Request) Url(url string) *Request {
	parsed, err := urllib.Parse(url)
	if err != nil {
		r.err = err
		return r
	}
	r.url = *parsed
	return r
}

func (r *Request) GetError() error {
	return r.err
}

func (r *Request) WithContext(ctx context.Context) *Request {
	r.ctx = ctx
	return r
}

func (r *Request) AppendPath(path string) *Request {
	prefix := r.url.Path
	if !strings.HasSuffix(prefix, "/") {
		prefix += "/"
	}
	r.url.Path = prefix + path
	return r
}

func (r *Request) Json(json interface{}) *Request {
	if json == nil {
		return nil
	}
	var buffer = &bytes.Buffer{}
	encoder := jsonlib.NewEncoder(buffer)
	err := encoder.Encode(json)
	if err != nil {
		r.err = err
		return r
	}
	r.body = buffer
	r.header["Content-Type"] = "application/json"
	return r
}

func (r *Request) SchemaQuery(b interface{}) *Request {
	p := urllib.Values{}
	err := encoder.Encode(b, p)
	if err != nil {
		r.err = err
		return r
	}
	r.AppendQueries(p)
	return r
}

func (r *Request) AppendQueries(p urllib.Values) *Request {
	query := r.url.Query()
	for key, vals := range p {
		for _, val := range vals {
			query.Add(key, val)
		}
	}
	r.url.RawQuery = query.Encode()
	return r
}

func (r *Request) AppendQuery(key, val string) *Request {
	query := r.url.Query()
	query.Add(key, val)
	r.url.RawQuery = query.Encode()
	return r
}

func (r *Request) AppendQueryIfVal(key, val string) *Request {
	if val == "" {
		return r
	}
	return r.AppendQuery(key, val)
}

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

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

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

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

func (r *Request) doRequest() (*http.Response, error) {
	req, err := r.Build()
	if err != nil {
		return nil, err
	}

	if r.client == nil {
		r.client = defaultHttpClient
	}

	resp, err := r.client.Do(req)
	return resp, err
}

func (r *Request) Do() (*http.Response, error) {
	if r.err != nil {
		return nil, r.err
	}
	resp, err := r.doRequest()
	if err != nil {
		return nil, err
	}
	if r.into != nil {
		defer resp.Body.Close()
		err := decodeBody(resp, r.into)
		if err != nil {
			return resp, err
		}
		return resp, nil
	} else {
		return resp, nil
	}
}

func decodeBody(resp *http.Response, rt interface{}) error {
	if resp.StatusCode != 200 {
		return fmt.Errorf("bad http code: %d", resp.StatusCode)
	}
	decoder := jsonlib.NewDecoder(resp.Body)
	decoder.DisallowUnknownFields()
	return decoder.Decode(rt)
}

func (r *Request) Method(method string) *Request {
	r.method = method
	return r
}

func (r *Request) Post() *Request {
	r.method = http.MethodPost
	return r
}

func (r *Request) Get() *Request {
	r.method = http.MethodGet
	return r
}

func (r *Request) Put() *Request {
	r.method = http.MethodPut
	return r
}

func (r *Request) Delete() *Request {
	r.method = http.MethodDelete
	return r
}
