// SPDX-License-Identifier: Apache-2.0
// Copyright Authors of Cilium

package cmd

import (
	"errors"
	"fmt"
	"io/fs"
	"log/slog"
	"os"
	"strings"

	"github.com/vishvananda/netlink"

	"github.com/cilium/cilium/pkg/datapath/linux/safenetlink"
	"github.com/cilium/cilium/pkg/endpointmanager"
	"github.com/cilium/cilium/pkg/logging/logfields"
	"github.com/cilium/cilium/pkg/maps/ctmap"
	"github.com/cilium/cilium/pkg/maps/fragmap"
	ipcachemap "github.com/cilium/cilium/pkg/maps/ipcache"
	"github.com/cilium/cilium/pkg/maps/lxcmap"
	"github.com/cilium/cilium/pkg/maps/metricsmap"
	"github.com/cilium/cilium/pkg/maps/nat"
	"github.com/cilium/cilium/pkg/maps/neighborsmap"
	"github.com/cilium/cilium/pkg/maps/policymap"
	"github.com/cilium/cilium/pkg/maps/tunnel"
	"github.com/cilium/cilium/pkg/option"
)

// listFilterIfs returns a map of interfaces based on the given filter.
// The filter should take a link and, if found, return the index of that
// interface, if not found return -1.
func listFilterIfs(filter func(netlink.Link) int) (map[int]netlink.Link, error) {
	ifs, err := safenetlink.LinkList()
	if err != nil {
		return nil, err
	}
	vethLXCIdxs := map[int]netlink.Link{}
	for _, intf := range ifs {
		if idx := filter(intf); idx != -1 {
			vethLXCIdxs[idx] = intf
		}
	}
	return vethLXCIdxs, nil
}

// clearCiliumVeths checks all veths created by cilium and removes all that
// are considered a leftover from failed attempts to connect the container.
func clearCiliumVeths(logger *slog.Logger) error {
	logger.Info("Removing stale endpoint interfaces")

	leftVeths, err := listFilterIfs(func(intf netlink.Link) int {
		// Filter by veth and return the index of the interface.
		if intf.Type() == "veth" {
			return intf.Attrs().Index
		}
		return -1
	})
	if err != nil {
		return fmt.Errorf("unable to retrieve host network interfaces: %w", err)
	}

	for _, v := range leftVeths {
		peerIndex := v.Attrs().ParentIndex
		parentVeth, found := leftVeths[peerIndex]

		// In addition to name matching, double check whether the parent of the
		// parent is the interface itself, to avoid removing the interface in
		// case we hit an index clash, and the actual parent of the interface is
		// in a different network namespace. Notably, this can happen in the
		// context of Kind nodes, as eth0 is a veth interface itself; if an
		// lxcxxxxxx interface ends up having the same ifindex of the eth0 parent
		// (which is actually located in the root network namespace), we would
		// otherwise end up deleting the eth0 interface, with the obvious
		// ill-fated consequences.
		if found && peerIndex != 0 && strings.HasPrefix(parentVeth.Attrs().Name, "lxc") &&
			parentVeth.Attrs().ParentIndex == v.Attrs().Index {
			scopedLog := logger.With(logfields.Device, v.Attrs().Name)

			scopedLog.Debug("Deleting stale veth device")
			err := netlink.LinkDel(v)
			if err != nil {
				scopedLog.Warn("Unable to delete stale veth device", logfields.Error, err)
			}
		}
	}
	return nil
}

// EndpointMapManager is a wrapper around an endpointmanager as well as the
// filesystem for removing maps related to endpoints from the filesystem.
type EndpointMapManager struct {
	logger *slog.Logger
	endpointmanager.EndpointManager
}

// RemoveDatapathMapping unlinks the endpointID from the global policy map, preventing
// packets that arrive on this node from being forwarded to the endpoint that
// used to exist with the specified ID.
func (e *EndpointMapManager) RemoveDatapathMapping(endpointID uint16) error {
	return policymap.RemoveGlobalMapping(e.logger, uint32(endpointID))
}

// RemoveMapPath removes the specified path from the filesystem.
func (e *EndpointMapManager) RemoveMapPath(path string) {
	if err := os.RemoveAll(path); err != nil {
		e.logger.Warn(
			"Error while deleting stale map file",
			logfields.Path, path,
		)
	} else {
		e.logger.Info(
			"Removed stale bpf map",
			logfields.Path, path,
		)
	}
}

// ListMapsDir gives names of files (or subdirectories) found in the specified path.
func (e *EndpointMapManager) ListMapsDir(path string) []string {
	var maps []string

	entries, err := os.ReadDir(path)
	if err != nil {
		e.logger.Warn(
			"Error while listing maps dir",
			logfields.Path, path,
			logfields.Error, err,
		)
		return maps
	}

	for _, e := range entries {
		maps = append(maps, e.Name())
	}

	return maps
}

// initMaps opens all BPF maps (and creates them if they do not exist). This
// must be done *before* any operations which read BPF maps, especially
// restoring endpoints and services.
func (d *Daemon) initMaps() error {
	if option.Config.DryMode {
		return nil
	}

	if err := lxcmap.LXCMap(d.params.MetricsRegistry).OpenOrCreate(); err != nil {
		return fmt.Errorf("initializing lxc map: %w", err)
	}

	// The ipcache is shared between endpoints. Unpin the old ipcache map created
	// by any previous instances of the agent to prevent new endpoints from
	// picking up the old map pin. The old ipcache will continue to be used by
	// loaded bpf programs, it will just no longer be updated by the agent.
	//
	// This is to allow existing endpoints that have not been regenerated yet to
	// continue using the existing ipcache until the endpoint is regenerated for
	// the first time and its bpf programs have been replaced. Existing endpoints
	// are using a policy map which is potentially out of sync as local identities
	// are re-allocated on startup.
	if err := ipcachemap.IPCacheMap(d.params.MetricsRegistry).Recreate(); err != nil {
		return fmt.Errorf("initializing ipcache map: %w", err)
	}

	if err := metricsmap.Metrics.OpenOrCreate(); err != nil {
		return fmt.Errorf("initializing metrics map: %w", err)
	}

	// Tunnel map is no longer used, not even in tunnel routing mode.
	// Therefore, make sure it gets unpinned at startup.
	err := tunnel.TunnelMap().Unpin()
	if err != nil && !errors.Is(err, fs.ErrNotExist) {
		return fmt.Errorf("removing tunnel map: %w", err)
	}

	for _, m := range ctmap.GlobalMaps(option.Config.EnableIPv4,
		option.Config.EnableIPv6) {
		if err := m.Create(); err != nil {
			return fmt.Errorf("initializing conntrack map %s: %w", m.Name(), err)
		}
	}

	ipv4Nat, ipv6Nat := nat.GlobalMaps(d.params.MetricsRegistry, option.Config.EnableIPv4,
		option.Config.EnableIPv6, d.params.KPRConfig.KubeProxyReplacement || option.Config.EnableBPFMasquerade)
	if ipv4Nat != nil {
		if err := ipv4Nat.Create(); err != nil {
			return fmt.Errorf("initializing ipv4nat map: %w", err)
		}
	}
	if ipv6Nat != nil {
		if err := ipv6Nat.Create(); err != nil {
			return fmt.Errorf("initializing ipv6nat map: %w", err)
		}
	}

	if d.params.KPRConfig.KubeProxyReplacement {
		if err := neighborsmap.InitMaps(option.Config.EnableIPv4,
			option.Config.EnableIPv6); err != nil {
			return fmt.Errorf("initializing neighbors map: %w", err)
		}
	}
	if d.params.KPRConfig.KubeProxyReplacement || option.Config.EnableBPFMasquerade {
		if err := nat.CreateRetriesMaps(option.Config.EnableIPv4,
			option.Config.EnableIPv6); err != nil {
			return fmt.Errorf("initializing NAT retries map: %w", err)
		}
	}

	if option.Config.EnableIPv4FragmentsTracking {
		if err := fragmap.InitMap4(d.params.MetricsRegistry, option.Config.FragmentsMapEntries); err != nil {
			return fmt.Errorf("initializing fragments map: %w", err)
		}
	}

	if option.Config.EnableIPv6FragmentsTracking {
		if err := fragmap.InitMap6(d.params.MetricsRegistry, option.Config.FragmentsMapEntries); err != nil {
			return fmt.Errorf("initializing fragments map: %w", err)
		}
	}

	if !option.Config.RestoreState {
		// If we are not restoring state, all endpoints can be
		// deleted. Entries will be re-populated.
		lxcmap.LXCMap(d.params.MetricsRegistry).DeleteAll()
	}

	return nil
}
