// Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License"). You may
// not use this file except in compliance with the License. A copy of the
// License is located at
//
//	http://aws.amazon.com/apache2.0/
//
// or in the "license" file accompanying this file. This file 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.

package platforminfo

import (
	"bytes"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"net/http"
	"os/exec"
	"strconv"
	"strings"

	"github.com/aws/aws-app-mesh-agent/agent/client"
	"github.com/aws/aws-app-mesh-agent/agent/envoy_bootstrap/env"

	log "github.com/sirupsen/logrus"
)

const (
	metadataNamespace = "aws.appmesh.platformInfo"

	// K8s Info
	k8sVersionEnvVar               = "APPMESH_PLATFORM_K8S_VERSION"
	podUidEnvVar                   = "APPMESH_PLATFORM_K8S_POD_UID"
	appMeshControllerVersionEnvVar = "APPMESH_PLATFORM_APP_MESH_CONTROLLER_VERSION"
	k8sPlatformInfoKey             = "k8sPlatformInfo"
	k8sVersionKey                  = "k8sVersion"
	podUidKey                      = "podUid"
	appMeshControllerVersionKey    = "appMeshControllerVersion"

	// ECS Info
	ecsExecutionEnvVar            = "AWS_EXECUTION_ENV"
	ecsContainerMetadataUriEnv    = "ECS_CONTAINER_METADATA_URI"
	ecsContainerMetadataUriV4Env  = "ECS_CONTAINER_METADATA_URI_V4"
	ecsContainerMetadataTaskPath  = "/task"
	ecsPlatformInfoKey            = "ecsPlatformInfo"
	ecsLaunchTypeKey              = "ecsLaunchType"
	ecsClusterArnKey              = "ecsClusterArn"
	ecsTaskArnKey                 = "ecsTaskArn"
	ecsEnvoyContainerCpuLimit     = "CPU"
	ecsEnvoyContainerMemoryLimit  = "Memory"
	ecsContainerInstanceArnEnvVar = "ECS_CONTAINER_INSTANCE_ARN"
	ecsContainerInstanceArnKey    = "ecsContainerInstanceArn"

	// Platform independent information
	ec2MetadataUriEnvForTesting = "EC2_METADATA_HOST_ONLY_FOR_TESTING"
	ec2MetadataHost             = "http://169.254.169.254"
	azQuery                     = "placement/availability-zone"
	azIDQuery                   = "placement/availability-zone-id"
	AvailabilityZoneKey         = "AvailabilityZone"
	AvailabilityZoneIDKey       = "AvailabilityZoneID"
	supportedIPFamiliesKey      = "supportedIPFamilies"
	ec2MetadataTokenResource    = "/latest/api/token"
	ec2ImdsTokenHeader          = "X-aws-ec2-metadata-token"
	ec2ImdsTokenTtlHeader       = "X-aws-ec2-metadata-token-ttl-seconds"

	// System Information
	systemInfoKey       = "systemInfo"
	sysPlatformKey      = "systemPlatform"
	sysKernelVersionKey = "systemKernelVersion"
)

func buildMetadataForK8sPlatform(mapping map[string]interface{}) {
	k8sVersion := env.Get(k8sVersionEnvVar)
	podUid := env.Get(podUidEnvVar)
	appMeshControllerVersion := env.Get(appMeshControllerVersionEnvVar)

	// TODO: Add EKS cluster info when available
	if k8sVersion != "" && podUid != "" && appMeshControllerVersion != "" {
		mapping[k8sPlatformInfoKey] = map[string]interface{}{
			k8sVersionKey:               k8sVersion,
			podUidKey:                   podUid,
			appMeshControllerVersionKey: appMeshControllerVersion,
		}

		// Since IMDS is not accessible from inside ECS, making below 2 calls only on EKS platform.
		// Fetch AZ from EC2 instance metadata if possible.
		if availabilityZone, err := getEc2InstanceMetadata(azQuery); err != nil {
			log.Warnf("Couldn't determine the AZ due to: %v", err)
		} else if availabilityZone != "" {
			mapping[AvailabilityZoneKey] = availabilityZone
		}
		// Fetch AZ ID info as AZ can map differently for each account but AZ IDs are the same for
		// every account https://docs.aws.amazon.com/ram/latest/userguide/working-with-az-ids.html
		if availabilityZoneID, err := getEc2InstanceMetadata(azIDQuery); err != nil {
			// Just log info if we can't get this information
			log.Warnf("Couldn't determine the AZ ID due to: %v", err)
		} else if availabilityZoneID != "" {
			mapping[AvailabilityZoneIDKey] = availabilityZoneID
		}
	}
}

func buildMetadataForEcsPlatform(mapping map[string]interface{}) {
	// ECS platform information

	// Networks info: supportedIPFamilies, it's not an ECS only info, for others we may also need to set this
	supportedIPFamilies := ""

	ecsLaunchType := env.Get(ecsExecutionEnvVar)
	if ecsLaunchType != "" {
		ecsMetadata := map[string]interface{}{
			ecsLaunchTypeKey: ecsLaunchType,
		}

		ecsContainerInstanceArn := env.Get(ecsContainerInstanceArnEnvVar)
		if ecsContainerInstanceArn != "" {
			ecsMetadata[ecsContainerInstanceArnKey] = ecsContainerInstanceArn
		}

		// Look for V4 URI first and fallback on V3 URI
		ecsContainerMetadataUri := env.Or(ecsContainerMetadataUriV4Env, env.Get(ecsContainerMetadataUriEnv))
		// Get ECS container metadata
		if ecsContainerMetadataUri != "" {
			getEcsContainerMetadata(ecsContainerMetadataUri+ecsContainerMetadataTaskPath, ecsMetadata)
			getEcsEnvoyContainerMetadata(ecsContainerMetadataUri, ecsMetadata)
			supportedIPFamilies = getEcsContainerSupportedIPFamilies(ecsContainerMetadataUri + ecsContainerMetadataTaskPath)
		}
		// The AZ info is available from ECS container metadata itself
		if availabilityZone, exists := ecsMetadata[AvailabilityZoneKey]; exists {
			mapping[AvailabilityZoneKey] = availabilityZone
			delete(ecsMetadata, AvailabilityZoneKey)
		}

		// Build SupportedIPFamilies info in platform
		if supportedIPFamilies != "" {
			mapping[supportedIPFamiliesKey] = supportedIPFamilies
		}
		mapping[ecsPlatformInfoKey] = ecsMetadata
	}
}

func buildMetadataFromSystemInfo(mapping map[string]interface{}) {
	// System information
	systemInfo := make(map[string]interface{})
	if platform, err := RunCommand("uname", "-p"); err != nil {
		log.Errorf("Unable to get system platform info: %v", err)
	} else {
		systemInfo[sysPlatformKey] = platform
	}
	if kernelVersion, err := RunCommand("uname", "-r"); err != nil {
		log.Errorf("Unable to get system kernel version: %v", err)
	} else {
		systemInfo[sysKernelVersionKey] = kernelVersion
	}

	if len(systemInfo) > 0 {
		mapping[systemInfoKey] = systemInfo
	}
}

func BuildMetadata() (*map[string]interface{}, error) {
	md := make(map[string]interface{})
	mapping := make(map[string]interface{})

	buildMetadataForK8sPlatform(mapping)
	buildMetadataForEcsPlatform(mapping)
	buildMetadataFromSystemInfo(mapping)

	if len(mapping) != 0 {
		md[metadataNamespace] = mapping
	}

	return &md, nil
}

func getEcsContainerMetadata(uri string, ecsMetadata map[string]interface{}) {
	metadataMap, err := getEcsMetadata(uri)
	if err != nil {
		log.Warnf("Failed generating ECS platform info from ECS metadata: %v", err)
		return
	}

	// For reference on all the information that is returned from the task metadata endpoint, see
	// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html
	// Here we only pick the ones that are needed.
	if ecsClusterArn := metadataMap["Cluster"]; ecsClusterArn != "" {
		ecsMetadata[ecsClusterArnKey] = ecsClusterArn
	}
	if ecsTaskArn := metadataMap["TaskARN"]; ecsTaskArn != "" {
		ecsMetadata[ecsTaskArnKey] = ecsTaskArn
	}
	if availabilityZone := metadataMap["AvailabilityZone"]; availabilityZone != "" {
		ecsMetadata[AvailabilityZoneKey] = availabilityZone
	}

}

func getEcsEnvoyContainerMetadata(uri string, ecsMetadata map[string]interface{}) {

	response, err := http.Get(uri)
	if err != nil {
		log.Warnf("Unable to fetch ECS envoy container metadata from %s: %v", uri, err)
		return
	}
	defer response.Body.Close()
	responseBody, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Warnf("Unable to read ECS envoy container metadata: %v", err)
		return
	}

	var metadataMap map[string]interface{}
	err = json.Unmarshal(responseBody, &metadataMap)
	if err != nil {
		log.Warnf("Unable to parse ECS envoy container metadata: %v", err)
		return
	}

	// For reference on all the information that is returned from the task metadata endpoint, see
	// https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task-metadata-endpoint-v4.html
	// Here we only pick the ones that are needed.
	if CPULimit := fmt.Sprintf("%v", metadataMap["Limits"].(map[string]interface{})["CPU"]); CPULimit != "" {
		ecsMetadata[ecsEnvoyContainerCpuLimit] = CPULimit
	}
	if MemoryLimit := fmt.Sprintf("%v", metadataMap["Limits"].(map[string]interface{})["Memory"]); MemoryLimit != "" {
		ecsMetadata[ecsEnvoyContainerMemoryLimit] = MemoryLimit
	}
}

func getEcsMetadata(uri string) (map[string]interface{}, error) {
	var metadataMap map[string]interface{}
	response, err := http.Get(uri)
	if err != nil {
		log.Warnf("Unable to fetch ECS container metadata from %s: %v", uri, err)
		return nil, err
	}
	defer response.Body.Close()
	responseBody, err := ioutil.ReadAll(response.Body)
	if err != nil {
		log.Warnf("Unable to read ECS container metadata: %v", err)
		return nil, err
	}

	err = json.Unmarshal(responseBody, &metadataMap)
	if err != nil {
		log.Warnf("Unable to parse ECS container metadata: %s, %v", responseBody, err)
		return nil, err
	}
	return metadataMap, nil
}

func getEcsContainerSupportedIPFamilies(uri string) string {
	metadataMap, err := getEcsMetadata(uri)
	if err != nil {
		log.Warnf("Failed generating SupportedIPFamilies info from ECS metadata: %v", err)
		return ""
	}
	containers := metadataMap["Containers"]
	if containers == nil || len(containers.([]interface{})) == 0 {
		log.Warnf("Containers info not found in ECS metadata: %v", metadataMap)
		return ""
	}
	// all containers share the same networks
	containerInfo := containers.([]interface{})[0]
	networks := containerInfo.(map[string]interface{})["Networks"]
	if networks == nil || len(networks.([]interface{})) == 0 {
		log.Warnf("Networks info not found in container info in ECS metadata: %v", containerInfo)
		return ""
	}

	hasIPv4Addresses := false
	hasIPv6Addresses := false
	networksArray := networks.([]interface{})
	for i := 0; i < len(networksArray); i++ {
		if networksArray[i].(map[string]interface{})["IPv4Addresses"] != nil {
			hasIPv4Addresses = true
		}
		if networksArray[i].(map[string]interface{})["IPv6Addresses"] != nil {
			hasIPv6Addresses = true
		}
	}
	if hasIPv4Addresses && hasIPv6Addresses {
		return "ALL"
	}
	if hasIPv4Addresses {
		return "IPv4_ONLY"
	}
	if hasIPv6Addresses {
		return "IPv6_ONLY"
	}
	log.Warnf("Neither IPv4 or IPv6 addresses are found in ECS metadata Networks")
	return ""
}

func getEc2InstanceMetadata(query string) (string, error) {
	httpClient := client.CreateDefaultHttpClient()
	// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instancedata-data-retrieval.html
	// EC2 Instance Metadata url to get the token: http://169.254.169.254/latest/api/token
	token := ""
	tokenRequestUrl := env.Or(ec2MetadataUriEnvForTesting, ec2MetadataHost) + ec2MetadataTokenResource
	tokenRequest, err := client.CreateStandardAgentHttpRequest(http.MethodPut, tokenRequestUrl, nil)
	if err != nil {
		log.Debugf("unable to create http request: %v. request url: %s", err, tokenRequestUrl)
	} else {
		// Setting token expiry time to just 2 seconds instead of default 21600 seconds
		tokenRequest.Header.Add(ec2ImdsTokenTtlHeader, "2")
		tokenResponse, err := httpClient.Do(tokenRequest)
		if err != nil || tokenResponse == nil || tokenResponse.Body == nil {
			log.Debugf("unable to make a put call to EC2 Instance Metadata, request url: %s, error: %s "+
				"to fetch the instance metadata token. Falling back to insure way of calling EC2 Instance Metadata.",
				tokenRequestUrl, err)
		} else {
			defer tokenResponse.Body.Close()
			if tokenResponse.StatusCode != 200 {
				log.Debugf("unable to make a put call to EC2 Instance Metadata, request url: %s, code: %s "+
					"to fetch the instance metadata token. Falling back to insure way of calling EC2 Instance Metadata.",
					tokenRequestUrl, strconv.Itoa(tokenResponse.StatusCode))
			} else if responseBody, err := ioutil.ReadAll(tokenResponse.Body); err != nil {
				log.Debugf("unable to make a put call to EC2 Instance Metadata, request url: %s, error: %s "+
					"to fetch the instance metadata token. Falling back to insure way of calling EC2 Instance Metadata.",
					tokenRequestUrl, err)
			} else {
				log.Debugf("Successfully obtained token to make secure call to EC2 Instance Metadata")
				token = string(responseBody)
			}
		}
	}
	// EC2 Instance Metadata url: http://169.254.169.254/latest/meta-data/
	requestUrl := env.Or(ec2MetadataUriEnvForTesting, ec2MetadataHost) + "/latest/meta-data/" + query
	imdsRequest, err := client.CreateStandardAgentHttpRequest(http.MethodGet, requestUrl, nil)
	if err != nil {
		return "", fmt.Errorf("unable to create http request: %v. request url: %s", err, requestUrl)
	}
	if token != "" {
		imdsRequest.Header.Add(ec2ImdsTokenHeader, token)
	}
	response, err := httpClient.Do(imdsRequest)
	if err != nil {
		return "", fmt.Errorf("unable to query from IMDSv1, request url: %s, error: %s", requestUrl, err)
	}
	defer response.Body.Close()
	if responseBody, err := ioutil.ReadAll(response.Body); err != nil {
		return "", fmt.Errorf("unable to read EC2 instance metadata for query %s: %v", query, err)
	} else {
		return string(responseBody), nil
	}
}

func RunCommand(name string, args ...string) (string, error) {
	var out bytes.Buffer
	cmd := exec.Command(name, args...)
	cmd.Stdout = &out
	err := cmd.Run()
	return strings.TrimSuffix(out.String(), "\n"), err
}