package buildkit

import (
	"context"
	"net"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/containerd/log"
	"github.com/moby/buildkit/executor"
	"github.com/moby/buildkit/executor/resources"
	"github.com/moby/buildkit/executor/runcexecutor"
	"github.com/moby/buildkit/solver/pb"
	"github.com/moby/buildkit/util/network"
	"github.com/moby/buildkit/util/network/proxyprovider"
	"github.com/moby/moby/v2/daemon/internal/stringid"
	"github.com/opencontainers/runtime-spec/specs-go"
	"github.com/pkg/errors"
)

const networkName = "bridge"

func newExecutor(opts executorOpts) (executor.Executor, network.ProxyProvider, error) {
	netRoot := filepath.Join(opts.root, "net")
	networkProviders := map[pb.NetMode]network.Provider{
		pb.NetMode_UNSET: &bridgeProvider{Controller: opts.networkController, Root: netRoot},
		pb.NetMode_HOST:  network.NewHostProvider(),
		pb.NetMode_NONE:  network.NewNoneProvider(),
	}

	// make sure net state directory is cleared from previous state
	fis, err := os.ReadDir(netRoot)
	if err == nil {
		for _, fi := range fis {
			fp := filepath.Join(netRoot, fi.Name())
			if err := os.RemoveAll(fp); err != nil {
				log.G(context.TODO()).WithError(err).Errorf("failed to delete old network state: %v", fp)
			}
		}
	}

	// Returning a non-nil but empty *IdentityMapping breaks BuildKit:
	// https://github.com/moby/moby/pull/39444
	idmap := &opts.identityMapping
	if opts.identityMapping.Empty() {
		idmap = nil
	}

	rm, err := resources.NewMonitor()
	if err != nil {
		return nil, nil, err
	}

	// TODO: FIXME: testing env var, replace with something better or remove in a major version or two
	runcCmds := []string{"runc"}
	if runcOverride := os.Getenv("DOCKER_BUILDKIT_RUNC_COMMAND"); runcOverride != "" {
		runcCmds = []string{runcOverride}
	}

	proxyProvider := opts.proxyProvider
	ownsProxyProvider := false
	if proxyProvider == nil && proxyprovider.Supported() {
		hostProvider := networkProviders[pb.NetMode_HOST]
		egressProviders := map[pb.NetMode]network.Provider{
			pb.NetMode_UNSET: loopbackFilteredProvider{provider: hostProvider},
			pb.NetMode_HOST:  hostProvider,
		}
		proxyProvider, err = proxyprovider.New(proxyprovider.Opt{
			Root:            filepath.Join(opts.root, "proxy"),
			EgressProviders: egressProviders,
		})
		if err != nil {
			return nil, nil, err
		}
		ownsProxyProvider = true
	}

	exec, err := runcexecutor.New(runcexecutor.Opt{
		Root:                filepath.Join(opts.root, "executor"),
		CommandCandidates:   runcCmds,
		DefaultCgroupParent: opts.cgroupParent,
		Rootless:            opts.rootless,
		NoPivot:             os.Getenv("DOCKER_RAMDISK") != "",
		IdentityMapping:     idmap,
		DNS:                 opts.dnsConfig,
		ApparmorProfile:     opts.apparmorProfile,
		ResourceMonitor:     rm,
		CDIManager:          opts.cdiManager,
		ProxyProvider:       proxyProvider,
	}, networkProviders)
	if err != nil {
		if ownsProxyProvider {
			_ = proxyProvider.Close()
		}
		return nil, nil, err
	}
	return exec, proxyProvider, nil
}

// newExecutorGD calls newExecutor() on Linux. It returns a stubExecutor on
// other platforms.
func newExecutorGD(opts executorOpts) (executor.Executor, network.ProxyProvider, error) {
	return newExecutor(opts)
}

type loopbackFilteredProvider struct {
	provider network.Provider
}

func (p loopbackFilteredProvider) New(ctx context.Context, hostname string, opt network.NamespaceOptions) (network.Namespace, error) {
	ns, err := p.provider.New(ctx, hostname, opt)
	if err != nil {
		return nil, err
	}
	return loopbackFilteredNS{Namespace: ns}, nil
}

func (p loopbackFilteredProvider) Close() error {
	return nil
}

type loopbackFilteredNS struct {
	network.Namespace
}

func (n loopbackFilteredNS) DialContext(ctx context.Context, networkName, address string) (net.Conn, error) {
	if isLoopbackAddress(ctx, address) {
		return nil, errors.Errorf("proxy egress to loopback address %s is not allowed", address)
	}
	dialer, ok := n.Namespace.(network.Dialer)
	if !ok {
		return nil, errors.Errorf("proxy egress network does not support dialing")
	}
	return dialer.DialContext(ctx, networkName, address)
}

func isLoopbackAddress(ctx context.Context, address string) bool {
	host, _, err := net.SplitHostPort(address)
	if err != nil {
		host = address
	}
	host = strings.Trim(host, "[]")
	if strings.EqualFold(host, "localhost") {
		return true
	}
	if ip := net.ParseIP(host); ip != nil {
		return ip.IsLoopback()
	}
	addrs, err := net.DefaultResolver.LookupIPAddr(ctx, host)
	if err != nil {
		return false
	}
	for _, addr := range addrs {
		if addr.IP.IsLoopback() {
			return true
		}
	}
	return false
}

func (iface *lnInterface) Set(s *specs.Spec) error {
	<-iface.ready
	if iface.err != nil {
		log.G(context.TODO()).WithError(iface.err).Error("failed to set networking spec")
		return iface.err
	}
	shortNetCtlrID := stringid.TruncateID(iface.provider.Controller.ID())
	// attach netns to bridge within the container namespace, using reexec in a prestart hook
	s.Hooks = &specs.Hooks{
		Prestart: []specs.Hook{{
			Path: filepath.Join("/proc", strconv.Itoa(os.Getpid()), "exe"),
			Args: []string{"libnetwork-setkey", "-exec-root=" + iface.provider.Config().ExecRoot, iface.sbx.ContainerID(), shortNetCtlrID},
		}},
	}
	return nil
}
