/*
 * This file is part of the KubeVirt project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Copyright The KubeVirt Authors.
 *
 */

package cloudinit

import (
	"fmt"

	"kubevirt.io/kubevirt/pkg/pointer"
	"kubevirt.io/kubevirt/tests/libnet/cluster"
	"kubevirt.io/kubevirt/tests/libnet/dns"

	"sigs.k8s.io/yaml"
)

type (
	NetworkDataOption          func(*CloudInitNetworkData) error
	NetworkDataInterfaceOption func(*CloudInitInterface) error
)

func NewNetworkData(options ...NetworkDataOption) (string, error) {
	networkData := CloudInitNetworkData{
		Version: 2,
	}

	for _, option := range options {
		err := option(&networkData)
		if err != nil {
			return "", fmt.Errorf("failed defining network data when running options: %w", err)
		}
	}

	nd, err := yaml.Marshal(&networkData)
	if err != nil {
		return "", err
	}

	return string(nd), nil
}

func WithEthernet(name string, options ...NetworkDataInterfaceOption) NetworkDataOption {
	return func(networkData *CloudInitNetworkData) error {
		if networkData.Ethernets == nil {
			networkData.Ethernets = map[string]CloudInitInterface{}
		}

		networkDataInterface := CloudInitInterface{name: name}

		for _, option := range options {
			err := option(&networkDataInterface)
			if err != nil {
				return fmt.Errorf("failed defining network data ethernet device when running options: %w", err)
			}
		}

		networkData.Ethernets[name] = networkDataInterface
		return nil
	}
}

func WithAddresses(addresses ...string) NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		networkDataInterface.Addresses = append(networkDataInterface.Addresses, addresses...)
		return nil
	}
}

func WithDHCP4Enabled() NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		networkDataInterface.DHCP4 = pointer.P(true)
		return nil
	}
}

func WithDHCP6Enabled() NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		networkDataInterface.DHCP6 = pointer.P(true)
		return nil
	}
}

func WithDHCP4Disabled() NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		networkDataInterface.DHCP4 = pointer.P(false)
		return nil
	}
}

func WithDHCP6Disabled() NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		networkDataInterface.DHCP6 = pointer.P(false)
		return nil
	}
}

func WithGateway6(gateway6 string) NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		networkDataInterface.Routes = append(networkDataInterface.Routes, CloudInitRoute{
			To:  "::/0",
			Via: gateway6,
		})
		return nil
	}
}

func WithNameserverFromCluster() NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		dnsServerIP, err := dns.ClusterDNSServiceIP()
		if err != nil {
			return fmt.Errorf("failed defining network data nameservers when retrieving cluster DNS service IP: %w", err)
		}
		networkDataInterface.Nameservers = &CloudInitNameservers{
			Addresses: []string{dnsServerIP},
			Search:    dns.SearchDomains(),
		}
		return nil
	}
}

func WithMatchingMAC(macAddress string) NetworkDataInterfaceOption {
	return func(networkDataInterface *CloudInitInterface) error {
		networkDataInterface.Match = CloudInitMatch{
			MACAddress: macAddress,
		}
		networkDataInterface.SetName = networkDataInterface.name
		return nil
	}
}

type CloudInitNetworkData struct {
	Version   int                           `json:"version"`
	Ethernets map[string]CloudInitInterface `json:"ethernets,omitempty"`
}

type CloudInitInterface struct {
	name           string
	AcceptRA       *bool                 `json:"accept-ra,omitempty"`
	Addresses      []string              `json:"addresses,omitempty"`
	DHCP4          *bool                 `json:"dhcp4,omitempty"`
	DHCP6          *bool                 `json:"dhcp6,omitempty"`
	DHCPIdentifier string                `json:"dhcp-identifier,omitempty"` // "duid" or  "mac"
	Gateway4       string                `json:"gateway4,omitempty"`
	Gateway6       string                `json:"gateway6,omitempty"`
	Nameservers    *CloudInitNameservers `json:"nameservers,omitempty"`
	MACAddress     string                `json:"macaddress,omitempty"`
	Match          CloudInitMatch        `json:"match,omitempty"`
	MTU            int                   `json:"mtu,omitempty"`
	Routes         []CloudInitRoute      `json:"routes,omitempty"`
	SetName        string                `json:"set-name,omitempty"`
}

type CloudInitNameservers struct {
	Search    []string `json:"search,omitempty"`
	Addresses []string `json:"addresses,omitempty"`
}

type CloudInitMatch struct {
	Name       string `json:"name,omitempty"`
	MACAddress string `json:"macaddress,omitempty"`
	Driver     string `json:"driver,omitempty"`
}

type CloudInitRoute struct {
	From   string `json:"from,omitempty"`
	OnLink *bool  `json:"on-link,omitempty"`
	Scope  string `json:"scope,omitempty"`
	Table  *int   `json:"table,omitempty"`
	To     string `json:"to,omitempty"`
	Type   string `json:"type,omitempty"`
	Via    string `json:"via,omitempty"`
	Metric *int   `json:"metric,omitempty"`
}

const (
	DefaultIPv6Address = "fd10:0:2::2"
	DefaultIPv6CIDR    = DefaultIPv6Address + "/120"
	DefaultIPv6Gateway = "fd10:0:2::1"
)

// CreateDefaultCloudInitNetworkData returns cloud-init v2 network-config for eth0 from cluster IP family probes.
func CreateDefaultCloudInitNetworkData() string {
	supportsIPv4, errV4 := cluster.SupportsIpv4()
	if errV4 != nil {
		panic(errV4)
	}
	supportsIPv6, errV6 := cluster.SupportsIpv6()
	if errV6 != nil {
		panic(errV6)
	}

	var ifaceOpts []NetworkDataInterfaceOption
	switch {
	case supportsIPv4 && supportsIPv6: // DHCPv4 + static IPv6 + gateway6
		ifaceOpts = []NetworkDataInterfaceOption{
			WithDHCP4Enabled(),
			WithAddresses(DefaultIPv6CIDR),
			WithGateway6(DefaultIPv6Gateway),
			WithNameserverFromCluster(),
		}
	case supportsIPv4: // DHCPv4 only
		ifaceOpts = []NetworkDataInterfaceOption{
			WithDHCP4Enabled(),
			WithDHCP6Disabled(),
			WithNameserverFromCluster(),
		}
	case supportsIPv6: // static IPv6 + gateway6
		ifaceOpts = []NetworkDataInterfaceOption{
			WithDHCP4Disabled(),
			WithAddresses(DefaultIPv6CIDR),
			WithGateway6(DefaultIPv6Gateway),
			WithNameserverFromCluster(),
		}
	default: // probes ok but neither family — should not happen
		panic(fmt.Errorf("cluster IPv4/IPv6 probes succeeded but neither family is supported"))
	}

	data, err := NewNetworkData(WithEthernet("eth0", ifaceOpts...))
	if err != nil {
		panic(err)
	}
	return data
}

func GetFedoraToolsGuestAgentBlacklistUserData(commands string) string {
	return fmt.Sprintf(`#!/bin/bash
            echo -e "\n\nBLACKLIST_RPC=%s" | sudo tee -a /etc/sysconfig/qemu-ga
`, commands)
}
