// Copyright 2020 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause

package utils

import (
	"crypto/rand"
	"crypto/sha1"
	"encoding/hex"
	"fmt"
	"net"
	"os"
	"path/filepath"
	goruntime "runtime"
	"strings"

	"github.com/charmbracelet/log"
	"github.com/jsimonetti/rtnetlink/rtnl"
	clabconstants "github.com/srl-labs/containerlab/constants"
	"github.com/vishvananda/netlink"
	"github.com/vishvananda/netns"
)

const (
	parkingNetnsPrefix = "clab-park-"
	// Be conservative and keep the name comfortably within Linux NAME_MAX (255 bytes).
	maxParkingNetnsNameLen = 200
)

// BridgeByName returns a *netlink.Bridge referenced by its name.
func BridgeByName(name string) (*netlink.Bridge, error) {
	l, err := netlink.LinkByName(name)
	if err != nil {
		return nil, fmt.Errorf("could not lookup %q: %v", name, err)
	}
	br, ok := l.(*netlink.Bridge)
	if !ok {
		return nil, fmt.Errorf("%q already exists but is not a bridge", name)
	}
	return br, nil
}

// LinkContainerNS creates a symlink for containers network namespace
// so that it can be managed by iproute2 utility.
func LinkContainerNS(nspath, containerName string) error {
	CreateDirectory("/run/netns/", clabconstants.PermissionsDirDefault)
	dst := "/run/netns/" + containerName
	if _, err := os.Lstat(dst); err == nil {
		os.Remove(dst)
	}
	err := os.Symlink(nspath, dst)
	if err != nil {
		return err
	}
	return nil
}

// GenMac generates a random MAC address for a given OUI.
func GenMac(oui string) (net.HardwareAddr, error) {
	buf := make([]byte, 3)
	_, _ = rand.Read(buf)

	hwa, err := net.ParseMAC(fmt.Sprintf("%s:%02x:%02x:%02x", oui, buf[0], buf[1], buf[2]))
	return hwa, err
}

// DeleteNetnsSymlink deletes a network namespace and removes the symlink created by LinkContainerNS
// func.
func DeleteNetnsSymlink(n string) error {
	log.Debug("Deleting netns symlink: ", n)
	sl := fmt.Sprintf("/run/netns/%s", n)
	err := os.Remove(sl)
	if err != nil {
		log.Debug("Failed to delete netns symlink by path:", sl)
	}
	return nil
}

// ParkingNetnsName returns the deterministic named-netns identifier used to park
// a container's dataplane interfaces while the container is stopped.
func ParkingNetnsName(containerName string) string {
	name := parkingNetnsPrefix + containerName
	if len(name) <= maxParkingNetnsNameLen {
		return name
	}

	sum := sha1.Sum([]byte(containerName))
	suffix := hex.EncodeToString(sum[:])[:10]

	// leave room for "-" + suffix
	maxBaseLen := maxParkingNetnsNameLen - 1 - len(suffix)
	if maxBaseLen < 1 {
		return suffix
	}

	return name[:maxBaseLen] + "-" + suffix
}

// LinkIPs returns IPv4/IPv6 addresses assigned to a link referred by its name.
func LinkIPs(ln string) (v4addrs, v6addrs []netlink.Addr, err error) {
	l, err := netlink.LinkByName(ln)
	if err != nil {
		return nil, nil, fmt.Errorf("failed to lookup link %q: %w", ln, err)
	}

	v4addrs, err = netlink.AddrList(l, netlink.FAMILY_V4)
	if err != nil {
		return nil, nil, err
	}

	v6addrs, err = netlink.AddrList(l, netlink.FAMILY_V6)
	if err != nil {
		return nil, nil, err
	}

	return
}

// FirstLinkIPs returns string representation of the first IPv4/v6 address
// found for a link referenced by name.
func FirstLinkIPs(ln string) (v4, v6 string, err error) {
	v4addrs, v6addrs, err := LinkIPs(ln)
	if err != nil {
		return
	}

	if len(v4addrs) != 0 {
		v4 = v4addrs[0].IP.String()
	}

	if len(v6addrs) != 0 {
		v6 = v6addrs[0].IP.String()
	}

	return v4, v6, err
}

// GetLinksByNamePrefix returns a list of links whose name matches a prefix.
func GetLinksByNamePrefix(prefix string) ([]netlink.Link, error) {
	// filtered list of interfaces
	if prefix == "" {
		return nil, fmt.Errorf("prefix is not specified")
	}
	var fls []netlink.Link

	ls, err := netlink.LinkList()
	if err != nil {
		return nil, err
	}
	for _, l := range ls {
		if strings.HasPrefix(l.Attrs().Name, prefix) {
			fls = append(fls, l)
		}
	}
	if len(fls) == 0 {
		return nil, fmt.Errorf("no links found by specified prefix %s", prefix)
	}
	return fls, nil
}

func GetRouteForIP(ip net.IP) (*rtnl.Route, error) {
	conn, err := rtnl.Dial(nil)
	if err != nil {
		return nil, fmt.Errorf("can't establish netlink connection: %s", err)
	}
	defer conn.Close()

	r, err := conn.RouteGet(ip)

	return r, err
}

func CreateNamedNetNS(name string) (string, error) {
	nsPath := filepath.Join("/run/netns", name)

	goruntime.LockOSThread()
	defer goruntime.UnlockOSThread()

	currentNS, err := netns.Get()
	if err != nil {
		return "", err
	}
	defer currentNS.Close()

	defer func() {
		_ = netns.Set(currentNS)
	}()

	newNS, err := netns.NewNamed(name)
	if err != nil {
		if os.IsExist(err) && FileOrDirExists(nsPath) {
			return nsPath, nil
		}
		return "", err
	}

	newNS.Close()

	return nsPath, nil
}

// create a new netns or return the nspath if it exists.
func CreateOrGetNamedNetNS(name string) (string, error) {
	if nsPath, err := GetNamedNetNS(name); err == nil {
		return nsPath, nil
	}

	return CreateNamedNetNS(name)
}

func GetNamedNetNS(name string) (string, error) {
	nsPath := filepath.Join("/run/netns", name)
	if !FileOrDirExists(nsPath) {
		return "", fmt.Errorf("named netns %q does not exist", name)
	}

	return nsPath, nil
}

// SanitizeInterfaceName sanitizes the interface name by replacing '/' and ' ' with '-'.
// Making it suitable to write as AltName for the interface.
func SanitizeInterfaceName(ifaceName string) string {
	var sb strings.Builder
	sb.Grow(len(ifaceName))

	for _, char := range ifaceName {
		switch char {
		case '/', ' ':
			sb.WriteRune('-')
		default:
			sb.WriteRune(char)
		}
	}

	return sb.String()
}
