package main

import (
	"io"
	"log"
	"net"
	"strconv"
	"time"
)

const (
	silenceTimeout = 5 * time.Second
	reconnectDelay = 2 * time.Second
	readBufSize    = 65536
	udpRcvBufSize  = 8 * 1024 * 1024 // 8 MB OS receive buffer — absorbs bursts at high data rates
)

// UDPClient manages the connection to one MARTe2 streamer source.
// In unicast mode (multicastGroup == "") it uses a single UDP socket for control and data.
// In multicast mode it uses TCP for control (CONNECT/CONFIG/DISCONNECT) and
// joins a UDP multicast group to receive DATA packets.
type UDPClient struct {
	serverAddr     string
	sourceID       string
	hub            *Hub
	multicastGroup string // "" = unicast mode
	dataPort       int    // UDP data port in multicast mode; 0 = serverAddr port+1

	stopCh chan struct{}
}

// NewUDPClient creates a UDPClient bound to a specific source ID. Call Run() in a goroutine.
// multicastGroup: multicast IP (e.g. "239.0.0.1") or "" for unicast.
// dataPort: UDP data port; 0 = serverAddr port+1.
func NewUDPClient(serverAddr, sourceID string, hub *Hub, multicastGroup string, dataPort int) *UDPClient {
	return &UDPClient{
		serverAddr:     serverAddr,
		sourceID:       sourceID,
		hub:            hub,
		multicastGroup: multicastGroup,
		dataPort:       dataPort,
		stopCh:         make(chan struct{}),
	}
}

// Stop asks the client to disconnect and exit.
func (u *UDPClient) Stop() {
	close(u.stopCh)
}

// Run is the main loop; it reconnects automatically if the server goes silent.
func (u *UDPClient) Run() {
	for {
		select {
		case <-u.stopCh:
			return
		default:
		}

		u.hub.SetSourceState(u.sourceID, "connecting")
		log.Printf("[%s] connecting to %s", u.sourceID, u.serverAddr)

		var err error
		if u.multicastGroup != "" {
			err = u.runMulticastSession()
		} else {
			err = u.runSession()
		}
		if err != nil {
			log.Printf("[%s] session ended: %v", u.sourceID, err)
		}
		u.hub.SetSourceState(u.sourceID, "disconnected")

		select {
		case <-u.stopCh:
			return
		case <-time.After(reconnectDelay):
		}
	}
}

// runSession opens a UDP socket, sends CONNECT, reads data until the server
// goes silent or an error occurs.
func (u *UDPClient) runSession() error {
	// Port 0 lets the OS pick a free local port.
	conn, err := net.ListenUDP("udp4", &net.UDPAddr{})
	if err != nil {
		return err
	}
	defer conn.Close()

	// Increase OS receive buffer to reduce kernel-level packet drops at high data rates.
	if err := conn.SetReadBuffer(udpRcvBufSize); err != nil {
		log.Printf("[%s] udp: SetReadBuffer: %v (proceeding with OS default)", u.sourceID, err)
	}

	serverAddr, err := net.ResolveUDPAddr("udp4", u.serverAddr)
	if err != nil {
		return err
	}

	if _, err := conn.WriteToUDP(BuildConnectPacket(), serverAddr); err != nil {
		return err
	}
	log.Printf("[%s] udp: sent CONNECT", u.sourceID)

	reassembler := NewReassembler(2 * time.Second)
	buf := make([]byte, readBufSize)
	var currentSigs []SignalInfo
	var currentPublishMode uint8

	for {
		conn.SetReadDeadline(time.Now().Add(silenceTimeout))

		n, _, err := conn.ReadFromUDP(buf)
		arrivalTime := time.Now()
		if err != nil {
			conn.WriteToUDP(BuildDisconnectPacket(), serverAddr)
			return err
		}

		if n < HeaderSize {
			log.Printf("[%s] udp: short datagram (%d bytes), skipping", u.sourceID, n)
			continue
		}

		hdr, err := ParseHeader(buf[:n])
		if err != nil {
			log.Printf("[%s] udp: parse header: %v", u.sourceID, err)
			continue
		}

		payload := make([]byte, n-HeaderSize)
		copy(payload, buf[HeaderSize:n])

		complete, ok := reassembler.AddFragment(hdr, payload)
		if hdr.Type == PktData {
			u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n, arrivalTime.UnixNano(), ok)
		}
		if !ok {
			continue
		}

		switch hdr.Type {
		case PktConfig:
			sigs, pm, err := ParseConfig(complete)
			if err != nil {
				log.Printf("[%s] udp: parse config: %v", u.sourceID, err)
				continue
			}
			currentSigs = sigs
			currentPublishMode = pm
			log.Printf("[%s] udp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(sigs), pm)
			u.hub.SetSourceState(u.sourceID, "connected")
			u.hub.UpdateConfigForSource(u.sourceID, sigs)

		case PktData:
			if len(currentSigs) == 0 {
				continue
			}
			samples, err := ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
			if err != nil {
				log.Printf("[%s] udp: parse data: %v", u.sourceID, err)
				continue
			}
			for _, s := range samples {
				u.hub.PushDataForSource(u.sourceID, s)
			}

		case PktACK:
			log.Printf("[%s] udp: received ACK (counter=%d)", u.sourceID, hdr.Counter)

		case PktDisconnect:
			log.Printf("[%s] udp: server sent DISCONNECT", u.sourceID)
			return nil

		default:
			log.Printf("[%s] udp: unknown packet type %d", u.sourceID, hdr.Type)
		}

		select {
		case <-u.stopCh:
			conn.WriteToUDP(BuildDisconnectPacket(), serverAddr)
			return nil
		default:
		}
	}
}

// runMulticastSession handles the multicast mode session:
//   - TCP connection to serverAddr for control (CONNECT → CONFIG, DISCONNECT)
//   - UDP multicast socket joined to multicastGroup:dataPort for DATA packets
func (u *UDPClient) runMulticastSession() error {
	// 1. Resolve TCP control address
	tcpAddr, err := net.ResolveTCPAddr("tcp4", u.serverAddr)
	if err != nil {
		return err
	}

	// 2. Dial TCP control connection
	tcpConn, err := net.DialTCP("tcp4", nil, tcpAddr)
	if err != nil {
		return err
	}
	defer tcpConn.Close()

	// 3. Send CONNECT via TCP
	if _, err := tcpConn.Write(BuildConnectPacket()); err != nil {
		return err
	}
	log.Printf("[%s] tcp: sent CONNECT to %s", u.sourceID, u.serverAddr)

	// 4. Read CONFIG header via TCP (io.ReadFull to handle partial reads)
	hdrBuf := make([]byte, HeaderSize)
	if _, err := io.ReadFull(tcpConn, hdrBuf); err != nil {
		return err
	}
	cfgHdr, err := ParseHeader(hdrBuf)
	if err != nil {
		return err
	}
	if cfgHdr.Type != PktConfig {
		return net.ErrClosed // unexpected packet type
	}
	cfgPayload := make([]byte, cfgHdr.PayloadBytes)
	if cfgHdr.PayloadBytes > 0 {
		if _, err := io.ReadFull(tcpConn, cfgPayload); err != nil {
			return err
		}
	}
	currentSigs, currentPublishMode, err := ParseConfig(cfgPayload)
	if err != nil {
		return err
	}
	log.Printf("[%s] tcp: received CONFIG (%d signals, publishMode=%d)", u.sourceID, len(currentSigs), currentPublishMode)
	u.hub.SetSourceState(u.sourceID, "connected")
	u.hub.UpdateConfigForSource(u.sourceID, currentSigs)

	// 5. Determine multicast data port (dataPort or serverAddr port+1)
	mcastPort := u.dataPort
	if mcastPort == 0 {
		mcastPort = tcpAddr.Port + 1
	}

	// 6. Join multicast group for DATA
	mcastIP := net.ParseIP(u.multicastGroup)
	if mcastIP == nil {
		return &net.AddrError{Err: "invalid multicast group IP", Addr: u.multicastGroup}
	}
	mcastAddr := &net.UDPAddr{IP: mcastIP, Port: mcastPort}
	mcastConn, err := net.ListenMulticastUDP("udp4", nil, mcastAddr)
	if err != nil {
		return err
	}
	defer mcastConn.Close()
	if err := mcastConn.SetReadBuffer(udpRcvBufSize); err != nil {
		log.Printf("[%s] multicast SetReadBuffer: %v", u.sourceID, err)
	}
	log.Printf("[%s] joined multicast %s:%s", u.sourceID, u.multicastGroup,
		strconv.Itoa(mcastPort))

	// 7. Background goroutine: watch TCP conn for DISCONNECT or closure
	tcpDone := make(chan error, 1)
	go func() {
		buf := make([]byte, HeaderSize+64)
		for {
			n, readErr := tcpConn.Read(buf)
			if readErr != nil {
				tcpDone <- readErr
				return
			}
			if n >= HeaderSize {
				hdr, parseErr := ParseHeader(buf[:n])
				if parseErr == nil && hdr.Type == PktDisconnect {
					tcpDone <- nil
					return
				}
			}
		}
	}()

	// 8. Main loop: receive DATA packets from multicast group
	reassembler := NewReassembler(2 * time.Second)
	buf := make([]byte, readBufSize)

	for {
		mcastConn.SetReadDeadline(time.Now().Add(silenceTimeout))
		n, _, readErr := mcastConn.ReadFromUDP(buf)
		arrivalTime := time.Now()
		if readErr != nil {
			select {
			case <-tcpDone:
				return nil
			default:
			}
			tcpConn.Write(BuildDisconnectPacket())
			return readErr
		}

		if n < HeaderSize {
			continue
		}
		hdr, parseErr := ParseHeader(buf[:n])
		if parseErr != nil {
			log.Printf("[%s] multicast: parse header: %v", u.sourceID, parseErr)
			continue
		}

		payload := make([]byte, n-HeaderSize)
		copy(payload, buf[HeaderSize:n])

		complete, ok := reassembler.AddFragment(hdr, payload)
		if hdr.Type == PktData {
			u.hub.RecordDataFragment(u.sourceID, hdr.Counter, n,
				arrivalTime.UnixNano(), ok)
		}
		if !ok {
			continue
		}

		if hdr.Type == PktData {
			if len(currentSigs) == 0 {
				continue
			}
			samples, parseErr := ParseData(complete, currentSigs, currentPublishMode, arrivalTime)
			if parseErr != nil {
				log.Printf("[%s] multicast: parse data: %v", u.sourceID, parseErr)
				continue
			}
			for _, s := range samples {
				u.hub.PushDataForSource(u.sourceID, s)
			}
		}

		select {
		case <-u.stopCh:
			tcpConn.Write(BuildDisconnectPacket())
			return nil
		case tcpErr := <-tcpDone:
			log.Printf("[%s] tcp control closed: %v", u.sourceID, tcpErr)
			return nil
		default:
		}
	}
}
