package instance

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"math/rand"
	"net"
	"net/http"
	"strconv"
	"time"

	"github.com/scaleway/scaleway-sdk-go/internal/errors"
)

var (
	metadataURL           = "http://169.254.42.42"
	metadataRetryBindPort = 200
)

// MetadataAPI metadata API
type MetadataAPI struct {
}

// NewMetadataAPI returns a MetadataAPI object from a Scaleway client.
func NewMetadataAPI() *MetadataAPI {
	return &MetadataAPI{}
}

// GetMetadata returns the metadata available from the server
func (*MetadataAPI) GetMetadata() (m *Metadata, err error) {
	resp, err := http.Get(metadataURL + "/conf?format=json")
	if err != nil {
		return nil, errors.Wrap(err, "error getting metadataURL")
	}
	defer resp.Body.Close()

	metadata := &Metadata{}
	err = json.NewDecoder(resp.Body).Decode(metadata)
	if err != nil {
		return nil, errors.Wrap(err, "error decoding metadata")
	}
	return metadata, nil
}

// Metadata represents the struct return by the metadata API
type Metadata struct {
	ID             string `json:"id,omitempty"`
	Name           string `json:"name,omitempty"`
	Hostname       string `json:"hostname,omitempty"`
	Organization   string `json:"organization,omitempty"`
	CommercialType string `json:"commercial_type,omitempty"`
	PublicIP       struct {
		Dynamic bool   `json:"dynamic,omitempty"`
		ID      string `json:"id,omitempty"`
		Address string `json:"address,omitempty"`
	} `json:"public_ip,omitempty"`
	PrivateIP string `json:"private_ip,omitempty"`
	IPv6      struct {
		Netmask string `json:"netmask,omitempty"`
		Gateway string `json:"gateway,omitempty"`
		Address string `json:"address,omitempty"`
	} `json:"ipv6,omitempty"`
	Location struct {
		PlatformID   string `json:"platform_id,omitempty"`
		HypervisorID string `json:"hypervisor_id,omitempty"`
		NodeID       string `json:"node_id,omitempty"`
		ClusterID    string `json:"cluster_id,omitempty"`
		ZoneID       string `json:"zone_id,omitempty"`
	} `json:"location,omitempty"`
	Tags          []string `json:"tags,omitempty"`
	StateDetail   string   `json:"state_detail,omitempty"`
	SSHPublicKeys []struct {
		Description      string `json:"description,omitempty"`
		ModificationDate string `json:"modification_date,omitempty"`
		IP               string `json:"ip,omitempty"`
		Email            string `json:"email,omitempty"`
		UserAgent        struct {
			Platform string `json:"platform,omitempty"`
			Version  string `json:"version,omitempty"`
			String   string `json:"string,omitempty"`
			Browser  string `json:"browser,omitempty"`
		} `json:"user_agent,omitempty"`
		Key          string `json:"key,omitempty"`
		Fingerprint  string `json:"fingerprint,omitempty"`
		ID           string `json:"id,omitempty"`
		CreationDate string `json:"creation_date,omitempty"`
		Port         int    `json:"port,omitempty"`
	} `json:"ssh_public_keys,omitempty"`
	Timezone   string `json:"timezone,omitempty"`
	Bootscript struct {
		Kernel       string `json:"kernel,omitempty"`
		Title        string `json:"title,omitempty"`
		Default      bool   `json:"default,omitempty"`
		Dtb          string `json:"dtb,omitempty"`
		Public       bool   `json:"publc,omitempty"`
		Initrd       string `json:"initrd,omitempty"`
		Bootcmdargs  string `json:"bootcmdargs,omitempty"`
		Architecture string `json:"architecture,omitempty"`
		Organization string `json:"organization,omitempty"`
		ID           string `json:"id,omitempty"`
	} `json:"bootscript,omitempty"`
	Volumes map[string]struct {
		Name             string `json:"name,omitempty"`
		ModificationDate string `json:"modification_date,omitempty"`
		ExportURI        string `json:"export_uri,omitempty"`
		VolumeType       string `json:"volume_type,omitempty"`
		CreationDate     string `json:"creation_date,omitempty"`
		State            string `json:"state,omitempty"`
		Organization     string `json:"organization,omitempty"`
		Server           struct {
			ID   string `json:"id,omitempty"`
			Name string `json:"name,omitempty"`
		} `json:"server,omitempty"`
		ID   string `json:"id,omitempty"`
		Size int    `json:"size,omitempty"`
	}
}

// ListUserdata returns the metadata available from the server
func (*MetadataAPI) ListUserdata() (res *Userdata, err error) {
	retries := 0
	for retries <= metadataRetryBindPort {
		port := rand.Intn(1024)
		localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
		if err != nil {
			return nil, errors.Wrap(err, "error resolving tcp address")
		}

		userdataClient := &http.Client{
			Transport: &http.Transport{
				DialContext: (&net.Dialer{
					LocalAddr:     localTCPAddr,
					DualStack:     false,
					FallbackDelay: time.Second * -1,
				}).DialContext,
			},
		}

		resp, err := userdataClient.Get(metadataURL + "/user_data?format=json")
		if err != nil {
			retries++ // retry with a different source port
			continue
		}
		defer resp.Body.Close()

		userdata := &Userdata{}
		err = json.NewDecoder(resp.Body).Decode(userdata)
		if err != nil {
			return nil, errors.Wrap(err, "error decoding userdata")
		}
		return userdata, nil
	}
	return nil, errors.New("too many bind port retries for ListUserdata")
}

// GetUserdata returns the value for the given metadata key
func (*MetadataAPI) GetUserdata(key string) ([]byte, error) {
	if key == "" {
		return make([]byte, 0), errors.New("key must not be empty in GetUserdata")
	}

	retries := 0
	for retries <= metadataRetryBindPort {
		port := rand.Intn(1024)
		localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
		if err != nil {
			return make([]byte, 0), errors.Wrap(err, "error resolving tcp address")
		}

		userdataClient := &http.Client{
			Transport: &http.Transport{
				DialContext: (&net.Dialer{
					LocalAddr:     localTCPAddr,
					DualStack:     false,
					FallbackDelay: time.Second * -1,
				}).DialContext,
			},
		}

		resp, err := userdataClient.Get(metadataURL + "/user_data/" + key)
		if err != nil {
			retries++ // retry with a different source port
			continue
		}
		defer resp.Body.Close()

		body, err := ioutil.ReadAll(resp.Body)
		if err != nil {
			return make([]byte, 0), errors.Wrap(err, "error reading userdata body")
		}

		return body, nil
	}
	return make([]byte, 0), errors.New("too may bind port retries for GetUserdata")
}

// SetUserdata sets the userdata key with the given value
func (*MetadataAPI) SetUserdata(key string, value []byte) error {
	if key == "" {
		return errors.New("key must not be empty in SetUserdata")
	}

	retries := 0
	for retries <= metadataRetryBindPort {
		port := rand.Intn(1024)
		localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
		if err != nil {
			return errors.Wrap(err, "error resolving tcp address")
		}

		userdataClient := &http.Client{
			Transport: &http.Transport{
				DialContext: (&net.Dialer{
					LocalAddr:     localTCPAddr,
					DualStack:     false,
					FallbackDelay: time.Second * -1,
				}).DialContext,
			},
		}
		request, err := http.NewRequest("PATCH", metadataURL+"/user_data/"+key, bytes.NewBuffer(value))
		if err != nil {
			return errors.Wrap(err, "error creating patch userdata request")
		}
		request.Header.Set("Content-Type", "text/plain")
		_, err = userdataClient.Do(request)
		if err != nil {
			retries++ // retry with a different source port
			continue
		}

		return nil
	}
	return errors.New("too may bind port retries for SetUserdata")
}

// DeleteUserdata deletes the userdata key and the associated value
func (*MetadataAPI) DeleteUserdata(key string) error {
	if key == "" {
		return errors.New("key must not be empty in DeleteUserdata")
	}

	retries := 0
	for retries <= metadataRetryBindPort {
		port := rand.Intn(1024)
		localTCPAddr, err := net.ResolveTCPAddr("tcp", ":"+strconv.Itoa(port))
		if err != nil {
			return errors.Wrap(err, "error resolving tcp address")
		}

		userdataClient := &http.Client{
			Transport: &http.Transport{
				DialContext: (&net.Dialer{
					LocalAddr:     localTCPAddr,
					DualStack:     false,
					FallbackDelay: time.Second * -1,
				}).DialContext,
			},
		}
		request, err := http.NewRequest("DELETE", metadataURL+"/user_data/"+key, bytes.NewBuffer([]byte("")))
		if err != nil {
			return errors.Wrap(err, "error creating delete userdata request")
		}
		_, err = userdataClient.Do(request)
		if err != nil {
			retries++ // retry with a different source port
			continue
		}

		return nil
	}
	return errors.New("too may bind port retries for DeleteUserdata")
}

// Userdata represents the user data
type Userdata struct {
	UserData []string `json:"user_data,omitempty"`
}
