package cl_104

import (
	"context"
	"encoding/json"
	"net/http"
	"sync"
	"sync/atomic"
	"time"

	"github.com/gorilla/websocket"
)

const upConnLimit int64 = 10

var upConnNum int64

func (h *CL104) serveUpstream(res http.ResponseWriter, req *http.Request) {
	select {
	case <-h.close:
		res.WriteHeader(http.StatusGone)
		return
	default:
		if atomic.AddInt64(&upConnNum, 1) > upConnLimit {
			atomic.AddInt64(&upConnNum, -1)
			res.WriteHeader(http.StatusTooManyRequests)
			return
		}
	}
	defer atomic.AddInt64(&upConnNum, -1)

	upConn, err := upgrader.Upgrade(res, req, nil)
	if err != nil {
		h.Log.Error(err)
		return
	}
	defer upConn.Close()

	clConn, _, err := websocket.DefaultDialer.Dial(h.CLURL, nil)
	if err != nil {
		h.Log.Error("dial clURL:", err)
		return
	}
	defer clConn.Close()

	stopCtx, stopCancel := context.WithCancel(context.Background())
	defer stopCancel()

	upSession := &wsSession{
		conn:   upConn,
		ctx:    stopCtx,
		cancel: stopCancel,
		ctrlCh: make(chan wsMsg, 2),
	}

	clSession := &wsSession{
		conn:   clConn,
		ctx:    stopCtx,
		cancel: stopCancel,
		ctrlCh: make(chan wsMsg, 2),
	}

	if err := h.setupSessionConn(upSession); err != nil {
		h.Log.Error(err)
		return
	}
	if err := h.setupSessionConn(clSession); err != nil {
		h.Log.Error(err)
		return
	}

	upChan := make(chan []byte, 16)
	clChan := make(chan []byte, 16)

	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		h.startUpWorkersWithCh(upSession, upChan, clChan)
	}()
	go func() {
		defer wg.Done()
		h.startClWorkersWithCh(clSession, upChan, clChan)
	}()

	wg.Wait()
}

func (h *CL104) startUpWorkers(session *wsSession) {
	upChan := make(chan []byte, 16)
	clChan := make(chan []byte, 16)
	h.startUpWorkersWithCh(session, upChan, clChan)
}

func (h *CL104) startUpWorkersWithCh(session *wsSession, upChan, clChan chan []byte) {
	var wg sync.WaitGroup

	wg.Add(1)
	go func() {
		defer wg.Done()
		h.monitorUpWrite(session, upChan)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		h.monitorUpRead(session, clChan)
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		h.sendPing(session)
	}()

	wg.Wait()
}

func (h *CL104) monitorUpWrite(session *wsSession, upChan chan []byte) {
	var err error
	for {
		select {
		case <-session.ctx.Done():
			return
		case ctrl := <-session.ctrlCh:
			err = session.conn.SetWriteDeadline(time.Now().Add(time.Duration(h.WriteWait)))
			if err != nil {
				session.cancel()
				return
			}
			err = session.conn.WriteControl(ctrl.mt, ctrl.data, time.Now().Add(time.Duration(h.WriteWait)))
			if err != nil {
				session.cancel()
				return
			}
		case msg := <-upChan:
			err = session.conn.SetWriteDeadline(time.Now().Add(time.Duration(h.WriteWait)))
			if err != nil {
				session.cancel()
				return
			}
			err = session.conn.WriteMessage(websocket.TextMessage, msg)
			if err != nil {
				session.cancel()
				return
			}
		}
	}
}

func (h *CL104) monitorUpRead(session *wsSession, clChan chan []byte) {
	for {
		select {
		case <-session.ctx.Done():
			return
		default:
			mt, rm, err := session.conn.ReadMessage()
			if err != nil {
				if websocket.IsCloseError(err, websocket.CloseNormalClosure) {
					session.cancel()
					return
				}
				if ce, ok := err.(*websocket.CloseError); ok {
					h.Log.Infof("client closed with code:", ce.Code, "text:", ce.Text)
					session.cancel()
					return
				}
				h.Log.Error("server read:", err)
				session.cancel()
				return
			}

			switch mt {
			case websocket.TextMessage:
				if err := h.fromUpstream(rm, clChan); err != nil {
					h.Log.Error(err)
				}
			default:
				h.Log.Info("not text:", string(rm))
			}
		}
	}
}

func (h *CL104) fromUpstream(m []byte, clChan chan []byte) error {
	msg := new(msg)
	if err := json.Unmarshal(m, msg); err != nil {
		return err
	}

	if msg.TI >= 45 && msg.TI <= 69 || msg.TI >= 100 && msg.TI <= 109 {
		select {
		case clChan <- m:
		case <-time.After(time.Second * 5):
			h.Log.Error("drop cl msg:", msg)
		}
	}

	return nil
}
