package nftables

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

	"github.com/cenkalti/backoff/v4"
	"github.com/google/nftables"
	log "github.com/sirupsen/logrus"
)

const (
	externalMonitorReconcileDelay = 500 * time.Millisecond
	externalMonitorInitInterval   = 5 * time.Second
	externalMonitorMaxInterval    = 5 * time.Minute
	externalMonitorRandomization  = 0.5
)

// externalChainReconciler re-applies passthrough accept rules to external
// nftables chains. Implementations must be safe to call from the monitor
// goroutine; the Manager locks its mutex internally.
type externalChainReconciler interface {
	reconcileExternalChains() error
}

// externalChainMonitor watches nftables netlink events and triggers a
// reconcile when a new table or chain appears (e.g. after
// `firewall-cmd --reload`). Netlink errors trigger exponential-backoff
// reconnect.
type externalChainMonitor struct {
	reconciler externalChainReconciler

	mu     sync.Mutex
	cancel context.CancelFunc
	done   chan struct{}
}

func newExternalChainMonitor(r externalChainReconciler) *externalChainMonitor {
	return &externalChainMonitor{reconciler: r}
}

func (m *externalChainMonitor) start() {
	m.mu.Lock()
	defer m.mu.Unlock()

	if m.cancel != nil {
		return
	}

	ctx, cancel := context.WithCancel(context.Background())
	m.cancel = cancel
	done := make(chan struct{})
	m.done = done

	go m.run(ctx, done)
}

func (m *externalChainMonitor) stop() {
	m.mu.Lock()
	cancel := m.cancel
	done := m.done
	m.cancel = nil
	m.done = nil
	m.mu.Unlock()

	if cancel == nil {
		return
	}
	cancel()
	<-done
}

func (m *externalChainMonitor) run(ctx context.Context, done chan struct{}) {
	defer close(done)

	bo := &backoff.ExponentialBackOff{
		InitialInterval:     externalMonitorInitInterval,
		RandomizationFactor: externalMonitorRandomization,
		Multiplier:          backoff.DefaultMultiplier,
		MaxInterval:         externalMonitorMaxInterval,
		MaxElapsedTime:      0,
		Clock:               backoff.SystemClock,
	}
	bo.Reset()

	for ctx.Err() == nil {
		err := m.watch(ctx)
		if ctx.Err() != nil {
			return
		}

		delay := bo.NextBackOff()
		log.Warnf("external chain monitor: %v, reconnecting in %s", err, delay)
		select {
		case <-ctx.Done():
			return
		case <-time.After(delay):
		}
	}
}

func (m *externalChainMonitor) watch(ctx context.Context) error {
	events, closeMon, err := m.subscribe()
	if err != nil {
		return err
	}
	defer closeMon()

	debounce := time.NewTimer(time.Hour)
	if !debounce.Stop() {
		<-debounce.C
	}
	defer debounce.Stop()

	pending := false
	for {
		select {
		case <-ctx.Done():
			return nil
		case <-debounce.C:
			pending = false
			m.reconcile()
		case ev, ok := <-events:
			if !ok {
				return errors.New("monitor channel closed")
			}
			if ev.Error != nil {
				return fmt.Errorf("monitor event: %w", ev.Error)
			}
			if !isRelevantMonitorEvent(ev) {
				continue
			}
			resetDebounce(debounce, pending)
			pending = true
		}
	}
}

func (m *externalChainMonitor) subscribe() (chan *nftables.MonitorEvent, func(), error) {
	conn := &nftables.Conn{}
	mon := nftables.NewMonitor(
		nftables.WithMonitorAction(nftables.MonitorActionNew),
		nftables.WithMonitorObject(nftables.MonitorObjectChains|nftables.MonitorObjectTables),
	)
	events, err := conn.AddMonitor(mon)
	if err != nil {
		return nil, nil, fmt.Errorf("add netlink monitor: %w", err)
	}
	return events, func() { _ = mon.Close() }, nil
}

// resetDebounce reschedules a pending debounce timer without leaking a stale
// fire on its channel. pending must reflect whether the timer is armed.
func resetDebounce(t *time.Timer, pending bool) {
	if pending && !t.Stop() {
		select {
		case <-t.C:
		default:
		}
	}
	t.Reset(externalMonitorReconcileDelay)
}

func (m *externalChainMonitor) reconcile() {
	if err := m.reconciler.reconcileExternalChains(); err != nil {
		log.Warnf("reconcile external chain rules: %v", err)
	}
}

// isRelevantMonitorEvent returns true for table/chain creation events on
// families we care about. The reconciler filters to actual external filter
// chains.
func isRelevantMonitorEvent(ev *nftables.MonitorEvent) bool {
	switch ev.Type {
	case nftables.MonitorEventTypeNewChain:
		chain, ok := ev.Data.(*nftables.Chain)
		if !ok || chain == nil || chain.Table == nil {
			return false
		}
		return isMonitoredFamily(chain.Table.Family)
	case nftables.MonitorEventTypeNewTable:
		table, ok := ev.Data.(*nftables.Table)
		if !ok || table == nil {
			return false
		}
		return isMonitoredFamily(table.Family)
	}
	return false
}

func isMonitoredFamily(family nftables.TableFamily) bool {
	switch family {
	case nftables.TableFamilyIPv4, nftables.TableFamilyIPv6, nftables.TableFamilyINet:
		return true
	}
	return false
}
