package nftables

import (
	"context"
	"fmt"
	"net/netip"

	"github.com/google/nftables"
	"github.com/google/nftables/expr"
	"github.com/qdm12/gluetun/internal/models"
)

// cidrMask returns the binary mask for a CIDR prefix length.
//
//nolint:mnd
func cidrMask(bits, addrLen int) []byte {
	result := make([]byte, addrLen)
	fullBytes := bits / 8
	remainingBits := bits % 8
	for i := range fullBytes {
		result[i] = 0xff
	}
	if remainingBits > 0 {
		result[fullBytes] = byte((0xff << (8 - remainingBits)) & 0xff)
	}
	return result
}

func (f *Firewall) AcceptIpv6MulticastOutput(_ context.Context, intf string) error {
	f.mutex.Lock()
	defer f.mutex.Unlock()

	conn, err := nftables.New()
	if err != nil {
		return fmt.Errorf("creating nftables connection: %w", err)
	}

	table, _, _, outputChain := setupFilterWithBaseChains(conn)

	const maxExprsLen = 6
	exprs := make([]expr.Any, 0, maxExprsLen)

	if intf != "" && intf != "*" {
		exprs = append(exprs,
			&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
			&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte(intf + "\x00")},
		)
	}

	// ff02::1:ff00:0/104 mask is 13 bytes of 0xff
	mask := []byte{
		0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
		0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00, 0x00,
	}
	addr := []byte{
		0xff, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x01, 0xff, 0x00, 0x00, 0x00,
	}

	exprs = append(exprs,
		&expr.Payload{
			DestRegister: 1,
			Base:         expr.PayloadBaseNetworkHeader,
			Offset:       24, //nolint:mnd // IPv6 Destination Address offset
			Len:          16, //nolint:mnd
		},
		&expr.Bitwise{
			SourceRegister: 1,
			DestRegister:   1,
			Len:            16, //nolint:mnd
			Mask:           mask,
			Xor:            []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0},
		},
		&expr.Cmp{
			Op:       expr.CmpOpEq,
			Register: 1,
			Data:     addr,
		},
		&expr.Verdict{Kind: expr.VerdictAccept},
	)

	rule := &nftables.Rule{
		Table: table,
		Chain: outputChain,
		Exprs: exprs,
	}

	conn.AddRule(rule)

	err = conn.Flush()
	if err != nil {
		return fmt.Errorf("flushing: %w", err)
	}

	return nil
}

func (f *Firewall) AcceptOutputTrafficToVPN(_ context.Context, defaultInterface string,
	connection models.Connection, remove bool,
) error {
	f.mutex.Lock()
	defer f.mutex.Unlock()

	conn, err := nftables.New()
	if err != nil {
		return fmt.Errorf("creating nftables connection: %w", err)
	}

	table, _, _, outputChain := setupFilterWithBaseChains(conn)

	// Prepare the destination IP and port
	const maxExprsLen = 7
	exprs := make([]expr.Any, 0, maxExprsLen)

	// Interface filter
	if defaultInterface != "" && defaultInterface != "*" {
		exprs = append(exprs,
			&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
			&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte(defaultInterface + "\x00")},
		)
	}

	// Destination IP address
	if connection.IP.Is4() {
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       16, //nolint:mnd // IPv4 destination address offset
				Len:          4,  //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     connection.IP.AsSlice(),
			},
		)
	} else { // IPv6
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       24, //nolint:mnd// IPv6 destination address offset
				Len:          16, //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     connection.IP.AsSlice(),
			},
		)
	}

	// Protocol (tcp or udp)
	var protocolByte uint8
	switch connection.Protocol {
	case "tcp", "tcp-client":
		protocolByte = 6 // TCP
	case "udp":
		protocolByte = 17 // UDP
	default:
		return fmt.Errorf("unsupported protocol: %s", connection.Protocol)
	}

	exprs = append(exprs,
		&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
		&expr.Cmp{
			Op:       expr.CmpOpEq,
			Register: 1,
			Data:     []byte{protocolByte},
		},
	)

	// Destination port
	portBytes := []byte{byte(connection.Port >> 8), byte(connection.Port)} //nolint:mnd,gosec
	exprs = append(exprs,
		&expr.Payload{
			DestRegister: 1,
			Base:         expr.PayloadBaseTransportHeader,
			Offset:       2, //nolint:mnd// destination port offset
			Len:          2, //nolint:mnd
		},
		&expr.Cmp{
			Op:       expr.CmpOpEq,
			Register: 1,
			Data:     portBytes,
		},
		&expr.Verdict{Kind: expr.VerdictAccept},
	)

	rule := &nftables.Rule{
		Table: table,
		Chain: outputChain,
		Exprs: exprs,
	}

	if !remove {
		conn.AddRule(rule)
		f.rules = append(f.rules, rule)
	} else {
		err = f.deleteRule(conn, rule)
		if err != nil {
			return fmt.Errorf("deleting rule: %w", err)
		}
	}

	err = conn.Flush()
	if err != nil {
		if !remove {
			f.rules = f.rules[:len(f.rules)-1]
		}
		return fmt.Errorf("flushing: %w", err)
	}

	return nil
}

func (f *Firewall) AcceptOutputFromIPPortToIPPort(_ context.Context, protocol, intf string,
	source, destination netip.AddrPort, remove bool,
) error {
	f.mutex.Lock()
	defer f.mutex.Unlock()

	conn, err := nftables.New()
	if err != nil {
		return fmt.Errorf("creating nftables connection: %w", err)
	}

	table, _, _, outputChain := setupFilterWithBaseChains(conn)

	const maxExprsLen = 10 //nolint:mnd
	exprs := make([]expr.Any, 0, maxExprsLen)

	if intf != "" && intf != "*" {
		exprs = append(exprs,
			&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
			&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte(intf + "\x00")},
		)
	}

	if source.Addr().Is4() {
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       12, //nolint:mnd
				Len:          4,  //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     source.Addr().AsSlice(),
			},
		)
	} else {
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       8,  //nolint:mnd
				Len:          16, //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     source.Addr().AsSlice(),
			},
		)
	}

	if destination.Addr().Is4() {
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       16, //nolint:mnd
				Len:          4,  //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     destination.Addr().AsSlice(),
			},
		)
	} else {
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       24, //nolint:mnd
				Len:          16, //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     destination.Addr().AsSlice(),
			},
		)
	}

	var protocolByte uint8
	switch protocol {
	case "tcp":
		protocolByte = 6 //nolint:mnd
	case "udp":
		protocolByte = 17 //nolint:mnd
	default:
		return fmt.Errorf("unsupported protocol: %s", protocol) //nolint:err113
	}

	exprs = append(exprs,
		&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
		&expr.Cmp{
			Op:       expr.CmpOpEq,
			Register: 1,
			Data:     []byte{protocolByte},
		},
	)

	sourcePortBytes := []byte{byte(source.Port() >> 8), byte(source.Port())}                //nolint:mnd
	destinationPortBytes := []byte{byte(destination.Port() >> 8), byte(destination.Port())} //nolint:mnd
	exprs = append(exprs,
		&expr.Payload{
			DestRegister: 1,
			Base:         expr.PayloadBaseTransportHeader,
			Offset:       0, //nolint:mnd
			Len:          2, //nolint:mnd
		},
		&expr.Cmp{
			Op:       expr.CmpOpEq,
			Register: 1,
			Data:     sourcePortBytes,
		},
		&expr.Payload{
			DestRegister: 1,
			Base:         expr.PayloadBaseTransportHeader,
			Offset:       2, //nolint:mnd
			Len:          2, //nolint:mnd
		},
		&expr.Cmp{
			Op:       expr.CmpOpEq,
			Register: 1,
			Data:     destinationPortBytes,
		},
		&expr.Verdict{Kind: expr.VerdictAccept},
	)

	rule := &nftables.Rule{
		Table: table,
		Chain: outputChain,
		Exprs: exprs,
	}

	if !remove {
		conn.AddRule(rule)
		f.rules = append(f.rules, rule)
	} else {
		err = f.deleteRule(conn, rule)
		if err != nil {
			return fmt.Errorf("deleting rule: %w", err)
		}
	}

	err = conn.Flush()
	if err != nil {
		if !remove {
			f.rules = f.rules[:len(f.rules)-1]
		}
		return fmt.Errorf("flushing: %w", err)
	}

	return nil
}

func (f *Firewall) AcceptOutputFromIPToSubnet(_ context.Context, intf string, assignedIP netip.Addr,
	subnet netip.Prefix, remove bool,
) error {
	f.mutex.Lock()
	defer f.mutex.Unlock()

	conn, err := nftables.New()
	if err != nil {
		return fmt.Errorf("creating nftables connection: %w", err)
	}

	table, _, _, outputChain := setupFilterWithBaseChains(conn)

	const maxExprsLen = 8
	exprs := make([]expr.Any, 0, maxExprsLen)

	if intf != "" && intf != "*" {
		exprs = append(exprs,
			&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
			&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte(intf + "\x00")},
		)
	}

	if assignedIP.Is4() {
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       12, //nolint:mnd
				Len:          4,  //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     assignedIP.AsSlice(),
			},
		)
	} else {
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       8,  //nolint:mnd
				Len:          16, //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     assignedIP.AsSlice(),
			},
		)
	}

	if subnet.Addr().Is4() {
		mask := cidrMask(subnet.Bits(), 4) //nolint:mnd
		networkAddr := subnet.Masked().Addr().AsSlice()
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       16, //nolint:mnd
				Len:          4,  //nolint:mnd
			},
			&expr.Bitwise{
				SourceRegister: 1,
				DestRegister:   1,
				Len:            4, //nolint:mnd
				Mask:           mask,
				Xor:            []byte{0, 0, 0, 0}, //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     networkAddr,
			},
		)
	} else {
		mask := cidrMask(subnet.Bits(), 16) //nolint:mnd
		networkAddr := subnet.Masked().Addr().AsSlice()
		exprs = append(exprs,
			&expr.Payload{
				DestRegister: 1,
				Base:         expr.PayloadBaseNetworkHeader,
				Offset:       24, //nolint:mnd
				Len:          16, //nolint:mnd
			},
			&expr.Bitwise{
				SourceRegister: 1,
				DestRegister:   1,
				Len:            16, //nolint:mnd
				Mask:           mask,
				Xor:            []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}, //nolint:mnd
			},
			&expr.Cmp{
				Op:       expr.CmpOpEq,
				Register: 1,
				Data:     networkAddr,
			},
		)
	}

	exprs = append(exprs,
		&expr.Verdict{Kind: expr.VerdictAccept},
	)

	rule := &nftables.Rule{
		Table: table,
		Chain: outputChain,
		Exprs: exprs,
	}

	if !remove {
		conn.AddRule(rule)
		f.rules = append(f.rules, rule)
	} else {
		err = f.deleteRule(conn, rule)
		if err != nil {
			return fmt.Errorf("deleting rule: %w", err)
		}
	}

	err = conn.Flush()
	if err != nil {
		if !remove {
			f.rules = f.rules[:len(f.rules)-1]
		}
		return fmt.Errorf("flushing: %w", err)
	}

	return nil
}

func (f *Firewall) AcceptOutputThroughInterface(_ context.Context, intf string, remove bool) error {
	f.mutex.Lock()
	defer f.mutex.Unlock()

	conn, err := nftables.New()
	if err != nil {
		return fmt.Errorf("creating nftables connection: %w", err)
	}

	table, _, _, outputChain := setupFilterWithBaseChains(conn)

	const maxExprsLen = 3
	exprs := make([]expr.Any, 0, maxExprsLen)

	if intf != "" && intf != "*" {
		exprs = append(exprs,
			&expr.Meta{Key: expr.MetaKeyOIFNAME, Register: 1},
			&expr.Cmp{Op: expr.CmpOpEq, Register: 1, Data: []byte(intf + "\x00")},
		)
	}

	exprs = append(exprs,
		&expr.Verdict{Kind: expr.VerdictAccept},
	)

	rule := &nftables.Rule{
		Table: table,
		Chain: outputChain,
		Exprs: exprs,
	}

	if !remove {
		conn.AddRule(rule)
		f.rules = append(f.rules, rule)
	} else {
		err = f.deleteRule(conn, rule)
		if err != nil {
			return fmt.Errorf("deleting rule: %w", err)
		}
	}

	err = conn.Flush()
	if err != nil {
		if !remove {
			f.rules = f.rules[:len(f.rules)-1]
		}
		return fmt.Errorf("flushing: %w", err)
	}

	return nil
}
