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

package nodes

import (
	"context"
	"errors"
	"fmt"
	"regexp"

	"github.com/charmbracelet/log"
	"github.com/containernetworking/plugins/pkg/ns"
	clabcert "github.com/srl-labs/containerlab/cert"
	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"
	"github.com/vishvananda/netlink"
	"golang.org/x/crypto/ssh"
)

const (
	// default connection mode for vrnetlab based containers.
	VrDefConnMode = "tc"
	// keys for the map returned by GetImages.
	ImageKey = "image"
)

// LinkApplyMode describes how apply should handle dataplane link changes for a
// node that already exists in the running lab.
type LinkApplyMode = clabtypes.LinkApplyMode

const (
	// LinkApplyModeRecreate deletes and creates the node so generated runtime
	// metadata, container env, binds, and startup files are rebuilt.
	LinkApplyModeRecreate LinkApplyMode = clabtypes.LinkApplyModeRecreate
	// LinkApplyModeRestart adds/removes links first and then restarts the same
	// container object.
	LinkApplyModeRestart LinkApplyMode = clabtypes.LinkApplyModeRestart
	// LinkApplyModeLive applies link changes directly without node lifecycle
	// actions.
	LinkApplyModeLive LinkApplyMode = clabtypes.LinkApplyModeLive
)

// linkApplyModePermissiveness orders modes from most disruptive to least
// disruptive; a higher rank means apply touches the node lifecycle less.
var linkApplyModePermissiveness = map[LinkApplyMode]int{
	LinkApplyModeRecreate: 0,
	LinkApplyModeRestart:  1,
	LinkApplyModeLive:     2,
}

// LinkApplyModeForNode asks a node how apply should handle dataplane link changes.
// A link-apply-mode set in the topology file takes precedence over the kind's own
// declaration.
func LinkApplyModeForNode(ctx context.Context, node Node) LinkApplyMode {
	if node == nil {
		return LinkApplyModeRecreate
	}

	kindMode := LinkApplyModeRecreate
	switch mode := node.LinkApplyMode(ctx); mode {
	case LinkApplyModeLive, LinkApplyModeRestart, LinkApplyModeRecreate:
		kindMode = mode
	}

	override := LinkApplyModeOverrideForNode(node)
	if override == "" {
		return kindMode
	}

	if linkApplyModePermissiveness[override] > linkApplyModePermissiveness[kindMode] {
		log.Warn(
			"link-apply-mode is more permissive than the kind default; "+
				"make sure the NOS picks up interface changes applied this way",
			"node", node.Config().ShortName,
			"link-apply-mode", override,
			"kind-default", kindMode,
		)
	}

	return override
}

// LinkApplyModeOverrideForNode returns the validated link-apply-mode set for the
// node in the topology file, or an empty string when no override is set.
func LinkApplyModeOverrideForNode(node Node) LinkApplyMode {
	if node == nil || node.Config() == nil {
		return ""
	}

	switch mode := node.Config().LinkApplyMode; mode {
	case LinkApplyModeLive, LinkApplyModeRestart, LinkApplyModeRecreate:
		return mode
	case "":
		return ""
	default:
		log.Warn(
			"Ignoring invalid link-apply-mode",
			"node", node.Config().ShortName,
			"link-apply-mode", node.Config().LinkApplyMode,
		)
		return ""
	}
}

var (
	// a map of node kinds overriding the default global runtime.
	NonDefaultRuntimes = map[string]string{}

	// ErrCommandExecError is an error returned when a command is failed to execute on a given node.
	ErrCommandExecError = errors.New("command execution error")
	// ErrContainersNotFound indicated that for a given node no containers where found in the
	// runtime.
	ErrContainersNotFound = errors.New("containers not found")
	// ErrNoStartupConfig indicates that we are supposed to enforce a startup config but none are
	// provided.
	ErrNoStartupConfig = errors.New("no startup-config provided")
	// ErrIncompatibleOptions for options that are mutually exclusive.
	ErrIncompatibleOptions = errors.New("incompatible options")
)

// SetNonDefaultRuntimePerKind sets a non default runtime for kinds that requires that (see cvx).
func SetNonDefaultRuntimePerKind(kindnames []string, runtime string) error {
	for _, kindname := range kindnames {
		if _, exists := NonDefaultRuntimes[kindname]; exists {
			return fmt.Errorf(
				"non default runtime config for kind with the name '%s' exists already",
				kindname,
			)
		}
		NonDefaultRuntimes[kindname] = runtime
	}
	return nil
}

// SaveConfigResult contains the result of a node's config save operation.
type SaveConfigResult struct {
	// ConfigPath is the absolute path to the saved configuration file on the host.
	// Empty when the node kind does not support config save or
	// the config is saved internally without host-visible file.
	ConfigPath string
}

type PreDeployParams struct {
	Cert         *clabcert.Cert
	TopologyName string
	TopoPaths    *clabtypes.TopoPaths
	SSHPubKeys   []ssh.PublicKey
}

// DeployParams contains parameters for the Deploy function.
type DeployParams struct {
	Nodes map[string]Node
}

// PostDeployParams contains parameters for the PostDeploy function.
type PostDeployParams struct {
	Nodes map[string]Node
}

type ReconcileResult struct {
	Action    clabtypes.TopologyDiffAction
	Restarted []string
	Recreated []string
	Created   []string
	Deleted   []string
}

// Node is an interface that defines the behavior of a node.
type Node interface {
	Init(*clabtypes.NodeConfig, ...NodeOption) error
	// GetContainers returns a pointer to GenericContainer that the node uses.
	GetContainers(ctx context.Context) ([]clabruntime.GenericContainer, error)
	DeleteNetnsSymlink() (err error)
	Config() *clabtypes.NodeConfig // Config returns the nodes configuration
	// CheckDeploymentConditions checks if node-scoped deployment conditions are met.
	CheckDeploymentConditions(ctx context.Context) error
	PreDeploy(ctx context.Context, params *PreDeployParams) error
	Deploy(context.Context, *DeployParams) error // Deploy triggers the deployment of this node
	PostDeploy(ctx context.Context, params *PostDeployParams) error
	WithMgmtNet(
		*clabtypes.MgmtNet,
	) // WithMgmtNet provides the management network for the node
	WithRuntime(clabruntime.ContainerRuntime) // WithRuntime provides the runtime for the node
	PullImage(ctx context.Context) error      // PullImage pulls the image for the node
	// CalculateInterfaceIndex returns with the interface index offset from the first valid
	// dataplane interface based on the interface name. Errors otherwise.
	CalculateInterfaceIndex(ifName string) (int, error)
	// CheckInterfaceName checks if a name of the interface referenced in the topology file is
	// correct for this node
	CheckInterfaceName() error
	// VerifyStartupConfig checks for existence of the referenced file and maybe performs additional
	// config checks
	VerifyStartupConfig(topoDir string) error
	// LinkApplyMode returns how apply handles dataplane link changes for this node.
	LinkApplyMode(context.Context) LinkApplyMode
	SaveConfig(
		context.Context,
	) (*SaveConfigResult, error) // SaveConfig saves the nodes configuration to an external file
	Delete(context.Context) error // Delete triggers the deletion of this node
	// Stop parks dataplane interfaces and stops the container.
	Stop(context.Context) error
	// Start restarts a stopped container and restores its parked interfaces.
	Start(context.Context) error
	GetImages(context.Context) map[string]string // GetImages returns the images used for this kind
	GetRuntime() clabruntime.ContainerRuntime    // GetRuntime returns the nodes assigned runtime
	GenerateConfig(dst, templ string) error      // Generate the nodes configuration
	// UpdateConfigWithRuntimeInfo updates node config with runtime info like IP addresses assigned
	// by runtime
	UpdateConfigWithRuntimeInfo(context.Context) error
	// RunExec execute a single command for a given node.
	RunExec(ctx context.Context, execCmd *clabexec.ExecCmd) (*clabexec.ExecResult, error)
	// Adds the given link to the Node (container). After adding the Link to the node,
	// the given function f is called within the Nodes namespace to setup the link.
	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 clablinks.Endpoint) error
	// AdoptEndpoint adds an endpoint already owned by this node to its endpoint list.
	AdoptEndpoint(e clablinks.Endpoint) error
	// ReleaseEndpoint removes an endpoint owned by this node from its endpoint list.
	ReleaseEndpoint(e clablinks.Endpoint) error
	GetEndpoints() []clablinks.Endpoint
	GetLinkEndpointType() clablinks.LinkEndpointType
	GetShortName() string
	// DeployEndpoints deploys the links for the node.
	DeployEndpoints(ctx context.Context) error
	// PostDeployEndpoints runs endpoint fixups after dataplane links exist.
	PostDeployEndpoints(ctx context.Context) error
	// ParkEndpoints parks dataplane interfaces before node recreation.
	ParkEndpoints(ctx context.Context) error
	// RestoreEndpoints restores parked dataplane interfaces after node recreation.
	RestoreEndpoints(ctx context.Context) error
	// ExecFunction executes the given function within the nodes network namespace
	ExecFunction(context.Context, func(ns.NetNS) error) error
	GetState() clabnodesstate.NodeState
	SetState(clabnodesstate.NodeState)
	GetSSHConfig() *clabtypes.SSHConfig
	// RunExecFromConfig executes the topologyfile defined exec commands
	RunExecFromConfig(context.Context, *clabexec.ExecCollection) error
	IsHealthy(ctx context.Context) (bool, error)
	GetContainerStatus(ctx context.Context) clabruntime.ContainerStatus
	GetNSPath(ctx context.Context) (string, error)
	// Generate the host entries for this node
	GetHostsEntries(ctx context.Context) (clabtypes.HostEntries, error)
	// ComputeDiff diffs old node cfg with the new one and
	// returns the diff.
	ComputeDiff(oldCfg, newCfg *clabtypes.NodeConfig) *clabtypes.TopologyDiff
	// GetReconcilePlan decides the action. Useful for dry-run and actual
	// reconciliation.
	GetReconcilePlan(ctx context.Context, diff *clabtypes.TopologyDiff) (*ReconcileResult, error)
	// Reconcile determines what action to take based on diff and executes it.
	Reconcile(ctx context.Context, diff *clabtypes.TopologyDiff) (*ReconcileResult, error)
}

type NodeOption func(Node)

func WithMgmtNet(mgmt *clabtypes.MgmtNet) NodeOption {
	return func(n Node) {
		if mgmt == nil {
			n.WithMgmtNet(new(clabtypes.MgmtNet))
			return
		}
		n.WithMgmtNet(mgmt)
	}
}

func WithRuntime(r clabruntime.ContainerRuntime) NodeOption {
	return func(n Node) {
		n.WithRuntime(r)
	}
}

// GenericVMInterfaceCheck checks interface names for generic VM-based nodes.
// These nodes could only have interfaces named ethX, where X is >0.
func GenericVMInterfaceCheck(nodeName string, eps []clablinks.Endpoint) error {
	ifRe := regexp.MustCompile(`eth[1-9]\d*$`) // skipcq: GO-C4007
	for _, e := range eps {
		if !ifRe.MatchString(e.GetIfaceName()) {
			return fmt.Errorf(
				"%q interface name %q doesn't match the required pattern. It should be named as ethX, where X is >0",
				nodeName,
				e.GetIfaceName(),
			)
		}
	}

	return nil
}
