package bind

import (
	"context"
	"errors"
	"fmt"
	"net"
	"net/netip"
	"strings"
	"sync"

	log "github.com/sirupsen/logrus"

	"github.com/netbirdio/netbird/client/iface/bind"
)

type ProxyBind struct {
	Bind *bind.ICEBind

	fakeNetIP      *netip.AddrPort
	wgBindEndpoint *bind.Endpoint
	remoteConn     net.Conn
	ctx            context.Context
	cancel         context.CancelFunc
	closeMu        sync.Mutex
	closed         bool

	pausedMu  sync.Mutex
	paused    bool
	isStarted bool
}

// AddTurnConn adds a new connection to the bind.
// endpoint is the NetBird address of the remote peer. The SetEndpoint return with the address what will be used in the
// WireGuard configuration.
func (p *ProxyBind) AddTurnConn(ctx context.Context, nbAddr *net.UDPAddr, remoteConn net.Conn) error {
	fakeNetIP, err := fakeAddress(nbAddr)
	if err != nil {
		return err
	}

	p.fakeNetIP = fakeNetIP
	p.wgBindEndpoint = &bind.Endpoint{AddrPort: *fakeNetIP}
	p.remoteConn = remoteConn
	p.ctx, p.cancel = context.WithCancel(ctx)
	return nil

}
func (p *ProxyBind) EndpointAddr() *net.UDPAddr {
	return &net.UDPAddr{
		IP:   p.fakeNetIP.Addr().AsSlice(),
		Port: int(p.fakeNetIP.Port()),
		Zone: p.fakeNetIP.Addr().Zone(),
	}
}

func (p *ProxyBind) Work() {
	if p.remoteConn == nil {
		return
	}

	p.Bind.SetEndpoint(p.fakeNetIP.Addr(), p.remoteConn)

	p.pausedMu.Lock()
	p.paused = false
	p.pausedMu.Unlock()

	// Start the proxy only once
	if !p.isStarted {
		p.isStarted = true
		go p.proxyToLocal(p.ctx)
	}
}

func (p *ProxyBind) Pause() {
	if p.remoteConn == nil {
		return
	}

	p.pausedMu.Lock()
	p.paused = true
	p.pausedMu.Unlock()
}

func (p *ProxyBind) CloseConn() error {
	if p.cancel == nil {
		return fmt.Errorf("proxy not started")
	}
	return p.close()
}

func (p *ProxyBind) close() error {
	p.closeMu.Lock()
	defer p.closeMu.Unlock()

	if p.closed {
		return nil
	}
	p.closed = true

	p.cancel()

	p.Bind.RemoveEndpoint(p.fakeNetIP.Addr())

	if rErr := p.remoteConn.Close(); rErr != nil && !errors.Is(rErr, net.ErrClosed) {
		return rErr
	}
	return nil
}

func (p *ProxyBind) proxyToLocal(ctx context.Context) {
	defer func() {
		if err := p.close(); err != nil {
			log.Warnf("failed to close remote conn: %s", err)
		}
	}()

	for {
		buf := make([]byte, 1500)
		n, err := p.remoteConn.Read(buf)
		if err != nil {
			if ctx.Err() != nil {
				return
			}
			log.Errorf("failed to read from remote conn: %s, %s", p.remoteConn.RemoteAddr(), err)
			return
		}

		p.pausedMu.Lock()
		if p.paused {
			p.pausedMu.Unlock()
			continue
		}

		msg := bind.RecvMessage{
			Endpoint: p.wgBindEndpoint,
			Buffer:   buf[:n],
		}
		p.Bind.RecvChan <- msg
		p.pausedMu.Unlock()
	}
}

// fakeAddress returns a fake address that is used to as an identifier for the peer.
// The fake address is in the format of 127.1.x.x where x.x is the last two octets of the peer address.
func fakeAddress(peerAddress *net.UDPAddr) (*netip.AddrPort, error) {
	octets := strings.Split(peerAddress.IP.String(), ".")
	if len(octets) != 4 {
		return nil, fmt.Errorf("invalid IP format")
	}

	fakeIP, err := netip.ParseAddr(fmt.Sprintf("127.1.%s.%s", octets[2], octets[3]))
	if err != nil {
		return nil, fmt.Errorf("failed to parse new IP: %w", err)
	}

	netipAddr := netip.AddrPortFrom(fakeIP, uint16(peerAddress.Port))
	return &netipAddr, nil
}
