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

package runtime

import (
	"context"
	"fmt"
	"io"
	"time"

	"github.com/charmbracelet/log"
	clabexec "github.com/srl-labs/containerlab/exec"
	clablinks "github.com/srl-labs/containerlab/links"
	clabtypes "github.com/srl-labs/containerlab/types"
)

type ContainerRuntime interface {
	// Initializes the Container runtime struct
	Init(...RuntimeOption) error
	// Mgmt return management network configuration of a runtime
	Mgmt() *clabtypes.MgmtNet
	// Adds custom configuration items to the container runtime struct
	WithConfig(*RuntimeConfig)
	// Set the network management details (generated by the config.go)
	WithMgmtNet(*clabtypes.MgmtNet)
	// Instructs the runtime not to delete the mgmt network on destroy
	WithKeepMgmtNet()
	// Create container (bridge) network
	CreateNet(context.Context) error
	// Delete container (bridge) network
	DeleteNet(context.Context) error
	// Pull container image if not present
	PullImage(context.Context, string, clabtypes.PullPolicyValue) error
	// CreateContainer creates a container, but does not start it
	CreateContainer(context.Context, *clabtypes.NodeConfig) (string, error)
	// Start pre-created container by its name. Returns an extra interface that can be used to
	// receive signals
	// about the container life-cycle after it was created, e.g. for post-deploy tasks
	StartContainer(context.Context, string, Node) (any, error)
	// Stop running container by its name AND signal to use
	StopContainer(ctx context.Context, name string, stopSignal clabtypes.Signal) error
	// Pause a container identified by its name
	PauseContainer(context.Context, string) error
	// UnPause / resume a container identified by its name
	UnpauseContainer(context.Context, string) error
	// List all containers matching labels
	ListContainers(context.Context, []*clabtypes.GenericFilter) ([]GenericContainer, error)
	// Get a netns path using the name of a container
	GetNSPath(context.Context, string) (string, error)
	// LogNonRunningContainerOutput fetches recent stdout/stderr from a container that is not
	// running and logs it (best-effort; no-op on inspect failure). Used so deploy failures
	// surface root cause when link attach runs after a fast exit.
	LogNonRunningContainerOutput(ctx context.Context, containerName string)
	// Executes cmd on container identified with id and returns stdout, stderr bytes and an error
	Exec(ctx context.Context, cID string, execCmd *clabexec.ExecCmd) (*clabexec.ExecResult, error)
	// ExecNotWait executes cmd on container identified with id but doesn't wait for output nor
	// attaches stdout/err
	ExecNotWait(ctx context.Context, cID string, execCmd *clabexec.ExecCmd) error
	// Delete container by its name
	DeleteContainer(context.Context, string) error
	// Getter for runtime config options
	Config() RuntimeConfig
	GetName() string
	// GetHostsPath returns fs path to a file which is mounted as /etc/hosts into a given container
	GetHostsPath(context.Context, string) (string, error)
	// GetContainerStatus retrieves the ContainerStatus of the named container
	GetContainerStatus(ctx context.Context, cID string) ContainerStatus
	// IsHealthy returns true is the container is reported as being healthy, false otherwise
	IsHealthy(ctx context.Context, cID string) (bool, error)
	// Immediately write to the stdin of a container, returns error
	WriteToStdinNoWait(ctx context.Context, cID string, data []byte) error
	// CheckConnectivity returns an error if it cannot connect to the runtime, nil otherwise
	CheckConnection(ctx context.Context) error
	// GetRuntimeSocket returns the path to the control socket
	GetRuntimeSocket() (string, error)
	// GetCooCBindMounts returns the extra mounts a container running this runtime in
	// Container-outside-of-Container (CooC - General case – container uses host container
	// runtime) does need to function properly
	GetCooCBindMounts() clabtypes.Binds
	// StreamLogs returns a reader for the container's logs
	// The caller needs to close the returned ReadCloser.
	StreamLogs(ctx context.Context, containerName string) (io.ReadCloser, error)
	// StreamEvents streams runtime events that match provided options.
	StreamEvents(
		ctx context.Context,
		opts EventStreamOptions,
	) (<-chan ContainerEvent, <-chan error, error)
	// InspectImage returns detailed information about a container image
	InspectImage(ctx context.Context, imageName string) (*ImageInspect, error)
	// CopyToContainer copies the contents of the given host path into the named container's
	// destination path.
	// The path must be a file, and the the destination directory must exist inside the container
	CopyToContainer(ctx context.Context, cID string, dstPath string, srcPath string) error
}

// ContainerStatus summarizes container lifecycle as seen by the runtime.
// Running and Paused imply a joinable network namespace for typical Linux containers;
// use ContainerHasJoinableNetns for that check. Stopped means exited or dead
// (no running task / no usable netns). Created is not yet started (e.g. Docker "created").
type ContainerStatus string

const (
	NotFound   ContainerStatus = "NotFound"
	Running    ContainerStatus = "Running"
	Stopped    ContainerStatus = "Stopped" // exited or dead (terminal)
	Paused     ContainerStatus = "Paused"
	Created    ContainerStatus = "Created" // exists but not started
	Restarting ContainerStatus = "Restarting"
	Removing   ContainerStatus = "Removing"
)

// ContainerHasJoinableNetns reports whether the container likely has a network
// namespace that GetNSPath can reference (PID-based path under /proc) based on the container
// status.
func ContainerHasJoinableNetns(s ContainerStatus) bool {
	return s == Running || s == Paused
}

const (
	EventTypeContainer = "container"
)

const (
	EventActionStart   = "start"
	EventActionUnpause = "unpause"
	EventActionRestart = "restart"
	EventActionDie     = "die"
	EventActionStop    = "stop"
	EventActionDestroy = "destroy"
	EventActionKill    = "kill"
)

type EventStreamOptions struct {
	Labels map[string]string
}

type ContainerEvent struct {
	Timestamp   time.Time
	Type        string
	Action      string
	ActorID     string
	ActorName   string
	ActorFullID string
	Attributes  map[string]string
}

type Initializer func() ContainerRuntime

type RuntimeOption func(ContainerRuntime)

type RuntimeConfig struct {
	Timeout          time.Duration
	GracefulShutdown bool
	Debug            bool
	KeepMgmtNet      bool
	VerifyLinkParams *clablinks.VerifyLinkParams
}

// ImageInspect holds relevant image inspection data.
type ImageInspect struct {
	ID          string
	Config      ImageConfig
	RootFS      RootFS
	GraphDriver GraphDriver
}

// ImageConfig holds image configuration data.
type ImageConfig struct {
	Labels map[string]string
}

// RootFS holds the root filesystem information of an image.
type RootFS struct {
	Type   string
	Layers []string
}

// GraphDriver holds information about the storage driver.
type GraphDriver struct {
	Name string
	Data GraphDriverData
}

// GraphDriverData holds the driver-specific data.
type GraphDriverData struct {
	UpperDir  string
	WorkDir   string
	MergedDir string
}

var ContainerRuntimes = map[string]Initializer{}

func Register(name string, initFn Initializer) {
	ContainerRuntimes[name] = initFn
}

func WithConfig(cfg *RuntimeConfig) RuntimeOption {
	return func(r ContainerRuntime) {
		r.WithConfig(cfg)
	}
}

func WithMgmtNet(mgmt *clabtypes.MgmtNet) RuntimeOption {
	return func(r ContainerRuntime) {
		r.WithMgmtNet(mgmt)
	}
}

// WaitForContainerRunning waits for container to become running by polling its status.
func WaitForContainerRunning(
	ctx context.Context,
	r ContainerRuntime,
	contName, nodeName string,
) error {
	ticker := time.NewTicker(3 * time.Second)
	timeout := time.After(15 * time.Minute)

	startTime := time.Now()

	for {
		select {
		case <-ticker.C:
			runtimeStatus := r.GetContainerStatus(ctx, contName)

			if runtimeStatus == Running {
				return nil
			}

			log.Infof(
				"node %q depends on external container %q, which is not running yet. "+
					"Waited %s. Retrying...",
				nodeName,
				contName,
				time.Since(startTime).Truncate(time.Second),
			)
		case <-timeout:
			log.Errorf(
				"node %q waited %s for external dependency container %q to come up, "+
					"which did not happen. Giving up now",
				nodeName,
				time.Since(startTime),
				contName,
			)

			return fmt.Errorf(
				"node %q waited %s for external dependency container %q to come up, "+
					"which did not happen. Giving up now",
				nodeName,
				time.Since(startTime),
				contName,
			)
		}
	}
}

// Node is an interface that represents a node in the lab
// and is implemented by containerlab nodes.
type Node interface {
	Config() *clabtypes.NodeConfig
	GetEndpoints() []clablinks.Endpoint
}

// NewEndpointlessNode returns a EndpointlessNode -- a node w/ only a config, that can satisfy
// the Node interface.
func NewEndpointlessNode(c *clabtypes.NodeConfig) Node {
	return &EndpointlessNode{
		c: c,
	}
}

type EndpointlessNode struct {
	c *clabtypes.NodeConfig
}

func (n *EndpointlessNode) Config() *clabtypes.NodeConfig {
	return n.c
}

func (*EndpointlessNode) GetEndpoints() []clablinks.Endpoint {
	return nil
}
