// 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 containermetadata

import (
	"context"
	"encoding/json"
	"fmt"
	"os"

	apitask "github.com/aws/amazon-ecs-agent/agent/api/task"
	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/dockerclient"
	dockercontainer "github.com/docker/docker/api/types/container"
)

const (
	// metadataEnvironmentVariable is the environment variable passed to the
	// container for the metadata file path.
	metadataEnvironmentVariable = "ECS_CONTAINER_METADATA_FILE"
	metadataFile                = "ecs-container-metadata.json"
	metadataPerm                = 0644
)

// Manager is an interface that allows us to abstract away the metadata
// operations
type Manager interface {
	SetContainerInstanceARN(string)
	SetAvailabilityZone(string)
	SetHostPrivateIPv4Address(string)
	SetHostPublicIPv4Address(string)
	SetHostIPv6Address(string)
	Create(*dockercontainer.Config, *dockercontainer.HostConfig, *apitask.Task, string, []string) error
	Update(context.Context, string, *apitask.Task, string) error
	Clean(string) error
}

// metadataManager implements the Manager interface
type metadataManager struct {
	// client is the Docker API Client that the metadata manager uses. It defaults
	// to 1.21 on Linux and 1.24 on Windows
	client DockerMetadataClient
	// cluster is the cluster where this agent is run
	cluster string
	// dataDir is the directory where the metadata is being written. For Linux
	// this is a container directory
	dataDir string
	// dataDirOnHost is the directory from which dataDir is mounted for Linux
	// version of the agent
	dataDirOnHost string
	// containerInstanceARN is the Container Instance ARN registered for this agent
	containerInstanceARN string
	// availabilityZone is the availabiltyZone where task is in
	availabilityZone string
	// hostPrivateIPv4Address is the private IPv4 address associated with the EC2 instance
	hostPrivateIPv4Address string
	// hostPublicIPv4Address is the public IPv4 address associated with the EC2 instance
	hostPublicIPv4Address string
	// hostIPv6Address is the IPv6 address associated with the EC2 instance
	hostIPv6Address string
}

// NewManager creates a metadataManager for a given DockerTaskEngine settings.
func NewManager(client DockerMetadataClient, cfg *config.Config) Manager {
	return &metadataManager{
		client:        client,
		cluster:       cfg.Cluster,
		dataDir:       cfg.DataDir,
		dataDirOnHost: cfg.DataDirOnHost,
	}
}

// SetContainerInstanceARN sets the metadataManager's ContainerInstanceArn which is not available
// at its creation as this information is not present immediately at the agent's startup
func (manager *metadataManager) SetContainerInstanceARN(containerInstanceARN string) {
	manager.containerInstanceARN = containerInstanceARN
}

// SetAvailabilityzone sets the metadataManager's AvailabilityZone which is not available
// at its creation as this information is not present immediately at the agent's startup
func (manager *metadataManager) SetAvailabilityZone(availabilityZone string) {
	manager.availabilityZone = availabilityZone
}

// SetHostPrivateIPv4Address sets the metadataManager's hostPrivateIPv4Address which is not available
// at its creation as this information is not present immediately at the agent's startup
func (manager *metadataManager) SetHostPrivateIPv4Address(ipv4address string) {
	manager.hostPrivateIPv4Address = ipv4address
}

// SetHostPublicIPv4Address sets the metadataManager's hostPublicIPv4Address which is not available
// at its creation as this information is not present immediately at the agent's startup
func (manager *metadataManager) SetHostPublicIPv4Address(ipv4address string) {
	manager.hostPublicIPv4Address = ipv4address
}

// SetHostIPv6Address sets the metadataManager's hostIPv6Address which is not available
// at its creation as this information is not present immediately at the agent's startup
func (manager *metadataManager) SetHostIPv6Address(ipv6address string) {
	manager.hostIPv6Address = ipv6address
}

var mkdirAll = os.MkdirAll

// Create creates the metadata file and adds the metadata directory to
// the container's mounted host volumes
// Pointer hostConfig is modified directly so there is risk of concurrency errors.
func (manager *metadataManager) Create(config *dockercontainer.Config, hostConfig *dockercontainer.HostConfig,
	task *apitask.Task, containerName string, dockerSecurityOptions []string) error {
	// Create task and container directories if they do not yet exist
	metadataDirectoryPath, err := getMetadataFilePath(task.Arn, containerName, manager.dataDir)
	// Stop metadata creation if path is malformed for any reason
	if err != nil {
		return fmt.Errorf("container metadata create for task %s container %s: %v", task.Arn, containerName, err)
	}

	err = mkdirAll(metadataDirectoryPath, os.ModePerm)
	if err != nil {
		return fmt.Errorf("creating metadata directory for task %s: %v", task.Arn, err)
	}

	// Acquire the metadata then write it in JSON format to the file
	metadata := manager.parseMetadataAtContainerCreate(task, containerName)
	err = manager.marshalAndWrite(metadata, task.Arn, containerName)
	if err != nil {
		return err
	}

	// Add the directory of this container's metadata to the container's mount binds
	// Then add the destination directory as an environment variable in the container $METADATA
	binds, env := createBindsEnv(hostConfig.Binds, config.Env, manager.dataDirOnHost, metadataDirectoryPath, dockerSecurityOptions)
	config.Env = env
	hostConfig.Binds = binds
	return nil
}

// Update updates the metadata file after container starts and dynamic metadata is available
func (manager *metadataManager) Update(ctx context.Context, dockerID string, task *apitask.Task, containerName string) error {
	// Get docker container information through api call
	dockerContainer, err := manager.client.InspectContainer(ctx, dockerID, dockerclient.InspectContainerTimeout)
	if err != nil {
		return err
	}

	// Ensure we do not update a container that is invalid or is not running
	if dockerContainer == nil || !dockerContainer.State.Running {
		return fmt.Errorf("container metadata update for container %s in task %s: container not running or invalid", containerName, task.Arn)
	}

	// Acquire the metadata then write it in JSON format to the file
	metadata := manager.parseMetadata(dockerContainer, task, containerName)
	return manager.marshalAndWrite(metadata, task.Arn, containerName)
}

var removeAll = os.RemoveAll

// Clean removes the metadata files of all containers associated with a task
func (manager *metadataManager) Clean(taskARN string) error {
	metadataPath, err := getTaskMetadataDir(taskARN, manager.dataDir)
	if err != nil {
		return fmt.Errorf("clean task metadata: unable to get metadata directory for task %s: %v", taskARN, err)
	}
	return removeAll(metadataPath)
}

func (manager *metadataManager) marshalAndWrite(metadata Metadata, taskARN string, containerName string) error {
	data, err := json.MarshalIndent(metadata, "", "\t")
	if err != nil {
		return fmt.Errorf("create metadata for container %s in task %s: failed to marshal metadata: %v", containerName, taskARN, err)
	}

	// Write the metadata to file
	return writeToMetadataFile(data, taskARN, containerName, manager.dataDir)
}
