package ws

import (
	"errors"
	"log"
	"net/http"
	"sync"
	"time"

	"gordenko.dev/dima/web"

	"github.com/gorilla/websocket"
)

type PublicServerOptions struct {
	Logger                *log.Logger
	MessageCodec          MessageCodec
	MaxInputMessageSize   int
	SendTimeout           time.Duration
	LostConnectionTimeout time.Duration
	DisconnectWaitTimeout time.Duration
	Ping                  bool
	CheckOrigin           bool
	HandshakeTimeout      time.Duration
}

type PublicServer struct {
	mutex                 sync.Mutex
	OnMessage             func(*Conn, []byte)
	logger                *log.Logger
	maxInputMessageSize   int64
	sendTimeout           time.Duration
	lostConnectionTimeout time.Duration
	disconnectWaitTimeout time.Duration
	ping                  bool
	upgrader              websocket.Upgrader
	codec                 MessageCodec
	connectListeners      []func(*Conn)
	disconnectListeners   []func(*Conn)
	funcMap               map[string]funcData
}

func NewPublicServer(opt PublicServerOptions) (*PublicServer, error) {
	if opt.Logger == nil {
		return nil, errors.New("Logger option is required")
	}
	s := &PublicServer{
		logger:  opt.Logger,
		ping:    opt.Ping,
		funcMap: make(map[string]funcData),
		upgrader: websocket.Upgrader{
			HandshakeTimeout: opt.HandshakeTimeout,
		},
	}
	if !opt.CheckOrigin {
		s.upgrader.CheckOrigin = func(req *http.Request) bool {
			return true
		}
	}
	if opt.MaxInputMessageSize <= 0 {
		s.maxInputMessageSize = defaultMaxInputMessageSize
	} else {
		s.maxInputMessageSize = int64(opt.MaxInputMessageSize)
	}
	if opt.SendTimeout <= 0 {
		s.sendTimeout = defaultSendTimeout
	} else {
		s.sendTimeout = time.Duration(opt.SendTimeout) * time.Second
	}
	if opt.DisconnectWaitTimeout <= 0 {
		s.disconnectWaitTimeout = defaultDisconnectWaitTimeout
	} else {
		s.disconnectWaitTimeout = opt.DisconnectWaitTimeout
	}
	if opt.LostConnectionTimeout <= 0 {
		s.lostConnectionTimeout = defaultLostConnectionTimeout
	} else {
		s.lostConnectionTimeout = opt.LostConnectionTimeout
	}
	if opt.MessageCodec == nil {
		s.codec = NewJSONMessageCodec()
	} else {
		s.codec = opt.MessageCodec
	}
	// задаю отправку pong в ответ на ping
	s.Func("ping", s.onPing)
	return s, nil
}

func (s *PublicServer) onPing(conn *Conn) {
	conn.Send("pong", nil)
}

func (s *PublicServer) Handle(state *web.State, reply *web.Reply) (err error) {
	conn, err := s.upgrader.Upgrade(state.ResponseWriter(), state.Request(), nil)
	if err != nil {
		s.logger.Printf("upgrader.Upgrade: %s\n", err)
		return
	}
	defer conn.Close()
	//
	wrappedConn := &Conn{
		mutex:                 new(sync.Mutex),
		signalCh:              make(chan struct{}, 1),
		cancelCh:              make(chan struct{}),
		forcedCloseCh:         make(chan _forcedClose, 1),
		pongCh:                make(chan string, 1),
		logger:                s.logger,
		sendTimeout:           s.sendTimeout,
		lostConnectionTimeout: s.lostConnectionTimeout,
		disconnectWaitTimeout: s.disconnectWaitTimeout,
		ping:                  s.ping,
		conn:                  conn,
		publicOnMessage:       s.OnMessage,
		funcMap:               s.funcMap,
		codec:                 s.codec,
	}
	conn.SetReadLimit(s.maxInputMessageSize)
	// запускаем в отдельной горутине писателя в вебсокет
	go wrappedConn.writer()
	for _, listener := range s.connectListeners {
		listener(wrappedConn)
	}
	wrappedConn.reader()
	for _, listener := range s.disconnectListeners {
		listener(wrappedConn)
	}
	return
}

func (s *PublicServer) OnConnected(f func(*Conn)) {
	s.connectListeners = append(s.connectListeners, f)
}

func (s *PublicServer) OnDisconnected(f func(*Conn)) {
	s.disconnectListeners = append(s.disconnectListeners, f)
}

func (s *PublicServer) Func(messageType string, handler any) {
	s.funcMap[messageType] = getPublicFuncData(handler)
}

/*
Broadcast - цель метода - оптимизация при отправке одной порции данных большому
количеству клиентов. Метод пакует данные в массив байт 1 раз, нет перерасхода
памяти и CPU.
*/
func (s *PublicServer) Broadcast(proc string, param any, conns map[*Conn]bool) {
	data, err := s.codec.EncodeMessage(proc, param)
	if err != nil {
		s.logger.Println(err)
		return
	}
	for conn := range conns {
		conn.SendBytes(data)
	}
}
