//go:build linux

package ike

import (
	"context"
	"encoding/hex"
	"errors"
	"fmt"
	"net"
	"os/exec"
	"strconv"
	"strings"
	"sync"

	"vocat/internal/vowifi"
)

type linuxXFRMInstaller struct {
	ipCommand string
}

func defaultChildSAInstaller() ChildSAInstaller {
	return linuxChildSAInstallerRouter{ipCommand: "ip"}
}

type linuxChildSAInstallerRouter struct {
	ipCommand string
}

func (router linuxChildSAInstallerRouter) Install(ctx context.Context, config ChildSAConfig) (ChildSAHandle, error) {
	if config.ProxyMode == vowifi.ProxyModeSOCKS5 || config.UDPEncapsulation {
		return (linuxUserspaceInstaller{ipCommand: router.ipCommand}).Install(ctx, config)
	}
	return (linuxXFRMInstaller{ipCommand: router.ipCommand}).Install(ctx, config)
}

type linuxXFRMHandle struct {
	mu        sync.Mutex
	ipCommand string
	config    ChildSAConfig
	reqid     string
	closed    bool
}

func (*linuxXFRMHandle) DataplaneMode() string { return "xfrm" }

func (installer linuxXFRMInstaller) Install(ctx context.Context, config ChildSAConfig) (ChildSAHandle, error) {
	if config.ProxyMode == vowifi.ProxyModeSOCKS5 || config.UDPEncapsulation {
		return nil, errors.New("NAT-T and SOCKS5 require a user-space ESP/TUN installer using NATTPacketRelay; kernel XFRM cannot own the user-space UDP association")
	}
	if config.OuterLocal == nil || config.OuterRemote == nil {
		return nil, errors.New("outer IP addresses are required")
	}
	if config.InboundSPI == 0 || config.OutboundSPI == 0 {
		return nil, errors.New("ESP SPIs must be nonzero")
	}
	command := installer.ipCommand
	if command == "" {
		command = "ip"
	}
	if _, err := exec.LookPath(command); err != nil {
		return nil, errors.New("Linux iproute2 is required to install the CHILD_SA")
	}
	handle := &linuxXFRMHandle{
		ipCommand: command,
		config:    cloneChildSAConfig(config),
		reqid:     strconv.FormatUint(uint64(config.InboundSPI), 10),
	}
	if err := handle.install(ctx); err != nil {
		_ = handle.Close(context.Background())
		return nil, err
	}
	return handle, nil
}

func (handle *linuxXFRMHandle) install(ctx context.Context) error {
	config := handle.config
	if err := handle.run(ctx, "create tunnel interface", "link", "add", config.Name, "type", "dummy"); err != nil {
		return err
	}
	if config.InnerLocalIPv4 != nil {
		if err := handle.run(ctx, "assign tunnel IPv4 address", "address", "add", config.InnerLocalIPv4.String()+"/32", "dev", config.Name); err != nil {
			return err
		}
	}
	if config.InnerLocalIPv6 != nil {
		if err := handle.run(ctx, "assign tunnel IPv6 address", "-6", "address", "add", fmt.Sprintf("%s/%d", config.InnerLocalIPv6.String(), config.InnerIPv6Prefix), "dev", config.Name); err != nil {
			return err
		}
	}
	if err := handle.run(ctx, "enable tunnel interface", "link", "set", "dev", config.Name, "up"); err != nil {
		return err
	}
	outboundState := handle.stateArguments(
		config.OuterLocal, config.OuterRemote, config.OutboundSPI,
		config.OutboundEncKey, config.OutboundAuthKey,
	)
	if err := handle.run(ctx, "install outbound ESP state", append([]string{"xfrm", "state", "add"}, outboundState...)...); err != nil {
		return err
	}
	inboundState := handle.stateArguments(
		config.OuterRemote, config.OuterLocal, config.InboundSPI,
		config.InboundEncKey, config.InboundAuthKey,
	)
	if err := handle.run(ctx, "install inbound ESP state", append([]string{"xfrm", "state", "add"}, inboundState...)...); err != nil {
		return err
	}
	for _, initiator := range config.InitiatorSelectors {
		for _, responder := range config.ResponderSelectors {
			if (initiator.StartIP.To4() == nil) != (responder.StartIP.To4() == nil) {
				continue
			}
			if err := handle.installPolicyPair(ctx, initiator, responder); err != nil {
				return err
			}
		}
	}
	return nil
}

func (handle *linuxXFRMHandle) installPolicyPair(
	ctx context.Context,
	initiator trafficSelector,
	responder trafficSelector,
) error {
	initiatorPrefix, err := selectorPrefix(initiator)
	if err != nil {
		return err
	}
	responderPrefix, err := selectorPrefix(responder)
	if err != nil {
		return err
	}
	if initiator.IPProtocol != responder.IPProtocol &&
		initiator.IPProtocol != 0 && responder.IPProtocol != 0 {
		return errors.New("negotiated traffic selectors use conflicting IP protocols")
	}
	protocol := initiator.IPProtocol
	if protocol == 0 {
		protocol = responder.IPProtocol
	}
	family := "-4"
	if initiator.StartIP.To4() == nil {
		family = "-6"
	}
	outbound := []string{
		family, "xfrm", "policy", "add",
		"src", initiatorPrefix, "dst", responderPrefix, "dir", "out",
	}
	inbound := []string{
		family, "xfrm", "policy", "add",
		"src", responderPrefix, "dst", initiatorPrefix, "dir", "in",
	}
	if protocol != 0 {
		outbound = append(outbound, "proto", strconv.Itoa(int(protocol)))
		inbound = append(inbound, "proto", strconv.Itoa(int(protocol)))
	}
	outbound, err = appendSelectorPorts(outbound, initiator, responder)
	if err != nil {
		return err
	}
	inbound, err = appendSelectorPorts(inbound, responder, initiator)
	if err != nil {
		return err
	}
	outbound = append(outbound,
		"tmpl", "src", handle.config.OuterLocal.String(), "dst", handle.config.OuterRemote.String(),
		"proto", "esp", "mode", "tunnel", "reqid", handle.reqid,
	)
	inbound = append(inbound,
		"tmpl", "src", handle.config.OuterRemote.String(), "dst", handle.config.OuterLocal.String(),
		"proto", "esp", "mode", "tunnel", "reqid", handle.reqid,
	)
	if err := handle.run(ctx, "install outbound ESP policy", outbound...); err != nil {
		return err
	}
	return handle.run(ctx, "install inbound ESP policy", inbound...)
}

func appendSelectorPorts(
	arguments []string,
	source trafficSelector,
	destination trafficSelector,
) ([]string, error) {
	appendPort := func(label string, start uint16, end uint16) error {
		if start == 0 && end == 65535 {
			return nil
		}
		if start != end {
			return fmt.Errorf("negotiated %s port range %d-%d cannot be represented safely by XFRM", label, start, end)
		}
		arguments = append(arguments, label, strconv.Itoa(int(start)))
		return nil
	}
	if err := appendPort("sport", source.StartPort, source.EndPort); err != nil {
		return nil, err
	}
	if err := appendPort("dport", destination.StartPort, destination.EndPort); err != nil {
		return nil, err
	}
	return arguments, nil
}

func selectorPrefix(selector trafficSelector) (string, error) {
	start := selector.StartIP
	end := selector.EndIP
	bits := 128
	if start4 := start.To4(); start4 != nil {
		start = start4
		end = end.To4()
		bits = 32
	} else {
		start = start.To16()
		end = end.To16()
	}
	if start == nil || end == nil || len(start) != len(end) {
		return "", errors.New("negotiated traffic selector IP range is invalid")
	}
	prefix := 0
	different := false
	for index := 0; index < len(start); index++ {
		for bit := 7; bit >= 0; bit-- {
			startBit := start[index] & (1 << bit)
			endBit := end[index] & (1 << bit)
			if !different && startBit == endBit {
				prefix++
				continue
			}
			different = true
			if startBit != 0 || endBit == 0 {
				return "", errors.New("negotiated traffic selector range is not a CIDR prefix")
			}
		}
	}
	network := &net.IPNet{IP: start, Mask: net.CIDRMask(prefix, bits)}
	return network.String(), nil
}

func (handle *linuxXFRMHandle) stateArguments(
	source net.IP,
	destination net.IP,
	spi uint32,
	encryptionKey []byte,
	integrityKey []byte,
) []string {
	arguments := []string{
		"src", source.String(),
		"dst", destination.String(),
		"proto", "esp",
		"spi", fmt.Sprintf("0x%08x", spi),
		"reqid", handle.reqid,
		"mode", "tunnel",
	}
	switch handle.config.Integrity {
	case "hmac-sha1-96":
		arguments = append(arguments, "auth-trunc", "hmac(sha1)", "0x"+hex.EncodeToString(integrityKey), "96")
	case "hmac-sha2-256-128":
		arguments = append(arguments, "auth-trunc", "hmac(sha256)", "0x"+hex.EncodeToString(integrityKey), "128")
	}
	arguments = append(arguments, "enc", "cbc(aes)", "0x"+hex.EncodeToString(encryptionKey))
	if handle.config.UDPEncapsulation {
		arguments = append(arguments, "encap", "espinudp", "4500", "4500", "0.0.0.0")
	}
	return arguments
}

func (handle *linuxXFRMHandle) run(ctx context.Context, operation string, arguments ...string) error {
	command := exec.CommandContext(ctx, handle.ipCommand, arguments...)
	output, err := command.CombinedOutput()
	if err != nil {
		message := strings.TrimSpace(string(output))
		if message == "" {
			message = err.Error()
		}
		return fmt.Errorf("%s: %s", operation, message)
	}
	return nil
}

func (handle *linuxXFRMHandle) Close(ctx context.Context) error {
	handle.mu.Lock()
	defer handle.mu.Unlock()
	if handle.closed {
		return nil
	}
	handle.closed = true
	config := handle.config
	var errs []error
	deletePolicy := func(family, source, destination, direction string) {
		command := exec.CommandContext(ctx, handle.ipCommand,
			family, "xfrm", "policy", "delete",
			"src", source, "dst", destination, "dir", direction,
		)
		if err := command.Run(); err != nil {
			errs = append(errs, err)
		}
	}
	for _, initiator := range config.InitiatorSelectors {
		for _, responder := range config.ResponderSelectors {
			if (initiator.StartIP.To4() == nil) != (responder.StartIP.To4() == nil) {
				continue
			}
			initiatorPrefix, initiatorErr := selectorPrefix(initiator)
			responderPrefix, responderErr := selectorPrefix(responder)
			if initiatorErr != nil || responderErr != nil {
				continue
			}
			family := "-4"
			if initiator.StartIP.To4() == nil {
				family = "-6"
			}
			deletePolicy(family, initiatorPrefix, responderPrefix, "out")
			deletePolicy(family, responderPrefix, initiatorPrefix, "in")
		}
	}
	deleteState := func(source net.IP, destination net.IP, spi uint32) {
		command := exec.CommandContext(ctx, handle.ipCommand,
			"xfrm", "state", "delete",
			"src", source.String(), "dst", destination.String(),
			"proto", "esp", "spi", fmt.Sprintf("0x%08x", spi),
		)
		if err := command.Run(); err != nil {
			errs = append(errs, err)
		}
	}
	deleteState(config.OuterLocal, config.OuterRemote, config.OutboundSPI)
	deleteState(config.OuterRemote, config.OuterLocal, config.InboundSPI)
	if err := exec.CommandContext(ctx, handle.ipCommand, "link", "delete", config.Name).Run(); err != nil {
		errs = append(errs, err)
	}
	return errors.Join(errs...)
}

func cloneChildSAConfig(config ChildSAConfig) ChildSAConfig {
	config.OuterLocal = append(net.IP(nil), config.OuterLocal...)
	config.OuterRemote = append(net.IP(nil), config.OuterRemote...)
	config.InnerLocalIPv4 = append(net.IP(nil), config.InnerLocalIPv4...)
	config.InnerLocalIPv6 = append(net.IP(nil), config.InnerLocalIPv6...)
	config.InboundEncKey = append([]byte(nil), config.InboundEncKey...)
	config.InboundAuthKey = append([]byte(nil), config.InboundAuthKey...)
	config.OutboundEncKey = append([]byte(nil), config.OutboundEncKey...)
	config.OutboundAuthKey = append([]byte(nil), config.OutboundAuthKey...)
	config.InitiatorSelectors = cloneTrafficSelectors(config.InitiatorSelectors)
	config.ResponderSelectors = cloneTrafficSelectors(config.ResponderSelectors)
	config.PCSCF = cloneIPs(config.PCSCF)
	config.DNS = cloneIPs(config.DNS)
	return config
}

func cloneTrafficSelectors(selectors []trafficSelector) []trafficSelector {
	result := append([]trafficSelector(nil), selectors...)
	for index := range result {
		result[index].StartIP = append(net.IP(nil), result[index].StartIP...)
		result[index].EndIP = append(net.IP(nil), result[index].EndIP...)
	}
	return result
}
