// Package stun provides an embedded STUN server for NAT traversal discovery.
package stun

import (
	"errors"
	"fmt"
	"net"
	"sync"

	"github.com/hashicorp/go-multierror"
	nberrors "github.com/netbirdio/netbird/client/errors"
	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/formatter"
	"github.com/pion/stun/v3"
)

// ErrServerClosed is returned by Listen when the server is shut down gracefully.
var ErrServerClosed = errors.New("stun: server closed")

// ErrNoListeners is returned by Listen when no UDP connections were provided.
var ErrNoListeners = errors.New("stun: no listeners configured")

// Server implements a STUN server that responds to binding requests
// with the client's reflexive transport address.
type Server struct {
	conns    []*net.UDPConn
	logger   *log.Entry
	logLevel log.Level

	wg sync.WaitGroup
}

// NewServer creates a new STUN server with the given UDP listeners.
// The caller is responsible for creating and providing the listeners.
// logLevel can be: panic, fatal, error, warn, info, debug, trace
func NewServer(conns []*net.UDPConn, logLevel string) *Server {
	level, err := log.ParseLevel(logLevel)
	if err != nil {
		level = log.InfoLevel
	}

	// Create a separate logger with its own level setting
	// This allows --stun-log-level to work independently of --log-level
	stunLogger := log.New()
	stunLogger.SetOutput(log.StandardLogger().Out)
	stunLogger.SetLevel(level)
	// Use the formatter package to set up formatter, ReportCaller, and context hook
	formatter.SetTextFormatter(stunLogger)

	logger := stunLogger.WithField("component", "stun-server")
	logger.Infof("STUN server log level set to: %s", level.String())

	return &Server{
		conns:    conns,
		logger:   logger,
		logLevel: level,
	}
}

// Listen starts the STUN server and blocks until the server is shut down.
// Returns ErrServerClosed when shut down gracefully via Shutdown.
// Returns ErrNoListeners if no UDP connections were provided.
func (s *Server) Listen() error {
	if len(s.conns) == 0 {
		return ErrNoListeners
	}

	// Start a read loop for each listener
	for _, conn := range s.conns {
		s.logger.Infof("STUN server listening on %s", conn.LocalAddr())
		s.wg.Add(1)
		go s.readLoop(conn)
	}

	s.wg.Wait()
	return ErrServerClosed
}

// readLoop continuously reads UDP packets and handles STUN requests.
func (s *Server) readLoop(conn *net.UDPConn) {
	defer s.wg.Done()
	buf := make([]byte, 1500) // Standard MTU size
	for {
		n, remoteAddr, err := conn.ReadFromUDP(buf)

		if err != nil {
			// Check if the connection was closed externally
			if errors.Is(err, net.ErrClosed) {
				s.logger.Info("UDP connection closed, stopping read loop")
				return
			}
			s.logger.Warnf("failed to read UDP packet: %v", err)
			continue
		}

		// Handle packet in the same goroutine to avoid complexity
		// STUN responses are small and fast
		s.handlePacket(conn, buf[:n], remoteAddr)
	}
}

// handlePacket processes a STUN request and sends a response.
func (s *Server) handlePacket(conn *net.UDPConn, data []byte, addr *net.UDPAddr) {
	localPort := conn.LocalAddr().(*net.UDPAddr).Port

	s.logger.Debugf("[port:%d] received %d bytes from %s", localPort, len(data), addr)

	// Check if it's a STUN message
	if !stun.IsMessage(data) {
		s.logger.Debugf("[port:%d] not a STUN message (first bytes: %x)", localPort, data[:min(len(data), 8)])
		return
	}

	// Parse the STUN message
	msg := &stun.Message{Raw: data}
	if err := msg.Decode(); err != nil {
		s.logger.Warnf("[port:%d] failed to decode STUN message from %s: %v", localPort, addr, err)
		return
	}

	s.logger.Debugf("[port:%d] received STUN %s from %s (tx=%x)", localPort, msg.Type, addr, msg.TransactionID[:8])

	// Only handle binding requests
	if msg.Type != stun.BindingRequest {
		s.logger.Debugf("[port:%d] ignoring non-binding request: %s", localPort, msg.Type)
		return
	}

	// Build the response
	response, err := stun.Build(
		stun.NewTransactionIDSetter(msg.TransactionID),
		stun.BindingSuccess,
		&stun.XORMappedAddress{
			IP:   addr.IP,
			Port: addr.Port,
		},
		stun.Fingerprint,
	)
	if err != nil {
		s.logger.Errorf("[port:%d] failed to build STUN response: %v", localPort, err)
		return
	}

	// Send the response on the same connection it was received on
	n, err := conn.WriteToUDP(response.Raw, addr)
	if err != nil {
		s.logger.Errorf("[port:%d] failed to send STUN response to %s: %v", localPort, addr, err)
		return
	}

	s.logger.Debugf("[port:%d] sent STUN BindingSuccess to %s (%d bytes) with XORMappedAddress %s:%d", localPort, addr, n, addr.IP, addr.Port)
}

// Shutdown gracefully stops the STUN server.
func (s *Server) Shutdown() error {
	s.logger.Info("shutting down STUN server")

	var merr *multierror.Error

	for _, conn := range s.conns {
		if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) {
			merr = multierror.Append(merr, fmt.Errorf("close STUN UDP connection: %w", err))
		}
	}

	// Wait for all readLoops to finish
	s.wg.Wait()
	return nberrors.FormatErrorOrNil(merr)
}
