package ws

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"sync"
	"time"

	"gordenko.dev/dima/fixme"
	"gordenko.dev/dima/web"

	"github.com/gorilla/websocket"
)

type PrivateServerOptions struct {
	Logger                *log.Logger
	MessageCodec          MessageCodec
	MaxInputMessageSize   int
	SendTimeout           time.Duration
	LostConnectionTimeout time.Duration
	DisconnectWaitTimeout time.Duration
	Ping                  bool
	CheckOrigin           bool
	HandshakeTimeout      time.Duration
	AuthTokenToUserID     func(string) (int64, string, error)
}

type PrivateServer struct {
	mutex                 sync.Mutex
	OnMessage             func(*Conn, int64, []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, int64)
	disconnectListeners   []func(*Conn, int64)
	funcMap               map[string]funcData
	authTokenToUserID     func(string) (int64, string, error)
	tokenIDToConn         map[string]*Conn
}

func NewPrivateServer(opt PrivateServerOptions) (*PrivateServer, error) {
	if opt.Logger == nil {
		return nil, errors.New("Logger option is required")
	}
	if opt.AuthTokenToUserID == nil {
		return nil, errors.New("AuthTokenToUserID option is required")
	}
	s := &PrivateServer{
		logger:            opt.Logger,
		ping:              opt.Ping,
		funcMap:           make(map[string]funcData),
		authTokenToUserID: opt.AuthTokenToUserID,
		tokenIDToConn:     make(map[string]*Conn),
		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 *PrivateServer) onPing(conn *Conn, userID int64) {
	conn.Send("pong", nil)
}

func (s *PrivateServer) 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,
		privateOnMessage:      s.OnMessage,
		funcMap:               s.funcMap,
		codec:                 s.codec,
	}
	// Ждем сообщения с auth token
	conn.SetReadDeadline(time.Now().Add(20 * time.Second))
	raw, err := wrappedConn.Read()
	if err != nil {
		return fmt.Errorf("read auth token: %s", err)
	}
	var (
		authToken string
		userID    int64
		tokenID   string
	)
	err = json.Unmarshal(raw, &authToken)
	if err != nil {
		wrappedConn.CloseByReasonInplace(BadMessageCode)
		return fmt.Errorf("json.Unmarshal(authToken): %s", err)
	}
	userID, tokenID, err = s.authTokenToUserID(authToken)
	if err != nil {
		var fixable fixme.Error
		if errors.As(err, &fixable) {
			switch fixable.Code {
			case "TOKEN_EXPIRED", "INVALID_TOKEN", "MISSING_AUTHORIZATION_HEADER", "MALFORMED_AUTHORIZATION_HEADER":
				wrappedConn.CloseByReasonInplace(NoSessionCode)
				return fmt.Errorf("session %s not found", authToken)
			}
		}
		wrappedConn.CloseByReasonInplace(websocket.CloseInternalServerErr)
		return fmt.Errorf("authTokenToUserID: %s", err)
	}
	if userID == 0 {
		wrappedConn.CloseByReasonInplace(NoSessionCode)
		return fmt.Errorf("session %s not found", authToken)
	}
	s.mutex.Lock()
	s.tokenIDToConn[tokenID] = wrappedConn
	s.mutex.Unlock()
	//
	wrappedConn.userID = userID
	conn.SetReadLimit(s.maxInputMessageSize)
	// запускаем в отдельной горутине писателя в вебсокет
	go wrappedConn.writer()
	for _, listener := range s.connectListeners {
		listener(wrappedConn, userID)
	}
	wrappedConn.reader()
	s.mutex.Lock()
	delete(s.tokenIDToConn, tokenID)
	s.mutex.Unlock()
	for _, listener := range s.disconnectListeners {
		listener(wrappedConn, userID)
	}
	return
}

func (s *PrivateServer) ProcessInvalidatedTokens(invalidatedAuthTokenIDs []string) {
	var connsToClose []*Conn
	s.mutex.Lock()
	for _, tokenID := range invalidatedAuthTokenIDs {
		wrappedConn, ok := s.tokenIDToConn[tokenID]
		if ok {
			connsToClose = append(connsToClose, wrappedConn)
		}
	}
	s.mutex.Unlock()
	for _, wrappedConn := range connsToClose {
		s.logger.Printf("conn %s is closing by invalidated token\n", wrappedConn)
		go wrappedConn.CloseByReasonInplace(NoSessionCode)
	}
}

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

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

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

/*
Broadcast - цель метода - оптимизация при отправке одной порции данных большому
количеству клиентов. Метод пакует данные в массив байт 1 раз, нет перерасхода
памяти и CPU.
*/
func (s *PrivateServer) 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)
	}
}
