package proto_socket

import (
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"net"
	"net/http"
	"sync"

	"google.golang.org/protobuf/proto"
	"nhooyr.io/websocket"
)

type WsServer struct {
	host              string
	port              int
	path              string
	tlsConfig         *tls.Config
	newClient         func(*websocket.Conn) *WsClient
	clients           []*WsClient
	mu                sync.Mutex
	httpSrv           *http.Server
	listener          net.Listener
	started           bool
	OnClientConnected func(*WsClient)
	acceptOptions     *websocket.AcceptOptions
}

func NewWsServer(host string, port int, path string, newClient func(*websocket.Conn) *WsClient) *WsServer {
	return &WsServer{
		host:              host,
		port:              port,
		path:              path,
		newClient:         newClient,
		OnClientConnected: func(*WsClient) {},
	}
}

func NewWsServerTLS(host string, port int, path string, tlsCfg *tls.Config, newClient func(*websocket.Conn) *WsClient) *WsServer {
	s := NewWsServer(host, port, path, newClient)
	s.tlsConfig = tlsCfg
	return s
}

type WsServerOptions struct {
	AcceptOptions *websocket.AcceptOptions
}

func NewWsServerWithOptions(host string, port int, path string, options WsServerOptions, newClient func(*websocket.Conn) *WsClient) *WsServer {
	s := NewWsServer(host, port, path, newClient)
	s.acceptOptions = options.AcceptOptions
	return s
}

func NewWsServerTLSWithOptions(host string, port int, path string, tlsCfg *tls.Config, options WsServerOptions, newClient func(*websocket.Conn) *WsClient) *WsServer {
	s := NewWsServerTLS(host, port, path, tlsCfg, newClient)
	s.acceptOptions = options.AcceptOptions
	return s
}

func (s *WsServer) Started() bool {
	s.mu.Lock()
	defer s.mu.Unlock()
	return s.started
}

func (s *WsServer) IsSecure() bool {
	return s.tlsConfig != nil
}

func (s *WsServer) Clients() []*WsClient {
	s.mu.Lock()
	defer s.mu.Unlock()
	return append([]*WsClient{}, s.clients...)
}

func (s *WsServer) Start(ctx context.Context) error {
	mux := http.NewServeMux()
	mux.HandleFunc(s.path, s.handleWebSocket)
	s.httpSrv = &http.Server{
		Addr:      fmt.Sprintf("%s:%d", s.host, s.port),
		Handler:   mux,
		TLSConfig: s.tlsConfig,
	}

	ln, err := net.Listen("tcp", s.httpSrv.Addr)
	if err != nil {
		return err
	}
	if s.tlsConfig != nil {
		ln = tls.NewListener(ln, s.tlsConfig)
	}

	s.mu.Lock()
	s.listener = ln
	s.started = true
	s.mu.Unlock()

	go func() {
		<-ctx.Done()
		_ = s.Stop()
	}()
	go func() {
		err := s.httpSrv.Serve(ln)
		if err != nil && !errors.Is(err, http.ErrServerClosed) && !errors.Is(err, net.ErrClosed) {
			_ = s.Stop()
		}
	}()
	return nil
}

func (s *WsServer) Stop() error {
	s.mu.Lock()
	if !s.started {
		s.mu.Unlock()
		return nil
	}
	s.started = false
	srv := s.httpSrv
	clients := append([]*WsClient{}, s.clients...)
	s.clients = nil
	s.mu.Unlock()

	var err error
	if srv != nil {
		err = srv.Close()
	}
	for _, client := range clients {
		_ = client.Close()
	}
	return err
}

func (s *WsServer) Broadcast(m proto.Message) error {
	clients := s.Clients()
	var errs []error
	for _, client := range clients {
		if err := client.Send(m); err != nil {
			errs = append(errs, err)
		}
	}
	return errors.Join(errs...)
}

func (s *WsServer) handleWebSocket(w http.ResponseWriter, r *http.Request) {
	conn, err := websocket.Accept(w, r, s.acceptOptions)
	if err != nil {
		return
	}
	client := s.newClient(conn)
	client.AddDisconnectListener(func(c *WsClient) {
		s.removeClient(c)
	})

	s.mu.Lock()
	s.clients = append(s.clients, client)
	onConnected := s.OnClientConnected
	s.mu.Unlock()
	onConnected(client)
}

func (s *WsServer) removeClient(client *WsClient) {
	s.mu.Lock()
	defer s.mu.Unlock()
	for i, item := range s.clients {
		if item == client {
			s.clients = append(s.clients[:i], s.clients[i+1:]...)
			return
		}
	}
}
