package tunnel

import (
	"context"
	"net"
	"time"

	"github.com/gofrs/uuid/v5"
	"github.com/sagernet/sing-box/adapter"
	"github.com/sagernet/sing-box/adapter/endpoint"
	"github.com/sagernet/sing-box/adapter/outbound"
	sbUot "github.com/sagernet/sing-box/common/uot"
	C "github.com/sagernet/sing-box/constant"
	"github.com/sagernet/sing-box/log"
	"github.com/sagernet/sing-box/option"
	"github.com/sagernet/sing/common"
	E "github.com/sagernet/sing/common/exceptions"
	"github.com/sagernet/sing/common/logger"
	M "github.com/sagernet/sing/common/metadata"
	N "github.com/sagernet/sing/common/network"
	"github.com/sagernet/sing/common/uot"
	"github.com/sagernet/sing/service"
)

func RegisterServerEndpoint(registry *endpoint.Registry) {
	endpoint.Register[option.TunnelServerEndpointOptions](registry, C.TypeTunnelServer, NewServerEndpoint)
}

type ServerEndpoint struct {
	outbound.Adapter
	logger    logger.ContextLogger
	inbound   adapter.Inbound
	router    adapter.ConnectionRouterEx
	uuid      uuid.UUID
	users     map[uuid.UUID]uuid.UUID
	keys      map[uuid.UUID]uuid.UUID
	conns     map[uuid.UUID]chan net.Conn
	timeout   time.Duration
	uotClient *uot.Client
}

func NewServerEndpoint(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TunnelServerEndpointOptions) (adapter.Endpoint, error) {
	serverUUID, err := uuid.FromString(options.UUID)
	if err != nil {
		return nil, err
	}
	server := &ServerEndpoint{
		Adapter: outbound.NewAdapter(C.TypeTunnelServer, tag, []string{N.NetworkTCP, N.NetworkUDP}, []string{}),
		logger:  logger,
		router:  sbUot.NewRouter(router, logger),
		uuid:    serverUUID,
	}
	inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
	inbound, err := inboundRegistry.Create(ctx, NewRouter(router, logger, server.connHandler), logger, options.Inbound.Tag, options.Inbound.Type, options.Inbound.Options)
	if err != nil {
		return nil, err
	}
	server.inbound = inbound
	server.users = make(map[uuid.UUID]uuid.UUID, len(options.Users))
	server.keys = make(map[uuid.UUID]uuid.UUID, len(options.Users))
	server.conns = make(map[uuid.UUID]chan net.Conn)
	for _, user := range options.Users {
		key, err := uuid.FromString(user.Key)
		if err != nil {
			return nil, err
		}
		uuid, err := uuid.FromString(user.UUID)
		if err != nil {
			return nil, err
		}
		server.users[key] = uuid
		server.keys[uuid] = key
		server.conns[uuid] = make(chan net.Conn, 10)
	}
	if options.ConnectTimeout != 0 {
		server.timeout = time.Duration(options.ConnectTimeout)
	} else {
		server.timeout = C.TCPConnectTimeout
	}
	server.uotClient = &uot.Client{
		Dialer:  server,
		Version: uot.Version,
	}
	return server, nil
}

func (s *ServerEndpoint) Start(stage adapter.StartStage) error {
	return s.inbound.Start(stage)
}

func (s *ServerEndpoint) DialContext(ctx context.Context, network string, destination M.Socksaddr) (net.Conn, error) {
	if N.NetworkName(network) == N.NetworkUDP {
		return s.uotClient.DialContext(ctx, network, destination)
	}
	var sourceUUID *uuid.UUID
	var ch chan net.Conn
	if metadata := adapter.ContextFrom(ctx); metadata != nil {
		if metadata.TunnelDestination != "" {
			tunnelDestination, err := uuid.FromString(metadata.TunnelDestination)
			if err != nil {
				return nil, err
			}
			var ok bool
			ch, ok = s.conns[tunnelDestination]
			if !ok {
				return nil, E.New("user ", metadata.TunnelDestination, " not found")
			}
		}
		if metadata.TunnelSource != "" {
			tunnelSource, err := uuid.FromString(metadata.TunnelSource)
			if err != nil {
				return nil, err
			}
			sourceUUID = &tunnelSource
		}
	}
	if ch == nil {
		return nil, E.New("tunnel destination not set")
	}
	if sourceUUID == nil {
		sourceUUID = &s.uuid
	}
	ctx, cancel := context.WithTimeout(ctx, s.timeout)
	defer cancel()
	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
		}
		select {
		case conn := <-ch:
			err := WriteRequest(conn, &Request{UUID: *sourceUUID, Command: CommandTCP, Destination: destination})
			if err != nil {
				conn.Close()
				s.logger.ErrorContext(ctx, err)
				continue
			}
			return conn, nil
		case <-ctx.Done():
			return nil, ctx.Err()
		}
	}
}

func (s *ServerEndpoint) ListenPacket(ctx context.Context, destination M.Socksaddr) (net.PacketConn, error) {
	return s.uotClient.ListenPacket(ctx, destination)
}

func (s *ServerEndpoint) Close() error {
	return common.Close(s.inbound)
}

func (s *ServerEndpoint) connHandler(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, onClose N.CloseHandlerFunc) error {
	if metadata.Destination != Destination {
		s.router.RouteConnectionEx(ctx, conn, metadata, onClose)
		return nil
	}
	request, err := ReadRequest(conn)
	if err != nil {
		return err
	}
	if request.Command == CommandInbound {
		uuid, ok := s.users[request.UUID]
		if !ok {
			return E.New("key ", request.UUID.String(), " not found")
		}
		ch := s.conns[uuid]
		select {
		case ch <- conn:
		default:
			oldConn := <-ch
			oldConn.Close()
			ch <- conn
		}
		return nil
	}
	if request.Command == CommandTCP {
		sourceUUID, ok := s.users[request.UUID]
		if !ok {
			return E.New("key ", request.UUID, " not found")
		}
		if sourceUUID == request.DestinationUUID {
			return E.New("routing loop on ", sourceUUID)
		}
		if request.DestinationUUID != s.uuid {
			_, ok = s.keys[request.DestinationUUID]
			if !ok {
				return E.New("user ", request.DestinationUUID, " not found")
			}
		}
		metadata.Inbound = s.Tag()
		metadata.InboundType = C.TypeTunnelServer
		metadata.Destination = request.Destination
		metadata.TunnelSource = sourceUUID.String()
		metadata.TunnelDestination = request.DestinationUUID.String()
		s.router.RouteConnectionEx(ctx, conn, metadata, onClose)
		return nil
	}
	return E.New("command ", request.Command, " not found")
}
