// SPDX-License-Identifier: 0BSD
// Copyright (c) 2024-2026 Sudo-Ivan / Quad4.io
//go:build !tinygo
// +build !tinygo

package interfaces

import (
	"bytes"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"net"
	"sync"
	"time"

	"git.quad4.io/Networks/Reticulum-Go/pkg/common"
	"git.quad4.io/Networks/Reticulum-Go/pkg/debug"
)

const (
	HW_MTU                 = 1196
	DEFAULT_DISCOVERY_PORT = 29716
	DEFAULT_DATA_PORT      = 42671
	DEFAULT_GROUP_ID       = "reticulum"
	BITRATE_GUESS          = 10 * 1000 * 1000
	PEERING_TIMEOUT        = 22 * time.Second
	ANNOUNCE_INTERVAL      = 1600 * time.Millisecond
	PEER_JOB_INTERVAL      = 4 * time.Second
	MCAST_ECHO_TIMEOUT     = 6500 * time.Millisecond

	SCOPE_LINK         = "2"
	SCOPE_ADMIN        = "4"
	SCOPE_SITE         = "5"
	SCOPE_ORGANISATION = "8"
	SCOPE_GLOBAL       = "e"

	MCAST_ADDR_TYPE_PERMANENT = "0"
	MCAST_ADDR_TYPE_TEMPORARY = "1"
)

type AutoInterface struct {
	BaseInterface
	groupID            []byte
	groupHash          []byte
	discoveryPort      int
	dataPort           int
	discoveryScope     string
	multicastAddrType  string
	mcastDiscoveryAddr string
	ifacNetname        string
	peers              map[string]*Peer
	linkLocalAddrs     []string
	adoptedInterfaces  map[string]*AdoptedInterface
	interfaceServers   map[string]*net.UDPConn
	discoveryServers   map[string]*net.UDPConn
	multicastEchoes    map[string]time.Time
	timedOutInterfaces map[string]time.Time
	allowedInterfaces  []string
	ignoredInterfaces  []string
	outboundConn       *net.UDPConn
	announceInterval   time.Duration
	peerJobInterval    time.Duration
	peeringTimeout     time.Duration
	mcastEchoTimeout   time.Duration
	done               chan struct{}
	stopOnce           sync.Once
}

type AdoptedInterface struct {
	name          string
	linkLocalAddr string
	index         int
}

type Peer struct {
	ifaceName string
	lastHeard time.Time
	addr      *net.UDPAddr
}

func NewAutoInterface(name string, config *common.InterfaceConfig) (*AutoInterface, error) {
	groupID := DEFAULT_GROUP_ID
	if config.GroupID != "" {
		groupID = config.GroupID
	}

	discoveryScope := SCOPE_LINK
	if config.DiscoveryScope != "" {
		discoveryScope = normalizeScope(config.DiscoveryScope)
	}

	multicastAddrType := MCAST_ADDR_TYPE_TEMPORARY

	discoveryPort := DEFAULT_DISCOVERY_PORT
	if config.DiscoveryPort != 0 {
		discoveryPort = config.DiscoveryPort
	}

	dataPort := DEFAULT_DATA_PORT
	if config.DataPort != 0 {
		dataPort = config.DataPort
	}

	groupHash := sha256.Sum256([]byte(groupID))

	ifacNetname := hex.EncodeToString(groupHash[:])[:16]
	mcastAddr := fmt.Sprintf("ff%s%s::%s", discoveryScope, multicastAddrType, ifacNetname)

	ai := &AutoInterface{
		BaseInterface: BaseInterface{
			Name:     name,
			Mode:     common.IF_MODE_FULL,
			Type:     common.IF_TYPE_AUTO,
			Online:   false,
			Enabled:  config.Enabled,
			Detached: false,
			IN:       true,
			OUT:      false,
			MTU:      HW_MTU,
			Bitrate:  BITRATE_GUESS,
		},
		groupID:            []byte(groupID),
		groupHash:          groupHash[:],
		discoveryPort:      discoveryPort,
		dataPort:           dataPort,
		discoveryScope:     discoveryScope,
		multicastAddrType:  multicastAddrType,
		mcastDiscoveryAddr: mcastAddr,
		ifacNetname:        ifacNetname,
		peers:              make(map[string]*Peer),
		linkLocalAddrs:     make([]string, 0),
		adoptedInterfaces:  make(map[string]*AdoptedInterface),
		interfaceServers:   make(map[string]*net.UDPConn),
		discoveryServers:   make(map[string]*net.UDPConn),
		multicastEchoes:    make(map[string]time.Time),
		timedOutInterfaces: make(map[string]time.Time),
		allowedInterfaces:  make([]string, 0),
		ignoredInterfaces:  make([]string, 0),
		announceInterval:   ANNOUNCE_INTERVAL,
		peerJobInterval:    PEER_JOB_INTERVAL,
		peeringTimeout:     PEERING_TIMEOUT,
		mcastEchoTimeout:   MCAST_ECHO_TIMEOUT,
		done:               make(chan struct{}),
	}

	debug.Log(debug.DEBUG_INFO, "AutoInterface configured", "name", name, "group", groupID, "mcast_addr", mcastAddr)
	return ai, nil
}

func normalizeScope(scope string) string {
	switch scope {
	case "link", "2":
		return SCOPE_LINK
	case "admin", "4":
		return SCOPE_ADMIN
	case "site", "5":
		return SCOPE_SITE
	case "organisation", "organization", "8":
		return SCOPE_ORGANISATION
	case "global", "e":
		return SCOPE_GLOBAL
	default:
		return SCOPE_LINK
	}
}

func normalizeMulticastType(mtype string) string {
	switch mtype {
	case "permanent", "0":
		return MCAST_ADDR_TYPE_PERMANENT
	case "temporary", "1":
		return MCAST_ADDR_TYPE_TEMPORARY
	default:
		return MCAST_ADDR_TYPE_TEMPORARY
	}
}

func (ai *AutoInterface) Start() error {
	ai.Mutex.Lock()
	// Only recreate done if it's nil or was closed
	select {
	case <-ai.done:
		ai.done = make(chan struct{})
		ai.stopOnce = sync.Once{}
	default:
		if ai.done == nil {
			ai.done = make(chan struct{})
			ai.stopOnce = sync.Once{}
		}
	}
	ai.Mutex.Unlock()

	interfaces, err := net.Interfaces()
	if err != nil {
		return fmt.Errorf("failed to list interfaces: %v", err)
	}

	for _, iface := range interfaces {
		if ai.shouldIgnoreInterface(iface.Name) {
			debug.Log(debug.DEBUG_TRACE, "Ignoring interface", "name", iface.Name)
			continue
		}

		if len(ai.allowedInterfaces) > 0 && !ai.isAllowedInterface(iface.Name) {
			debug.Log(debug.DEBUG_TRACE, "Interface not in allowed list", "name", iface.Name)
			continue
		}

		ifaceCopy := iface
		// bearer:disable go_gosec_memory_memory_aliasing
		if err := ai.configureInterface(&ifaceCopy); err != nil {
			debug.Log(debug.DEBUG_VERBOSE, "Failed to configure interface", "name", iface.Name, "error", err)
			continue
		}
	}

	if len(ai.adoptedInterfaces) == 0 {
		return fmt.Errorf("no suitable interfaces found")
	}

	ai.Online = true
	ai.IN = true
	ai.OUT = true

	go ai.peerJobs()
	go ai.announceLoop()

	debug.Log(debug.DEBUG_INFO, "AutoInterface started", "adopted", len(ai.adoptedInterfaces))
	return nil
}

func (ai *AutoInterface) shouldIgnoreInterface(name string) bool {
	ignoreList := []string{"lo", "lo0", "tun0", "awdl0", "llw0", "en5", "dummy0"}

	for _, ignored := range ai.ignoredInterfaces {
		if name == ignored {
			return true
		}
	}

	for _, ignored := range ignoreList {
		if name == ignored {
			return true
		}
	}

	return false
}

func (ai *AutoInterface) isAllowedInterface(name string) bool {
	for _, allowed := range ai.allowedInterfaces {
		if name == allowed {
			return true
		}
	}
	return false
}

func (ai *AutoInterface) configureInterface(iface *net.Interface) error {
	if iface.Flags&net.FlagUp == 0 {
		return fmt.Errorf("interface is down")
	}

	if iface.Flags&net.FlagLoopback != 0 {
		return fmt.Errorf("loopback interface")
	}

	addrs, err := iface.Addrs()
	if err != nil {
		return err
	}

	var linkLocalAddr string
	for _, addr := range addrs {
		if ipnet, ok := addr.(*net.IPNet); ok {
			if ipnet.IP.To4() == nil && ipnet.IP.IsLinkLocalUnicast() {
				linkLocalAddr = ipnet.IP.String()
				break
			}
		}
	}

	if linkLocalAddr == "" {
		return fmt.Errorf("no link-local IPv6 address found")
	}

	ai.Mutex.Lock()
	ai.adoptedInterfaces[iface.Name] = &AdoptedInterface{
		name:          iface.Name,
		linkLocalAddr: linkLocalAddr,
		index:         iface.Index,
	}
	ai.linkLocalAddrs = append(ai.linkLocalAddrs, linkLocalAddr)
	ai.multicastEchoes[iface.Name] = time.Now()
	ai.Mutex.Unlock()

	if err := ai.startDiscoveryListener(iface); err != nil {
		return fmt.Errorf("failed to start discovery listener: %v", err)
	}

	if err := ai.startDataListener(iface); err != nil {
		return fmt.Errorf("failed to start data listener: %v", err)
	}

	debug.Log(debug.DEBUG_INFO, "Configured interface", "name", iface.Name, "addr", linkLocalAddr)
	return nil
}

func (ai *AutoInterface) startDiscoveryListener(iface *net.Interface) error {
	addr := &net.UDPAddr{
		IP:   net.ParseIP(ai.mcastDiscoveryAddr),
		Port: ai.discoveryPort,
		Zone: iface.Name,
	}

	conn, err := net.ListenMulticastUDP("udp6", iface, addr)
	if err != nil {
		return err
	}

	if err := conn.SetReadBuffer(common.NUM_1024); err != nil {
		debug.Log(debug.DEBUG_ERROR, "Failed to set discovery read buffer", "error", err)
	}

	ai.Mutex.Lock()
	ai.discoveryServers[iface.Name] = conn
	ai.Mutex.Unlock()

	go ai.handleDiscovery(conn, iface.Name)
	debug.Log(debug.DEBUG_VERBOSE, "Discovery listener started", "interface", iface.Name, "addr", ai.mcastDiscoveryAddr)
	return nil
}

func (ai *AutoInterface) startDataListener(iface *net.Interface) error {
	adoptedIface, exists := ai.adoptedInterfaces[iface.Name]
	if !exists {
		return fmt.Errorf("interface not adopted")
	}

	addr := &net.UDPAddr{
		IP:   net.ParseIP(adoptedIface.linkLocalAddr),
		Port: ai.dataPort,
		Zone: iface.Name,
	}

	conn, err := net.ListenUDP("udp6", addr)
	if err != nil {
		debug.Log(debug.DEBUG_ERROR, "Failed to listen on data port", "addr", addr, "error", err)
		return err
	}

	if err := conn.SetReadBuffer(ai.MTU); err != nil {
		debug.Log(debug.DEBUG_ERROR, "Failed to set data read buffer", "error", err)
	}

	ai.Mutex.Lock()
	ai.interfaceServers[iface.Name] = conn
	ai.Mutex.Unlock()

	go ai.handleData(conn, iface.Name)
	debug.Log(debug.DEBUG_VERBOSE, "Data listener started", "interface", iface.Name, "addr", addr)
	return nil
}

func (ai *AutoInterface) handleDiscovery(conn *net.UDPConn, ifaceName string) {
	buf := make([]byte, common.NUM_1024)
	for {
		ai.Mutex.RLock()
		done := ai.done
		ai.Mutex.RUnlock()

		select {
		case <-done:
			return
		default:
		}

		n, remoteAddr, err := conn.ReadFromUDP(buf)
		if err != nil {
			if ai.IsOnline() {
				debug.Log(debug.DEBUG_ERROR, "Discovery read error", "interface", ifaceName, "error", err)
			}
			return
		}

		if n >= len(ai.groupHash) {
			receivedHash := buf[:len(ai.groupHash)]
			if bytes.Equal(receivedHash, ai.groupHash) {
				ai.handlePeerAnnounce(remoteAddr, ifaceName)
			} else {
				debug.Log(debug.DEBUG_TRACE, "Received discovery with mismatched group hash", "interface", ifaceName)
			}
		}
	}
}

func (ai *AutoInterface) handleData(conn *net.UDPConn, ifaceName string) {
	buf := make([]byte, ai.GetMTU())
	for {
		ai.Mutex.RLock()
		done := ai.done
		ai.Mutex.RUnlock()

		select {
		case <-done:
			return
		default:
		}

		n, _, err := conn.ReadFromUDP(buf)
		if err != nil {
			if ai.IsOnline() {
				debug.Log(debug.DEBUG_ERROR, "Data read error", "interface", ifaceName, "error", err)
			}
			return
		}

		if callback := ai.GetPacketCallback(); callback != nil {
			callback(buf[:n], ai)
		}
	}
}

func (ai *AutoInterface) handlePeerAnnounce(addr *net.UDPAddr, ifaceName string) {
	ai.Mutex.Lock()
	defer ai.Mutex.Unlock()

	peerIP := addr.IP.String()

	for _, localAddr := range ai.linkLocalAddrs {
		if peerIP == localAddr {
			ai.multicastEchoes[ifaceName] = time.Now()
			debug.Log(debug.DEBUG_TRACE, "Received own multicast echo", "interface", ifaceName)
			return
		}
	}

	peerKey := peerIP + "%" + ifaceName

	if peer, exists := ai.peers[peerKey]; exists {
		peer.lastHeard = time.Now()
		debug.Log(debug.DEBUG_TRACE, "Updated peer", "peer", peerIP, "interface", ifaceName)
	} else {
		ai.peers[peerKey] = &Peer{
			ifaceName: ifaceName,
			lastHeard: time.Now(),
			addr:      addr,
		}
		debug.Log(debug.DEBUG_INFO, "Discovered new peer", "peer", peerIP, "interface", ifaceName)
	}
}

func (ai *AutoInterface) announceLoop() {
	ticker := time.NewTicker(ai.announceInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			if !ai.IsOnline() {
				return
			}
			ai.sendPeerAnnounce()
		case <-ai.done:
			return
		}
	}
}

func (ai *AutoInterface) sendPeerAnnounce() {
	ai.Mutex.RLock()
	defer ai.Mutex.RUnlock()

	for ifaceName, adoptedIface := range ai.adoptedInterfaces {
		mcastAddr := &net.UDPAddr{
			IP:   net.ParseIP(ai.mcastDiscoveryAddr),
			Port: ai.discoveryPort,
			Zone: ifaceName,
		}

		if ai.outboundConn == nil {
			var err error
			ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
			if err != nil {
				debug.Log(debug.DEBUG_ERROR, "Failed to create outbound socket", "error", err)
				return
			}
		}

		if _, err := ai.outboundConn.WriteToUDP(ai.groupHash, mcastAddr); err != nil {
			debug.Log(debug.DEBUG_VERBOSE, "Failed to send peer announce", "interface", ifaceName, "error", err)
		} else {
			debug.Log(debug.DEBUG_TRACE, "Sent peer announce", "interface", adoptedIface.name)
		}
	}
}

func (ai *AutoInterface) peerJobs() {
	ticker := time.NewTicker(ai.peerJobInterval)
	defer ticker.Stop()

	for {
		select {
		case <-ticker.C:
			if !ai.IsOnline() {
				return
			}

			ai.Mutex.Lock()
			now := time.Now()

			for peerKey, peer := range ai.peers {
				if now.Sub(peer.lastHeard) > ai.peeringTimeout {
					delete(ai.peers, peerKey)
					debug.Log(debug.DEBUG_VERBOSE, "Removed timed out peer", "peer", peerKey)
				}
			}

			for ifaceName, echoTime := range ai.multicastEchoes {
				if now.Sub(echoTime) > ai.mcastEchoTimeout {
					if _, exists := ai.timedOutInterfaces[ifaceName]; !exists {
						debug.Log(debug.DEBUG_INFO, "Interface timed out", "interface", ifaceName)
						ai.timedOutInterfaces[ifaceName] = now
					}
				} else {
					delete(ai.timedOutInterfaces, ifaceName)
				}
			}

			ai.Mutex.Unlock()
		case <-ai.done:
			return
		}
	}
}

func (ai *AutoInterface) Send(data []byte, address string) error {
	if !ai.IsOnline() {
		return fmt.Errorf("interface offline")
	}

	ai.Mutex.RLock()
	defer ai.Mutex.RUnlock()

	if len(ai.peers) == 0 {
		debug.Log(debug.DEBUG_TRACE, "No peers available for sending")
		return nil
	}

	if ai.outboundConn == nil {
		var err error
		ai.outboundConn, err = net.ListenUDP("udp6", &net.UDPAddr{Port: 0})
		if err != nil {
			return fmt.Errorf("failed to create outbound socket: %v", err)
		}
	}

	sentCount := 0
	for _, peer := range ai.peers {
		targetAddr := &net.UDPAddr{
			IP:   peer.addr.IP,
			Port: ai.dataPort,
			Zone: peer.ifaceName,
		}

		if _, err := ai.outboundConn.WriteToUDP(data, targetAddr); err != nil {
			debug.Log(debug.DEBUG_VERBOSE, "Failed to send to peer", "interface", peer.ifaceName, "error", err)
			continue
		}
		sentCount++
	}

	if sentCount > 0 {
		debug.Log(debug.DEBUG_TRACE, "Sent data to peers", "count", sentCount, "bytes", len(data))
	}

	return nil
}

func (ai *AutoInterface) Stop() error {
	ai.Mutex.Lock()
	ai.Online = false
	ai.IN = false
	ai.OUT = false

	for _, server := range ai.interfaceServers {
		server.Close() // #nosec G104
	}

	for _, server := range ai.discoveryServers {
		server.Close() // #nosec G104
	}

	if ai.outboundConn != nil {
		ai.outboundConn.Close() // #nosec G104
	}
	ai.Mutex.Unlock()

	ai.stopOnce.Do(func() {
		if ai.done != nil {
			close(ai.done)
		}
	})

	debug.Log(debug.DEBUG_INFO, "AutoInterface stopped")
	return nil
}
