package request

import (
	"bytes"
	"context"
	"encoding/json"
	"encoding/xml"
	"fmt"
	"io"
	"net/http"
	"net/url"
	"os"
	"path"
	"reflect"
	"regexp"
	"strings"
)

const (
	JSON = "application/json"
	XML  = "application/xml"

	plainTextType   = "text/plain; charset=utf-8"
	jsonContentType = "application/json"
	formContentType = "application/x-www-form-urlencoded"
)

var (
	jsonCheck = regexp.MustCompile(`(?i:(application|text)/(json|.*\+json|json\-.*)(;|$))`)
	xmlCheck  = regexp.MustCompile(`(?i:(application|text)/(xml|.*\+xml)(;|$))`)
)

type Request struct {
	context       context.Context
	method        string
	uri           string
	url           *url.URL
	body          any
	query         url.Values
	formData      url.Values
	header        http.Header
	contentType   string
	authorization Authorization
	client        *Client
	rawRequest    *http.Request
	rawResponse   *http.Response
}

func (r *Request) detectContentType(body interface{}) string {
	contentType := plainTextType
	kind := reflect.Indirect(reflect.ValueOf(body)).Type().Kind()
	switch kind {
	case reflect.Struct, reflect.Map:
		contentType = jsonContentType
	case reflect.String:
		contentType = plainTextType
	default:
		if b, ok := body.([]byte); ok {
			contentType = http.DetectContentType(b)
		} else if kind == reflect.Slice {
			contentType = jsonContentType
		}
	}
	return contentType
}

func (r *Request) readRequestBody(contentType string, body any) (reader io.Reader, err error) {
	var (
		ok  bool
		s   string
		buf []byte
	)
	kind := reflect.Indirect(reflect.ValueOf(body)).Type().Kind()
	if reader, ok = r.body.(io.Reader); ok {
		return reader, nil
	}
	if buf, ok = r.body.([]byte); ok {
		goto __end
	}
	if s, ok = r.body.(string); ok {
		buf = []byte(s)
		goto __end
	}
	if jsonCheck.MatchString(contentType) && (kind == reflect.Struct || kind == reflect.Map || kind == reflect.Slice) {
		buf, err = json.Marshal(r.body)
		goto __end
	}
	if xmlCheck.MatchString(contentType) && (kind == reflect.Struct) {
		buf, err = xml.Marshal(r.body)
		goto __end
	}
	err = fmt.Errorf("unmarshal content type %s", contentType)
__end:
	if err == nil {
		if len(buf) > 0 {
			return bytes.NewReader(buf), nil
		}
	}
	return
}

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

func (r *Request) AddQuery(k, v string) *Request {
	r.query.Add(k, v)
	return r
}

func (r *Request) SetQuery(vs map[string]string) *Request {
	for k, v := range vs {
		r.query.Set(k, v)
	}
	return r
}

func (r *Request) AddFormData(k, v string) *Request {
	r.contentType = formContentType
	r.formData.Add(k, v)
	return r
}

func (r *Request) SetFormData(vs map[string]string) *Request {
	r.contentType = formContentType
	for k, v := range vs {
		r.formData.Set(k, v)
	}
	return r
}

func (r *Request) SetBody(v any) *Request {
	r.body = v
	return r
}

func (r *Request) SetContentType(v string) *Request {
	r.contentType = v
	return r
}

func (r *Request) AddHeader(k, v string) *Request {
	r.header.Add(k, v)
	return r
}

func (r *Request) SetHeader(h http.Header) *Request {
	r.header = h
	return r
}

func (r *Request) Do() (res *http.Response, err error) {
	var s string
	s = r.formData.Encode()
	if len(s) > 0 {
		r.body = s
	}
	r.url.RawQuery = r.query.Encode()
	r.uri = r.url.String()
	return r.client.execute(r)
}

func (r *Request) Response(v any) (err error) {
	var (
		res         *http.Response
		buf         []byte
		contentType string
	)
	if res, err = r.Do(); err != nil {
		return
	}
	defer func() {
		_ = res.Body.Close()
	}()
	if res.StatusCode/100 != 2 {
		if buf, err = io.ReadAll(res.Body); err == nil && len(buf) > 0 {
			err = fmt.Errorf("http response %s(%d): %s", res.Status, res.StatusCode, string(buf))
		} else {
			err = fmt.Errorf("http response %d: %s", res.StatusCode, res.Status)
		}
		return
	}
	contentType = strings.ToLower(res.Header.Get("Content-Type"))
	extName := path.Ext(r.rawRequest.URL.String())
	if strings.Contains(contentType, JSON) || extName == ".json" {
		err = json.NewDecoder(res.Body).Decode(v)
	} else if strings.Contains(contentType, XML) || extName == ".xml" {
		err = xml.NewDecoder(res.Body).Decode(v)
	} else {
		err = fmt.Errorf("unsupported content type: %s", contentType)
	}
	return
}

func (r *Request) Download(s string) (err error) {
	var (
		fp  *os.File
		res *http.Response
	)
	if res, err = r.Do(); err != nil {
		return
	}
	defer func() {
		_ = res.Body.Close()
	}()
	if fp, err = os.OpenFile(s, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644); err != nil {
		return
	}
	defer func() {
		_ = fp.Close()
	}()
	_, err = io.Copy(fp, res.Body)
	return
}

func newRequest(method string, uri string, client *Client) *Request {
	var (
		err error
	)
	r := &Request{
		context:  context.Background(),
		method:   method,
		uri:      uri,
		header:   make(http.Header),
		formData: make(url.Values),
		client:   client,
	}
	if r.url, err = url.Parse(uri); err == nil {
		r.query = r.url.Query()
	} else {
		r.query = make(url.Values)
	}
	return r
}