package ws

import (
	"fmt"
	"log"
	"net"
	"reflect"
	"sync"
	"time"

	"github.com/gorilla/websocket"
)

type Conn struct {
	mutex                 *sync.Mutex
	queue                 [][]byte
	conn                  *websocket.Conn
	logger                *log.Logger
	signalCh              chan struct{} // для сигнала что есть данные на отправку
	cancelCh              chan struct{} // для отмены writer-а
	forcedCloseCh         chan _forcedClose
	pongCh                chan string
	lostConnectionTimeout time.Duration
	sendTimeout           time.Duration
	disconnectWaitTimeout time.Duration
	ping                  bool
	publicOnMessage       func(*Conn, []byte)
	privateOnMessage      func(*Conn, int64, []byte)
	readDeadline          time.Time
	codec                 MessageCodec
	funcMap               map[string]funcData
	ownFuncMap            map[string]funcData
	userID                int64
}

func (s *Conn) String() string {
	return s.conn.RemoteAddr().String()
}

func (s *Conn) Handle(messageType string, handler any) {
	if s.ownFuncMap == nil {
		s.ownFuncMap = make(map[string]funcData)
	}
	if s.userID > 0 {
		s.ownFuncMap[messageType] = getPrivateFuncData(handler)
	} else {
		s.ownFuncMap[messageType] = getPublicFuncData(handler)
	}
}

func (s *Conn) Unhandle(messageType string) {
	if s.ownFuncMap != nil {
		delete(s.ownFuncMap, messageType)
	}
}

func (s *Conn) Send(messageType string, param any) {
	data, err := s.codec.EncodeMessage(messageType, param)
	if err != nil {
		s.logger.Printf("codec.EncodeMessage: %s\n", err)
		return
	}
	s.SendBytes(data)
}

func (s *Conn) SendBytes(data []byte) {
	s.mutex.Lock()
	s.queue = append(s.queue, data)
	s.mutex.Unlock()
	select {
	case s.signalCh <- struct{}{}:
	default:
	}
}

func (s *Conn) Abort() {
	s.conn.Close()
}

func (s *Conn) Disconnect() {
	// Закрытие произойдет в reader
	s.CloseByReasonOutside(websocket.CloseNormalClosure)
}

func (s *Conn) RemoteAddr() net.Addr {
	return s.conn.RemoteAddr()
}

func (s *Conn) Read() (raw []byte, err error) {
	s.readDeadline = time.Now().Add(s.lostConnectionTimeout)
	s.conn.SetReadDeadline(s.readDeadline)
	_, data, err := s.conn.ReadMessage()
	if err != nil {
		if ce, ok := err.(*websocket.CloseError); ok {
			err = fmt.Errorf("conn %s closed by code=%d and text=%q",
				s.RemoteAddr(), ce.Code, ce.Text)
		} else {
			err = fmt.Errorf("conn.ReadMessage %s: %s", s.RemoteAddr(), err)
		}
		return
	}
	return data, nil
}

// Для ситуации когда по умолчанию ожидание lostConnectionTimeout, но после записи
// в сокет команды - нужно ждать меньше времени
func (s *Conn) SetReadDeadline(t time.Time) {
	s.conn.SetReadDeadline(t)
}

func (s *Conn) writeQueue() (err error) {
	s.mutex.Lock()
	queue := s.queue
	s.queue = nil
	s.mutex.Unlock()
	for _, msg := range queue {
		err = s.writeTextMessage(msg)
		if err != nil {
			return
		}
	}
	return
}

func (s *Conn) writeTextMessage(msg []byte) (err error) {
	s.conn.SetWriteDeadline(time.Now().Add(s.sendTimeout))
	err = s.conn.WriteMessage(websocket.TextMessage, msg)
	if err != nil {
		s.logger.Printf("conn.WriteMessage %s: %s\n", s.RemoteAddr(), err)
		s.conn.Close()
	}
	return
}

func (s *Conn) writerWithPings() {
	interval := s.lostConnectionTimeout * 9 / 10
	ticker := time.NewTicker(interval)
	for {
		select {
		case <-s.signalCh:
			s.writeQueue()
		case <-ticker.C:
			err := s.conn.WriteControl(
				websocket.PingMessage,
				nil,
				time.Now().Add(s.sendTimeout),
			)
			if err != nil {
				s.logger.Printf("write ping: %s\n", err)
				return
			}
		case <-s.cancelCh:
			return
		case task := <-s.forcedCloseCh:
			s.writeCloseMessage(task.Code)
			if task.WaitCh != nil {
				close(task.WaitCh)
			}
		case appData := <-s.pongCh:
			s.writePong(appData)
		}
	}
}

func (s *Conn) writePong(appData string) {
	err := s.conn.WriteControl(
		websocket.PongMessage,
		[]byte(appData),
		time.Now().Add(s.sendTimeout),
	)
	if err != nil {
		s.logger.Printf("write pong: %s\n", err)
	}
}

func (s *Conn) writerNoPings() {
	for {
		select {
		case <-s.signalCh:
			s.writeQueue()
		case <-s.cancelCh:
			return
		case task := <-s.forcedCloseCh:
			s.writeCloseMessage(task.Code)
			if task.WaitCh != nil {
				close(task.WaitCh)
			}
		case appData := <-s.pongCh:
			s.writePong(appData)
		}
	}
}

func (s *Conn) writer() {
	if s.ping {
		s.writerWithPings()
	} else {
		s.writerNoPings()
	}
}

func (s *Conn) reader() {
	s.conn.SetPingHandler(func(appData string) error {
		s.readDeadline = time.Now().Add(s.lostConnectionTimeout)
		s.conn.SetReadDeadline(s.readDeadline)
		s.pongCh <- appData
		return nil
	})
	s.conn.SetPongHandler(func(string) error {
		s.readDeadline = time.Now().Add(s.lostConnectionTimeout)
		s.conn.SetReadDeadline(s.readDeadline)
		return nil
	})
	for {
		data, err := s.Read()
		if err != nil {
			s.logger.Println(err)
			s.conn.Close()
			close(s.cancelCh)
			return
		}
		err = s.processMessage(data)
		if err != nil {
			s.logger.Println(err)
			s.conn.Close()
			close(s.cancelCh)
			return
		}
	}
}

func (s *Conn) processMessage(data []byte) (err error) {
	if s.privateOnMessage != nil || s.publicOnMessage != nil {
		if s.userID > 0 {
			s.privateOnMessage(s, s.userID, data)
		} else {
			s.publicOnMessage(s, data)
		}
	} else {
		err = s.decodeAndRouteMessage(s, data)
		if err != nil {
			s.logger.Printf("decodeAndRouteMessage: %s\n", err)
		}
	}
	return
}

func (s *Conn) decodeAndRouteMessage(conn *Conn, data []byte) (err error) {
	messageType, messageBody, err := s.codec.DecodeMessageHeader(data)
	if err != nil {
		s.CloseByReasonInReader(BadMessageCode)
		return
	}
	var (
		fd    funcData
		found bool
	)
	if s.ownFuncMap != nil {
		fd, found = s.ownFuncMap[messageType]
	}
	if !found {
		fd, found = s.funcMap[messageType]
	}
	if !found {
		s.CloseByReasonInReader(UnknownMessageTypeCode)
		return fmt.Errorf("unknown messageType %q", messageType)
	}
	switch fd.InSignature {
	// Private
	case privateUserAndT:
		var arg reflect.Value
		arg, err = s.codec.DecodeMessageBody(messageBody, fd.ParamType)
		if err != nil {
			s.CloseByReasonInReader(BadMessageCode)
			return
		}
		fd.Func.Call([]reflect.Value{reflect.ValueOf(s.userID), arg.Elem()})
	case connAndPrivateUserAndT:
		var arg reflect.Value
		arg, err = s.codec.DecodeMessageBody(messageBody, fd.ParamType)
		if err != nil {
			s.CloseByReasonInReader(BadMessageCode)
			return
		}
		fd.Func.Call([]reflect.Value{
			reflect.ValueOf(conn),
			reflect.ValueOf(s.userID),
			arg.Elem(),
		})
	case connAndPrivateUser:
		fd.Func.Call([]reflect.Value{
			reflect.ValueOf(conn),
			reflect.ValueOf(s.userID),
		})
	case privateUserOnly:
		fd.Func.Call([]reflect.Value{
			reflect.ValueOf(s.userID),
		})
	case userInOnly:
		var arg reflect.Value
		arg, err = s.codec.DecodeMessageBody(messageBody, fd.ParamType)
		if err != nil {
			s.CloseByReasonInReader(BadMessageCode)
			return
		}
		fd.Func.Call([]reflect.Value{
			arg.Elem(),
		})
	case bothIn:
		var arg reflect.Value
		arg, err = s.codec.DecodeMessageBody(messageBody, fd.ParamType)
		if err != nil {
			s.CloseByReasonInReader(BadMessageCode)
			return
		}
		fd.Func.Call([]reflect.Value{
			reflect.ValueOf(conn),
			arg.Elem(),
		})
	case connInOnly:
		fd.Func.Call([]reflect.Value{
			reflect.ValueOf(conn),
		})
	case emptyIn:
		fd.Func.Call([]reflect.Value{})
	}
	return
}

// 3. Сразу после открытия соединения, до запуска Reader и Writer на этапе чтения
// и проверки SID. Можно сразу напрямую писать Control фрейм и читать ответ.
func (s *Conn) CloseByReasonInplace(code int) {
	// половину времени ожидаю записи CloseMessage и половину времени на получение ответа
	err := s.writeCloseMessage(code)
	if err != nil {
		return
	}
	s.waitConnClose()
}

func (s *Conn) writeCloseMessage(code int) error {
	// Отправляю другой стороне CloseMessage с кодом и кратким текстом ошибки
	return s.conn.WriteControl(
		websocket.CloseMessage,
		websocket.FormatCloseMessage(code, closeCode2Text[code]),
		time.Now().Add(s.disconnectWaitTimeout/2),
	)
}

func (s *Conn) waitConnClose() {
	// По протоколу в ответ на CloseMessage другая сторона должна
	// отправить CloseMessage
	s.conn.SetReadDeadline(time.Now().Add(s.disconnectWaitTimeout / 2))
	for {
		// в цикле жду ошибки (CloseMessage - это тоже ошибка с типом
		// *websocket.CloseError)
		_, _, err := s.conn.ReadMessage()
		if err != nil {
			return
		}
	}
}

type _forcedClose struct {
	Code   int
	WaitCh chan struct{}
}

// 2. Если это ошибка парсинга сообщения в Reader, писать все равно нужно
// в одном потоке - Writer-а. Нужно передать сигнал и waitCh. Дождаться отправки
// и затем читать в ожидании закрытия соединения. Читать нужно на месте, чтобы не допустить
// чтения и обработки других сообщений.
func (s *Conn) CloseByReasonInReader(code int) {
	waitCh := make(chan struct{})
	s.forcedCloseCh <- _forcedClose{
		Code:   code,
		WaitCh: waitCh,
	}
	<-waitCh
	s.waitConnClose()
}

// 1. Если это Disconnect, либо закрытие истекших сессий - я должен передать
// сигнал Writer-у, который запишет Control фрейм. Закрытие произойдет в Reader,
// отследить можно по cancelCh или waitCh, но не нужно.
func (s *Conn) CloseByReasonOutside(code int) {
	// Закрытие произойдет в reader
	s.forcedCloseCh <- _forcedClose{
		Code: code,
	}
}
