package cninetwork

import (
	"bufio"
	"context"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"path"
	"path/filepath"
	"strings"

	"github.com/containerd/containerd"
	gocni "github.com/containerd/go-cni"
	"github.com/pkg/errors"
)

const (
	// CNIBinDir describes the directory where the CNI binaries are stored
	CNIBinDir = "/opt/cni/bin"

	// CNIConfDir describes the directory where the CNI plugin's configuration is stored
	CNIConfDir = "/etc/cni/net.d"

	// NetNSPathFmt gives the path to the a process network namespace, given the pid
	NetNSPathFmt = "/proc/%d/ns/net"

	// CNIDataDir is the directory CNI stores allocated IP for containers
	CNIDataDir = "/var/run/cni"

	// defaultCNIConfFilename is the vanity filename of default CNI configuration file
	defaultCNIConfFilename = "10-openfaas.conflist"

	// defaultNetworkName names the "docker-bridge"-like CNI plugin-chain installed when no other CNI configuration is present.
	// This value appears in iptables comments created by CNI.
	defaultNetworkName = "openfaas-cni-bridge"

	// defaultBridgeName is the default bridge device name used in the defaultCNIConf
	defaultBridgeName = "openfaas0"

	// defaultSubnet is the default subnet used in the defaultCNIConf -- this value is set to not collide with common container networking subnets:
	defaultSubnet = "10.62.0.0/16"

	// defaultIfPrefix is the interface name to be created in the container
	defaultIfPrefix = "eth"
)

// defaultCNIConf is a CNI configuration that enables network access to containers (docker-bridge style)
var defaultCNIConf = fmt.Sprintf(`
{
    "cniVersion": "0.4.0",
    "name": "%s",
    "plugins": [
      {
        "type": "bridge",
        "bridge": "%s",
        "isGateway": true,
        "ipMasq": true,
        "ipam": {
            "type": "host-local",
            "subnet": "%s",
            "dataDir": "%s",
            "routes": [
                { "dst": "0.0.0.0/0" }
            ]
        }
      },
      {
        "type": "firewall"
      }
    ]
}
`, defaultNetworkName, defaultBridgeName, defaultSubnet, CNIDataDir)

// InitNetwork writes configlist file and initializes CNI network
func InitNetwork() (gocni.CNI, error) {

	log.Printf("Writing network config...\n")
	if !dirExists(CNIConfDir) {
		if err := os.MkdirAll(CNIConfDir, 0755); err != nil {
			return nil, fmt.Errorf("cannot create directory: %s", CNIConfDir)
		}
	}

	netConfig := path.Join(CNIConfDir, defaultCNIConfFilename)
	if err := os.WriteFile(netConfig, []byte(defaultCNIConf), 0644); err != nil {
		return nil, fmt.Errorf("cannot write network config: %s", defaultCNIConfFilename)
	}

	// Initialize CNI library
	cni, err := gocni.New(
		gocni.WithPluginConfDir(CNIConfDir),
		gocni.WithPluginDir([]string{CNIBinDir}),
		gocni.WithInterfacePrefix(defaultIfPrefix),
	)

	if err != nil {
		return nil, fmt.Errorf("error initializing cni: %s", err)
	}

	// Load the cni configuration
	if err := cni.Load(gocni.WithLoNetwork, gocni.WithConfListFile(filepath.Join(CNIConfDir, defaultCNIConfFilename))); err != nil {
		return nil, fmt.Errorf("failed to load cni configuration: %v", err)
	}

	return cni, nil
}

// CreateCNINetwork creates a CNI network interface and attaches it to the context
func CreateCNINetwork(ctx context.Context, cni gocni.CNI, task containerd.Task, labels map[string]string) (*gocni.CNIResult, error) {
	id := netID(task)
	netns := netNamespace(task)
	result, err := cni.Setup(ctx, id, netns, gocni.WithLabels(labels))
	if err != nil {
		return nil, errors.Wrapf(err, "Failed to setup network for task %q: %v", id, err)
	}

	return result, nil
}

// DeleteCNINetwork deletes a CNI network based on task ID and Pid
func DeleteCNINetwork(ctx context.Context, cni gocni.CNI, client *containerd.Client, name string) error {
	container, containerErr := client.LoadContainer(ctx, name)
	if containerErr == nil {
		task, err := container.Task(ctx, nil)
		if err != nil {
			log.Printf("[Delete] unable to find task for container: %s\n", name)
			return nil
		}

		log.Printf("[Delete] removing CNI network for: %s\n", task.ID())

		id := netID(task)
		netns := netNamespace(task)

		if err := cni.Remove(ctx, id, netns); err != nil {
			return errors.Wrapf(err, "Failed to remove network for task: %q, %v", id, err)
		}
		log.Printf("[Delete] removed: %s from namespace: %s, ID: %s\n", name, netns, id)

		return nil
	}

	return errors.Wrapf(containerErr, "Unable to find container: %s, error: %s", name, containerErr)
}

// GetIPAddress returns the IP address from container based on container name and PID
func GetIPAddress(container string, PID uint32) (string, error) {
	CNIDir := path.Join(CNIDataDir, defaultNetworkName)

	files, err := os.ReadDir(CNIDir)
	if err != nil {
		return "", fmt.Errorf("failed to read CNI dir for container %s: %v", container, err)
	}

	for _, file := range files {
		// each fileName is an IP address
		fileName := file.Name()

		resultsFile := filepath.Join(CNIDir, fileName)
		found, err := isCNIResultForPID(resultsFile, container, PID)
		if err != nil {
			return "", err
		}

		if found {
			return fileName, nil
		}
	}

	return "", fmt.Errorf("unable to get IP address for container: %s", container)
}

// isCNIResultForPID confirms if the CNI result file contains the
// process name, PID and interface name
//
// Example:
//
// /var/run/cni/openfaas-cni-bridge/10.62.0.2
//
// nats-621
// eth1
func isCNIResultForPID(fileName, container string, PID uint32) (bool, error) {
	found := false

	f, err := os.Open(fileName)
	if err != nil {
		return false, fmt.Errorf("failed to open CNI IP file for %s: %v", fileName, err)
	}
	defer f.Close()

	reader := bufio.NewReader(f)
	processLine, _ := reader.ReadString('\n')
	if strings.Contains(processLine, fmt.Sprintf("%s-%d", container, PID)) {
		ethNameLine, _ := reader.ReadString('\n')
		if strings.Contains(ethNameLine, defaultIfPrefix) {
			found = true
		}
	}

	return found, nil
}

// CNIGateway returns the gateway for default subnet
func CNIGateway() (string, error) {
	ip, _, err := net.ParseCIDR(defaultSubnet)
	if err != nil {
		return "", fmt.Errorf("error formatting gateway for network %s", defaultSubnet)
	}
	ip = ip.To4()
	ip[3] = 1
	return ip.String(), nil
}

// netID generates the network IF based on task name and task PID
func netID(task containerd.Task) string {
	return fmt.Sprintf("%s-%d", task.ID(), task.Pid())
}

// netNamespace generates the namespace path based on task PID.
func netNamespace(task containerd.Task) string {
	return fmt.Sprintf(NetNSPathFmt, task.Pid())
}

func dirEmpty(dirname string) (isEmpty bool) {
	if !dirExists(dirname) {
		return
	}

	f, err := os.Open(dirname)
	if err != nil {
		return
	}
	defer func() { _ = f.Close() }()

	// If the first file is EOF, the directory is empty
	if _, err = f.Readdir(1); err == io.EOF {
		isEmpty = true
	}
	return isEmpty
}

func dirExists(dirname string) bool {
	exists, info := pathExists(dirname)
	if !exists {
		return false
	}

	return info.IsDir()
}

func pathExists(path string) (bool, os.FileInfo) {
	info, err := os.Stat(path)
	if os.IsNotExist(err) {
		return false, nil
	}

	return true, info
}
