package hcn

import (
	"encoding/json"
	"errors"

	"github.com/Microsoft/go-winio/pkg/guid"
	"github.com/Microsoft/hcsshim/internal/interop"
	"github.com/sirupsen/logrus"
)

// Route is assoicated with a subnet.
type Route struct {
	NextHop           string `json:",omitempty"`
	DestinationPrefix string `json:",omitempty"`
	Metric            uint16 `json:",omitempty"`
}

// Subnet is assoicated with a Ipam.
type Subnet struct {
	IpAddressPrefix string            `json:",omitempty"`
	Policies        []json.RawMessage `json:",omitempty"`
	Routes          []Route           `json:",omitempty"`
}

// Ipam (Internet Protocol Addres Management) is assoicated with a network
// and represents the address space(s) of a network.
type Ipam struct {
	Type    string   `json:",omitempty"` // Ex: Static, DHCP
	Subnets []Subnet `json:",omitempty"`
}

// MacRange is associated with MacPool and respresents the start and end addresses.
type MacRange struct {
	StartMacAddress string `json:",omitempty"`
	EndMacAddress   string `json:",omitempty"`
}

// MacPool is assoicated with a network and represents pool of MacRanges.
type MacPool struct {
	Ranges []MacRange `json:",omitempty"`
}

// Dns (Domain Name System is associated with a network.
type Dns struct {
	Domain     string   `json:",omitempty"`
	Search     []string `json:",omitempty"`
	ServerList []string `json:",omitempty"`
	Options    []string `json:",omitempty"`
}

// NetworkType are various networks.
type NetworkType string

// NetworkType const
const (
	NAT         NetworkType = "NAT"
	Transparent NetworkType = "Transparent"
	L2Bridge    NetworkType = "L2Bridge"
	L2Tunnel    NetworkType = "L2Tunnel"
	ICS         NetworkType = "ICS"
	Private     NetworkType = "Private"
	Overlay     NetworkType = "Overlay"
)

// NetworkFlags are various network flags.
type NetworkFlags uint32

// NetworkFlags const
const (
	None                NetworkFlags = 0
	EnableNonPersistent NetworkFlags = 8
)

// HostComputeNetwork represents a network
type HostComputeNetwork struct {
	Id            string          `json:"ID,omitempty"`
	Name          string          `json:",omitempty"`
	Type          NetworkType     `json:",omitempty"`
	Policies      []NetworkPolicy `json:",omitempty"`
	MacPool       MacPool         `json:",omitempty"`
	Dns           Dns             `json:",omitempty"`
	Ipams         []Ipam          `json:",omitempty"`
	Flags         NetworkFlags    `json:",omitempty"` // 0: None
	SchemaVersion SchemaVersion   `json:",omitempty"`
}

// NetworkResourceType are the 3 different Network settings resources.
type NetworkResourceType string

var (
	// NetworkResourceTypePolicy is for Network's policies. Ex: RemoteSubnet
	NetworkResourceTypePolicy NetworkResourceType = "Policy"
	// NetworkResourceTypeDNS is for Network's DNS settings.
	NetworkResourceTypeDNS NetworkResourceType = "DNS"
	// NetworkResourceTypeExtension is for Network's extension settings.
	NetworkResourceTypeExtension NetworkResourceType = "Extension"
)

// ModifyNetworkSettingRequest is the structure used to send request to modify an network.
// Used to update DNS/extension/policy on an network.
type ModifyNetworkSettingRequest struct {
	ResourceType NetworkResourceType `json:",omitempty"` // Policy, DNS, Extension
	RequestType  RequestType         `json:",omitempty"` // Add, Remove, Update, Refresh
	Settings     json.RawMessage     `json:",omitempty"`
}

type PolicyNetworkRequest struct {
	Policies []NetworkPolicy `json:",omitempty"`
}

func getNetwork(networkGuid guid.GUID, query string) (*HostComputeNetwork, error) {
	// Open network.
	var (
		networkHandle    hcnNetwork
		resultBuffer     *uint16
		propertiesBuffer *uint16
	)
	hr := hcnOpenNetwork(&networkGuid, &networkHandle, &resultBuffer)
	if err := checkForErrors("hcnOpenNetwork", hr, resultBuffer); err != nil {
		return nil, err
	}
	// Query network.
	hr = hcnQueryNetworkProperties(networkHandle, query, &propertiesBuffer, &resultBuffer)
	if err := checkForErrors("hcnQueryNetworkProperties", hr, resultBuffer); err != nil {
		return nil, err
	}
	properties := interop.ConvertAndFreeCoTaskMemString(propertiesBuffer)
	// Close network.
	hr = hcnCloseNetwork(networkHandle)
	if err := checkForErrors("hcnCloseNetwork", hr, nil); err != nil {
		return nil, err
	}
	// Convert output to HostComputeNetwork
	var outputNetwork HostComputeNetwork

	// If HNS sets the network type to NAT (i.e. '0' in HNS.Schema.Network.NetworkMode),
	// the value will be omitted from the JSON blob. We therefore need to initialize NAT here before
	// unmarshaling the JSON blob.
	outputNetwork.Type = NAT

	if err := json.Unmarshal([]byte(properties), &outputNetwork); err != nil {
		return nil, err
	}
	return &outputNetwork, nil
}

func enumerateNetworks(query string) ([]HostComputeNetwork, error) {
	// Enumerate all Network Guids
	var (
		resultBuffer  *uint16
		networkBuffer *uint16
	)
	hr := hcnEnumerateNetworks(query, &networkBuffer, &resultBuffer)
	if err := checkForErrors("hcnEnumerateNetworks", hr, resultBuffer); err != nil {
		return nil, err
	}

	networks := interop.ConvertAndFreeCoTaskMemString(networkBuffer)
	var networkIds []guid.GUID
	if err := json.Unmarshal([]byte(networks), &networkIds); err != nil {
		return nil, err
	}

	var outputNetworks []HostComputeNetwork
	for _, networkGuid := range networkIds {
		network, err := getNetwork(networkGuid, query)
		if err != nil {
			return nil, err
		}
		outputNetworks = append(outputNetworks, *network)
	}
	return outputNetworks, nil
}

func createNetwork(settings string) (*HostComputeNetwork, error) {
	// Create new network.
	var (
		networkHandle    hcnNetwork
		resultBuffer     *uint16
		propertiesBuffer *uint16
	)
	networkGuid := guid.GUID{}
	hr := hcnCreateNetwork(&networkGuid, settings, &networkHandle, &resultBuffer)
	if err := checkForErrors("hcnCreateNetwork", hr, resultBuffer); err != nil {
		return nil, err
	}
	// Query network.
	hcnQuery := defaultQuery()
	query, err := json.Marshal(hcnQuery)
	if err != nil {
		return nil, err
	}
	hr = hcnQueryNetworkProperties(networkHandle, string(query), &propertiesBuffer, &resultBuffer)
	if err := checkForErrors("hcnQueryNetworkProperties", hr, resultBuffer); err != nil {
		return nil, err
	}
	properties := interop.ConvertAndFreeCoTaskMemString(propertiesBuffer)
	// Close network.
	hr = hcnCloseNetwork(networkHandle)
	if err := checkForErrors("hcnCloseNetwork", hr, nil); err != nil {
		return nil, err
	}
	// Convert output to HostComputeNetwork
	var outputNetwork HostComputeNetwork

	// If HNS sets the network type to NAT (i.e. '0' in HNS.Schema.Network.NetworkMode),
	// the value will be omitted from the JSON blob. We therefore need to initialize NAT here before
	// unmarshaling the JSON blob.
	outputNetwork.Type = NAT

	if err := json.Unmarshal([]byte(properties), &outputNetwork); err != nil {
		return nil, err
	}
	return &outputNetwork, nil
}

func modifyNetwork(networkId string, settings string) (*HostComputeNetwork, error) {
	networkGuid, err := guid.FromString(networkId)
	if err != nil {
		return nil, errInvalidNetworkID
	}
	// Open Network
	var (
		networkHandle    hcnNetwork
		resultBuffer     *uint16
		propertiesBuffer *uint16
	)
	hr := hcnOpenNetwork(&networkGuid, &networkHandle, &resultBuffer)
	if err := checkForErrors("hcnOpenNetwork", hr, resultBuffer); err != nil {
		return nil, err
	}
	// Modify Network
	hr = hcnModifyNetwork(networkHandle, settings, &resultBuffer)
	if err := checkForErrors("hcnModifyNetwork", hr, resultBuffer); err != nil {
		return nil, err
	}
	// Query network.
	hcnQuery := defaultQuery()
	query, err := json.Marshal(hcnQuery)
	if err != nil {
		return nil, err
	}
	hr = hcnQueryNetworkProperties(networkHandle, string(query), &propertiesBuffer, &resultBuffer)
	if err := checkForErrors("hcnQueryNetworkProperties", hr, resultBuffer); err != nil {
		return nil, err
	}
	properties := interop.ConvertAndFreeCoTaskMemString(propertiesBuffer)
	// Close network.
	hr = hcnCloseNetwork(networkHandle)
	if err := checkForErrors("hcnCloseNetwork", hr, nil); err != nil {
		return nil, err
	}
	// Convert output to HostComputeNetwork
	var outputNetwork HostComputeNetwork

	// If HNS sets the network type to NAT (i.e. '0' in HNS.Schema.Network.NetworkMode),
	// the value will be omitted from the JSON blob. We therefore need to initialize NAT here before
	// unmarshaling the JSON blob.
	outputNetwork.Type = NAT

	if err := json.Unmarshal([]byte(properties), &outputNetwork); err != nil {
		return nil, err
	}
	return &outputNetwork, nil
}

func deleteNetwork(networkId string) error {
	networkGuid, err := guid.FromString(networkId)
	if err != nil {
		return errInvalidNetworkID
	}
	var resultBuffer *uint16
	hr := hcnDeleteNetwork(&networkGuid, &resultBuffer)
	if err := checkForErrors("hcnDeleteNetwork", hr, resultBuffer); err != nil {
		return err
	}
	return nil
}

// ListNetworks makes a call to list all available networks.
func ListNetworks() ([]HostComputeNetwork, error) {
	hcnQuery := defaultQuery()
	networks, err := ListNetworksQuery(hcnQuery)
	if err != nil {
		return nil, err
	}
	return networks, nil
}

// ListNetworksQuery makes a call to query the list of available networks.
func ListNetworksQuery(query HostComputeQuery) ([]HostComputeNetwork, error) {
	queryJson, err := json.Marshal(query)
	if err != nil {
		return nil, err
	}

	networks, err := enumerateNetworks(string(queryJson))
	if err != nil {
		return nil, err
	}
	return networks, nil
}

// GetNetworkByID returns the network specified by Id.
func GetNetworkByID(networkID string) (*HostComputeNetwork, error) {
	hcnQuery := defaultQuery()
	mapA := map[string]string{"ID": networkID}
	filter, err := json.Marshal(mapA)
	if err != nil {
		return nil, err
	}
	hcnQuery.Filter = string(filter)

	networks, err := ListNetworksQuery(hcnQuery)
	if err != nil {
		return nil, err
	}
	if len(networks) == 0 {
		return nil, NetworkNotFoundError{NetworkID: networkID}
	}
	return &networks[0], err
}

// GetNetworkByName returns the network specified by Name.
func GetNetworkByName(networkName string) (*HostComputeNetwork, error) {
	hcnQuery := defaultQuery()
	mapA := map[string]string{"Name": networkName}
	filter, err := json.Marshal(mapA)
	if err != nil {
		return nil, err
	}
	hcnQuery.Filter = string(filter)

	networks, err := ListNetworksQuery(hcnQuery)
	if err != nil {
		return nil, err
	}
	if len(networks) == 0 {
		return nil, NetworkNotFoundError{NetworkName: networkName}
	}
	return &networks[0], err
}

// Create Network.
func (network *HostComputeNetwork) Create() (*HostComputeNetwork, error) {
	logrus.Debugf("hcn::HostComputeNetwork::Create id=%s", network.Id)
	for _, ipam := range network.Ipams {
		for _, subnet := range ipam.Subnets {
			if subnet.IpAddressPrefix != "" {
				hasDefault := false
				for _, route := range subnet.Routes {
					if route.NextHop == "" {
						return nil, errors.New("network create error, subnet has address prefix but no gateway specified")
					}
					if route.DestinationPrefix == "0.0.0.0/0" || route.DestinationPrefix == "::/0" {
						hasDefault = true
					}
				}
				if !hasDefault {
					return nil, errors.New("network create error, no default gateway")
				}
			}
		}
	}

	jsonString, err := json.Marshal(network)
	if err != nil {
		return nil, err
	}

	logrus.Debugf("hcn::HostComputeNetwork::Create JSON: %s", jsonString)
	network, hcnErr := createNetwork(string(jsonString))
	if hcnErr != nil {
		return nil, hcnErr
	}
	return network, nil
}

// Delete Network.
func (network *HostComputeNetwork) Delete() error {
	logrus.Debugf("hcn::HostComputeNetwork::Delete id=%s", network.Id)

	if err := deleteNetwork(network.Id); err != nil {
		return err
	}
	return nil
}

// ModifyNetworkSettings updates the Policy for a network.
func (network *HostComputeNetwork) ModifyNetworkSettings(request *ModifyNetworkSettingRequest) error {
	logrus.Debugf("hcn::HostComputeNetwork::ModifyNetworkSettings id=%s", network.Id)

	networkSettingsRequest, err := json.Marshal(request)
	if err != nil {
		return err
	}

	_, err = modifyNetwork(network.Id, string(networkSettingsRequest))
	if err != nil {
		return err
	}
	return nil
}

// AddPolicy applies a Policy (ex: RemoteSubnet) on the Network.
func (network *HostComputeNetwork) AddPolicy(networkPolicy PolicyNetworkRequest) error {
	logrus.Debugf("hcn::HostComputeNetwork::AddPolicy id=%s", network.Id)

	settingsJson, err := json.Marshal(networkPolicy)
	if err != nil {
		return err
	}
	requestMessage := &ModifyNetworkSettingRequest{
		ResourceType: NetworkResourceTypePolicy,
		RequestType:  RequestTypeAdd,
		Settings:     settingsJson,
	}

	return network.ModifyNetworkSettings(requestMessage)
}

// RemovePolicy removes a Policy (ex: RemoteSubnet) from the Network.
func (network *HostComputeNetwork) RemovePolicy(networkPolicy PolicyNetworkRequest) error {
	logrus.Debugf("hcn::HostComputeNetwork::RemovePolicy id=%s", network.Id)

	settingsJson, err := json.Marshal(networkPolicy)
	if err != nil {
		return err
	}
	requestMessage := &ModifyNetworkSettingRequest{
		ResourceType: NetworkResourceTypePolicy,
		RequestType:  RequestTypeRemove,
		Settings:     settingsJson,
	}

	return network.ModifyNetworkSettings(requestMessage)
}

// CreateEndpoint creates an endpoint on the Network.
func (network *HostComputeNetwork) CreateEndpoint(endpoint *HostComputeEndpoint) (*HostComputeEndpoint, error) {
	isRemote := endpoint.Flags&EndpointFlagsRemoteEndpoint != 0
	logrus.Debugf("hcn::HostComputeNetwork::CreatEndpoint, networkId=%s remote=%t", network.Id, isRemote)

	endpoint.HostComputeNetwork = network.Id
	endpointSettings, err := json.Marshal(endpoint)
	if err != nil {
		return nil, err
	}
	newEndpoint, err := createEndpoint(network.Id, string(endpointSettings))
	if err != nil {
		return nil, err
	}
	return newEndpoint, nil
}

// CreateRemoteEndpoint creates a remote endpoint on the Network.
func (network *HostComputeNetwork) CreateRemoteEndpoint(endpoint *HostComputeEndpoint) (*HostComputeEndpoint, error) {
	endpoint.Flags = EndpointFlagsRemoteEndpoint | endpoint.Flags
	return network.CreateEndpoint(endpoint)
}
