package publisher

import (
	"errors"
	"fmt"
	"strings"
	"sync"

	"gordenko.dev/dima/textutil"
	"gordenko.dev/dima/ws"
)

type PublicPublisher struct {
	mutex                      sync.Mutex
	server                     *ws.PublicServer
	subscribers                map[string]map[*ws.Conn]bool
	connAsChannels             map[string]*ws.Conn
	subscribeRequestHandlers   map[string]func(*ws.Conn, string, string)
	unsubscribeRequestHandlers map[string]func(*ws.Conn, string, string)
}

type PublicPublisherOptions struct {
	WsPublicServer *ws.PublicServer
}

func NewPublicPublisher(opt PublicPublisherOptions) (*PublicPublisher, error) {
	if opt.WsPublicServer == nil {
		return nil, errors.New("WsPublicServer option is required")
	}
	s := &PublicPublisher{
		server:                     opt.WsPublicServer,
		subscribers:                make(map[string]map[*ws.Conn]bool),
		connAsChannels:             make(map[string]*ws.Conn),
		subscribeRequestHandlers:   make(map[string]func(*ws.Conn, string, string)),
		unsubscribeRequestHandlers: make(map[string]func(*ws.Conn, string, string)),
	}
	s.server.OnDisconnected(s.onDisconnected)

	s.server.Func(WsMsgSubscribe, s.onSubscribe)
	s.server.Func(WsMsgUnsubscribe, s.onUnsubscribe)
	s.server.Func(WsMsgUnsubscribeToAll, s.onUnsubscribeToAll)
	s.server.Func(WsMsgMakeConnAsChannel, s.onMakeConnAsChannel)
	return s, nil
}

func (s *PublicPublisher) onDisconnected(conn *ws.Conn) {
	s.onUnsubscribeToAll(conn)
}

func (s *PublicPublisher) onSubscribe(conn *ws.Conn, topic string) {
	if strings.HasPrefix(topic, "#") {
		return
	}
	prefix, arg := parseTopic(topic)
	f, ok := s.subscribeRequestHandlers[prefix]
	if ok {
		f(conn, topic, arg)
	}
}

func (s *PublicPublisher) onUnsubscribe(conn *ws.Conn, topic string) {
	if strings.HasPrefix(topic, "#") {
		s.mutex.Lock()
		delete(s.connAsChannels, topic)
		s.mutex.Unlock()
	} else {
		s.unsubscribeToTopic(conn, topic)
	}
}

func (s *PublicPublisher) onUnsubscribeToAll(expiredConn *ws.Conn) {
	var (
		connAsChannels []string
		topics         []string
	)
	s.mutex.Lock()
	for channel, conn := range s.connAsChannels {
		if conn == expiredConn {
			connAsChannels = append(connAsChannels, channel)
		}
	}
	for _, channel := range connAsChannels {
		delete(s.connAsChannels, channel)
	}
	for topic, connMap := range s.subscribers {
		if _, ok := connMap[expiredConn]; ok {
			topics = append(topics, topic)
		}
	}
	s.mutex.Unlock()
	for _, topic := range topics {
		s.unsubscribeToTopic(expiredConn, topic)
	}
}

func (s *PublicPublisher) unsubscribeToTopic(conn *ws.Conn, topic string) {
	prefix, arg := parseTopic(topic)
	f, ok := s.unsubscribeRequestHandlers[prefix]
	if ok {
		f(conn, topic, arg)
	} else {
		s.UnsubscribeTo(conn, topic)
	}
}

func (s *PublicPublisher) onMakeConnAsChannel(conn *ws.Conn) {
	s.mutex.Lock()
	defer s.mutex.Unlock()
	for {
		channel := "#" + textutil.RandomString(textutil.Alpha52Digit, 16)
		_, ok := s.connAsChannels[channel]
		if !ok {
			s.connAsChannels[channel] = conn
			conn.Send(WsMsgConnAsChannel, channel)
			return
		}
	}
}

func (s *PublicPublisher) SubscribeTo(conn *ws.Conn, topic string) {
	s.mutex.Lock()
	connMap, ok := s.subscribers[topic]
	if !ok {
		connMap = make(map[*ws.Conn]bool)
		s.subscribers[topic] = connMap
	}
	connMap[conn] = true
	s.mutex.Unlock()
}

func (s *PublicPublisher) UnsubscribeTo(conn *ws.Conn, topic string) {
	s.mutex.Lock()
	connMap, ok := s.subscribers[topic]
	if ok {
		delete(connMap, conn)
		if len(connMap) == 0 {
			delete(s.subscribers, topic)
		}
	}
	s.mutex.Unlock()
}

func (s *PublicPublisher) Publish(topic string, messageType string, message any) {
	s.mutex.Lock()
	defer s.mutex.Unlock()
	if strings.HasPrefix(topic, "#") {
		conn := s.connAsChannels[topic]
		if conn != nil {
			conn.Send(messageType, message)
		}
	} else {
		connMap, ok := s.subscribers[topic]
		if ok {
			s.server.Broadcast(messageType, message, connMap)
		}
	}
}

func (s *PublicPublisher) HasSubscribers(topic string) bool {
	s.mutex.Lock()
	_, has := s.subscribers[topic]
	s.mutex.Unlock()
	return has
}

func (s *PublicPublisher) OnTopicSubscribeRequest(topicPrefix string, f func(*ws.Conn, string, string)) {
	if strings.Contains(topicPrefix, TopicSeparator) {
		panic(fmt.Sprintf("wrong topic prefix '%s', because of topic separator '%s'",
			topicPrefix, TopicSeparator))
	}
	s.mutex.Lock()
	s.subscribeRequestHandlers[topicPrefix] = f
	s.mutex.Unlock()
}

func (s *PublicPublisher) OnTopicUnsubscribeRequest(topicPrefix string, f func(*ws.Conn, string, string)) {
	if strings.Contains(topicPrefix, TopicSeparator) {
		panic(fmt.Sprintf("wrong topic prefix '%s', because of topic separator '%s'",
			topicPrefix, TopicSeparator))
	}
	s.mutex.Lock()
	s.unsubscribeRequestHandlers[topicPrefix] = f
	s.mutex.Unlock()
}
