package core

import (
	"context"
	"fmt"
	"sort"

	"github.com/charmbracelet/log"
	clablinks "github.com/srl-labs/containerlab/links"
	clabruntime "github.com/srl-labs/containerlab/runtime"
	clabtypes "github.com/srl-labs/containerlab/types"
	"github.com/vishvananda/netlink"
	"github.com/vishvananda/netns"
)

// ListContainers lists all containers using provided filter.
func (c *CLab) ListContainers(
	ctx context.Context,
	options ...ListOption,
) ([]clabruntime.GenericContainer, error) {
	opts := NewListOptions()

	for _, opt := range options {
		opt(opts)
	}

	var containers []clabruntime.GenericContainer

	for _, r := range c.Runtimes {
		ctrs, err := r.ListContainers(ctx, opts.ToFilters())
		if err != nil {
			return containers, fmt.Errorf("could not list containers: %v", err)
		}

		containers = append(containers, ctrs...)
	}

	return containers, nil
}

// ListNodesContainers lists all containers based on the nodes stored in clab instance.
func (c *CLab) ListNodesContainers(
	ctx context.Context,
) ([]clabruntime.GenericContainer, error) {
	var containers []clabruntime.GenericContainer

	for _, n := range c.Nodes {
		cts, err := n.GetContainers(ctx)
		if err != nil {
			return containers, fmt.Errorf(
				"could not get container for node %s: %v",
				n.Config().LongName, err,
			)
		}

		containers = append(containers, cts...)
	}

	return containers, nil
}

// ListNodesContainersIgnoreNotFound lists all containers based on the nodes stored in clab
// instance, ignoring errors for non found containers.
func (c *CLab) ListNodesContainersIgnoreNotFound(
	ctx context.Context,
) ([]clabruntime.GenericContainer, error) {
	var containers []clabruntime.GenericContainer

	for _, n := range c.Nodes {
		cts, err := n.GetContainers(ctx)
		if err != nil {
			continue
		}

		containers = append(containers, cts...)
	}

	return containers, nil
}

// ListContainerInterfaces list interfaces of the given container.
func (c *CLab) ListContainerInterfaces(
	ctx context.Context,
	container *clabruntime.GenericContainer,
) (*clabtypes.ContainerInterfaces, error) {
	containerInterfaces := clabtypes.ContainerInterfaces{}

	if len(container.Names) > 0 {
		containerInterfaces.ContainerName = container.Names[0]
	}

	// Retrieve the path to the container network NS
	nodeNsPath, err := container.Runtime.GetNSPath(ctx, containerInterfaces.ContainerName)
	if err != nil {
		return nil, err
	}

	// Get network NS handle
	var containerNsHandle netns.NsHandle

	switch {
	case nodeNsPath != "":
		containerNsHandle, err = netns.GetFromPath(nodeNsPath)
		if err != nil {
			return nil, fmt.Errorf("unable to get container network NS handle: %w", err)
		}
		defer containerNsHandle.Close()
	case container.Runtime.GetName() == "podman":
		// Network NS path is empty and the runtime is Podman -> host network mode
		// Manually get the handle for the root network namespace
		containerNsHandle, err = netns.Get()
		if err != nil {
			return nil, fmt.Errorf("unable to get root network NS handle: %w", err)
		}
		defer containerNsHandle.Close()
	default:
		log.Warnf("Container %v has no namespace set, skipping!", containerInterfaces.ContainerName)
		containerInterfaces.Interfaces = make([]*clabtypes.ContainerInterfaceDetails, 0)

		return &containerInterfaces, nil
	}

	// Get Netlink handle in container network NS
	netlinkHandle, err := netlink.NewHandleAt(containerNsHandle)
	if err != nil {
		return nil, fmt.Errorf("unable to enter container network NS: %w", err)
	}
	defer netlinkHandle.Close()

	interfaces, err := netlinkHandle.LinkList()
	if err != nil {
		return nil, fmt.Errorf("unable to list network interfaces: %w", err)
	}

	containerInterfaces.Interfaces = make(
		[]*clabtypes.ContainerInterfaceDetails,
		0,
		len(interfaces),
	)

	lab, node := TopoIdentity(container.Labels)

	for _, iface := range interfaces {
		reported, alias := reportedInterface(iface, lab, node)
		attrs := reported.Attrs()

		ifaceDetails := clabtypes.ContainerInterfaceDetails{
			InterfaceName:  attrs.Name,
			InterfaceAlias: alias,
			InterfaceMTU:   attrs.MTU,
			InterfaceMAC:   attrs.HardwareAddr.String(),
			InterfaceIndex: attrs.Index,
			InterfaceType:  reported.Type(),
			InterfaceState: attrs.OperState.String(),
		}
		log.Debugf("Interface info: %+v", ifaceDetails)

		containerInterfaces.Interfaces = append(containerInterfaces.Interfaces, &ifaceDetails)
	}

	log.Debugf("Fetched %v interfaces for %v", len(interfaces), containerInterfaces.ContainerName)

	return &containerInterfaces, nil
}

// reportedInterface substitutes the host-side stitch interface for iface when
// one exists, keeping the topology name as alias so the swap is transparent.
func reportedInterface(iface netlink.Link, lab, node string) (netlink.Link, string) {
	alias := iface.Attrs().Alias

	topoName := alias
	if topoName == "" {
		topoName = iface.Attrs().Name
	}

	if tools, ok := clablinks.ToolsInterface(lab, node, topoName); ok {
		return tools, topoName
	}

	return iface, alias
}

// ListContainersInterfaces list interfaces of all given containers.
func (c *CLab) ListContainersInterfaces(
	ctx context.Context,
	containers []clabruntime.GenericContainer,
) ([]*clabtypes.ContainerInterfaces, error) {
	containerInterfaces := make([]*clabtypes.ContainerInterfaces, 0, len(containers))

	for idx := range containers {
		cIfs, err := c.ListContainerInterfaces(ctx, &containers[idx])
		if err != nil {
			containerName := ""
			if len(containers[idx].Names) > 0 {
				containerName = containers[idx].Names[0]
			}
			log.Warn("Skipping interface detection", "node", containerName, "err", err)
			continue
		}

		if len(cIfs.Interfaces) > 0 {
			sort.Slice(cIfs.Interfaces, func(i, j int) bool {
				return cIfs.Interfaces[i].InterfaceName < cIfs.Interfaces[j].InterfaceName
			})
		} else {
			log.Warnf("No interfaces found for container %v", cIfs.ContainerName)
		}

		containerInterfaces = append(containerInterfaces, cIfs)
	}

	sort.Slice(containerInterfaces, func(i, j int) bool {
		return containerInterfaces[i].ContainerName < containerInterfaces[j].ContainerName
	})

	return containerInterfaces, nil
}
