package system

import (
	"bufio"
	_ "embed"
	"fmt"
	"io/ioutil"
	"net/http"
	"os"
	"path/filepath"
	"syscall"

	"github.com/pkg/errors"
	"github.com/vishvananda/netlink"
	"gopkg.in/yaml.v2"

	"github.com/aws/eks-anywhere-build-tooling/bottlerocket-bootstrap/pkg/files"
	"github.com/aws/eks-anywhere-build-tooling/bottlerocket-bootstrap/pkg/utils"
)

//go:embed config/net.toml
var netConfig string

const (
	rootfs            = "/.bottlerocket/rootfs"
	metadataServiceIP = "169.254.169.254"
	networkFilePath   = "/tmp/network.yaml"
)

var (
	netConfigPath = filepath.Join(rootfs, "/var/lib/netdog/net.toml")
	currentIPPath = filepath.Join(rootfs, "/var/lib/netdog/current_ip")
)

type Network struct {
	DeviceIP string     `yaml:"deviceIP"`
	DniCount int        `yaml:"dniCount"`
	Static   []StaticIP `yaml:"static,omitempty"`
}

type StaticIP struct {
	Address string `yaml:"address"`
	Gateway string `yaml:"gateway"`
	Primary bool   `yaml:"primary,omitempty"`
}

type NetworkMapping struct {
	DNI string
	*StaticIP
}

func configureDNI() error {
	if files.PathExists(netConfigPath) {
		return nil
	}

	instanceIP, err := instanceIP()
	if err != nil {
		return errors.Wrap(err, "error getting local instance ip")
	}

	defaultGateway, err := defaultGateway()
	if err != nil {
		return errors.Wrap(err, "error getting default gateway")
	}

	network, err := networkMapping()
	if err != nil {
		return errors.Wrap(err, "error generating network mapping")
	}

	data := map[string]interface{}{
		"network":           network,
		"instanceIP":        instanceIP,
		"defaultGateway":    defaultGateway,
		"metadataServiceIP": metadataServiceIP,
	}

	b, err := GenerateNetworkTemplate(data)
	if err != nil {
		return errors.Wrap(err, "error generating network template")
	}

	if err := files.Write(netConfigPath, b, 0o640); err != nil {
		return errors.Wrapf(err, "error writing network configuration to %s", netConfigPath)
	}

	return nil
}

func GenerateNetworkTemplate(data map[string]interface{}) ([]byte, error) {
	return files.ExecuteTemplate(netConfig, data)
}

// networkMapping returns a single network mapping of DNI and optional static IP.
// currently only support single primary DNI and static IP configuration.
func networkMapping() ([]NetworkMapping, error) {
	dniList, err := dniList()
	if err != nil {
		return nil, errors.Wrap(err, "error getting DNI list")
	}

	network, err := parseNetworkFile()
	if err != nil {
		return nil, errors.Wrap(err, "error parsing network file")
	}

	if len(dniList) != network.DniCount {
		return nil, errors.Wrap(err, "the number of DNI found does not match the dniCount in the network file")
	}

	if len(network.Static) > 0 && len(network.Static) < network.DniCount {
		return nil, errors.Wrap(err, "mix of using DHCP and static IP is not supported")
	}

	m := make([]NetworkMapping, 0, network.DniCount)
	for i, dni := range dniList {
		n := NetworkMapping{
			DNI: dni,
		}
		if len(network.Static) > 0 {
			n.StaticIP = &network.Static[i]
		}
		m = append(m, n)
	}

	return m, nil
}

func parseNetworkFile() (*Network, error) {
	userData, err := utils.ResolveBootstrapContainerUserData()
	if err != nil {
		return nil, errors.Wrap(err, "error resolving user data")
	}
	for _, file := range userData.WriteFiles {
		if file.Path == networkFilePath {
			network := &Network{}
			err := yaml.Unmarshal([]byte(file.Content), network)
			if err != nil {
				return nil, errors.Wrap(err, "error unmarshalling network file content")
			}
			return network, nil
		}
	}
	return nil, nil
}

func dniList() ([]string, error) {
	devices, err := netlink.LinkList()
	if err != nil {
		return nil, errors.Wrap(err, "error getting the device list of links")
	}

	dnis := []string{}
	for _, device := range devices {
		name := device.Attrs().Name
		if name != "lo" && name != "eth0" {
			dnis = append(dnis, name)
		}
	}
	return dnis, nil
}

func defaultGateway() (string, error) {
	routes, err := netlink.RouteList(nil, syscall.AF_INET)
	if err != nil {
		return "", errors.Wrap(err, "error getting the route list")
	}

	for _, route := range routes {
		if route.Dst == nil {
			return route.Gw.String(), nil
		}
	}

	return "", errors.New("default gateway not found")
}

func instanceIP() (string, error) {
	url := fmt.Sprintf("http://%s/latest/meta-data/local-ipv4", metadataServiceIP)
	body, err := httpGet(url)
	if err != nil {
		return "", errors.Wrap(err, "error requesting instance ip through http")
	}

	return string(body), nil
}

func currentIP() (string, error) {
	f, err := os.Open(currentIPPath)
	if err != nil {
		return "", err
	}

	defer f.Close()

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		return scanner.Text(), nil
	}

	return "", nil
}

func deviceIP() (string, error) {
	network, err := parseNetworkFile()
	if err != nil {
		return "", errors.Wrap(err, "error parsing network file")
	}

	return network.DeviceIP, nil
}

func httpGet(url string) (string, error) {
	resp, err := http.Get(url)
	if err != nil {
		return "", errors.Wrapf(err, "error sending http GET request to %s", url)
	}
	if resp.StatusCode != http.StatusOK {
		return "", errors.Errorf("requesting %s returns status code %d", url, resp.StatusCode)
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", errors.Wrap(err, "error reading response body")
	}

	return string(body), nil
}
