package links

import (
	"context"
	"crypto/sha1"
	"encoding/hex"
	"errors"
	"fmt"
	"net/netip"
	"strings"
	"syscall"
	"time"

	"github.com/charmbracelet/log"
	"github.com/containernetworking/plugins/pkg/ns"
	"github.com/google/uuid"
	clabinternalslices "github.com/srl-labs/containerlab/internal/slices"
	clabnodesstate "github.com/srl-labs/containerlab/nodes/state"
	clabutils "github.com/srl-labs/containerlab/utils"
	"github.com/vishvananda/netlink"
	"gopkg.in/yaml.v2"
)

type LinkDeploymentState uint8

const (
	LinkDeploymentStateNotDeployed = iota
	// LinkDeploymentStateHalfDeployed is a state in which one of the endpoints
	// of the links finished deploying and the other one is not yet deployed.
	LinkDeploymentStateHalfDeployed
	LinkDeploymentStateFullDeployed
	LinkDeploymentStateRemoved

	ownershipAltNamePrefix = "clab-o-"

	linkDeployRetries      = 3
	linkDeployRetryBackoff = 100 * time.Millisecond
)

var linkAddAltName = netlink.LinkAddAltName

// LinkCommonParams represents the common parameters for all link types.
type LinkCommonParams struct {
	MTU             int                 `yaml:"mtu,omitempty"`
	Labels          map[string]string   `yaml:"labels,omitempty"`
	IPv4            []string            `yaml:"ipv4,omitempty"`
	IPv6            []string            `yaml:"ipv6,omitempty"`
	Vars            map[string]any      `yaml:"vars,omitempty"`
	DeploymentState LinkDeploymentState `yaml:",omitempty"`
}

// GetMTU returns the MTU of the link.
func (l *LinkCommonParams) GetMTU() int {
	return l.MTU
}

// GetVars returns the link-level vars.
func (l *LinkCommonParams) GetVars() map[string]any {
	return l.Vars
}

// LinkDefinition represents a link definition in the topology file.
type LinkDefinition struct {
	Type string  `yaml:"type,omitempty"`
	Link RawLink `yaml:",inline"`
}

// LinkType represents the type of a link definition.
type LinkType string

const (
	LinkTypeVEth        LinkType = "veth"
	LinkTypeVethStitch  LinkType = "veth-stitch"
	LinkTypeMgmtNet     LinkType = "mgmt-net"
	LinkTypeMacVLan     LinkType = "macvlan"
	LinkTypeHost        LinkType = "host"
	LinkTypeVxlan       LinkType = "vxlan"
	LinkTypeVxlanStitch LinkType = "vxlan-stitch"
	LinkTypeDummy       LinkType = "dummy"
	LinkTypeBridge      LinkType = "bridge"

	// LinkTypeBrief is a link definition where link types
	// are encoded in the endpoint definition as string and allow users
	// to quickly type out link endpoints in a yaml file.
	LinkTypeBrief LinkType = "brief"
)

// parseLinkType parses a string representation of a link type into a LinkDefinitionType.
func parseLinkType(s string) (LinkType, error) {
	switch strings.TrimSpace(strings.ToLower(s)) {
	case string(LinkTypeMacVLan):
		return LinkTypeMacVLan, nil

	case string(LinkTypeVEth):
		return LinkTypeVEth, nil

	case string(LinkTypeVethStitch):
		return LinkTypeVethStitch, nil

	case string(LinkTypeMgmtNet):
		return LinkTypeMgmtNet, nil

	case string(LinkTypeHost):
		return LinkTypeHost, nil

	case string(LinkTypeBrief):
		return LinkTypeBrief, nil

	case string(LinkTypeVxlan):
		return LinkTypeVxlan, nil

	case string(LinkTypeVxlanStitch):
		return LinkTypeVxlanStitch, nil

	case string(LinkTypeDummy):
		return LinkTypeDummy, nil

	default:
		return "", fmt.Errorf("unable to parse %q as LinkType", s)
	}
}

var _ yaml.Unmarshaler = (*LinkDefinition)(nil)

// UnmarshalYAML deserializes links passed via topology file into LinkDefinition struct.
// It supports both the brief and specific link type notations.
func (ld *LinkDefinition) UnmarshalYAML( //nolint: funlen
	unmarshal func(any) error,
) error { // skipcq: GO-R1005
	// struct to avoid recursion when unmarshaling
	// used only to unmarshal the type field.
	var a struct {
		Type string `yaml:"type"`
	}

	// yaml.TypeError is returned when the yaml parser encounters
	// an unknown field. We want to ignore this error and continue
	// parsing the rest of the fields as we only care about the type field
	// in the a struct.
	var e *yaml.TypeError

	err := unmarshal(&a)
	if err != nil && !errors.As(err, &e) {
		return err
	}

	var lt LinkType

	// if no type is specified, we assume that brief notation of a link definition is used.
	if a.Type == "" {
		lt = LinkTypeBrief
		ld.Type = string(LinkTypeBrief)
	} else {
		ld.Type = a.Type

		lt, err = parseLinkType(a.Type)
		if err != nil {
			return err
		}
	}

	switch lt {
	case LinkTypeVEth:
		var l struct {
			// the Type field is injected artificially
			// to allow strict yaml parsing to work.
			Type        string `yaml:"type"`
			LinkVEthRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		ld.Link = &l.LinkVEthRaw

	case LinkTypeVethStitch:
		var l struct {
			// the Type field is injected artificially
			// to allow strict yaml parsing to work.
			Type                string `yaml:"type"`
			LinkVEthStitchedRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		ld.Link = &l.LinkVEthStitchedRaw

	case LinkTypeMgmtNet:
		var l struct {
			Type           string `yaml:"type"`
			LinkMgmtNetRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		ld.Link = &l.LinkMgmtNetRaw

	case LinkTypeHost:
		var l struct {
			Type        string `yaml:"type"`
			LinkHostRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		ld.Link = &l.LinkHostRaw

	case LinkTypeMacVLan:
		var l struct {
			Type           string `yaml:"type"`
			LinkMacVlanRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		ld.Link = &l.LinkMacVlanRaw

	case LinkTypeVxlan:
		var l struct {
			Type         string `yaml:"type"`
			LinkVxlanRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		l.LinkVxlanRaw.LinkType = LinkTypeVxlan
		ld.Link = &l.LinkVxlanRaw

	case LinkTypeVxlanStitch:
		var l struct {
			Type         string `yaml:"type"`
			LinkVxlanRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		l.LinkVxlanRaw.LinkType = LinkTypeVxlanStitch
		ld.Link = &l.LinkVxlanRaw

	case LinkTypeDummy:
		var l struct {
			Type         string `yaml:"type"`
			LinkDummyRaw `yaml:",inline"`
		}
		err := unmarshal(&l)
		if err != nil {
			return err
		}
		ld.Link = &l.LinkDummyRaw

	case LinkTypeBrief:
		// brief link's endpoint format
		var l struct {
			Type         string `yaml:"type"`
			LinkBriefRaw `yaml:",inline"`
		}

		err := unmarshal(&l)
		if err != nil {
			return err
		}

		ld.Type = string(LinkTypeBrief)

		ld.Link, err = l.LinkBriefRaw.ToTypeSpecificRawLink()
		if err != nil {
			return err
		}

		if veth, ok := ld.Link.(*LinkVEthRaw); ok {
			veth.fromBrief = true
		}

	default:
		return fmt.Errorf("unknown link type %q", lt)
	}

	return nil
}

// MarshalYAML serializes LinkDefinition (e.g when used with generate command).
// As of now it falls back to converting the LinkConfig into a
// RawVEthLink, such that the generated LinkConfigs adhere to the new LinkDefinition
// format instead of the brief one.
func (r *LinkDefinition) MarshalYAML() (any, error) {
	switch r.Link.GetType() {
	case LinkTypeHost:
		x := struct {
			LinkHostRaw `yaml:",inline"`
			Type        string `yaml:"type"`
		}{
			LinkHostRaw: *r.Link.(*LinkHostRaw),
			Type:        string(LinkTypeVEth),
		}
		return x, nil
	case LinkTypeVEth:
		x := struct {
			// the Type field is injected artificially
			// to allow strict yaml parsing to work.
			Type        string `yaml:"type"`
			LinkVEthRaw `yaml:",inline"`
		}{
			LinkVEthRaw: *r.Link.(*LinkVEthRaw),
			Type:        string(LinkTypeVEth),
		}
		return x, nil
	case LinkTypeVethStitch:
		x := struct {
			Type                string `yaml:"type"`
			LinkVEthStitchedRaw `yaml:",inline"`
		}{
			LinkVEthStitchedRaw: *r.Link.(*LinkVEthStitchedRaw),
			Type:                string(LinkTypeVethStitch),
		}
		return x, nil
	case LinkTypeMgmtNet:
		x := struct {
			Type           string `yaml:"type"`
			LinkMgmtNetRaw `yaml:",inline"`
		}{
			LinkMgmtNetRaw: *r.Link.(*LinkMgmtNetRaw),
			Type:           string(LinkTypeMgmtNet),
		}
		return x, nil
	case LinkTypeMacVLan:
		x := struct {
			Type           string `yaml:"type"`
			LinkMacVlanRaw `yaml:",inline"`
		}{
			LinkMacVlanRaw: *r.Link.(*LinkMacVlanRaw),
			Type:           string(LinkTypeMacVLan),
		}
		return x, nil
	case LinkTypeVxlan:
		x := struct {
			Type         string `yaml:"type"`
			LinkVxlanRaw `yaml:",inline"`
		}{
			LinkVxlanRaw: *r.Link.(*LinkVxlanRaw),
			Type:         string(LinkTypeVxlan),
		}
		return x, nil
	case LinkTypeDummy:
		x := struct {
			Type         string `yaml:"type"`
			LinkDummyRaw `yaml:",inline"`
		}{
			LinkDummyRaw: *r.Link.(*LinkDummyRaw),
			Type:         string(LinkTypeDummy),
		}
		return x, nil
	case LinkTypeBrief:
		return r.Link, nil
	}

	return nil, fmt.Errorf("unable to marshall")
}

// RawLink is an interface that all raw link types must implement.
// Raw link types define the links as they are defined in the topology file
// and solely a product of unmarshaling.
// Raw links are later "resolved" to concrete link types (e.g LinkVeth).
type RawLink interface {
	Resolve(params *ResolveParams) (Link, error)
	GetType() LinkType
}

type filteredLinkCleaner interface {
	cleanupFilteredLink(context.Context, string, []string) error
}

// CleanupFilteredLinks lets raw link types remove resources that cannot be
// reached through resolved links after a node filter excludes one endpoint.
func CleanupFilteredLinks(
	ctx context.Context,
	linkDefs []*LinkDefinition,
	labName string,
	nodesFilter []string,
) error {
	if len(nodesFilter) == 0 {
		return nil
	}

	for i, linkDef := range linkDefs {
		if linkDef == nil {
			continue
		}
		cleaner, ok := linkDef.Link.(filteredLinkCleaner)
		if !ok {
			continue
		}
		if err := cleaner.cleanupFilteredLink(ctx, labName, nodesFilter); err != nil {
			return fmt.Errorf("failed cleaning up filtered link %d: %w", i, err)
		}
	}

	return nil
}

// Link is an interface that all concrete link types must implement.
// Concrete link types are resolved from raw links and become part of CLab.Links.
type Link interface {
	// Deploy deploys the link. Endpoint is the endpoint that triggers the creation of the link.
	Deploy(context.Context, Endpoint) error
	// PostDeploy runs link work that must happen after endpoint deployment.
	PostDeploy(context.Context) error
	// Remove removes the link.
	Remove(context.Context) error
	// GetType returns the type of the link.
	GetType() LinkType
	// GetEndpoints returns the endpoints of the link.
	GetEndpoints() []Endpoint
	// GetRuntimeEndpoints returns endpoints apply/deploy may create, discover, or remove.
	// Parent-only and remote metadata endpoints are excluded where applicable.
	GetRuntimeEndpoints() []Endpoint
	// GetMTU returns the Link MTU.
	GetMTU() int
	// GetVars returns the link-level vars.
	GetVars() map[string]any
}

func extractHostNodeInterfaceData(
	lb *LinkBriefRaw,
	specialEPIndex int,
) (host, hostIf, node, nodeIf string, err error) {
	// the index of the node is the specialEndpointIndex +1  modulo 2
	nodeindex := (specialEPIndex + 1) % 2

	hostData := strings.SplitN(lb.Endpoints[specialEPIndex], ":", 2)
	nodeData := strings.SplitN(lb.Endpoints[nodeindex], ":", 2)

	if len(hostData) != 2 {
		return "", "", "", "",
			fmt.Errorf(
				"invalid link endpoint format. expected <node>:<port>, got %s",
				lb.Endpoints[specialEPIndex],
			)
	}

	if len(nodeData) != 2 {
		return "", "", "", "",
			fmt.Errorf(
				"invalid link endpoint format. expected <node>:<port>, got %s",
				lb.Endpoints[nodeindex],
			)
	}

	host = hostData[0]
	hostIf = hostData[1]
	node = nodeData[0]
	nodeIf = nodeData[1]

	log.Debugf(
		"extractHostNodeInterfaceData: eps=%v specialIndex=%d -> host=%s hostIf=%s node=%s nodeIf=%s",
		lb.Endpoints,
		specialEPIndex,
		host,
		hostIf,
		node,
		nodeIf,
	)
	return host, hostIf, node, nodeIf, nil
}

func mapBriefVarsToEndpoints(lb *LinkBriefRaw, endpoints []*EndpointRaw) error {
	if len(lb.LinkCommonParams.IPv4) > 0 {
		if err := parseVarIPBrief("ipv4", lb.LinkCommonParams.IPv4, endpoints); err != nil {
			return err
		}
	}

	// Map IPv6 addresses from link-level to endpoints
	if len(lb.LinkCommonParams.IPv6) > 0 {
		if err := parseVarIPBrief("ipv6", lb.LinkCommonParams.IPv6, endpoints); err != nil {
			return err
		}
	}
	return nil
}

func parseVarIPBrief(af string, vals []string, endpoints []*EndpointRaw) error {
	nVals := len(vals)

	if nVals == 0 {
		return nil
	} else if nVals > len(endpoints) {
		return fmt.Errorf("number of defined %s vars exceed number of endpoints", af)
	}

	for i, cidr := range vals {
		cidr = strings.TrimSpace(cidr)
		if cidr == "" {
			// empty entry = no addr on this interface
			continue
		}
		prefix, err := netip.ParsePrefix(cidr)
		if err != nil {
			return fmt.Errorf("endpoint %s var has invalid prefix %q (%v)", af, cidr, err)
		}
		switch af {
		case "ipv4":
			if !prefix.Addr().Is4() {
				return fmt.Errorf("endpoint %s var has non-IPv4 prefix %q", af, prefix)
			}
			endpoints[i].IPv4 = cidr
		case "ipv6":
			if !prefix.Addr().Is6() {
				return fmt.Errorf("endpoint %s var has non-IPv6 prefix %q", af, prefix)
			}
			endpoints[i].IPv6 = cidr
		}
	}
	return nil
}

func genRandomIfName() string {
	return "clab-" + genRandomString(8)
}

func genRandomString(length int) string {
	s, _ := uuid.New().MarshalText() // .MarshalText() always return a nil error
	return string(s[:length])
}

// Node interface is an interface that is satisfied by all nodes.
// It is used a subset of the nodes.Node interface and is used to pass nodes.Nodes
// to the link resolver without causing a circular dependency.
type Node interface {
	// AddLinkToContainer adds a link to the node (container).
	// In case of a regular container, it will push the link into the
	// network namespace and then run the function f within the namespace
	// this is to rename the link, set mtu, set the interface up, e.g. see
	// link.SetNameMACAndUpInterface()
	//
	// In case of a bridge node (ovs or regular linux bridge) it will take the interface and make
	// the bridge
	// the master of the interface and bring the interface up.
	AddLinkToContainer(ctx context.Context, link netlink.Link, f func(ns.NetNS) error) error
	// AddEndpoint attaches an endpoint discovered from topology resolution and may normalize
	// endpoint identity first, such as interface-name remapping.
	AddEndpoint(e Endpoint) error
	AdoptEndpoint(e Endpoint) error
	ReleaseEndpoint(e Endpoint) error
	GetLinkEndpointType() LinkEndpointType
	GetShortName() string
	GetEndpoints() []Endpoint
	ExecFunction(context.Context, func(ns.NetNS) error) error
	GetState() clabnodesstate.NodeState
	Delete(ctx context.Context) error
}

type LinkEndpointType string

const (
	LinkEndpointTypeVeth     = "veth"
	LinkEndpointTypeBridge   = "bridge"
	LinkEndpointTypeBridgeNS = "bridge-ns"
	LinkEndpointTypeHost     = "host"
)

// SetNameMACAndUpInterface is a helper function that will bind interface name and Mac
// and return a function that can run in the netns.Do() call for execution in a network namespace.
func SetNameMACAndUpInterface(l netlink.Link, endpt Endpoint) func(ns.NetNS) error {
	return func(_ ns.NetNS) error {
		// rename the link created with random name if its length is acceptable by linux
		if IsValidInterfaceName(endpt.GetIfaceName()) {
			err := netlink.LinkSetName(l, endpt.GetIfaceName())
			if err != nil {
				return fmt.Errorf(
					"failed to rename link: %v", err)
			}
		} else {
			// when the name is too long, we add a sanitized interface name as AltName
			sanitizedIfaceName := clabutils.SanitizeInterfaceName(endpt.GetIfaceName())
			err := netlink.LinkAddAltName(l, sanitizedIfaceName)
			if err != nil {
				return fmt.Errorf(
					"failed to add altname: %v", err)
			}
		}

		// lets set the MAC address if provided
		if len(endpt.GetMac()) == 6 {
			err := netlink.LinkSetHardwareAddr(l, endpt.GetMac())
			if err != nil {
				return err
			}
		}

		if endpt.GetIfaceAlias() != "" {
			err := netlink.LinkSetAlias(l, endpt.GetIfaceAlias())
			if err != nil {
				return err
			}
			// Set a sanitized altname for ease of access. '/', and ' ' are changed to '-'
			sanitizedIfaceName := clabutils.SanitizeInterfaceName(endpt.GetIfaceAlias())
			err = netlink.LinkAddAltName(l, sanitizedIfaceName)
			if err != nil {
				return err
			}
		}

		if err := addOwnershipAltName(l, endpt); err != nil {
			return err
		}

		// bring the given link up
		if err := netlink.LinkSetUp(l); err != nil {
			return fmt.Errorf("failed to set %q up: %v",
				endpt.GetIfaceName(), err)
		}

		return nil
	}
}

func ownershipAltName(endpt Endpoint) string {
	return ownershipAltNameFor(endpt.GetNode().GetShortName(), endpt.GetIfaceName())
}

func ownershipAltNameFor(nodeName, ifaceName string) string {
	sum := sha1.Sum([]byte(nodeName + "\x00" + ifaceName))
	return ownershipAltNamePrefix + hex.EncodeToString(sum[:8])
}

func hasOwnershipAltName(link netlink.Link) bool {
	for _, altName := range link.Attrs().AltNames {
		if strings.HasPrefix(altName, ownershipAltNamePrefix) {
			return true
		}
	}

	return false
}

// HasOwnershipAltName reports whether link carries a containerlab ownership marker.
func HasOwnershipAltName(link netlink.Link) bool {
	return hasOwnershipAltName(link)
}

// HasOwnershipAltNameFor reports whether link is owned by the logical endpoint
// identified by nodeName and ifaceName.
func HasOwnershipAltNameFor(link netlink.Link, nodeName, ifaceName string) bool {
	if link == nil || nodeName == "" || ifaceName == "" {
		return false
	}

	want := ownershipAltNameFor(nodeName, ifaceName)
	for _, altName := range link.Attrs().AltNames {
		if altName == want {
			return true
		}
	}

	return false
}

func addOwnershipAltName(link netlink.Link, endpt Endpoint) error {
	if hasOwnershipAltName(link) {
		return nil
	}

	altName := ownershipAltName(endpt)
	if err := linkAddAltName(link, altName); err != nil {
		if isAltNameNotSupportedErr(err) {
			log.Warnf(
				"kernel does not support interface altname capability; consider upgrading the kernel for full containerlab compatibility. Continuing without containerlab ownership marker %q for %s",
				altName,
				endpt,
			)
			return nil
		}

		return fmt.Errorf("failed to add containerlab ownership altname: %w", err)
	}

	return nil
}

func isAltNameNotSupportedErr(err error) bool {
	return errors.Is(err, syscall.EOPNOTSUPP)
}

// isTransientNetlinkErr returns true for errors caused by transient kernel-level
// race conditions during concurrent netlink operations (e.g. EFAULT, ENODEV).
func isTransientNetlinkErr(err error) bool {
	for _, errno := range []syscall.Errno{
		syscall.EFAULT, // "bad address" - concurrent namespace operations
		syscall.ENODEV, // "no such device" - interface not yet visible
		syscall.ENOENT, // "no such file or directory" - namespace path race
	} {
		if errors.Is(err, errno) {
			return true
		}
	}
	return false
}

// retryTransientNetlink runs fn retrying up to linkDeployRetries times with
// backoff on the transient races (see isTransientNetlinkErr).
func retryTransientNetlink(ctx context.Context, operationDesc string, fn func() error) error {
	var lastErr error

	for attempt := range linkDeployRetries {
		lastErr = fn()
		if lastErr == nil || !isTransientNetlinkErr(lastErr) {
			return lastErr
		}

		log.Debugf("transient netlink error during %s (attempt %d/%d): %v",
			operationDesc, attempt+1, linkDeployRetries, lastErr)

		select {
		case <-ctx.Done():
			return ctx.Err()
		case <-time.After(linkDeployRetryBackoff << attempt):
		}
	}

	return lastErr
}

// ResolveParams is a struct that is passed to the Resolve() function of a raw link
// to resolve it to a concrete link type.
// Parameters include all nodes of a topology and the name of the management bridge.
type ResolveParams struct {
	Nodes          map[string]Node
	MgmtBridgeName string
	// LabName is the name of the lab, used to scope generated host-global
	// resources such as veth-stitch network namespaces.
	LabName string
	// list of node shortnames that user
	// passed as a node filter
	NodesFilter []string
	// for the tools command we need to overwrite the
	// veth interface name on the host side. So this can
	// be set and will thereby overwrite the general interface
	// name generation.
	VxlanIfaceNameOverwrite string
}

type VerifyLinkParams struct {
	RunBridgeExistsCheck  bool
	AllowExistingEndpoint bool
}

func NewVerifyLinkParams() *VerifyLinkParams {
	return &VerifyLinkParams{
		RunBridgeExistsCheck: true,
	}
}

// isInFilter returns true if the endpoints of the link
// are part of the nodes filter which means that the link
// should be resolved and deployed.
// In other words, returning true means that the link should be deployed.
func isInFilter(params *ResolveParams, endpoints []*EndpointRaw) bool {
	// empty filter means that all links should be deployed
	if len(params.NodesFilter) == 0 {
		return true
	}

	for _, e := range endpoints {
		if !clabinternalslices.Contains(params.NodesFilter, e.Node) {
			return false
		}
	}

	return true
}

// IsValidInterfaceName checks if the interface name is valid
// by checking its length and the presence of spaces or slashes.
func IsValidInterfaceName(ifaceName string) bool {
	if len(ifaceName) > 15 {
		return false
	}

	// interface name can not contain spaces or slashes
	if strings.ContainsAny(ifaceName, " /") {
		return false
	}

	return true
}
