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

package nodes

import (
	"context"
	"errors"
	"fmt"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"regexp"
	"strconv"
	"strings"
	"sync"

	"github.com/charmbracelet/log"
	"github.com/containernetworking/plugins/pkg/ns"
	clabcert "github.com/srl-labs/containerlab/cert"
	clabconstants "github.com/srl-labs/containerlab/constants"
	clabexec "github.com/srl-labs/containerlab/exec"
	clablinks "github.com/srl-labs/containerlab/links"
	clabnodesstate "github.com/srl-labs/containerlab/nodes/state"
	clabruntime "github.com/srl-labs/containerlab/runtime"
	clabtypes "github.com/srl-labs/containerlab/types"
	clabutils "github.com/srl-labs/containerlab/utils"
	"github.com/vishvananda/netlink"
	"github.com/vishvananda/netns"
)

const (
	// container validation variables.
	containerNamePattern = "^[a-zA-Z0-9][a-zA-Z0-9-._]+$"
	dnsIncompatibleChars = "._"
	maxNameLength        = 60

	boxenImageVendorLabel = "org.opencontainers.image.vendor"
	boxenImageVendorValue = "Boxen"
)

var containerNamePatternRe = regexp.MustCompile(containerNamePattern)

// DefaultNode implements the Node interface and is embedded to the structs of all other nodes.
// It has common fields and methods that every node should typically have. Nodes can override
// methods if needed.
type DefaultNode struct {
	Cfg              *clabtypes.NodeConfig
	Mgmt             *clabtypes.MgmtNet
	Runtime          clabruntime.ContainerRuntime
	HostRequirements *clabtypes.HostRequirements
	// SSHConfig is the SSH client configuration that a clab node requires.
	SSHConfig *clabtypes.SSHConfig
	// Indicates that the node should not start without no license file defined
	LicensePolicy clabtypes.LicensePolicy
	// OverwriteNode stores the interface used to overwrite methods defined
	// for DefaultNode, so that particular nodes can provide custom implementations.
	OverwriteNode NodeOverwrites
	// List of link endpoints that are connected to the node.
	Endpoints []clablinks.Endpoint
	// Interface aliasing-related variables
	InterfaceRegexp       *regexp.Regexp
	InterfaceMappedPrefix string
	InterfaceOffset       int
	InterfaceHelp         string
	FirstDataIfIndex      int
	// State of the node
	state      clabnodesstate.NodeState
	statemutex sync.RWMutex
	StopSignal clabtypes.Signal
}

// NewDefaultNode initializes the DefaultNode structure and receives a NodeOverwrites interface
// which is implemented by the node struct of a particular kind.
// This allows DefaultNode to access fields of the specific node struct in the methods defined for
// DefaultNode.
func NewDefaultNode(n NodeOverwrites) *DefaultNode {
	dn := &DefaultNode{
		HostRequirements: clabtypes.NewHostRequirements(),
		OverwriteNode:    n,
		LicensePolicy:    clabtypes.LicensePolicyNone,
		SSHConfig:        clabtypes.NewSSHConfig(),
	}

	dn.InterfaceMappedPrefix = "eth"
	dn.InterfaceOffset = 0
	dn.FirstDataIfIndex = 0

	return dn
}

func (d *DefaultNode) WithMgmtNet(mgmt *clabtypes.MgmtNet)                   { d.Mgmt = mgmt }
func (d *DefaultNode) WithRuntime(r clabruntime.ContainerRuntime)            { d.Runtime = r }
func (d *DefaultNode) GetRuntime() clabruntime.ContainerRuntime              { return d.Runtime }
func (d *DefaultNode) Config() *clabtypes.NodeConfig                         { return d.Cfg }
func (*DefaultNode) PostDeploy(_ context.Context, _ *PostDeployParams) error { return nil }
func (*DefaultNode) PreStop(context.Context) error                           { return nil }

// PreDeploy is a common method for all nodes that is called before the node is deployed.
func (d *DefaultNode) PreDeploy(_ context.Context, params *PreDeployParams) error {
	if d.Cfg.IsRootNamespaceBased || d.Cfg.SkipUniquenessCheck {
		return nil
	}
	_, err := d.LoadOrGenerateCertificate(params.Cert, params.TopologyName)
	if err != nil {
		return fmt.Errorf("loading or generating certificate for node %q: %w", d.Cfg.ShortName, err)
	}
	return nil
}

func (d *DefaultNode) SaveConfig(_ context.Context) (*SaveConfigResult, error) {
	// nodes should have the save method defined on their respective structs.
	// By default SaveConfig is a noop.
	log.Debugf("Save operation is currently not supported for %q node kind", d.Cfg.Kind)
	return nil, nil
}

// CheckDeploymentConditions wraps individual functions that check if a node
// satisfies deployment requirements.
func (d *DefaultNode) CheckDeploymentConditions(ctx context.Context) error {
	err := d.OverwriteNode.VerifyHostRequirements()
	if err != nil {
		return err
	}

	err = d.OverwriteNode.VerifyStartupConfig(d.Cfg.LabDir)
	if err != nil {
		return err
	}

	err = d.OverwriteNode.CheckInterfaceName()
	if err != nil {
		return err
	}

	err = d.OverwriteNode.VerifyLicenseFileExists(ctx)
	if err != nil {
		return err
	}

	err = d.OverwriteNode.VerifyContainerName()
	if err != nil {
		return err
	}

	return nil
}

func (d *DefaultNode) PullImage(ctx context.Context) error {
	for imageKey, imageName := range d.OverwriteNode.GetImages(ctx) {
		if imageName == "" {
			return fmt.Errorf(
				"missing required %q attribute for node %q",
				imageKey,
				d.Cfg.ShortName,
			)
		}
		err := d.Runtime.PullImage(ctx, imageName, d.Config().ImagePullPolicy)
		if err != nil {
			return err
		}
	}
	return nil
}

func (d *DefaultNode) VerifyHostRequirements() error {
	return d.HostRequirements.Verify(d.Cfg.Kind, d.Cfg.ShortName)
}

func (d *DefaultNode) Deploy(ctx context.Context, _ *DeployParams) error {
	// Set the "CLAB_INTFS" variable to the number of interfaces (endpoints) a node has.
	// This env var does not count in the eth0 interface that is automatically created by the
	// container runtime. This env var is used by some containers (e.g. vrnetlab systems) to
	// postpone the startup until all interfaces
	// have been added to the container namespace.
	d.Config().Env[clabconstants.ClabEnvIntfs] = strconv.Itoa(len(d.GetEndpoints()))

	// create the container
	cID, err := d.Runtime.CreateContainer(ctx, d.Cfg)
	if err != nil {
		return err
	}

	// start the container
	_, err = d.Runtime.StartContainer(ctx, cID, d)
	if err != nil {
		return err
	}

	// Update the nodes state
	d.SetState(clabnodesstate.Deployed)

	return nil
}

// GetNSPath retrieves the nodes nspath.
func (d *DefaultNode) GetNSPath(ctx context.Context) (string, error) {
	var err error
	nsp := ""

	if d.Cfg.IsRootNamespaceBased {
		netns, err := ns.GetCurrentNS()
		if err != nil {
			return "", err
		}
		nsp = netns.Path()
	}
	if nsp == "" {
		nsp, err = d.Runtime.GetNSPath(ctx, d.OverwriteNode.GetContainerName())
		if err != nil {
			log.Errorf("Unable to determine NetNS Path for node %s: %v", d.Cfg.ShortName, err)
			return "", err
		}
	}

	return nsp, err
}

// ShouldSkipLifecycle reports whether lifecycle operations should no-op for this node.
func (d *DefaultNode) ShouldSkipLifecycle() bool {
	return d.Cfg.IsRootNamespaceBased || d.Cfg.AutoRemove
}

// LinkApplyMode returns the conservative default for node kinds that do not
// explicitly opt into live hotplug or restart handling.
func (d *DefaultNode) LinkApplyMode(ctx context.Context) LinkApplyMode {
	if d.Cfg != nil && (d.Cfg.IsRootNamespaceBased || d.Cfg.SkipUniquenessCheck) {
		return LinkApplyModeLive
	}
	return d.ImageLinkApplyMode(ctx, LinkApplyModeRecreate)
}

// ImageLinkApplyMode allows node kinds to keep their normal fallback behavior
// while opting into live link apply when their image declares support for it.
func (d *DefaultNode) ImageLinkApplyMode(
	ctx context.Context,
	fallback LinkApplyMode,
) LinkApplyMode {
	if d.imageHasBoxenVendorLabel(ctx) {
		return LinkApplyModeLive
	}

	return fallback
}

func (d *DefaultNode) imageHasBoxenVendorLabel(ctx context.Context) bool {
	if d == nil || d.Cfg == nil || d.Cfg.Image == "" {
		return false
	}
	if d.Runtime == nil {
		log.Debug("Skipping image label check for apply link hotplug support",
			"image", d.Cfg.Image,
			"reason", "node runtime is not initialized",
		)
		return false
	}

	inspect, err := d.Runtime.InspectImage(ctx, d.Cfg.Image)
	if err != nil {
		log.Debug("Failed to inspect image for apply link hotplug support",
			"image", d.Cfg.Image,
			"error", err,
		)
		return false
	}
	if inspect == nil || inspect.Config.Labels == nil {
		return false
	}

	return inspect.Config.Labels[boxenImageVendorLabel] == boxenImageVendorValue
}

func (d *DefaultNode) ComputeDiff(oldCfg, newCfg *clabtypes.NodeConfig) *clabtypes.TopologyDiff {
	diff := &clabtypes.TopologyDiff{}

	if oldCfg == nil || newCfg == nil {
		return diff
	}

	if oldCfg.NodeType != newCfg.NodeType {
		diff.Fields = append(diff.Fields, "Type")
	}
	if oldCfg.Kind != newCfg.Kind {
		diff.Fields = append(diff.Fields, "Kind")
	}
	if oldCfg.Image != newCfg.Image {
		diff.Fields = append(diff.Fields, "Image")
	}
	if oldCfg.GetHostname() != newCfg.GetHostname() {
		diff.Fields = append(diff.Fields, "Hostname")
	}
	if oldCfg.Entrypoint != newCfg.Entrypoint {
		diff.Fields = append(diff.Fields, "Entrypoint")
	}
	if oldCfg.Cmd != newCfg.Cmd {
		diff.Fields = append(diff.Fields, "Cmd")
	}
	if !clabutils.SlicesEqualOrBothEmpty(oldCfg.Exec, newCfg.Exec) {
		diff.Fields = append(diff.Fields, "Exec")
	}
	if !clabutils.MapsEqualOrBothEmpty(oldCfg.Env, newCfg.Env) {
		diff.Fields = append(diff.Fields, "Env")
	}
	if !clabutils.SlicesEqualOrBothEmpty(oldCfg.Binds, newCfg.Binds) {
		diff.Fields = append(diff.Fields, "Binds")
	}
	if !clabutils.SlicesEqualOrBothEmpty(oldCfg.Devices, newCfg.Devices) {
		diff.Fields = append(diff.Fields, "Devices")
	}
	if !clabutils.SlicesEqualOrBothEmpty(oldCfg.CapAdd, newCfg.CapAdd) {
		diff.Fields = append(diff.Fields, "CapAdd")
	}
	if oldCfg.Privileged != newCfg.Privileged {
		diff.Fields = append(diff.Fields, "Privileged")
	}
	if oldCfg.CgroupnsMode != newCfg.CgroupnsMode {
		diff.Fields = append(diff.Fields, "CgroupnsMode")
	}
	if oldCfg.CgroupParent != newCfg.CgroupParent {
		diff.Fields = append(diff.Fields, "CgroupParent")
	}
	if oldCfg.PidMode != newCfg.PidMode {
		diff.Fields = append(diff.Fields, "PidMode")
	}
	if !clabutils.MapsEqualOrBothEmpty(oldCfg.Tmpfs, newCfg.Tmpfs) {
		diff.Fields = append(diff.Fields, "Tmpfs")
	}
	if !clabutils.SlicesEqualOrBothEmpty(oldCfg.SecurityOpts, newCfg.SecurityOpts) {
		diff.Fields = append(diff.Fields, "SecurityOpts")
	}
	if oldCfg.ShmSize != newCfg.ShmSize {
		diff.Fields = append(diff.Fields, "ShmSize")
	}
	if !clabutils.PortSetsEqual(oldCfg.PortSet, newCfg.PortSet) {
		diff.Fields = append(diff.Fields, "Ports")
	}
	if oldCfg.User != newCfg.User {
		diff.Fields = append(diff.Fields, "User")
	}
	if oldCfg.NetworkMode != newCfg.NetworkMode {
		diff.Fields = append(diff.Fields, "NetworkMode")
	}
	if oldCfg.Runtime != newCfg.Runtime {
		diff.Fields = append(diff.Fields, "Runtime")
	}
	if oldCfg.CPU != newCfg.CPU {
		diff.Fields = append(diff.Fields, "CPU")
	}
	if oldCfg.CPUSet != newCfg.CPUSet {
		diff.Fields = append(diff.Fields, "CPUSet")
	}
	if oldCfg.Memory != newCfg.Memory {
		diff.Fields = append(diff.Fields, "Memory")
	}
	if oldCfg.License != newCfg.License {
		diff.Fields = append(diff.Fields, "License")
	}
	if (len(oldCfg.Components) > 0 || len(newCfg.Components) > 0) &&
		!reflect.DeepEqual(oldCfg.Components, newCfg.Components) {
		diff.Fields = append(diff.Fields, "Components")
	}

	return diff
}

func (d *DefaultNode) ComputeReconcilePlan(diff *clabtypes.TopologyDiff) *ReconcileResult {
	result := &ReconcileResult{Action: clabtypes.TopologyDiffActionNone}

	if diff == nil || !diff.HasDiff() {
		return result
	}

	action := diff.DefaultAction()
	result.Action = action

	switch action {
	case clabtypes.TopologyDiffActionRestart:
		result.Restarted = []string{d.Cfg.LongName}
	case clabtypes.TopologyDiffActionRecreate:
		result.Recreated = []string{d.Cfg.LongName}
	}

	return result
}

func (d *DefaultNode) GetReconcilePlan(
	_ context.Context,
	diff *clabtypes.TopologyDiff,
) (*ReconcileResult, error) {
	return d.ComputeReconcilePlan(diff), nil
}

// Reconcile applies the diff and executes the appropriate action (restart/recreate).
func (d *DefaultNode) Reconcile(
	ctx context.Context,
	diff *clabtypes.TopologyDiff,
) (*ReconcileResult, error) {
	result := d.ComputeReconcilePlan(diff)

	if result.Action == clabtypes.TopologyDiffActionNone {
		return result, nil
	}

	log.Info(
		"Applying node changes",
		"node",
		d.Cfg.ShortName,
		"action",
		result.Action,
		"fields",
		diff.Fields,
	)

	switch result.Action {
	case clabtypes.TopologyDiffActionRestart:
		if err := d.Stop(ctx); err != nil {
			return result, fmt.Errorf("stop failed: %w", err)
		}
		if err := d.Start(ctx); err != nil {
			return result, fmt.Errorf("start failed: %w", err)
		}

	case clabtypes.TopologyDiffActionRecreate:
		if err := d.Delete(ctx); err != nil {
			return result, fmt.Errorf("delete failed: %w", err)
		}
	}

	return result, nil
}

func (d *DefaultNode) parkingNetNSName() string {
	return clabutils.ParkingNetnsName(d.Cfg.LongName)
}

func (d *DefaultNode) parkingNetNSPath() (string, error) {
	return clabutils.GetNamedNetNS(d.parkingNetNSName())
}

// cleanupParkingNetNS removes the node-owned parking namespace if it exists.
func (d *DefaultNode) cleanupParkingNetNS() error {
	if _, err := d.parkingNetNSPath(); err != nil {
		return nil
	}

	return netns.DeleteNamed(d.parkingNetNSName())
}

func (d *DefaultNode) Delete(ctx context.Context) error {
	for _, e := range d.Endpoints {
		err := e.GetLink().Remove(ctx)
		if err != nil {
			return err
		}
	}

	if d.OverwriteNode.GetContainerStatus(ctx) == clabruntime.NotFound {
		return nil
	}

	return d.Runtime.DeleteContainer(ctx, d.OverwriteNode.GetContainerName())
}

func (d *DefaultNode) GetImages(_ context.Context) map[string]string {
	return map[string]string{
		ImageKey: d.Cfg.Image,
	}
}

func (d *DefaultNode) GetContainers(ctx context.Context) ([]clabruntime.GenericContainer, error) {
	cnts, err := d.Runtime.ListContainers(ctx, []*clabtypes.GenericFilter{
		{
			FilterType: "name",
			Match:      d.OverwriteNode.GetContainerName(),
		},
	})
	if err != nil {
		return nil, err
	}
	// check that we retrieved some container information
	// otherwise throw ErrContainersNotFound error
	if len(cnts) == 0 {
		return nil, fmt.Errorf(
			"Node: %s. %w",
			d.OverwriteNode.GetContainerName(),
			ErrContainersNotFound,
		)
	}

	return cnts, err
}

func (d *DefaultNode) RunExecFromConfig(ctx context.Context, ec *clabexec.ExecCollection) error {
	for _, e := range d.Config().Exec {
		exec, err := clabexec.NewExecCmdFromString(e)
		if err != nil {
			log.Warnf("Failed to parse the command string: %s, %v", e, err)
		}

		res, err := d.OverwriteNode.RunExec(ctx, exec)
		if err != nil {
			// kinds which do not support exec functionality are skipped
			continue
		}

		ec.Add(d.GetShortName(), res)
	}
	return nil
}

func (d *DefaultNode) UpdateConfigWithRuntimeInfo(ctx context.Context) error {
	cnts, err := d.OverwriteNode.GetContainers(ctx)
	if err != nil {
		return err
	}

	if len(cnts) == 0 {
		return fmt.Errorf("no container runtime information retrieved")
	}

	// network settings of a first container only
	netSettings := cnts[0].NetworkSettings

	d.Cfg.MgmtIPv4Address = netSettings.IPv4addr
	d.Cfg.MgmtIPv4PrefixLength = netSettings.IPv4pLen
	d.Cfg.MgmtIPv6Address = netSettings.IPv6addr
	d.Cfg.MgmtIPv6PrefixLength = netSettings.IPv6pLen
	d.Cfg.MgmtIPv4Gateway = netSettings.IPv4Gw
	d.Cfg.MgmtIPv6Gateway = netSettings.IPv6Gw

	d.Cfg.ContainerID = cnts[0].ID

	d.Cfg.ResultingPortBindings = cnts[0].Ports

	return nil
}

// DeleteNetnsSymlink deletes the symlink file created for the container netns.
func (d *DefaultNode) DeleteNetnsSymlink() error {
	log.Debugf("Deleting %s network namespace", d.OverwriteNode.GetContainerName())
	return clabutils.DeleteNetnsSymlink(d.OverwriteNode.GetContainerName())
}

func (d *DefaultNode) CheckInterfaceOverlap() error {
	var seenEps []clablinks.Endpoint

	for _, ep := range d.Endpoints {
		ifName := ep.GetIfaceName()
		for _, seenEp := range seenEps {
			if ifName == seenEp.GetIfaceName() {
				return fmt.Errorf("%q and %q have overlapping interface names", ep, seenEp)
			}
		}
		seenEps = append(seenEps, ep)
	}

	return nil
}

// CheckInterfaceName checks if a name of the interface referenced in the topology file is in the
// expected range of name values.
// A no-op for the default node, specific nodes should implement this method.
func (d *DefaultNode) CheckInterfaceName() error {
	return d.CheckInterfaceOverlap()
}

// CalculateInterfaceIndex parses the supplied interface name with the InterfaceRegexp.
// Using the the port offset, it calculates the mapped interface index based on the InterfaceOffset
// and the first data interface index.
func (d *DefaultNode) CalculateInterfaceIndex(ifName string) (int, error) {
	captureGroups, err := clabutils.GetRegexpCaptureGroups(d.InterfaceRegexp, ifName)
	if err != nil {
		return 0, err
	}

	if parsedIndex, found := captureGroups["port"]; found {
		parsedIndexInt, err := strconv.Atoi(parsedIndex)
		if err != nil {
			return 0, fmt.Errorf(
				"%q parsed index %q could not be cast to an integer",
				ifName,
				parsedIndex,
			)
		}
		calculatedIndex := parsedIndexInt - d.InterfaceOffset + d.FirstDataIfIndex
		return calculatedIndex, nil
	} else {
		return 0, fmt.Errorf(
			"%q does not have extracted interface index with regexp %q, 'port' capture group missing?",
			ifName,
			d.InterfaceRegexp,
		)
	}
}

// GetMappedInterfaceName returns with a mapped interface name based on the mapped interface prefix
// and calculated mapped interface index.
func (d *DefaultNode) GetMappedInterfaceName(ifName string) (string, error) {
	ifIndex, err := d.OverwriteNode.CalculateInterfaceIndex(ifName)
	if err != nil {
		return "", err
	}
	if ifIndex < d.FirstDataIfIndex {
		return "", fmt.Errorf(
			"extracted interface index for %q is out of bounds: %d ! >= %d",
			ifName,
			ifIndex,
			d.FirstDataIfIndex,
		)
	}
	mappedIfName := fmt.Sprintf("%s%d", d.InterfaceMappedPrefix, ifIndex)

	return mappedIfName, nil
}

// VerifyStartupConfig verifies that startup config files exists on disks.
func (d *DefaultNode) VerifyStartupConfig(topoDir string) error {
	cfg := d.Config().StartupConfig
	if cfg == "" {
		return nil
	}

	rcfg := clabutils.ResolvePath(cfg, topoDir)
	if !clabutils.FileExists(rcfg) {
		return fmt.Errorf("node %q startup-config file not found by the path %s",
			d.OverwriteNode.GetContainerName(), rcfg)
	}

	return nil
}

// GenerateConfig generates configuration for the nodes
// out of the template `t` based on the node configuration and saves the result to dst.
// If the config file is already present in the node dir we do not regenerate the config unless
// EnforceStartupConfig is explicitly set to true and startup-config points to a file this will
// persist the changes that users make to a running config when booted from some startup config.
func (d *DefaultNode) GenerateConfig(dst, t string) error {
	// Check for incompatible options
	if d.Cfg.EnforceStartupConfig && d.Cfg.SuppressStartupConfig {
		return ErrIncompatibleOptions
	}
	if d.Cfg.EnforceStartupConfig && d.Cfg.StartupConfig == "" {
		return ErrNoStartupConfig
	}

	if d.Cfg.SuppressStartupConfig {
		log.Info("Startup config generation suppressed", "node", d.Cfg.ShortName)
		return nil
	}

	if !d.Cfg.EnforceStartupConfig && clabutils.FileExists(dst) {
		log.Debug("Existing config found", "node", d.Cfg.ShortName, "path", dst)
		return nil
	} else {
		log.Debug("Generating config", "node", d.Cfg.ShortName, "file", d.Cfg.StartupConfig)

		cfgBuf, err := clabutils.SubstituteEnvsAndTemplate(strings.NewReader(t), d.Cfg)
		if err != nil {
			return err
		}
		log.Debug("Generated config", "node", d.Cfg.ShortName, "content", cfgBuf.String())

		f, err := os.Create(dst)
		if err != nil {
			return err
		}
		defer f.Close()

		_, err = f.Write(cfgBuf.Bytes())
		if err != nil {
			return err
		}
	}

	return nil
}

// NodeOverwrites is an interface that every node implements.
// It is used to enable DefaultNode to get access to the particular node structs
// and is provided as an argument of the NewDefaultNode function.
// The methods defined for this interfaces are the methods that particular node has a custom
// implementation of.
type NodeOverwrites interface {
	VerifyStartupConfig(topoDir string) error
	CheckInterfaceName() error
	CalculateInterfaceIndex(ifName string) (int, error)
	GetMappedInterfaceName(ifName string) (string, error)
	VerifyHostRequirements() error
	PullImage(ctx context.Context) error
	GetImages(ctx context.Context) map[string]string
	GetContainers(ctx context.Context) ([]clabruntime.GenericContainer, error)
	GetContainerName() string
	GetContainerStatus(ctx context.Context) clabruntime.ContainerStatus
	VerifyContainerName() error
	VerifyLicenseFileExists(context.Context) error
	PreStop(context.Context) error
	RunExec(context.Context, *clabexec.ExecCmd) (*clabexec.ExecResult, error)
	GetNSPath(ctx context.Context) (string, error)
}

// LoadStartupConfigFileVr templates a startup-config using the file specified for VM-based nodes in
// the topo
// and puts the resulting config file by the LabDir/configDirName/startupCfgFName path.
func LoadStartupConfigFileVr(node Node, configDirName, startupCfgFName string) error {
	nodeCfg := node.Config()
	// create config directory that will be bind mounted to vrnetlab container at / path
	clabutils.CreateDirectory(path.Join(nodeCfg.LabDir, configDirName),
		clabconstants.PermissionsOpen)

	if nodeCfg.StartupConfig != "" {
		// dstCfg is a path to a file on the clab host that will have rendered configuration
		dstCfg := filepath.Join(nodeCfg.LabDir, configDirName, startupCfgFName)

		c, err := os.ReadFile(nodeCfg.StartupConfig)
		if err != nil {
			return err
		}

		cfgTemplate := string(c)

		err = node.GenerateConfig(dstCfg, cfgTemplate)
		if err != nil {
			log.Errorf("node=%s, failed to generate config: %v", nodeCfg.ShortName, err)
		}
	}
	return nil
}

// GetContainerName returns the name used by the runtime to identify the container
// e.g. ext-container nodes use the name as defined in the topo file, while most other containers
// use long (prefixed) name.
func (d *DefaultNode) GetContainerName() string {
	return d.Cfg.LongName
}

func (d *DefaultNode) VerifyContainerName() error {
	containerName := d.OverwriteNode.GetContainerName()
	if !containerNamePatternRe.MatchString(containerName) {
		return fmt.Errorf("Node name contains invalid characters: %s", containerName)
	}

	if len(containerName) > maxNameLength {
		log.Warn(
			"Node name will not resolve via DNS",
			"name",
			containerName,
			"reason",
			fmt.Sprintf("name exceeds %d characters", maxNameLength),
		)
	}

	if strings.ContainsAny(containerName, dnsIncompatibleChars) {
		log.Warn(
			"Node name will not resolve via DNS",
			"name",
			containerName,
			"reason",
			"name contains invalid characters such as '.' and/or '_'",
		)
	}

	return nil
}

// RunExec executes a single command for a node.
func (d *DefaultNode) RunExec(
	ctx context.Context,
	execCmd *clabexec.ExecCmd,
) (*clabexec.ExecResult, error) {
	execResult, err := d.GetRuntime().Exec(ctx, d.OverwriteNode.GetContainerName(), execCmd)
	if err != nil {
		log.Errorf("%s: failed to execute cmd: %q with error %v",
			d.OverwriteNode.GetContainerName(), execCmd.GetCmdString(), err)
		return nil, err
	}
	return execResult, nil
}

// RunExecNotWait executes a command for a node, and doesn't block waiting for the output.
// Should be overridden if the nodes implementation differs.
func (d *DefaultNode) RunExecNotWait(ctx context.Context, execCmd *clabexec.ExecCmd) error {
	err := d.GetRuntime().ExecNotWait(ctx, d.OverwriteNode.GetContainerName(), execCmd)
	if err != nil {
		log.Errorf("%s: failed to execute cmd: %q with error %v",
			d.OverwriteNode.GetContainerName(), execCmd.GetCmdString(), err)
		return err
	}
	return nil
}

// VerifyLicenseFileExists checks if a license file with a provided path exists.
func (d *DefaultNode) VerifyLicenseFileExists(_ context.Context) error {
	if d.Config().License == "" {
		switch d.LicensePolicy {
		// if a license is required by the kind but not provided
		case clabtypes.LicensePolicyRequired:
			return fmt.Errorf(
				"node %s of kind %s requires a license. Provide one via 'license' knob in the topology file",
				d.Config().ShortName,
				d.Cfg.Kind,
			)
		case clabtypes.LicensePolicyWarn:
			// just warn when no license is provided
			log.Warnf(
				"node %s of kind %s requires a license. Make sure to provide it in some way (e.g. license knob or baked into image)",
				d.Config().ShortName,
				d.Cfg.Kind,
			)
			return nil
		case clabtypes.LicensePolicyNone:
			// license is not required
			return nil
		default:
			return fmt.Errorf("unknown license policy value %s for node %s kind %s",
				d.LicensePolicy, d.Config().ShortName, d.Cfg.Kind)
		}
	}
	// if license is provided check path exists
	rlic := clabutils.ResolvePath(d.Config().License, d.Cfg.LabDir)
	if !clabutils.FileExists(rlic) {
		return fmt.Errorf(
			"license file for node %q is not found by the path %s",
			d.Config().ShortName,
			rlic,
		)
	}

	return nil
}

// LoadOrGenerateCertificate loads a certificate using a certificate storage provider
// provided in certInfra or generates a new one if it does not exist.
func (d *DefaultNode) LoadOrGenerateCertificate(
	certInfra *clabcert.Cert,
	topoName string,
) (nodeCert *clabcert.Certificate, err error) {
	// early return if certificate generation is not required
	if d.Cfg.Certificate == nil || !*d.Cfg.Certificate.Issue {
		return nil, nil
	}

	nodeConfig := d.Cfg

	// try loading existing certificates from disk and generate new ones if they do not exist
	nodeCert, err = certInfra.LoadNodeCert(nodeConfig.ShortName)
	if err != nil {
		log.Debugf("creating node certificate for %s", nodeConfig.ShortName)

		hosts := []string{
			nodeConfig.ShortName,
			nodeConfig.LongName,
			nodeConfig.ShortName + "." + topoName + ".io",
		}
		// add the SANs provided via config
		hosts = append(hosts, nodeConfig.Certificate.SANs...)

		// add mgmt IPs as SANs to CSR
		for _, ip := range []string{d.Cfg.MgmtIPv4Address, d.Cfg.MgmtIPv6Address} {
			if ip != "" {
				hosts = append(hosts, ip)
			}
		}

		certInput := &clabcert.NodeCSRInput{
			CommonName:   nodeConfig.ShortName + "." + topoName + ".io",
			Hosts:        hosts,
			Organization: "containerlab",
			Country:      "US",
			KeySize:      d.Cfg.Certificate.KeySize,
			Expiry:       d.Cfg.Certificate.ValidityDuration,
		}
		// Generate the cert for the node
		nodeCert, err = certInfra.GenerateAndSignNodeCert(certInput)
		if err != nil {
			return nil, err
		}

		// persist the cert via certStorage
		err = certInfra.StoreNodeCert(nodeConfig.ShortName, nodeCert)
		if err != nil {
			return nil, err
		}
	}

	return nodeCert, nil
}

func (d *DefaultNode) GetHostsEntries(ctx context.Context) (clabtypes.HostEntries, error) {
	containers, err := d.OverwriteNode.GetContainers(ctx)
	if err != nil {
		return nil, err
	}

	result := clabtypes.HostEntries{}

	for idx := range containers {
		if len(containers[idx].Names) == 0 {
			continue
		}
		if containers[idx].NetworkSettings.IPv4addr != "" {
			result = append(result, clabtypes.NewHostEntry(
				containers[idx].NetworkSettings.IPv4addr,
				containers[idx].Names[0],
				clabtypes.IpVersionV4,
			).SetDescription(fmt.Sprintf("Kind: %s", d.Cfg.Kind)).SetContainerID(containers[idx].ID))
		}
		if containers[idx].NetworkSettings.IPv6addr != "" {
			result = append(result, clabtypes.NewHostEntry(
				containers[idx].NetworkSettings.IPv6addr,
				containers[idx].Names[0],
				clabtypes.IpVersionV6,
			).SetDescription(fmt.Sprintf("Kind: %s", d.Cfg.Kind)).SetContainerID(containers[idx].ID))
		}
	}
	return result, nil
}

func (d *DefaultNode) AddLinkToContainer(
	ctx context.Context,
	link netlink.Link,
	f func(ns.NetNS) error,
) error {
	if !d.Cfg.IsRootNamespaceBased {
		st := d.OverwriteNode.GetContainerStatus(ctx)
		if !clabruntime.ContainerHasJoinableNetns(st) {
			if st == clabruntime.Stopped {
				containerName := d.OverwriteNode.GetContainerName()
				d.GetRuntime().LogNonRunningContainerOutput(ctx, containerName)
			}
			return fmt.Errorf(
				"node %q: cannot attach link, container network namespace is not available (status=%s)",
				d.Cfg.ShortName,
				st,
			)
		}
	}
	// retrieve nodes nspath
	nsp, err := d.OverwriteNode.GetNSPath(ctx)
	if err != nil {
		return err
	}
	// retrieve the namespace handle
	netns, err := ns.GetNS(nsp)
	if err != nil {
		return err
	}
	// move veth endpoint to namespace
	if err := netlink.LinkSetNsFd(link, int(netns.Fd())); err != nil {
		return err
	}
	// execute the given function
	err = netns.Do(f)
	if err != nil {
		return err
	}
	return nil
}

// ExecFunction executes the given function in the nodes network namespace.
func (d *DefaultNode) ExecFunction(ctx context.Context, f func(ns.NetNS) error) error {
	if !d.Cfg.IsRootNamespaceBased {
		st := d.OverwriteNode.GetContainerStatus(ctx)
		if !clabruntime.ContainerHasJoinableNetns(st) {
			if st == clabruntime.Stopped {
				containerName := d.OverwriteNode.GetContainerName()
				d.GetRuntime().LogNonRunningContainerOutput(ctx, containerName)
			}
			return fmt.Errorf(
				"node %q: cannot exec a command, container network namespace is not available (status=%s)",
				d.Cfg.ShortName,
				st,
			)
		}
	}
	// retrieve nodes nspath
	nspath, err := d.OverwriteNode.GetNSPath(ctx)
	if err != nil {
		return err
	}

	if d.Cfg.IsRootNamespaceBased {
		nshandle, err := ns.GetCurrentNS()
		if err != nil {
			return err
		}
		nspath = nshandle.Path()
	}

	if nspath == "" {
		return fmt.Errorf("nspath is not set for node %q", d.GetShortName())
	}

	// retrieve the namespace handle
	netns, err := ns.GetNS(nspath)
	if err != nil {
		return err
	}
	// execute the given function
	return netns.Do(f)
}

// AddEndpoint maps the endpoint name to before adding it to the node endpoints if it matches the
// interface alias regexp. Returns an error if the mapping goes wrong.
func (d *DefaultNode) AddEndpoint(e clablinks.Endpoint) error {
	endpointName := e.GetIfaceName()
	if d.InterfaceRegexp != nil && d.InterfaceRegexp.MatchString(endpointName) {
		mappedName, err := d.OverwriteNode.GetMappedInterfaceName(endpointName)
		if err != nil {
			return fmt.Errorf(
				"%q interface name %q could not be mapped: %w",
				d.Cfg.ShortName,
				e.GetIfaceName(),
				err,
			)
		}
		log.Debugf(
			"Interface Mapping: Mapping interface %q (ifAlias) to %q (ifName)",
			endpointName,
			mappedName,
		)
		e.SetIfaceName(mappedName)
		e.SetIfaceAlias(endpointName)
	}

	if e.GetNode() == nil {
		e.SetNode(d)
	}

	d.Endpoints = append(d.Endpoints, e)
	return nil
}

func (d *DefaultNode) AdoptEndpoint(e clablinks.Endpoint) error {
	if e == nil {
		return fmt.Errorf("node %q cannot adopt a nil endpoint", d.Cfg.ShortName)
	}

	owner := e.GetNode()
	if owner == nil {
		return fmt.Errorf(
			"node %q cannot adopt endpoint %q without an owner",
			d.Cfg.ShortName,
			e.GetIfaceName(),
		)
	}

	if owner.GetShortName() != d.Cfg.ShortName {
		return fmt.Errorf(
			"node %q cannot adopt endpoint %q owned by %q",
			d.Cfg.ShortName,
			e.GetIfaceName(),
			owner.GetShortName(),
		)
	}

	for _, owned := range d.Endpoints {
		if owned == e {
			return nil
		}
		if owned.GetIfaceName() == e.GetIfaceName() {
			return fmt.Errorf(
				"node %q already tracks interface %q",
				d.Cfg.ShortName,
				e.GetIfaceName(),
			)
		}
	}

	d.Endpoints = append(d.Endpoints, e)

	return nil
}

func (d *DefaultNode) ReleaseEndpoint(e clablinks.Endpoint) error {
	for i, ep := range d.Endpoints {
		if ep != e {
			continue
		}

		d.Endpoints = append(d.Endpoints[:i], d.Endpoints[i+1:]...)
		return nil
	}

	return fmt.Errorf("node %q does not own endpoint %q", d.Cfg.ShortName, e.GetIfaceName())
}

func (d *DefaultNode) GetEndpoints() []clablinks.Endpoint {
	return d.Endpoints
}

// GetLinkEndpointType returns a veth link endpoint type for default nodes.
// The LinkEndpointTypeVeth indicates a veth endpoint which doesn't require special handling.
func (*DefaultNode) GetLinkEndpointType() clablinks.LinkEndpointType {
	return clablinks.LinkEndpointTypeVeth
}

func (d *DefaultNode) GetShortName() string {
	return d.Cfg.ShortName
}

// DeployEndpoints deploys endpoints associated with the node.
// The deployment of endpoints is done by deploying a link with the endpoint triggering it.
func (d *DefaultNode) DeployEndpoints(ctx context.Context) error {
	for _, ep := range d.Endpoints {
		if ep.IsRuntimeDiscovered() {
			continue
		}

		if err := ep.Deploy(ctx); err != nil {
			return err
		}
	}

	return nil
}

func (*DefaultNode) PostDeployEndpoints(context.Context) error {
	return nil
}

func (d *DefaultNode) GetState() clabnodesstate.NodeState {
	d.statemutex.RLock()
	defer d.statemutex.RUnlock()
	return d.state
}

func (d *DefaultNode) SetState(s clabnodesstate.NodeState) {
	d.statemutex.Lock()
	defer d.statemutex.Unlock()
	d.state = s
}

func (d *DefaultNode) GetSSHConfig() *clabtypes.SSHConfig {
	return d.SSHConfig
}

func (d *DefaultNode) GetContainerStatus(ctx context.Context) clabruntime.ContainerStatus {
	return d.Runtime.GetContainerStatus(ctx, d.OverwriteNode.GetContainerName())
}

func (d *DefaultNode) IsHealthy(ctx context.Context) (bool, error) {
	return d.Runtime.IsHealthy(ctx, d.OverwriteNode.GetContainerName())
}

// ParkEndpoints moves all tracked endpoints into the node-owned parking namespace.
func (d *DefaultNode) ParkEndpoints(ctx context.Context) error {
	parkPath, err := clabutils.CreateOrGetNamedNetNS(d.parkingNetNSName())
	if err != nil {
		return fmt.Errorf("failed to create parking netns for node %q: %w", d.Cfg.ShortName, err)
	}
	parkingNode := clablinks.NewParkingNode(d.Cfg.LongName, parkPath)

	if err := parkingNode.CaptureFrom(ctx, d); err != nil {
		_ = d.cleanupParkingNetNS()
		return err
	}

	if err := parkingNode.RepointSymlink(); err != nil {
		_, restoreErr := parkingNode.RestoreTo(ctx, d)
		cleanupErr := d.cleanupParkingNetNS()
		if restoreErr != nil || cleanupErr != nil {
			return errors.Join(
				fmt.Errorf("failed to repoint symlink for node %q: %w", d.Cfg.ShortName, err),
				restoreErr,
				cleanupErr,
			)
		}
		return fmt.Errorf("failed to repoint symlink for node %q: %w", d.Cfg.ShortName, err)
	}

	return nil
}

// RestoreEndpoints tries to get the parking node to unpark its interfaces.
func (d *DefaultNode) RestoreEndpoints(ctx context.Context) error {
	parkPath, err := d.parkingNetNSPath()
	if err != nil {
		// No parking netns means the node had no parked interfaces - e.g. it was
		// stopped outside of containerlab (so its interfaces were never parked) or
		// it has no dataplane links. Nothing to restore.
		log.Debugf("node %q has no parking netns, nothing to restore", d.Cfg.ShortName)
		return nil
	}
	parkingNode := clablinks.NewParkingNode(d.Cfg.LongName, parkPath)

	restored, err := parkingNode.RestoreTo(ctx, d)
	if err != nil {
		return err
	}

	for _, ep := range restored {
		log.Info("Restored link", "node", d.Cfg.ShortName, "interface", ep.GetIfaceName())
	}

	if err := d.cleanupParkingNetNS(); err != nil {
		return fmt.Errorf("failed to cleanup parking netns for node %q: %w", d.Cfg.ShortName, err)
	}

	return nil
}

func (d *DefaultNode) Stop(ctx context.Context) error {
	cfg := d.Config()

	if d.ShouldSkipLifecycle() {
		log.Debugf("node %q skips lifecycle stop", cfg.ShortName)
		return nil
	}

	status := d.OverwriteNode.GetContainerStatus(ctx)
	switch status {
	case clabruntime.Stopped:
		log.Debugf("node %q already stopped, skipping", cfg.ShortName)
		return nil
	case clabruntime.NotFound:
		return fmt.Errorf("node %q container %q not found", cfg.ShortName, cfg.LongName)
	}

	if err := d.OverwriteNode.PreStop(ctx); err != nil {
		return fmt.Errorf("node %q pre-stop hook failed: %w", cfg.ShortName, err)
	}

	if err := d.ParkEndpoints(ctx); err != nil {
		return err
	}

	if err := d.Runtime.StopContainer(ctx, cfg.LongName, d.StopSignal); err != nil {
		// Docker/podman may return an error while the container is already stopped.
		// if ctr is already stopped, this is OK
		if d.OverwriteNode.GetContainerStatus(ctx) == clabruntime.Stopped {
			log.Warnf(
				"node %q stop returned error but container is stopped: %v",
				cfg.ShortName,
				err,
			)
			return nil
		}

		if restoreErr := d.RestoreEndpoints(ctx); restoreErr != nil {
			return errors.Join(
				fmt.Errorf("node %q failed stopping container: %w", cfg.ShortName, err),
				fmt.Errorf("failed to restore endpoints: %w", restoreErr),
			)
		}

		return fmt.Errorf("node %q failed stopping container: %w", cfg.ShortName, err)
	}

	return nil
}

// Start restarts a stopped container and restores its parked dataplane interfaces.
func (d *DefaultNode) Start(ctx context.Context) error {
	cfg := d.Config()

	if d.ShouldSkipLifecycle() {
		log.Debugf("node %q skips lifecycle start", cfg.ShortName)
		return nil
	}

	status := d.OverwriteNode.GetContainerStatus(ctx)

	switch status {
	case clabruntime.Running:
		log.Debugf("node %q already running, skipping", cfg.ShortName)
		return nil
	case clabruntime.NotFound:
		return fmt.Errorf("node %q container %q not found", cfg.ShortName, cfg.LongName)
	}

	if _, err := d.Runtime.StartContainer(ctx, cfg.LongName, d); err != nil {
		return fmt.Errorf("node %q failed starting container: %w", cfg.ShortName, err)
	}

	if err := d.RestoreEndpoints(ctx); err != nil {
		_ = d.Runtime.StopContainer(ctx, cfg.LongName, d.StopSignal)
		return err
	}

	// Re-run topology exec commands on lifecycle start to restore node-local interface config
	// (for example IP addresses added during deploy exec phase).
	execCollection := clabexec.NewExecCollection()
	if err := d.RunExecFromConfig(ctx, execCollection); err != nil {
		log.Errorf(
			"failed to run exec commands for node %q on lifecycle start: %v",
			cfg.ShortName,
			err,
		)
	}
	execCollection.Log()

	return nil
}
