// Package nftables provides methods to create an nftables table and manage its maps, sets,
// chains, and rules.
//
// To use it, the first step is to create a [Table] using [NewTable]. Then, retrieve
// a [Modifier], add commands to it, and apply the updates.
//
// For example:
//
//	t, _ := NewTable(...)
//	tm := t.Modifier()
//	// Then a sequence of ...
//	tm.Create(<object>)
//	tm.Delete(<object>)
//	// Apply the updates with ...
//	err := tm.Apply(ctx)
//
// The objects are any of: [BaseChain], [Chain], [Rule], [Map], [MapElement],
// [Set], [SetElement]
//
// The modifier can be reused to apply the same set of commands again or, more
// usefully, reversed in order to revert its changes. See [Modifier.Reverse].
//
// [Modifier.Apply] can only be called after [Enable], and only if [Enable] returns
// true (meaning an "nft" executable was found). [Enabled] can be called to check
// whether nftables has been enabled.
//
// Be aware:
//   - The implementation is far from complete, only functionality needed so-far has
//     been included. Currently, there's only a limited set of chain/map/set types,
//     there's no way to delete sets/maps etc.
//   - This is a thin layer between code and "nft", it doesn't do much error checking. So,
//     for example, if you get the syntax of a rule wrong the issue won't be reported
//     until Apply is called.
//   - Also in the category of no-error-checking, there's no reference checking. If you
//     delete a chain that's still referred to by a map, set or another chain, "nft" will
//     report an error when Apply is called.
//   - Error checking here is meant to help spot logical errors in the code, like adding
//     a rule twice, which would be fine by "nft" as it'd just create a duplicate rule.
//   - The existing state of a table in the ruleset is irrelevant, once a Table is created
//     by this package it will be flushed. Putting it another way, this package is
//     write-only, it does not load any state from the host.
//   - Errors from "nft" are logged along with the line-numbered command that failed,
//     that's the place to look when things go wrong.
package nftables

import (
	"bytes"
	"context"
	"errors"
	"fmt"
	"iter"
	"runtime"
	"slices"
	"strconv"
	"strings"
	"sync"
	"text/template"

	"github.com/containerd/log"
)

// Prefix for OTEL span names.
const spanPrefix = "libnetwork.internal.nftables"

var (
	// enabled is set by [Enable].
	enabled bool
	// Error returned by Enable if nftables could not be initialised.
	nftEnableError error
	// incrementalUpdateTempl is a parsed text/template, used to apply incremental updates.
	incrementalUpdateTempl *template.Template
	// reloadTempl is a parsed text/template, used to apply a whole table.
	reloadTempl *template.Template
	// enableOnce is used by [Enable] to avoid parsing the templates more than once.
	enableOnce sync.Once
)

// BaseChainType enumerates the base chain types.
// See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_types
type BaseChainType string

const (
	BaseChainTypeFilter BaseChainType = "filter"
	BaseChainTypeRoute  BaseChainType = "route"
	BaseChainTypeNAT    BaseChainType = "nat"
)

// BaseChainHook enumerates the base chain hook types.
// See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_hooks
type BaseChainHook string

const (
	BaseChainHookIngress     BaseChainHook = "ingress"
	BaseChainHookPrerouting  BaseChainHook = "prerouting"
	BaseChainHookInput       BaseChainHook = "input"
	BaseChainHookForward     BaseChainHook = "forward"
	BaseChainHookOutput      BaseChainHook = "output"
	BaseChainHookPostrouting BaseChainHook = "postrouting"
)

// Standard priority values for base chains.
// (Not for the bridge family, those are different.)
const (
	BaseChainPriorityRaw      = -300
	BaseChainPriorityMangle   = -150
	BaseChainPriorityDstNAT   = -100
	BaseChainPriorityFilter   = 0
	BaseChainPrioritySecurity = 50
	BaseChainPrioritySrcNAT   = 100
)

// BaseChainPolicy enumerates base chain policies.
// See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Base_chain_policy
type BaseChainPolicy string

const (
	BaseChainPolicyAccept BaseChainPolicy = "accept"
	BaseChainPolicyDrop   BaseChainPolicy = "drop"
)

// Family enumerates address families.
type Family string

const (
	IPv4 Family = "ip"
	IPv6 Family = "ip6"
)

type SetTyper interface {
	setType() string
}

type MapTyper interface {
	mapType() string
}

// SetType represents named nft types that can be used to define sets or
// construct map types.
type SetType string

const (
	IPv4Addr    SetType = "ipv4_addr"
	IPv6Addr    SetType = "ipv6_addr"
	EtherAddr   SetType = "ether_addr"
	InetProto   SetType = "inet_proto"
	InetService SetType = "inet_service"
	Mark        SetType = "mark"
	Ifname      SetType = "ifname"
)

func (t SetType) setType() string {
	return "type " + string(t)
}

// Concat returns the tuple type formed by concatenating t and u.
func (t SetType) Concat(u SetType) SetType {
	return SetType(string(t) + " . " + string(u))
}

// MapType represents the named type of a map element, which is a compound type
// with a key and a value.
type MapType string

func (t MapType) mapType() string {
	return "type " + string(t)
}

// MapTo returns the map type where t is the key type and v is the value type.
func (t SetType) MapTo(v SetType) MapType {
	return MapType(string(t) + " : " + string(v))
}

// VMap returns the map type whose elements have t as the key type and contain
// a verdict as the value.
func (t SetType) VMap() MapType {
	return MapType(string(t) + " : verdict")
}

// Typeof represents an nft "typeof" expression.
type Typeof string

func (t Typeof) setType() string {
	return "typeof " + string(t)
}

func (t Typeof) Concat(u Typeof) Typeof {
	return Typeof(string(t) + " . " + string(u))
}

// MapTypeof represents the type of a map element defined by a "typeof"
// expression.
type MapTypeof string

func (t MapTypeof) mapType() string {
	return "typeof " + string(t)
}

// MapTo returns the map type where t is the key type and v is the value type.
func (t Typeof) MapTo(v Typeof) MapTypeof {
	return MapTypeof(string(t) + " : " + string(v))
}

// VMap returns the map type whose elements have t as the key type and contain
// a verdict as the value.
func (t Typeof) VMap() MapTypeof {
	return MapTypeof(string(t) + " : verdict")
}

// Enable tries once to initialise nftables.
func Enable() error {
	enableOnce.Do(func() {
		err := preflight()
		if err != nil {
			log.G(context.Background()).WithError(err).Warnf("Failed to initialize nftables")
			nftEnableError = err
			return
		}
		if err := parseTemplate(); err != nil {
			log.G(context.Background()).WithError(err).Error("Internal error while initialising nftables")
			nftEnableError = fmt.Errorf("internal error while initialising nftables: %w", err)
			return
		}
		enabled = true
	})
	return nftEnableError
}

// Enabled returns true if [Enable] has been called and nftables was initialized successfully.
func Enabled() bool {
	return enabled
}

// Disable undoes Enable. Intended for unit testing.
func Disable() {
	enabled = false
	incrementalUpdateTempl = nil
	reloadTempl = nil
	enableOnce = sync.Once{}
}

// RunCmd runs arbitrary nftables ruleset commands, like `nft -f`, irrespective
// of whether nftables is [Enabled].
//
// Most users of this package should use [Table] and [Modifier] to manage their
// nftables ruleset.
func RunCmd(ctx context.Context, nftCmd []byte) error {
	h, err := newNftCtx()
	if err != nil {
		return err
	}
	defer h.Close()
	return h.Apply(ctx, nftCmd)
}

//////////////////////////////
// Tables

// table is the internal representation of an nftables table.
// Its elements need to be exported for use by text/template, but they should only be
// manipulated via exported methods.
type table struct {
	Name   string
	Family Family

	Maps   map[string]*nftMap
	Sets   map[string]*set
	Chains map[string]*chain

	DeleteCommands []string
	MustFlush      bool

	applyLock sync.Mutex
	nftHandle *nftCtx // applyLock must be held to access
}

// nftApply executes the nftables commands in nftCmd.
// Acquire t.applyLock before calling this function.
func (t *table) nftApply(ctx context.Context, nftCmd []byte) error {
	if t.nftHandle == nil {
		h, err := newNftCtx()
		if err != nil {
			return err
		}
		t.nftHandle = h
	}
	return t.nftHandle.Apply(ctx, nftCmd)
}

// Table is a handle for an nftables table.
type Table struct {
	t *table
}

// IsValid returns true if t is a valid reference to a table.
func (t Table) IsValid() bool {
	return t.t != nil
}

// NewTable creates a new nftables table and returns a [Table]
//
// See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_tables
//
// To modify the table, instantiate a [Modifier], add commands to it, and call
// [Table.Apply].
//
// It's flushed in case it already exists in the host's nftables - when that
// happens, rules in its chains will be deleted but not the chains themselves,
// maps, sets, or elements of maps or sets. But, those un-flushed items can't do
// anything disruptive unless referred to by rules, and they will be flushed if
// they get re-created via the [Table], when [Table.Apply] is next called
// (so, before they can be used by a new rule).
//
// To fully delete an underlying nftables table, if one already exists,
// use [Table.Reload] after creating the table.
func NewTable(family Family, name string) (Table, error) {
	t := Table{
		t: &table{
			Name:      name,
			Family:    family,
			Maps:      map[string]*nftMap{},
			Sets:      map[string]*set{},
			Chains:    map[string]*chain{},
			MustFlush: true,
		},
	}
	return t, nil
}

// Close releases resources associated with the table. It does not modify or delete
// the underlying nftables table.
func (t Table) Close() error {
	if t.IsValid() {
		t.t.applyLock.Lock()
		defer t.t.applyLock.Unlock()
		if t.t.nftHandle != nil {
			t.t.nftHandle.Close()
			t.t.nftHandle = nil
		}
		t.t = nil
	}
	return nil
}

// Name returns the name of the table, or an empty string if t is not valid.
func (t Table) Name() string {
	if !t.IsValid() {
		return ""
	}
	return t.t.Name
}

// Family returns the address family of the nftables table described by [TableRef].
func (t Table) Family() Family {
	if !t.IsValid() {
		return ""
	}
	return t.t.Family
}

// incrementalUpdateTemplText is used with text/template to generate an nftables command file
// (which will be applied atomically). Updates using this template are always incremental.
// Steps are:
//   - declare the table and its sets/maps with empty versions of modified chains, so that
//     they can be flushed/deleted if they don't yet exist. (They need to be flushed in case
//     a version of them was left behind by an old incarnation of the daemon. But, it's an
//     error to flush or delete something that doesn't exist. So, avoid having to parse nft's
//     stderr to work out what happened by making sure they do exist before flushing.)
//   - if the table is newly declared, flush rules from its chains
//   - flush each newly declared map/set
//   - delete deleted map/set elements
//   - flush modified chains
//   - delete deleted chains
//   - re-populate modified chains
//   - add new map/set elements
const incrementalUpdateTemplText = `{{$family := .Family}}{{$tableName := .Name}}
table {{$family}} {{$tableName}} {
	{{range .Maps}}map {{.Name}} {
		{{.ElementTypeExpr}}
		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
	}
	{{end}}
	{{range .Sets}}set {{.Name}} {
		{{.ElementTypeExpr}}
		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
	}
	{{end}}
	{{range .Chains}}{{if .MustFlush}}chain {{.Name}} {
		{{if .ChainType}}type {{.ChainType}} hook {{.Hook}} priority {{.Priority}}; policy {{.Policy}}{{end}}
	} ; {{end}}{{end}}
}
{{if .MustFlush}}flush table {{$family}} {{$tableName}}{{end}}
{{range .Maps}}{{if .MustFlush}}flush map {{$family}} {{$tableName}} {{.Name}}
{{end}}{{end}}
{{range .Sets}}{{if .MustFlush}}flush set {{$family}} {{$tableName}} {{.Name}}
{{end}}{{end}}
{{range .Chains}}{{if .MustFlush}}flush chain {{$family}} {{$tableName}} {{.Name}}
{{end}}{{end}}
{{range .Maps}}{{if .DeletedElements}}delete element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .DeletedElements}}{{$k}}, {{end}} }
{{end}}{{end}}
{{range .Sets}}{{if .DeletedElements}}delete element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .DeletedElements}}{{$k}}, {{end}} }
{{end}}{{end}}
{{range .DeleteCommands}}{{.}}
{{end}}
table {{$family}} {{$tableName}} {
	{{range .Chains}}{{if .MustFlush}}chain {{.Name}} {
		{{if .ChainType}}type {{.ChainType}} hook {{.Hook}} priority {{.Priority}}; policy {{.Policy}}{{end}}
		{{range .Rules}}{{.}}
		{{end}}
	}
	{{end}}{{end}}
}
{{range .Maps}}{{if .AddedElements}}add element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .AddedElements}}{{$k}} : {{$v}}, {{end}} }
{{end}}{{end}}
{{range .Sets}}{{if .AddedElements}}add element {{$family}} {{$tableName}} {{.Name}} { {{range $k,$v := .AddedElements}}{{$k}}, {{end}} }
{{end}}{{end}}
`

// reloadTemplText is used with text/template to generate an nftables command file
// (which will be applied atomically), to fully re-create a table.
//
// It first declares the table so if it doesn't already exist, it can be deleted.
// Then it deletes the table and re-creates it.
const reloadTemplText = `{{$family := .Family}}{{$tableName := .Name}}
table {{$family}} {{$tableName}} {}
delete table {{$family}} {{$tableName}}
table {{$family}} {{$tableName}} {
	{{range .Maps}}map {{.Name}} {
		{{.ElementTypeExpr}}
		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
        {{if .Elements}}elements = {
			{{range $k,$v := .Elements}}{{$k}} : {{$v}},
            {{end -}}
		}{{end}}
	}
	{{end}}
	{{range .Sets}}set {{.Name}} {
		{{.ElementTypeExpr}}
		{{if len .Flags}}flags{{range .Flags}} {{.}}{{end}}{{end}}
        {{if .Elements}}elements = {
			{{range $k,$v := .Elements}}{{$k}},
            {{end -}}
		}{{end}}
	}
	{{end}}
	{{range .Chains}}chain {{.Name}} {
		{{if .ChainType}}type {{.ChainType}} hook {{.Hook}} priority {{.Priority}}; policy {{.Policy}}{{end}}
		{{range .Rules}}{{.}}
		{{end}}
	}
	{{end}}
}
`

// SetBaseChainPolicy sets the default policy for a base chain. The update
// is applied immediately, unlike creation/deletion of objects via a [Modifier]
// which are not applied until [Modifier.Apply] is called.
func (t Table) SetBaseChainPolicy(ctx context.Context, chainName string, policy BaseChainPolicy) error {
	if !t.IsValid() {
		return errors.New("invalid table")
	}
	c := t.t.Chains[chainName]
	if c == nil {
		return fmt.Errorf("cannot set base chain policy for '%s', it does not exist", chainName)
	}
	if c.ChainType == "" {
		return fmt.Errorf("cannot set base chain policy for '%s', it is not a base chain", chainName)
	}
	oldPolicy := c.Policy
	c.Policy = policy
	c.MustFlush = true

	if err := t.Apply(ctx, Modifier{}); err != nil {
		c.Policy = oldPolicy
		return err
	}
	return nil
}

// Obj is an object that can be given to a [Modifier], representing an
// nftables object for it to create or delete.
type Obj interface {
	create(context.Context, *table) (bool, error)
	delete(context.Context, *table) (bool, error)
}

// Modifier is used to apply changes to a Table.
type Modifier struct {
	cmds []command
}

// Create enqueues creation of object o, to be applied by tm.Apply.
func (tm *Modifier) Create(o Obj) {
	_, f, l, _ := runtime.Caller(1)
	tm.cmds = append(tm.cmds, command{
		obj:        o,
		callerFile: f,
		callerLine: l,
	})
}

// Delete enqueues deletion of object o, to be applied by tm.Apply.
func (tm *Modifier) Delete(o Obj) {
	_, f, l, _ := runtime.Caller(1)
	tm.cmds = append(tm.cmds, command{
		obj:        o,
		delete:     true,
		callerFile: f,
		callerLine: l,
	})
}

// Reverse returns a Modifier that will undo the actions of tm.
// Its operations are performed in reverse order, creates become
// deletes, and deletes become creates.
//
// Most operations are fully reversible (chains/maps/sets must be
// empty before they're deleted, so no information is lost). But,
// there are exceptions, noted in comments in the object definitions.
//
// Applying the updates in a reversed modifier may not work if
// any of the objects have been removed or modified since they
// were added. For example, if a Modifier creates a chain then another
// Modifier adds rules, the reversed Modifier will not be able to
// delete the chain as it is not empty.
func (tm *Modifier) Reverse() Modifier {
	rtm := Modifier{
		cmds: make([]command, len(tm.cmds)),
	}
	for i, cmd := range tm.cmds {
		cmd.delete = !cmd.delete
		rtm.cmds[len(tm.cmds)-i-1] = cmd
	}
	return rtm
}

// Apply makes incremental updates to nftables. If there's a validation
// error in any of the enqueued objects, or an error applying the updates
// to the underlying nftables, the [Table] will be unmodified.
func (t *Table) Apply(ctx context.Context, tm Modifier) (retErr error) {
	if !Enabled() {
		return errors.New("nftables is not enabled")
	}
	t.t.applyLock.Lock()
	defer t.t.applyLock.Unlock()

	var rollback []command
	defer func() {
		if retErr == nil {
			return
		}
		for _, c := range slices.Backward(rollback) {
			if _, err := c.rollback(ctx, t.t); err != nil {
				log.G(ctx).WithError(err).Error("Failed to roll back nftables updates")
			}
		}
		t.t.updatesApplied()
	}()

	// Apply tm's updates to the Table.
	for _, cmd := range tm.cmds {
		applied, err := cmd.apply(ctx, t.t)
		if err != nil {
			return fmt.Errorf("rule from %s:%d: %w", cmd.callerFile, cmd.callerLine, err)
		}
		if applied {
			rollback = append(rollback, cmd)
		}
	}

	// Update nftables.
	var buf bytes.Buffer
	if err := incrementalUpdateTempl.Execute(&buf, t.t); err != nil {
		return fmt.Errorf("failed to execute template nft ruleset: %w", err)
	}

	if err := t.t.nftApply(ctx, buf.Bytes()); err != nil {
		// On error, log a line-numbered version of the generated "nft" input (because
		// nft error messages refer to line numbers).
		var sb strings.Builder
		for i, line := range bytes.SplitAfter(buf.Bytes(), []byte("\n")) {
			sb.WriteString(strconv.Itoa(i + 1))
			sb.WriteString(":\t")
			sb.Write(line)
		}
		log.G(ctx).Error("nftables: failed to update nftables:\n", sb.String(), "\n", err)

		// It's possible something destructive has happened to nftables. For example, in
		// integration-cli tests, tests start daemons in the same netns as the integration
		// test's own daemon. They don't always use their own daemon, but they tend to leave
		// behind networks for the test infrastructure to clean up between tests. Starting
		// a daemon flushes the "docker-bridges" table, so the cleanup fails to delete a
		// rule that's been flushed. So, try reloading the whole table to get back in-sync.
		return t.t.reload(ctx)
	}

	// Note that updates have been applied.
	t.t.updatesApplied()
	return nil
}

// Reload deletes the table, then re-creates it, atomically.
func (t Table) Reload(ctx context.Context) error {
	if !Enabled() {
		return errors.New("nftables is not enabled")
	}
	if !t.IsValid() {
		return errors.New("invalid table")
	}
	t.t.applyLock.Lock()
	defer t.t.applyLock.Unlock()
	return t.t.reload(ctx)
}

func (t *table) reload(ctx context.Context) error {
	if !Enabled() {
		return errors.New("nftables is not enabled")
	}
	ctx = log.WithLogger(ctx, log.G(ctx).WithFields(log.Fields{"table": t.Name, "family": t.Family}))
	log.G(ctx).Warn("nftables: reloading table")

	// Build the update.
	var buf bytes.Buffer
	if err := reloadTempl.Execute(&buf, t); err != nil {
		return fmt.Errorf("failed to execute reload template: %w", err)
	}

	if err := t.nftApply(ctx, buf.Bytes()); err != nil {
		// On error, log a line-numbered version of the generated "nft" input (because
		// nft error messages refer to line numbers).
		var sb strings.Builder
		for i, line := range bytes.SplitAfter(buf.Bytes(), []byte("\n")) {
			sb.WriteString(strconv.Itoa(i + 1))
			sb.WriteString(":\t")
			sb.Write(line)
		}
		log.G(ctx).Error("nftables: failed to reload nftable:\n", sb.String(), "\n", err)
		return err
	}

	// Note that updates have been applied.
	t.updatesApplied()
	return nil
}

// ////////////////////////////
// Chains

// RuleGroup is used to allocate rules within a chain to a group. These groups are
// purely an internal construct, nftables knows nothing about them. Within groups
// rules retain the order in which they were added, and groups are ordered from
// lowest to highest numbered group.
type RuleGroup int

// chain is the internal representation of an nftables chain.
// Its elements need to be exported for use by text/template, but they should only be
// manipulated via exported methods.
type chain struct {
	table      *table
	Name       string
	ChainType  BaseChainType
	Hook       BaseChainHook
	Priority   int
	Policy     BaseChainPolicy
	MustFlush  bool
	ruleGroups map[RuleGroup][]string
}

// BaseChain constructs a new nftables base chain and returns a [ChainRef].
//
// See https://wiki.nftables.org/wiki-nftables/index.php/Configuring_chains#Adding_base_chains
//
// It is an error to create a base chain that already exists.
// If the underlying chain already exists, it will be flushed by the
// next [Table.Apply] before new rules are added.
type BaseChain struct {
	Name      string
	ChainType BaseChainType
	Hook      BaseChainHook
	Priority  int
	Policy    BaseChainPolicy // Defaults to BaseChainPolicyAccept
}

func (cd BaseChain) create(ctx context.Context, t *table) (bool, error) {
	if _, ok := t.Chains[cd.Name]; ok {
		return false, fmt.Errorf("base chain '%s' already exists", cd.Name)
	}
	if cd.Name == "" {
		return false, errors.New("base chain must have a name")
	}
	if cd.ChainType == "" || cd.Hook == "" {
		return false, fmt.Errorf("chain '%s': fields ChainType and Hook are required", cd.Name)
	}
	if cd.Policy == "" {
		// nftables will default to "accept" if unspecified, but the text/template
		// requires a policy string.
		cd.Policy = BaseChainPolicyAccept
	}
	c := &chain{
		table:      t,
		Name:       cd.Name,
		ChainType:  cd.ChainType,
		Hook:       cd.Hook,
		Priority:   cd.Priority,
		Policy:     cd.Policy,
		MustFlush:  true,
		ruleGroups: map[RuleGroup][]string{},
	}
	t.Chains[c.Name] = c
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"chain":  c.Name,
		"type":   c.ChainType,
		"hook":   c.Hook,
		"prio":   c.Priority,
	}).Debug("nftables: created base chain")
	return true, nil
}

func (cd BaseChain) delete(ctx context.Context, t *table) (bool, error) {
	return t.deleteChain(ctx, cd.Name)
}

// Chain implements the [Obj] interface, it can be passed to a
// [Modifier] to create or delete a chain.
type Chain struct {
	Name string
}

func (cd Chain) create(ctx context.Context, t *table) (bool, error) {
	if _, ok := t.Chains[cd.Name]; ok {
		return false, fmt.Errorf("chain '%s' already exists", cd.Name)
	}
	if cd.Name == "" {
		return false, errors.New("chain must have a name")
	}
	c := &chain{
		table:      t,
		Name:       cd.Name,
		MustFlush:  true,
		ruleGroups: map[RuleGroup][]string{},
	}
	t.Chains[c.Name] = c
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"chain":  cd.Name,
	}).Debug("nftables: created chain")
	return true, nil
}

func (cd Chain) delete(ctx context.Context, t *table) (bool, error) {
	return t.deleteChain(ctx, cd.Name)
}

// Rule implements the [Obj] interface, it can be passed to a
// [Modifier] to create or delete a rule in a chain.
type Rule struct {
	Chain string
	Group RuleGroup
	Rule  []string
	// IgnoreExist suppresses errors about deleting a rule that does not exist
	// or creating a rule that does already exist.
	//
	// Note that, when set, reversing the [Modifier] may not do what you want! For
	// example, if the original modifier deleted a rule that did not exist, the
	// reversed modifier will create that rule.
	IgnoreExist bool
}

func (ru Rule) create(ctx context.Context, t *table) (bool, error) {
	c := t.Chains[ru.Chain]
	if c == nil {
		return false, fmt.Errorf("chain '%s' does not exist", ru.Chain)
	}
	if len(ru.Rule) == 0 {
		return false, fmt.Errorf("chain '%s', cannot add empty rule", ru.Chain)
	}
	rule := strings.Join(ru.Rule, " ")
	if rg, ok := c.ruleGroups[ru.Group]; ok && slices.Contains(rg, rule) {
		if !ru.IgnoreExist {
			return false, fmt.Errorf("adding rule:'%s' chain:'%s' group:%d: rule exists", rule, ru.Chain, ru.Group)
		}
		return false, nil
	}
	c.ruleGroups[ru.Group] = append(c.ruleGroups[ru.Group], rule)
	c.MustFlush = true
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"chain":  c.Name,
		"group":  ru.Group,
		"rule":   rule,
	}).Debug("nftables: appended rule")
	return true, nil
}

func (ru Rule) delete(ctx context.Context, t *table) (bool, error) {
	rule := strings.Join(ru.Rule, " ")
	c := t.Chains[ru.Chain]
	if c == nil {
		return false, fmt.Errorf("deleting rule:'%s' - chain '%s' does not exist", rule, ru.Chain)
	}
	if rule == "" {
		return false, fmt.Errorf("chain '%s', cannot delete empty rule", ru.Chain)
	}
	rg, ok := c.ruleGroups[ru.Group]
	if !ok {
		if !ru.IgnoreExist {
			return false, fmt.Errorf("deleting rule:'%s' chain:'%s' rule group:%d does not exist", rule, ru.Chain, ru.Group)
		}
		return false, nil
	}
	origLen := len(rg)
	c.ruleGroups[ru.Group] = slices.DeleteFunc(rg, func(r string) bool { return r == rule })
	if len(c.ruleGroups[ru.Group]) == origLen {
		if !ru.IgnoreExist {
			return false, fmt.Errorf("deleting rule:'%s' chain:'%s' group:%d: rule does not exist", rule, ru.Chain, ru.Group)
		}
		return false, nil
	}
	if len(c.ruleGroups[ru.Group]) == 0 {
		delete(c.ruleGroups, ru.Group)
	}
	c.MustFlush = true
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"chain":  c.Name,
		"rule":   rule,
	}).Debug("nftables: deleted rule")
	return true, nil
}

// ////////////////////////////
// Maps

// nftMap is the internal representation of an nftables map (including verdict maps).
// Its elements need to be exported for use by text/template, but they should only be
// manipulated via exported methods.
type nftMap struct {
	table           *table
	Name            string
	ElementTypeExpr string
	Flags           []string
	Elements        map[string]string
	AddedElements   map[string]string
	DeletedElements map[string]string
	MustFlush       bool
}

// Map implements the [Obj] interface, it can be passed to a
// [Modifier] to create or delete a map.
type Map struct {
	Name        string
	ElementType MapTyper
	Flags       []string
}

func (m Map) create(ctx context.Context, t *table) (bool, error) {
	if m.Name == "" {
		return false, errors.New("map must have a name")
	}
	if _, ok := t.Maps[m.Name]; ok {
		return false, fmt.Errorf("map '%s' already exists", m.Name)
	}
	if m.ElementType == nil {
		return false, fmt.Errorf("map '%s' has no element type", m.Name)
	}
	nm := &nftMap{
		table:           t,
		Name:            m.Name,
		ElementTypeExpr: m.ElementType.mapType(),
		Flags:           slices.Clone(m.Flags),
		Elements:        map[string]string{},
		AddedElements:   map[string]string{},
		DeletedElements: map[string]string{},
		MustFlush:       true,
	}
	t.Maps[nm.Name] = nm
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"map":    nm.Name,
	}).Debug("nftables: created map")
	return true, nil
}

func (m Map) delete(ctx context.Context, t *table) (bool, error) {
	nm := t.Maps[m.Name]
	if nm == nil {
		return false, fmt.Errorf("cannot delete map '%s', it does not exist", m.Name)
	}
	if len(nm.Elements) != 0 {
		return false, fmt.Errorf("cannot delete map '%s', it contains %d elements", nm.Name, len(nm.Elements))
	}
	delete(t.Maps, nm.Name)
	t.DeleteCommands = append(t.DeleteCommands,
		fmt.Sprintf("delete map %s %s %s", t.Family, t.Name, nm.Name))
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"map":    nm.Name,
	}).Debug("nftables: deleted map")
	return true, nil
}

// MapElement implements the [Obj] interface, it can be passed to a
// [Modifier] to create or delete an entry in a map.
type MapElement struct {
	MapName string
	Key     string
	Value   string
}

func (me MapElement) create(ctx context.Context, t *table) (bool, error) {
	if me.MapName == "" {
		return false, errors.New("cannot add element to unnamed map")
	}
	nm := t.Maps[me.MapName]
	if nm == nil {
		return false, fmt.Errorf("cannot add to map '%s', it does not exist", me.MapName)
	}
	if me.Key == "" || me.Value == "" {
		return false, fmt.Errorf("cannot add to map '%s', element must have key and value", me.MapName)
	}
	if _, ok := nm.Elements[me.Key]; ok {
		return false, fmt.Errorf("map '%s' already contains element '%s'", me.MapName, me.Key)
	}
	nm.Elements[me.Key] = me.Value
	nm.AddedElements[me.Key] = me.Value
	delete(nm.DeletedElements, me.Key)
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"map":    me.MapName,
		"key":    me.Key,
		"value":  me.Value,
	}).Debug("nftables: added map element")
	return true, nil
}

func (me MapElement) delete(ctx context.Context, t *table) (bool, error) {
	nm := t.Maps[me.MapName]
	if nm == nil {
		return false, fmt.Errorf("cannot delete from map '%s', it does not exist", me.MapName)
	}
	oldValue, ok := nm.Elements[me.Key]
	if !ok {
		return false, fmt.Errorf("map '%s' does not contain element '%s'", me.MapName, me.Key)
	}
	if oldValue != me.Value {
		return false, fmt.Errorf("cannot delete map '%s' element '%s', value was '%s', not '%s'",
			me.MapName, me.Key, oldValue, me.Value)
	}
	delete(nm.Elements, me.Key)
	delete(nm.AddedElements, me.Key)
	nm.DeletedElements[me.Key] = me.Value
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"map":    me.MapName,
		"key":    me.Key,
		"value":  me.Value,
	}).Debug("nftables: deleted map element")
	return true, nil
}

// ////////////////////////////
// Sets

// set is the internal representation of an nftables set.
// Its elements need to be exported for use by text/template, but they should only be
// manipulated via exported methods.
type set struct {
	table           *table
	Name            string
	ElementTypeExpr string
	Flags           []string
	Elements        map[string]struct{}
	AddedElements   map[string]struct{}
	DeletedElements map[string]struct{}
	MustFlush       bool
}

// Set implements the [Obj] interface, it can be passed to a
// [Modifier] to create or delete a set.
type Set struct {
	Name        string
	ElementType SetTyper
	Flags       []string
}

// See https://wiki.nftables.org/wiki-nftables/index.php/Sets#Named_sets
func (sd Set) create(ctx context.Context, t *table) (bool, error) {
	if sd.Name == "" {
		return false, errors.New("set must have a name")
	}
	if _, ok := t.Sets[sd.Name]; ok {
		return false, fmt.Errorf("set '%s' already exists", sd.Name)
	}
	if sd.ElementType == nil {
		return false, fmt.Errorf("set '%s' must have a type", sd.Name)
	}
	s := &set{
		table:           t,
		Name:            sd.Name,
		Elements:        map[string]struct{}{},
		ElementTypeExpr: sd.ElementType.setType(),
		Flags:           slices.Clone(sd.Flags),
		AddedElements:   map[string]struct{}{},
		DeletedElements: map[string]struct{}{},
		MustFlush:       true,
	}
	t.Sets[sd.Name] = s
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"set":    s.Name,
	}).Debug("nftables: created set")
	return true, nil
}

func (sd Set) delete(ctx context.Context, t *table) (bool, error) {
	s := t.Sets[sd.Name]
	if s == nil {
		return false, fmt.Errorf("cannot delete set '%s', it does not exist", sd.Name)
	}
	if len(s.Elements) != 0 {
		return false, fmt.Errorf("cannot delete set '%s', it contains %d elements", s.Name, len(s.Elements))
	}
	delete(t.Sets, sd.Name)
	t.DeleteCommands = append(t.DeleteCommands,
		fmt.Sprintf("delete set %s %s %s", t.Family, t.Name, s.Name))
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"set":    sd.Name,
	}).Debug("nftables: deleted set")
	return true, nil
}

// SetElement implements the [Obj] interface, it can be passed to a
// [Modifier] to create or delete an entry in a set.
type SetElement struct {
	SetName string
	Element string
	// If true, deleting an element that does not exist or creating an
	// element that already exists will succeed.
	Idempotent bool
}

func (se SetElement) create(ctx context.Context, t *table) (bool, error) {
	s := t.Sets[se.SetName]
	if s == nil {
		return false, fmt.Errorf("cannot add to set '%s', it does not exist", se.SetName)
	}
	if se.Element == "" {
		return false, fmt.Errorf("cannot add to set '%s', element not specified", se.SetName)
	}
	if _, ok := s.Elements[se.Element]; ok {
		if se.Idempotent {
			return false, nil
		}
		return false, fmt.Errorf("set '%s' already contains element '%s'", s.Name, se.Element)
	}
	s.Elements[se.Element] = struct{}{}
	s.AddedElements[se.Element] = struct{}{}
	delete(s.DeletedElements, se.Element)
	log.G(ctx).WithFields(log.Fields{
		"family":  t.Family,
		"table":   t.Name,
		"set":     s.Name,
		"element": se.Element,
	}).Debug("nftables: added set element")
	return true, nil
}

func (se SetElement) delete(ctx context.Context, t *table) (bool, error) {
	s := t.Sets[se.SetName]
	if s == nil {
		return false, fmt.Errorf("cannot delete from set '%s', it does not exist", se.SetName)
	}
	if _, ok := s.Elements[se.Element]; !ok {
		if se.Idempotent {
			return false, nil
		}
		return false, fmt.Errorf("cannot delete '%s' from set '%s', it does not exist", se.Element, s.Name)
	}
	delete(s.Elements, se.Element)
	delete(s.AddedElements, se.Element)
	s.DeletedElements[se.Element] = struct{}{}
	log.G(ctx).WithFields(log.Fields{
		"family":  t.Family,
		"table":   t.Name,
		"set":     s.Name,
		"element": se.Element,
	}).Debug("nftables: deleted set element")
	return true, nil
}

// ////////////////////////////
// Internal

func (t *table) deleteChain(ctx context.Context, name string) (bool, error) {
	c := t.Chains[name]
	if c == nil {
		return false, fmt.Errorf("cannot delete chain '%s', it does not exist", name)
	}
	if len(c.ruleGroups) != 0 {
		return false, fmt.Errorf("cannot delete chain '%s', it is not empty", name)
	}
	delete(t.Chains, name)
	t.DeleteCommands = append(t.DeleteCommands,
		fmt.Sprintf("delete chain %s %s %s", t.Family, t.Name, name))
	log.G(ctx).WithFields(log.Fields{
		"family": t.Family,
		"table":  t.Name,
		"chain":  name,
	}).Debug("nftables: deleted chain")
	return true, nil
}

type command struct {
	obj        Obj
	callerFile string
	callerLine int
	delete     bool
}

func (c command) apply(ctx context.Context, t *table) (bool, error) {
	if c.delete {
		return c.obj.delete(ctx, t)
	}
	return c.obj.create(ctx, t)
}

func (c command) rollback(ctx context.Context, t *table) (bool, error) {
	if c.delete {
		return c.obj.create(ctx, t)
	}
	return c.obj.delete(ctx, t)
}

func (t *table) updatesApplied() {
	t.DeleteCommands = t.DeleteCommands[:0]
	for _, c := range t.Chains {
		c.MustFlush = false
	}
	for _, m := range t.Maps {
		m.AddedElements = map[string]string{}
		m.DeletedElements = map[string]string{}
		m.MustFlush = false
	}
	for _, s := range t.Sets {
		s.AddedElements = map[string]struct{}{}
		s.DeletedElements = map[string]struct{}{}
		s.MustFlush = false
	}
	t.MustFlush = false
}

// Rules returns an iterator that yields the chain's rules in order.
func (c *chain) Rules() iter.Seq[string] {
	groups := make([]RuleGroup, 0, len(c.ruleGroups))
	for group := range c.ruleGroups {
		groups = append(groups, group)
	}
	slices.Sort(groups)
	return func(yield func(string) bool) {
		for _, group := range groups {
			for _, rule := range c.ruleGroups[group] {
				if !yield(rule) {
					return
				}
			}
		}
	}
}

func parseTemplate() error {
	var err error
	incrementalUpdateTempl, err = template.New("ruleset").Parse(incrementalUpdateTemplText)
	if err != nil {
		return fmt.Errorf("parsing 'incrementalUpdateTemplText': %w", err)
	}
	reloadTempl, err = template.New("ruleset").Parse(reloadTemplText)
	if err != nil {
		return fmt.Errorf("parsing 'reloadTemplText': %w", err)
	}
	return nil
}
