// RootlessKit integration - if required by RootlessKit's port driver, let it know
// about port mappings as they're added and removed.
//
// This is based on / copied from rootlesskit-docker-proxy, which was previously
// installed as a proxy for docker-proxy:
// https://github.com/rootless-containers/rootlesskit/blob/4fb2e2cb80bf13eb28b7f2a4317b63406b89ad32/cmd/rootlesskit-docker-proxy/main.go

package rlkclient

import (
	"context"
	"errors"
	"fmt"
	"net/netip"
	"os"
	"path/filepath"
	"strings"

	"github.com/rootless-containers/rootlesskit/v3/pkg/api/client"
	"github.com/rootless-containers/rootlesskit/v3/pkg/port"
)

type PortDriverClient struct {
	client         client.Client
	portDriverName string
	protos         map[string]struct{}
	childIP        netip.Addr
}

func NewPortDriverClient(ctx context.Context) (*PortDriverClient, error) {
	stateDir := os.Getenv("ROOTLESSKIT_STATE_DIR")
	if stateDir == "" {
		return nil, errors.New("$ROOTLESSKIT_STATE_DIR needs to be set")
	}
	socketPath := filepath.Join(stateDir, "api.sock")
	c, err := client.New(socketPath)
	if err != nil {
		return nil, fmt.Errorf("error while connecting to RootlessKit API socket: %w", err)
	}

	info, err := c.Info(ctx)
	if err != nil {
		return nil, fmt.Errorf("failed to call info API, probably RootlessKit binary is too old (needs to be v0.14.0 or later): %w", err)
	}

	// info.PortDriver is currently nil for "none" and "implicit", but this may change in future
	if info.PortDriver == nil || info.PortDriver.Driver == "none" || info.PortDriver.Driver == "implicit" {
		return nil, nil
	}

	pdc := &PortDriverClient{
		client:         c,
		portDriverName: info.PortDriver.Driver,
	}

	if info.PortDriver.DisallowLoopbackChildIP {
		// i.e., port-driver="slirp4netns"
		if info.NetworkDriver.ChildIP == nil {
			return nil, fmt.Errorf("RootlessKit port driver (%q) does not allow loopback child IP, but network driver (%q) has no non-loopback IP",
				info.PortDriver.Driver, info.NetworkDriver.Driver)
		}
		childIP, ok := netip.AddrFromSlice(info.NetworkDriver.ChildIP)
		if !ok {
			return nil, fmt.Errorf("unable to use child IP %s from network driver (%q)",
				info.NetworkDriver.ChildIP, info.NetworkDriver.Driver)
		}
		pdc.childIP = childIP.Unmap()
	}

	pdc.protos = make(map[string]struct{}, len(info.PortDriver.Protos))
	for _, p := range info.PortDriver.Protos {
		pdc.protos[p] = struct{}{}
	}

	return pdc, nil
}

// proto normalizes the protocol to match what the rootlesskit API expects.
func (c *PortDriverClient) proto(proto string, hostIP netip.Addr) string {
	// proto is like "tcp", but we need to convert it to "tcp4" or "tcp6" explicitly
	// for libnetwork >= 20201216
	//
	// See https://github.com/moby/libnetwork/pull/2604/files#diff-8fa48beed55dd033bf8e4f8c40b31cf69d0b2cc5d4bb53cde8594670ea6c938aR20
	// See also https://github.com/rootless-containers/rootlesskit/issues/231
	apiProto := proto
	if !strings.HasSuffix(apiProto, "4") && !strings.HasSuffix(apiProto, "6") {
		if hostIP.Is6() {
			apiProto += "6"
		} else {
			apiProto += "4"
		}
	}
	return apiProto
}

// ChildHostIP returns the address that must be used in the child network
// namespace in place of hostIP, a host IP address. In particular, port
// mappings from host IP addresses, and DNAT rules, must use this child
// address in place of the real host address. It may return an invalid
// netip.Addr if the proto and IP family aren't supported.
func (c *PortDriverClient) ChildHostIP(proto string, hostIP netip.Addr) netip.Addr {
	if c == nil {
		return hostIP
	}
	if _, ok := c.protos[c.proto(proto, hostIP)]; !ok {
		// This happens when apiProto="tcp6", portDriverName="slirp4netns",
		// because "slirp4netns" port driver does not support listening on IPv6 yet.
		return netip.Addr{}
	}
	if c.childIP.IsValid() {
		return c.childIP
	}
	// Child namespaces only listen on loopback addresses. Preserve requested
	// loopback addresses (the child namespace's lo covers all of 127.0.0.0/8,
	// and collapsing them makes bindings on the same port but distinct
	// loopback addresses collide); otherwise use the canonical loopback
	// address for the family.
	if hostIP.IsLoopback() {
		return hostIP
	}
	if hostIP.Is6() {
		return netip.IPv6Loopback()
	}
	return netip.MustParseAddr("127.0.0.1")
}

// ProtocolUnsupportedError is returned when apiProto is not supported by portDriverName.
type ProtocolUnsupportedError struct {
	apiProto       string
	portDriverName string
}

func (e *ProtocolUnsupportedError) Error() string {
	return fmt.Sprintf("protocol %q is not supported by the RootlessKit port driver %q",
		e.apiProto, e.portDriverName)
}

// AddPort makes a request to RootlessKit asking it to set up a port
// mapping between a host IP address and a child host IP address.
//
// AddPort may return [ProtocolUnsupportedError].
func (c *PortDriverClient) AddPort(
	ctx context.Context,
	proto string,
	hostIP netip.Addr,
	childIP netip.Addr,
	hostPort int,
) (func() error, error) {
	if c == nil {
		return func() error { return nil }, nil
	}

	apiProto := c.proto(proto, hostIP)
	if _, ok := c.protos[apiProto]; !ok {
		// This happens when apiProto="tcp6", portDriverName="slirp4netns",
		// because "slirp4netns" port driver does not support listening on IPv6 yet.
		//
		// Note that "slirp4netns" port driver is not used by default,
		// even when network driver is set to "slirp4netns".
		//
		// Most users are using "builtin" port driver and will not see this warning.
		err := &ProtocolUnsupportedError{
			apiProto:       apiProto,
			portDriverName: c.portDriverName,
		}
		return nil, err
	}

	pm := c.client.PortManager()
	p := port.Spec{
		Proto:      apiProto,
		ParentIP:   hostIP.String(),
		ParentPort: hostPort,
		ChildIP:    childIP.String(),
		ChildPort:  hostPort,
	}
	st, err := pm.AddPort(ctx, p)
	if err != nil {
		return nil, fmt.Errorf("error while calling RootlessKit PortManager.AddPort(): %w", err)
	}
	deferFunc := func() error {
		if dErr := pm.RemovePort(context.WithoutCancel(ctx), st.ID); dErr != nil {
			return fmt.Errorf("error while calling RootlessKit PortManager.RemovePort(): %w", dErr)
		}
		return nil
	}
	return deferFunc, nil
}
