/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

package azure

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/http"
	"time"

	"github.com/edgelesssys/constellation/v2/internal/cloud"
	"github.com/edgelesssys/constellation/v2/internal/role"
)

// subset of azure imds API: https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux
// this is not yet available through the azure sdk (see https://github.com/Azure/azure-rest-api-specs/issues/4408)

const (
	imdsURL        = "http://169.254.169.254/metadata/instance"
	imdsAPIVersion = "2021-02-01"
	maxCacheAge    = 12 * time.Hour
)

// IMDSClient is a client for the Azure Instance Metadata Service.
type IMDSClient struct {
	client *http.Client

	cache     metadataResponse
	cacheTime time.Time
}

// NewIMDSClient creates a new IMDSClient.
func NewIMDSClient() *IMDSClient {
	// The default http client may use a system-wide proxy and it is recommended to disable the proxy explicitly:
	// https://docs.microsoft.com/en-us/azure/virtual-machines/windows/instance-metadata-service?tabs=linux#proxies
	// See also: https://github.com/microsoft/azureimds/blob/master/imdssample.go#L10
	return &IMDSClient{
		client: &http.Client{Transport: &http.Transport{Proxy: nil}},
	}
}

// Tags returns the tags of the instance the function is called from.
func (c *IMDSClient) Tags(ctx context.Context) (map[string]string, error) {
	if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 {
		if err := c.update(ctx); err != nil {
			return nil, err
		}
	}

	tags := make(map[string]string, len(c.cache.Compute.Tags))
	for _, tag := range c.cache.Compute.Tags {
		tags[tag.Name] = tag.Value
	}

	return tags, nil
}

// providerID returns the provider ID of the instance the function is called from.
func (c *IMDSClient) providerID(ctx context.Context) (string, error) {
	if c.timeForUpdate() || c.cache.Compute.ResourceID == "" {
		if err := c.update(ctx); err != nil {
			return "", err
		}
	}

	if c.cache.Compute.ResourceID == "" {
		return "", errors.New("unable to get provider id")
	}

	return c.cache.Compute.ResourceID, nil
}

func (c *IMDSClient) name(ctx context.Context) (string, error) {
	if c.timeForUpdate() || c.cache.Compute.OSProfile.ComputerName == "" {
		if err := c.update(ctx); err != nil {
			return "", err
		}
	}

	if c.cache.Compute.OSProfile.ComputerName == "" {
		return "", errors.New("unable to get name")
	}

	return c.cache.Compute.OSProfile.ComputerName, nil
}

// subscriptionID returns the subscription ID of the instance the function
// is called from.
func (c *IMDSClient) subscriptionID(ctx context.Context) (string, error) {
	if c.timeForUpdate() || c.cache.Compute.SubscriptionID == "" {
		if err := c.update(ctx); err != nil {
			return "", err
		}
	}

	if c.cache.Compute.SubscriptionID == "" {
		return "", errors.New("unable to get subscription id")
	}

	return c.cache.Compute.SubscriptionID, nil
}

// resourceGroup returns the resource group of the instance the function
// is called from.
func (c *IMDSClient) resourceGroup(ctx context.Context) (string, error) {
	if c.timeForUpdate() || c.cache.Compute.ResourceGroup == "" {
		if err := c.update(ctx); err != nil {
			return "", err
		}
	}

	if c.cache.Compute.ResourceGroup == "" {
		return "", errors.New("unable to get resource group")
	}

	return c.cache.Compute.ResourceGroup, nil
}

// uid returns the UID of the cluster, based on the tags on the instance
// the function is called from, which are inherited from the scale set.
func (c *IMDSClient) uid(ctx context.Context) (string, error) {
	if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 {
		if err := c.update(ctx); err != nil {
			return "", err
		}
	}

	for _, tag := range c.cache.Compute.Tags {
		if tag.Name == cloud.TagUID {
			return tag.Value, nil
		}
	}

	return "", fmt.Errorf("unable to get uid from metadata tags %v", c.cache.Compute.Tags)
}

// initSecretHash returns the hash of the init secret of the cluster, based on the tags on the instance
// the function is called from, which are inherited from the scale set.
func (c *IMDSClient) initSecretHash(ctx context.Context) (string, error) {
	if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 {
		if err := c.update(ctx); err != nil {
			return "", err
		}
	}

	for _, tag := range c.cache.Compute.Tags {
		if tag.Name == cloud.TagInitSecretHash {
			return tag.Value, nil
		}
	}

	return "", fmt.Errorf("unable to get tag %s from metadata tags %v", cloud.TagInitSecretHash, c.cache.Compute.Tags)
}

// role returns the role of the instance the function is called from.
func (c *IMDSClient) role(ctx context.Context) (role.Role, error) {
	if c.timeForUpdate() || len(c.cache.Compute.Tags) == 0 {
		if err := c.update(ctx); err != nil {
			return role.Unknown, err
		}
	}

	for _, tag := range c.cache.Compute.Tags {
		if tag.Name == cloud.TagRole {
			return role.FromString(tag.Value), nil
		}
	}

	return role.Unknown, fmt.Errorf("unable to get role from metadata tags %v", c.cache.Compute.Tags)
}

// timeForUpdate checks whether an update is needed due to cache age.
func (c *IMDSClient) timeForUpdate() bool {
	return time.Since(c.cacheTime) > maxCacheAge
}

// update updates instance metadata from the azure imds API.
func (c *IMDSClient) update(ctx context.Context) error {
	req, err := http.NewRequestWithContext(ctx, http.MethodGet, imdsURL, http.NoBody)
	if err != nil {
		return err
	}
	req.Header.Add("Metadata", "True")
	query := req.URL.Query()
	query.Add("format", "json")
	query.Add("api-version", imdsAPIVersion)
	req.URL.RawQuery = query.Encode()
	resp, err := c.client.Do(req)
	if err != nil {
		return err
	}
	defer resp.Body.Close()
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}
	var res metadataResponse
	if err := json.Unmarshal(body, &res); err != nil {
		return err
	}

	c.cache = res
	c.cacheTime = time.Now()
	return nil
}

// metadataResponse contains metadataResponse with only the required values.
type metadataResponse struct {
	Compute metadataResponseCompute `json:"compute,omitempty"`
}

type metadataResponseCompute struct {
	ResourceID     string        `json:"resourceId,omitempty"`
	SubscriptionID string        `json:"subscriptionId,omitempty"`
	ResourceGroup  string        `json:"resourceGroupName,omitempty"`
	Tags           []metadataTag `json:"tagsList,omitempty"`
	OSProfile      struct {
		ComputerName string `json:"computerName,omitempty"`
	} `json:"osProfile,omitempty"`
}

type metadataTag struct {
	Name  string `json:"name,omitempty"`
	Value string `json:"value,omitempty"`
}
