package identify

import (
	"context"
	"fmt"
	"sync"
	"time"

	"github.com/libp2p/go-libp2p/core/event"
	"github.com/libp2p/go-libp2p/core/host"
	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/p2p/host/eventbus"
)

type natEmitter struct {
	ctx             context.Context
	cancel          context.CancelFunc
	wg              sync.WaitGroup
	reachabilitySub event.Subscription
	reachability    network.Reachability
	eventInterval   time.Duration

	currentUDPNATDeviceType  network.NATDeviceType
	currentTCPNATDeviceType  network.NATDeviceType
	emitNATDeviceTypeChanged event.Emitter

	observedAddrMgr *ObservedAddrManager
}

func newNATEmitter(h host.Host, o *ObservedAddrManager, eventInterval time.Duration) (*natEmitter, error) {
	ctx, cancel := context.WithCancel(context.Background())
	n := &natEmitter{
		observedAddrMgr: o,
		ctx:             ctx,
		cancel:          cancel,
		eventInterval:   eventInterval,
	}
	reachabilitySub, err := h.EventBus().Subscribe(new(event.EvtLocalReachabilityChanged), eventbus.Name("identify (nat emitter)"))
	if err != nil {
		return nil, fmt.Errorf("failed to subscribe to reachability event: %s", err)
	}
	n.reachabilitySub = reachabilitySub

	emitter, err := h.EventBus().Emitter(new(event.EvtNATDeviceTypeChanged), eventbus.Stateful)
	if err != nil {
		return nil, fmt.Errorf("failed to create emitter for NATDeviceType: %s", err)
	}
	n.emitNATDeviceTypeChanged = emitter

	n.wg.Add(1)
	go n.worker()
	return n, nil
}

func (n *natEmitter) worker() {
	defer n.wg.Done()
	subCh := n.reachabilitySub.Out()
	ticker := time.NewTicker(n.eventInterval)
	pendingUpdate := false
	enoughTimeSinceLastUpdate := true
	for {
		select {
		case evt, ok := <-subCh:
			if !ok {
				subCh = nil
				continue
			}
			ev, ok := evt.(event.EvtLocalReachabilityChanged)
			if !ok {
				log.Error("invalid event: %v", evt)
				continue
			}
			n.reachability = ev.Reachability
		case <-ticker.C:
			enoughTimeSinceLastUpdate = true
			if pendingUpdate {
				n.maybeNotify()
				pendingUpdate = false
				enoughTimeSinceLastUpdate = false
			}
		case <-n.observedAddrMgr.addrRecordedNotif:
			pendingUpdate = true
			if enoughTimeSinceLastUpdate {
				n.maybeNotify()
				pendingUpdate = false
				enoughTimeSinceLastUpdate = false
			}
		case <-n.ctx.Done():
			return
		}
	}
}

func (n *natEmitter) maybeNotify() {
	if n.reachability == network.ReachabilityPrivate {
		tcpNATType, udpNATType := n.observedAddrMgr.getNATType()
		if tcpNATType != n.currentTCPNATDeviceType {
			n.currentTCPNATDeviceType = tcpNATType
			n.emitNATDeviceTypeChanged.Emit(event.EvtNATDeviceTypeChanged{
				TransportProtocol: network.NATTransportTCP,
				NatDeviceType:     n.currentTCPNATDeviceType,
			})
		}
		if udpNATType != n.currentUDPNATDeviceType {
			n.currentUDPNATDeviceType = udpNATType
			n.emitNATDeviceTypeChanged.Emit(event.EvtNATDeviceTypeChanged{
				TransportProtocol: network.NATTransportUDP,
				NatDeviceType:     n.currentUDPNATDeviceType,
			})
		}
	}
}

func (n *natEmitter) Close() {
	n.cancel()
	n.wg.Wait()
	n.reachabilitySub.Close()
	n.emitNATDeviceTypeChanged.Close()
}