package proxy

import (
	"log"
	"net"
	"sync"
	"time"

	"github.com/wormhole/wormhole/internal/protocol"
	"github.com/wormhole/wormhole/internal/store"
)

type tcpServer struct {
	manager *Manager
	tunnel  *store.Tunnel
	ln      net.Listener
	stop    chan struct{}
	wg      sync.WaitGroup
}

func newTCPServer(m *Manager, t *store.Tunnel) Service {
	return &tcpServer{
		manager: m,
		tunnel:  t,
		stop:    make(chan struct{}),
	}
}

func (s *tcpServer) ID() uint { return s.tunnel.ID }

func (s *tcpServer) Start() error {
	addr := net.JoinHostPort("0.0.0.0", itoa(s.tunnel.ListenPort))
	ln, err := net.Listen("tcp", addr)
	if err != nil {
		return err
	}
	s.ln = ln
	log.Printf("[proxy] tcp tunnel %d listening on %s -> %s", s.tunnel.ID, addr, s.tunnel.TargetAddr)
	go s.acceptLoop()
	return nil
}

func (s *tcpServer) acceptLoop() {
	for {
		conn, err := s.ln.Accept()
		if err != nil {
			select {
			case <-s.stop:
				return
			default:
				continue
			}
		}
		s.wg.Add(1)
		go func(c net.Conn) {
			defer s.wg.Done()
			s.manager.handleTCPConnection(s.tunnel, c)
		}(conn)
	}
}

func (s *tcpServer) Stop() error {
	close(s.stop)
	if s.ln != nil {
		_ = s.ln.Close()
	}
	s.wg.Wait()
	return nil
}

type udpSession struct {
	stream net.Conn
	last   time.Time
}

type udpServer struct {
	manager *Manager
	tunnel  *store.Tunnel
	conn    *net.UDPConn
	stop    chan struct{}
	mu      sync.Mutex
	sessions map[string]*udpSession
}

func newUDPServer(m *Manager, t *store.Tunnel) Service {
	return &udpServer{
		manager:  m,
		tunnel:   t,
		stop:     make(chan struct{}),
		sessions: make(map[string]*udpSession),
	}
}

func (s *udpServer) ID() uint { return s.tunnel.ID }

func (s *udpServer) Start() error {
	addr := net.JoinHostPort("0.0.0.0", itoa(s.tunnel.ListenPort))
	udpAddr, err := net.ResolveUDPAddr("udp", addr)
	if err != nil {
		return err
	}
	conn, err := net.ListenUDP("udp", udpAddr)
	if err != nil {
		return err
	}
	s.conn = conn
	log.Printf("[proxy] udp tunnel %d listening on %s -> %s", s.tunnel.ID, addr, s.tunnel.TargetAddr)
	go s.readLoop()
	go s.sweeper()
	return nil
}

func (s *udpServer) readLoop() {
	buf := make([]byte, 65535)
	for {
		n, remote, err := s.conn.ReadFromUDP(buf)
		if err != nil {
			select {
			case <-s.stop:
				return
			default:
				continue
			}
		}
		ip := remote.IP.String()
		s.manager.metrics.RecordRequestIP("tunnel", s.tunnel.ID, ip, ip)
		if !s.manager.security.Allow(ip, s.tunnel.ClientID, "tunnel", s.tunnel.ID) {
			continue
		}
		key := remote.String()
		s.mu.Lock()
		sess, ok := s.sessions[key]
		if !ok {
			sess = s.createSession(remote)
			if sess == nil {
				s.mu.Unlock()
				continue
			}
			s.sessions[key] = sess
			go s.runSession(key, sess, remote)
		}
		sess.last = time.Now()
		stream := sess.stream
		s.mu.Unlock()
		_, _ = stream.Write(buf[:n])
	}
}

func (s *udpServer) createSession(remote *net.UDPAddr) *udpSession {
	if !s.manager.bridge.IsOnline(s.tunnel.ClientID) {
		return nil
	}
	mode, _ := resolveProxyConfig(s.manager.store, s.tunnel.ClientID, s.tunnel.IPForwardMode, s.tunnel.CustomHeaders)
	meta := &protocol.LinkMeta{
		ConnType:   protocol.ConnTypeUDP,
		Target:     s.tunnel.TargetAddr,
		RemoteAddr: forwardIPForTCP(remote.String(), mode),
	}
	stream, err := s.manager.bridge.OpenStream(s.tunnel.ClientID, meta)
	if err != nil {
		return nil
	}
	return &udpSession{stream: stream, last: time.Now()}
}

func (s *udpServer) runSession(key string, sess *udpSession, remote *net.UDPAddr) {
	buf := make([]byte, 65535)
	for {
		n, err := sess.stream.Read(buf)
		if err != nil {
			break
		}
		_, _ = s.conn.WriteToUDP(buf[:n], remote)
		inlet := int64(n)
		s.manager.metrics.AddFlow("tunnel", s.tunnel.ID, s.tunnel.ClientID, inlet, 0)
	}
	s.mu.Lock()
	delete(s.sessions, key)
	s.mu.Unlock()
	_ = sess.stream.Close()
}

func (s *udpServer) sweeper() {
	ticker := time.NewTicker(time.Minute)
	defer ticker.Stop()
	for {
		select {
		case <-ticker.C:
			now := time.Now()
			s.mu.Lock()
			for k, sess := range s.sessions {
				if now.Sub(sess.last) > 120*time.Second {
					_ = sess.stream.Close()
					delete(s.sessions, k)
				}
			}
			s.mu.Unlock()
		case <-s.stop:
			return
		}
	}
}

func (s *udpServer) Stop() error {
	close(s.stop)
	if s.conn != nil {
		_ = s.conn.Close()
	}
	s.mu.Lock()
	for _, sess := range s.sessions {
		_ = sess.stream.Close()
	}
	s.sessions = make(map[string]*udpSession)
	s.mu.Unlock()
	return nil
}
