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

import (
	apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container"
	apitask "github.com/aws/amazon-ecs-agent/agent/api/task"
	"github.com/aws/amazon-ecs-agent/agent/data"
	"github.com/aws/amazon-ecs-agent/agent/engine/dockerstate"
	"github.com/aws/amazon-ecs-agent/agent/engine/image"
	"github.com/aws/amazon-ecs-agent/agent/utils"
	apicontainerstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status"

	"github.com/cihub/seelog"
	"github.com/pkg/errors"
)

// LoadState populates the task engine state with data in db.
func (engine *DockerTaskEngine) LoadState() error {
	if err := engine.loadTasks(); err != nil {
		return err
	}

	if err := engine.loadContainers(); err != nil {
		return err
	}

	if err := engine.loadImageStates(); err != nil {
		return err
	}

	if err := engine.loadResourceAttachments(); err != nil {
		return err
	}

	return engine.loadENIAttachments()
}

func (engine *DockerTaskEngine) loadTasks() error {
	tasks, err := engine.dataClient.GetTasks()
	if err != nil {
		return err
	}

	for _, task := range tasks {
		engine.state.AddTask(task)

		// Register the task role's ID in credentials manager as soon as possible.
		// This ensures TMDS can distinguish between invalid credentials requests (400) and
		// known credentials that aren't available yet (503) after agent restart.
		if credentialsID := task.GetCredentialsID(); credentialsID != "" {
			engine.credentialsManager.AddKnownCredentialsID(credentialsID)
		}

		// Also register execution role credentials ID for the same reason as above
		if executionCredentialsID := task.GetExecutionCredentialsID(); executionCredentialsID != "" {
			engine.credentialsManager.AddKnownCredentialsID(executionCredentialsID)
		}

		// TODO: Will need to clean up all of the STOPPED managed daemon tasks
		md, ok := task.IsManagedDaemonTask()
		if ok {
			engine.SetDaemonTask(md, task)
		}

		// Populate ip <-> task mapping if task has a local ip. This mapping is needed for serving v2 task metadata.
		if ip := task.GetLocalIPAddress(); ip != "" {
			engine.state.AddTaskIPAddress(ip, task.Arn)
		}
	}
	return nil
}

func (engine *DockerTaskEngine) loadContainers() error {
	containers, err := engine.dataClient.GetContainers()
	if err != nil {
		return err
	}

	for _, container := range containers {
		task, ok := engine.state.TaskByArn(container.Container.GetTaskARN())
		if !ok {
			// A task is saved to task table before its containers saved to container table. It is not expected
			// that we have a container from container table whose task is not in the task table.
			return errors.Errorf("did not find the task of container %s: %s", container.Container.Name,
				container.Container.GetTaskARN())
		}
		if container.Container.GetKnownStatus() == apicontainerstatus.ContainerPulled {
			engine.state.AddPulledContainer(container, task)
		} else {
			engine.state.AddContainer(container, task)
		}
	}

	// Update containers in task from data in container table. It stores more updated data of container
	// than the task table.
	tasks := engine.state.AllTasks()
	for _, task := range tasks {
		// A task's container map contains containers loaded from container table. It is ok
		// that there isn't any container in the map, because we first save a container to container table
		// when finished a transition on it, and before that the container is just stored as part of the task.
		dockerContainers, _ := engine.state.ContainerMapByArn(task.Arn)
		pulledContainers, _ := engine.state.PulledContainerMapByArn(task.Arn)

		for idx, container := range task.Containers {
			for _, pulledContainer := range pulledContainers {
				if container.Name == pulledContainer.Container.Name {
					task.Containers[idx] = pulledContainer.Container
				}
			}
			for _, dockerContainer := range dockerContainers {
				if container.Name == dockerContainer.Container.Name {
					task.Containers[idx] = dockerContainer.Container
				}
			}
		}
	}
	return nil
}

func (engine *DockerTaskEngine) loadImageStates() error {
	images, err := engine.dataClient.GetImageStates()
	if err != nil {
		return err
	}

	for _, image := range images {
		engine.state.AddImageState(image)
	}
	return nil
}

func (engine *DockerTaskEngine) loadResourceAttachments() error {
	resAttachments, err := engine.dataClient.GetResourceAttachments()
	if err != nil {
		return err
	}
	for _, resAttachment := range resAttachments {
		engine.state.AddEBSAttachment(resAttachment)
	}
	return nil
}

func (engine *DockerTaskEngine) loadENIAttachments() error {
	eniAttachments, err := engine.dataClient.GetENIAttachments()
	if err != nil {
		return err
	}

	for _, eniAttachment := range eniAttachments {
		engine.state.AddENIAttachment(eniAttachment)
	}
	return nil
}

// SaveState saves all the data in task engine state to db.
func (engine *DockerTaskEngine) SaveState() error {
	state := engine.state
	ensureCompatibleWithBoltDB(state)

	for _, task := range state.AllTasks() {
		if err := engine.dataClient.SaveTask(task); err != nil {
			return err
		}
	}

	for _, containerID := range state.GetAllContainerIDs() {
		container, ok := state.ContainerByID(containerID)
		if !ok {
			return errors.Errorf("container not found: %s", containerID)
		}
		if err := engine.dataClient.SaveDockerContainer(container); err != nil {
			return err
		}
	}

	for _, imageState := range state.AllImageStates() {
		if err := engine.dataClient.SaveImageState(imageState); err != nil {
			return err
		}
	}

	for _, eniAttachment := range state.AllENIAttachments() {
		if err := engine.dataClient.SaveENIAttachment(eniAttachment); err != nil {
			return err
		}
	}
	return nil
}

// ensureCompatibleWithBoltDB ensures that the data in the state is compatible with what we need
// in order to save into boltdb. Specifically, we need: (1) container to have its taskARN field populated;
// (2) if task has a local ip address, it's stored in the localIPAddressUnsafe field.
// Task launched with new agent will have those fields populated already, but task loaded
// from state file generated by old agent will not, which is why this method is needed.
func ensureCompatibleWithBoltDB(state dockerstate.TaskEngineState) {
	tasks := state.AllTasks()
	for _, t := range tasks {
		for _, c := range t.Containers {
			c.SetTaskARN(t.Arn)
		}

		addr, ok := state.GetIPAddressByTaskARN(t.Arn)
		if ok {
			t.SetLocalIPAddress(addr)
		}
	}
}

func (engine *DockerTaskEngine) saveTaskData(task *apitask.Task) {
	err := engine.dataClient.SaveTask(task)
	if err != nil {
		seelog.Errorf("Failed to save data for task %s: %v", task.Arn, err)
	}
}

func (engine *DockerTaskEngine) saveContainerData(container *apicontainer.Container) {
	err := engine.dataClient.SaveContainer(container)
	if err != nil {
		seelog.Errorf("Failed to save data for container %s: %v", container.Name, err)
	}
}

func (engine *DockerTaskEngine) saveDockerContainerData(container *apicontainer.DockerContainer) {
	err := engine.dataClient.SaveDockerContainer(container)
	if err != nil {
		seelog.Errorf("Failed to save data for docker container %s: %v", container.Container.Name, err)
	}
}

func (engine *DockerTaskEngine) removeTaskData(task *apitask.Task) {
	for _, c := range task.Containers {
		id, err := data.GetContainerID(c)
		if err != nil {
			seelog.Errorf("Failed to get container id from container %s: %v", c.Name, err)
			continue
		}
		err = engine.dataClient.DeleteContainer(id)
		if err != nil {
			seelog.Errorf("Failed to remove data for container %s: %v", c.Name, err)
		}
	}

	id, err := utils.GetTaskID(task.Arn)
	if err != nil {
		seelog.Errorf("Failed to get task id from task ARN %s: %v", task.Arn, err)
		return
	}
	err = engine.dataClient.DeleteTask(id)
	if err != nil {
		seelog.Errorf("Failed to remove data for task %s: %v", task.Arn, err)
	}
}

func (engine *DockerTaskEngine) removeENIAttachmentData(mac string) {
	attachmentToRemove, ok := engine.state.ENIByMac(mac)
	if !ok {
		seelog.Warnf("Unable to retrieve ENI Attachment for mac address %s: ", mac)
		return
	}
	attachmentId, err := utils.GetAttachmentId(attachmentToRemove.AttachmentARN)
	if err != nil {
		seelog.Errorf("Failed to get attachment id for %s: %v", attachmentToRemove.AttachmentARN, err)
	} else {
		err = engine.dataClient.DeleteENIAttachment(attachmentId)
		if err != nil {
			seelog.Errorf("Failed to remove data for eni attachment %s: %v", attachmentId, err)
		}
	}
}

func (imageManager *dockerImageManager) saveImageStateData(imageState *image.ImageState) {
	err := imageManager.dataClient.SaveImageState(imageState)
	if err != nil {
		seelog.Errorf("Failed to save data for image state %s:, %v", imageState.GetImageID(), err)
	}
}

func (imageManager *dockerImageManager) removeImageStateData(imageId string) {
	err := imageManager.dataClient.DeleteImageState(imageId)
	if err != nil {
		seelog.Errorf("Failed to remove data for image state %s:, %v", imageId, err)
	}
}
