package request

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

	"github.com/google/uuid"
)

type Client struct {
	scheme string
	host   string
	client *http.Client
	tr     *http.Transport
	debug  bool
}

func NewClient(scheme string, host string, timeout time.Duration, opts ...ReqOpt) *Client {
	req := &Client{
		scheme: scheme,
		host:   host,
		client: &http.Client{
			Timeout: timeout,
		},
		debug: false,
	}

	for _, opt := range opts {
		opt(req)
	}

	if req.tr != nil {
		req.client.Transport = req.tr
	}

	return req
}

func (c *Client) SetDebug(debug bool) {
	c.debug = debug
}

func (c *Client) SetTransport(tr *http.Transport) {
	c.client.Transport = tr
}

func sendRequest[T any](c *Client, method, path string, opts ...Opt) (*T, error) {
	u := url.URL{
		Scheme: c.scheme,
		Host:   c.host,
		Path:   path,
	}
	ctx := &Ctx{}
	rid := uuid.NewString()

	for _, opt := range opts {
		opt(ctx)
	}

	if len(ctx.query) > 0 {
		values := u.Query()
		for k, v := range ctx.query {
			values.Add(k, v)
		}
		u.RawQuery = values.Encode()
	}

	if c.debug {
		log.Printf("[REQ:%s] url: %s", rid, u.String())
	}

	var body io.Reader
	var writer *multipart.Writer
	if ctx.body != nil {
		bs, err := json.Marshal(ctx.body)
		if err != nil {
			return nil, err
		}
		switch ctx.contentType {
		case "multipart/form-data":
			m := make(map[string]string)
			if err := json.Unmarshal(bs, &m); err != nil {
				return nil, err
			}
			buf := &bytes.Buffer{}
			writer = multipart.NewWriter(buf)
			for k, v := range m {
				if err := writer.WriteField(k, v); err != nil {
					return nil, err
				}
			}
			writer.Close()
			body = buf
		case "application/x-www-form-urlencoded":
			m := make(map[string]string)
			if err := json.Unmarshal(bs, &m); err != nil {
				return nil, err
			}
			data := url.Values{}
			for k, v := range m {
				data.Add(k, v)
			}
			body = strings.NewReader(data.Encode())
		default:
			body = bytes.NewBuffer(bs)
		}

		if c.debug {
			buf := &bytes.Buffer{}
			json.Indent(buf, bs, "", "  ")
			log.Printf("[REQ:%s] body: %s", rid, buf.String())
		}
	}
	req, err := http.NewRequest(method, u.String(), body)
	if err != nil {
		return nil, err
	}
	for k, v := range ctx.header {
		req.Header.Add(k, v)
	}
	switch ctx.contentType {
	case "multipart/form-data":
		req.Header.Set("Content-Type", writer.FormDataContentType())
	case "application/x-www-form-urlencoded":
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	default:
		req.Header.Set("Content-Type", "application/json")
	}
	if c.debug {
		log.Printf("[REQ:%s] headers: %+v", rid, req.Header)
	}

	resp, err := c.client.Do(req)
	if err != nil {
		return nil, err
	}
	defer resp.Body.Close()

	b, err := io.ReadAll(resp.Body)
	if err != nil {
		return nil, err
	}
	if c.debug {
		buf := &bytes.Buffer{}
		if err := json.Indent(buf, b, "", "  "); err != nil {
			log.Printf("[REQ:%s] resp: %s", rid, string(b))
		} else {
			log.Printf("[REQ:%s] resp: %s", rid, buf.String())
		}
	}

	if resp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("status code: %d", resp.StatusCode)
	}

	var rr T
	if err := json.Unmarshal(b, &rr); err != nil {
		return nil, err
	}
	return &rr, nil
}

func Get[T any](c *Client, path string, opts ...Opt) (*T, error) {
	return sendRequest[T](c, http.MethodGet, path, opts...)
}

func Post[T any](c *Client, path string, body any, opts ...Opt) (*T, error) {
	opts = append(opts, WithBody(body))
	return sendRequest[T](c, http.MethodPost, path, opts...)
}

func Put[T any](c *Client, path string, body any, opts ...Opt) (*T, error) {
	opts = append(opts, WithBody(body))
	return sendRequest[T](c, http.MethodPut, path, opts...)
}

func Delete[T any](c *Client, path string, opts ...Opt) (*T, error) {
	return sendRequest[T](c, http.MethodDelete, path, opts...)
}

func GetHeaderMap(header string) map[string]string {
	headerMap := make(map[string]string)
	for _, h := range strings.Split(header, "\n") {
		if key, value, ok := strings.Cut(h, "="); ok {
			headerMap[key] = value
		}
	}
	return headerMap
}
