package transport

import (
	"bufio"
	"bytes"
	"io"
	"net"
	"net/http"
	"net/url"
	"sync"
	"time"

	"github.com/pkg/errors"

	log "go-micro.dev/v5/logger"
	"go-micro.dev/v5/internal/util/buf"
)

type httpTransportClient struct {
	dialOpts DialOptions
	conn     net.Conn
	ht       *httpTransport

	// request must be stored for response processing
	req  chan *http.Request
	buff *bufio.Reader
	addr string

	// local/remote ip
	local   string
	remote  string
	reqList []*http.Request

	sync.RWMutex

	once sync.Once

	closed bool
}

func (h *httpTransportClient) Local() string {
	return h.local
}

func (h *httpTransportClient) Remote() string {
	return h.remote
}

func (h *httpTransportClient) Send(m *Message) error {
	logger := h.ht.Options().Logger

	header := make(http.Header)
	for k, v := range m.Header {
		header.Set(k, v)
	}

	b := buf.New(bytes.NewBuffer(m.Body))
	defer func() {
		if err := b.Close(); err != nil {
			logger.Logf(log.ErrorLevel, "failed to close buffer: %v", err)
		}
	}()

	req := &http.Request{
		Method: http.MethodPost,
		URL: &url.URL{
			Scheme: "http",
			Host:   h.addr,
		},
		Header:        header,
		Body:          b,
		ContentLength: int64(b.Len()),
		Host:          h.addr,
		Close:         h.dialOpts.ConnClose,
	}

	if !h.dialOpts.Stream {
		h.Lock()
		if h.closed {
			h.Unlock()
			return io.EOF
		}

		h.reqList = append(h.reqList, req)

		select {
		case h.req <- h.reqList[0]:
			h.reqList = h.reqList[1:]
		default:
		}
		h.Unlock()
	}

	// set timeout if its greater than 0
	if h.ht.opts.Timeout > time.Duration(0) {
		if err := h.conn.SetDeadline(time.Now().Add(h.ht.opts.Timeout)); err != nil {
			return err
		}
	}

	return req.Write(h.conn)

}

// Recv receives a message.
func (h *httpTransportClient) Recv(msg *Message) (err error) {
	if msg == nil {
		return errors.New("message passed in is nil")
	}

	var req *http.Request

	if !h.dialOpts.Stream {

		var rc *http.Request
		var ok bool

		h.Lock()
		select {
		case rc, ok = <-h.req:
		default:
		}

		if !ok {
			if len(h.reqList) == 0 {
				h.Unlock()
				return io.EOF
			}

			rc = h.reqList[0]
			h.reqList = h.reqList[1:]
		}
		h.Unlock()

		req = rc
	}

	// set timeout if its greater than 0
	if h.ht.opts.Timeout > time.Duration(0) {
		if err = h.conn.SetDeadline(time.Now().Add(h.ht.opts.Timeout)); err != nil {
			return err
		}
	}

	h.Lock()
	defer h.Unlock()

	if h.closed {
		return io.EOF
	}

	rsp, err := http.ReadResponse(h.buff, req)
	if err != nil {
		return err
	}

	defer func() {
		if err2 := rsp.Body.Close(); err2 != nil {
			err = errors.Wrap(err2, "failed to close body")
		}
	}()

	b, err := io.ReadAll(rsp.Body)
	if err != nil {
		return err
	}

	if rsp.StatusCode != http.StatusOK {
		return errors.New(rsp.Status + ": " + string(b))
	}

	msg.Body = b

	if msg.Header == nil {
		msg.Header = make(map[string]string, len(rsp.Header))
	}

	for k, v := range rsp.Header {
		if len(v) > 0 {
			msg.Header[k] = v[0]
		} else {
			msg.Header[k] = ""
		}
	}

	return nil
}

func (h *httpTransportClient) Close() error {
	if !h.dialOpts.Stream {
		h.once.Do(
			func() {
				h.Lock()
				h.buff.Reset(nil)
				h.closed = true
				h.Unlock()
				close(h.req)
			},
		)

		return h.conn.Close()
	}

	err := h.conn.Close()
	h.once.Do(
		func() {
			h.Lock()
			h.buff.Reset(nil)
			h.closed = true
			h.Unlock()
			close(h.req)
		},
	)

	return err
}
