package turnconn

import (
	"io"
	"net"
	"sync"

	"github.com/pion/logging"
	"github.com/pion/turn/v2"
	"github.com/pion/webrtc/v3"
	"golang.org/x/net/proxy"
	"golang.org/x/xerrors"
)

var (
	// reservedAddress is a magic address that's used exclusively
	// for proxying via Coder. We don't proxy all TURN connections,
	// because that'd exclude the possibility of a customer using
	// their own TURN server.
	reservedAddress = "127.0.0.1:12345"
	credential      = "coder"
	localhost       = &net.TCPAddr{
		IP: net.IPv4(127, 0, 0, 1),
	}

	// Proxy is a an ICE Server that uses a special hostname
	// to indicate traffic should be proxied.
	Proxy = webrtc.ICEServer{
		URLs:       []string{"turns:" + reservedAddress},
		Username:   "coder",
		Credential: credential,
	}
)

// New constructs a new TURN server binding to the relay address provided.
// The relay address is used to broadcast the location of an accepted connection.
func New(relayAddress *turn.RelayAddressGeneratorStatic) (*Server, error) {
	if relayAddress == nil {
		relayAddress = &turn.RelayAddressGeneratorStatic{
			RelayAddress: localhost.IP,
			Address:      "127.0.0.1",
		}
	}
	logger := logging.NewDefaultLoggerFactory()
	logger.DefaultLogLevel = logging.LogLevelDisabled
	server := &Server{
		conns:  make(chan net.Conn, 1),
		closed: make(chan struct{}),
	}
	server.listener = &listener{
		srv: server,
	}
	var err error
	server.turn, err = turn.NewServer(turn.ServerConfig{
		AuthHandler: func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
			// TURN connections require credentials. It's not important
			// for our use-case, because our listener is entirely in-memory.
			return turn.GenerateAuthKey(Proxy.Username, "", credential), true
		},
		ListenerConfigs: []turn.ListenerConfig{{
			Listener:              server.listener,
			RelayAddressGenerator: relayAddress,
		}},
		LoggerFactory: logger,
	})
	if err != nil {
		return nil, xerrors.Errorf("create server: %w", err)
	}

	return server, nil
}

// Server accepts and connects TURN allocations.
//
// This is a thin wrapper around pion/turn that pipes
// connections directly to the in-memory handler.
type Server struct {
	listener *listener
	turn     *turn.Server

	closeMutex sync.Mutex
	closed     chan (struct{})
	conns      chan (net.Conn)
}

// Accept consumes a new connection into the TURN server.
// A unique remote address must exist per-connection.
// pion/turn indexes allocations based on the address.
func (s *Server) Accept(nc net.Conn, remoteAddress, localAddress *net.TCPAddr) *Conn {
	if localAddress == nil {
		localAddress = localhost
	}
	conn := &Conn{
		Conn:          nc,
		remoteAddress: remoteAddress,
		localAddress:  localAddress,
		closed:        make(chan struct{}),
	}
	s.conns <- conn
	return conn
}

// Close ends the TURN server.
func (s *Server) Close() error {
	s.closeMutex.Lock()
	defer s.closeMutex.Unlock()
	if s.isClosed() {
		return nil
	}
	err := s.turn.Close()
	close(s.conns)
	close(s.closed)
	return err
}

func (s *Server) isClosed() bool {
	select {
	case <-s.closed:
		return true
	default:
		return false
	}
}

// listener implements net.Listener for the TURN
// server to consume.
type listener struct {
	srv *Server
}

func (l *listener) Accept() (net.Conn, error) {
	conn, ok := <-l.srv.conns
	if !ok {
		return nil, io.EOF
	}
	return conn, nil
}

func (*listener) Close() error {
	return nil
}

func (*listener) Addr() net.Addr {
	return nil
}

type Conn struct {
	net.Conn
	closed        chan struct{}
	localAddress  *net.TCPAddr
	remoteAddress *net.TCPAddr
}

func (c *Conn) LocalAddr() net.Addr {
	return c.localAddress
}

func (c *Conn) RemoteAddr() net.Addr {
	return c.remoteAddress
}

// Closed returns a channel which is closed when
// the connection is.
func (c *Conn) Closed() <-chan struct{} {
	return c.closed
}

func (c *Conn) Close() error {
	err := c.Conn.Close()
	select {
	case <-c.closed:
	default:
		close(c.closed)
	}
	return err
}

type dialer func(network, addr string) (c net.Conn, err error)

func (d dialer) Dial(network, addr string) (c net.Conn, err error) {
	return d(network, addr)
}

// ProxyDialer accepts a proxy function that's called when the connection
// address matches the reserved host in the "Proxy" ICE server.
//
// This should be passed to WebRTC connections as an ICE dialer.
func ProxyDialer(proxyFunc func() (c net.Conn, err error)) proxy.Dialer {
	return dialer(func(network, addr string) (net.Conn, error) {
		if addr != reservedAddress {
			return proxy.Direct.Dial(network, addr)
		}
		netConn, err := proxyFunc()
		if err != nil {
			return nil, err
		}
		return &Conn{
			localAddress: localhost,
			closed:       make(chan struct{}),
			Conn:         netConn,
		}, nil
	})
}
