// Copyright (c) 2020-2021 Tigera, Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package nat

import (
	"fmt"
	"os"
	"path"
	"path/filepath"
	"strings"
	"time"

	log "github.com/sirupsen/logrus"

	"github.com/projectcalico/calico/felix/bpf"
	"github.com/projectcalico/calico/felix/bpf/bpfdefs"
	"github.com/projectcalico/calico/felix/bpf/bpfutils"
	"github.com/projectcalico/calico/felix/bpf/jump"
	"github.com/projectcalico/calico/felix/bpf/libbpf"
	"github.com/projectcalico/calico/felix/bpf/maps"
	"github.com/projectcalico/calico/felix/bpf/utils"
)

const (
	MapIndexCTLBConnectV6 = iota
	MapIndexCTLBSendV6
	MapIndexCTLBRecvV6
)

var ctlbProgToIndex = map[string]int{
	"calico_connect_v6": MapIndexCTLBConnectV6,
	"calico_sendmsg_v6": MapIndexCTLBSendV6,
	"calico_recvmsg_v6": MapIndexCTLBRecvV6,
}

var ConnMapParameters = maps.MapParameters{
	Type:       "prog_array",
	KeySize:    4,
	ValueSize:  4,
	MaxEntries: 1,
	Name:       "cali_ctlb_conn",
}

var SendMapParameters = maps.MapParameters{
	Type:       "prog_array",
	KeySize:    4,
	ValueSize:  4,
	MaxEntries: 1,
	Name:       "cali_ctlb_send",
}

var RecvMapParameters = maps.MapParameters{
	Type:       "prog_array",
	KeySize:    4,
	ValueSize:  4,
	MaxEntries: 1,
	Name:       "cali_ctlb_recv",
}

func ProgramsMap() []maps.Map {
	return []maps.Map{
		maps.NewPinnedMap(ConnMapParameters),
		maps.NewPinnedMap(SendMapParameters),
		maps.NewPinnedMap(RecvMapParameters),
	}
}

func RemoveConnectTimeLoadBalancer(ipv4Enabled bool, cgroupv2 string) error {
	if os.Getenv("FELIX_DebugSkipCTLBCleanup") == "true" {
		log.Info("FV special case: skipping CTLB cleanup")
		return nil
	}

	bpfMount, err := utils.MaybeMountBPFfs()
	if err != nil {
		return fmt.Errorf("failed to mount bpffs: %w", err)
	}

	pinDir := path.Join(bpfMount, bpfdefs.CtlbPinDir)
	defer bpf.CleanUpCalicoPins(pinDir)
	ctlbProgsMap := ProgramsMap()
	for _, index := range ctlbProgToIndex {
		if err := ctlbProgsMap[index].EnsureExists(); err != nil {
			return fmt.Errorf("failed to create ctlb jump map: %w", err)
		}
	}
	for _, index := range ctlbProgToIndex {
		err := ctlbProgsMap[index].Delete(jump.Key(0))
		if err != nil && !os.IsNotExist(err) {
			log.Errorf("failed to delete the ctlb jump map entry: %s", err)
		}
	}
	for _, index := range ctlbProgToIndex {
		ctlbProgsMap[index].Close()
		os.Remove(ctlbProgsMap[index].Path())
	}

	if err := detachCtlbPrograms(ipv4Enabled, pinDir, cgroupv2); err != nil {
		return err
	}
	bpf.CleanUpCalicoPins(pinDir)
	return nil
}

func detachCtlbPrograms(ipv4Enabled bool, pinDir, cgroupv2 string) error {
	numLinksDetached := 0
	err := filepath.Walk(pinDir, func(path string, info os.FileInfo, err error) error {
		if err != nil {
			return err
		}
		if strings.HasPrefix(info.Name(), "cali_") || strings.HasPrefix(info.Name(), "calico_") {
			numLinksDetached++
			log.WithField("path", path).Debug("Detaching pinned link")
			link, err := libbpf.OpenLink(path)
			if err != nil {
				log.WithField("path", path).Error("Error opening link")
				return err
			}
			defer link.Close()
			err = link.Detach()
			if err != nil {
				log.WithField("path", path).Error("Error detaching link")
				return err
			}
		}
		return nil
	})
	if err != nil && !os.IsNotExist(err) {
		log.WithError(err).Error("error detaching link")
		return err
	}
	if numLinksDetached == 0 {
		return detachLegacyCtlb(ipv4Enabled, cgroupv2)
	}
	return nil
}

func detachLegacyCtlb(ipv4Enabled bool, cgroupv2 string) error {
	cgroupPath, err := ensureCgroupPath(cgroupv2)
	if err != nil {
		return fmt.Errorf("failed to set-up cgroupv2: %w", err)
	}
	return libbpf.DetachCTLBProgramsLegacy(ipv4Enabled, cgroupPath)
}

func loadProgram(logLevel, ipver string, udpNotSeen time.Duration, excludeUDP bool) (*libbpf.Obj, error) {
	filename := path.Join(bpfdefs.ObjectDir, ProgFileName(logLevel, ipver))
	obj, err := bpf.LoadObject(filename, &libbpf.CTLBGlobalData{UDPNotSeen: udpNotSeen, ExcludeUDP: excludeUDP})
	if err != nil {
		return nil, fmt.Errorf("error loading %s:%w", filename, err)
	}
	return obj, nil
}

func attachProgram(name, ipver, bpfMount, cgroupPath string, udpNotSeen time.Duration, excludeUDP bool, obj *libbpf.Obj, legacy bool) error {
	progName := "calico_" + name + "_v" + ipver
	progPinPath := path.Join(bpfMount, progName)
	if _, err := os.Stat(progPinPath); err == nil {
		link, err := libbpf.OpenLink(progPinPath)
		if err != nil {
			return fmt.Errorf("error opening link %s : %w", progPinPath, err)
		}
		defer link.Close()
		if err := link.Update(obj, progName); err != nil {
			return fmt.Errorf("error updating program %s : %w", progName, err)
		}
		return nil
	}

	// Used only for UT to test legacy way to attach.
	if legacy {
		err := obj.AttachCGroupLegacy(cgroupPath, progName)
		if err != nil {
			return fmt.Errorf("failed to attach program %s: %w", progName, err)
		}
		return nil
	}

	// Tries non-legacy and fallsback to legacy if non-legacy fails.
	link, err := obj.AttachCGroup(cgroupPath, progName)
	if err != nil {
		err = obj.AttachCGroupLegacy(cgroupPath, progName)
		if err != nil {
			return fmt.Errorf("failed to attach program %s: %w", progName, err)
		}
		link = nil
	} else if link != nil {
		defer link.Close()
		err := link.Pin(progPinPath)
		if err != nil {
			return fmt.Errorf("failed to pin program %s:%w", progName, err)
		}
	}
	log.WithFields(log.Fields{"program": progName, "cgroup": cgroupPath}).Info("Loaded cgroup program")
	return nil
}

func updateCTLBJumpMap(jumpMap maps.Map, obj *libbpf.Obj, prog string) error {
	fd, err := obj.ProgramFD(prog)
	if err != nil {
		return fmt.Errorf("failed to get prog FD. Program = %s: %w", prog, err)
	}

	err = maps.UpdateMapEntry(jumpMap.MapFD(), jump.Key(0), jump.Value(uint32(fd)))
	if err != nil {
		log.WithError(err).Errorf("Failed to update %s map at index 0", prog)
		return err
	}
	return nil
}

func installCTLB(ipv4Enabled, ipv6Enabled bool, cgroupv2 string, logLevel string, udpNotSeen time.Duration, excludeUDP bool, ctlbProgsMap []maps.Map, legacy bool) error {
	bpfMount, err := utils.MaybeMountBPFfs()
	if err != nil {
		log.WithError(err).Error("Failed to mount bpffs, unable to do connect-time load balancing")
		return err
	}

	pinDir := path.Join(bpfMount, bpfdefs.CtlbPinDir)
	if err = os.MkdirAll(pinDir, 0700); err != nil {
		log.WithError(err).Error("Failed to create pin dir, unable to do connect-time load balancing")
		return err
	}

	cgroupPath, err := ensureCgroupPath(cgroupv2)
	if err != nil {
		return fmt.Errorf("failed to set-up cgroupv2: %w", err)
	}

	var v4Obj, v6Obj *libbpf.Obj

	// Load and attach v4, v46 CTLB program.
	if ipv4Enabled {
		v4Obj, err = loadProgram(logLevel, "4", udpNotSeen, excludeUDP)
		if err != nil {
			return err
		}
		defer v4Obj.Close()

		v46Obj, err := loadProgram(logLevel, "46", udpNotSeen, excludeUDP)
		if err != nil {
			return err
		}
		defer v46Obj.Close()
		err = attachProgram("connect", "4", pinDir, cgroupPath, udpNotSeen, excludeUDP, v4Obj, legacy)
		if err != nil {
			return err
		}
		err = attachProgram("connect", "46", pinDir, cgroupPath, udpNotSeen, excludeUDP, v46Obj, legacy)
		if err != nil {
			return err
		}

		if !excludeUDP {
			err = attachProgram("sendmsg", "4", pinDir, cgroupPath, udpNotSeen, false, v4Obj, legacy)
			if err != nil {
				return err
			}

			err = attachProgram("recvmsg", "4", pinDir, cgroupPath, udpNotSeen, false, v4Obj, legacy)
			if err != nil {
				return err
			}

			err = attachProgram("sendmsg", "46", pinDir, cgroupPath, udpNotSeen, false, v46Obj, legacy)
			if err != nil {
				return err
			}

			err = attachProgram("recvmsg", "46", pinDir, cgroupPath, udpNotSeen, false, v46Obj, legacy)
			if err != nil {
				return err
			}
		}
	}
	// Load the v6 CTLB program.
	if ipv6Enabled {
		v6Obj, err = loadProgram(logLevel, "6", udpNotSeen, excludeUDP)
		if err != nil {
			return err
		}
		defer v6Obj.Close()
		// If dual-stack, populate the jump maps with v6 ctlb programs.
		if ipv4Enabled {
			for prog, index := range ctlbProgToIndex {
				err = updateCTLBJumpMap(ctlbProgsMap[index], v6Obj, prog)
				if err != nil {
					return err
				}
			}
		} else {
			err = attachProgram("connect", "6", pinDir, cgroupPath, udpNotSeen, excludeUDP, v6Obj, legacy)
			if err != nil {
				return err
			}

			if !excludeUDP {
				err = attachProgram("sendmsg", "6", pinDir, cgroupPath, udpNotSeen, false, v6Obj, legacy)
				if err != nil {
					return err
				}

				err = attachProgram("recvmsg", "6", pinDir, cgroupPath, udpNotSeen, false, v6Obj, legacy)
				if err != nil {
					return err
				}
			}
		}
	}
	return nil
}

func InstallConnectTimeLoadBalancer(ipv4Enabled, ipv6Enabled bool, cgroupv2 string, logLevel string, udpNotSeen time.Duration, excludeUDP bool, ctlbProgramsMap []maps.Map) error {
	return installCTLB(ipv4Enabled, ipv6Enabled, cgroupv2, logLevel, udpNotSeen, excludeUDP, ctlbProgramsMap, false)
}

func InstallConnectTimeLoadBalancerLegacy(ipv4Enabled, ipv6Enabled bool, cgroupv2 string, logLevel string, udpNotSeen time.Duration, excludeUDP bool, ctlbProgramsMap []maps.Map) error {
	return installCTLB(ipv4Enabled, ipv6Enabled, cgroupv2, logLevel, udpNotSeen, excludeUDP, ctlbProgramsMap, true)
}

func ProgFileName(logLevel, ipver string) string {
	logLevel = strings.ToLower(logLevel)
	if logLevel == "off" {
		logLevel = "no_log"
	}

	btf := ""
	if bpfutils.BTFEnabled {
		btf = "_co-re"
	}

	return fmt.Sprintf("connect_balancer_%s%s_v%s.o", logLevel, btf, ipver)
}

func ensureCgroupPath(cgroupv2 string) (string, error) {
	cgroupRoot, err := utils.MaybeMountCgroupV2()
	if err != nil {
		return "", err
	}
	cgroupPath := cgroupRoot
	if cgroupv2 != "" {
		cgroupPath = path.Clean(path.Join(cgroupRoot, cgroupv2))
		if !strings.HasPrefix(cgroupPath, path.Clean(cgroupRoot)) {
			log.Panic("Invalid cgroup path outside the root")
		}
		err = os.MkdirAll(cgroupPath, 0766)
		if err != nil {
			log.WithError(err).Error("Failed to make cgroup")
			return "", fmt.Errorf("failed to create cgroup: %w", err)
		}
	}
	return cgroupPath, nil
}
