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

import (
	"encoding/json"

	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/taskresource"
	"github.com/aws/amazon-ecs-agent/agent/taskresource/fsxwindowsfileserver"
	taskresourcetypes "github.com/aws/amazon-ecs-agent/agent/taskresource/types"
	taskresourcevolume "github.com/aws/amazon-ecs-agent/agent/taskresource/volume"
	apiresource "github.com/aws/amazon-ecs-agent/ecs-agent/api/attachment/resource"

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

const (
	HostVolumeType                 = "host"
	DockerVolumeType               = "docker"
	EFSVolumeType                  = "efs"
	FSxWindowsFileServerVolumeType = "fsxWindowsFileServer"
	AttachmentType                 = "attachment"
)

// TaskVolume is a definition of all the volumes available for containers to
// reference within a task. It must be named.
type TaskVolume struct {
	Type   string `json:"type"`
	Name   string `json:"name"`
	Volume taskresourcevolume.Volume
}

// UnmarshalJSON for TaskVolume determines the name and volume type, and
// unmarshals it into the appropriate HostVolume fulfilling interfaces
func (tv *TaskVolume) UnmarshalJSON(b []byte) error {
	// Format: {name: volumeName, host: HostVolume, dockerVolumeConfiguration {}}
	intermediate := make(map[string]json.RawMessage)
	if err := json.Unmarshal(b, &intermediate); err != nil {
		return err
	}
	name, ok := intermediate["name"]
	if !ok {
		return errors.New("invalid Volume; must include a name")
	}
	if err := json.Unmarshal(name, &tv.Name); err != nil {
		return err
	}

	volumeType, ok := intermediate["type"]
	if !ok {
		volumeType = []byte(`"host"`)
		seelog.Infof("Unmarshal task volume: volume type not specified, default to host")
	}
	if err := json.Unmarshal(volumeType, &tv.Type); err != nil {
		return err
	}

	switch tv.Type {
	case HostVolumeType:
		return tv.unmarshalHostVolume(intermediate["host"])
	case DockerVolumeType:
		return tv.unmarshalDockerVolume(intermediate["dockerVolumeConfiguration"])
	case EFSVolumeType:
		return tv.unmarshalEFSVolume(intermediate["efsVolumeConfiguration"])
	case FSxWindowsFileServerVolumeType:
		return tv.unmarshalFSxWindowsFileServerVolume(intermediate["fsxWindowsFileServerVolumeConfiguration"])
	case apiresource.EBSTaskAttach:
		return tv.unmarshalEBSVolume(intermediate["ebsVolumeConfiguration"])
	case AttachmentType:
		seelog.Warn("Obtaining the volume configuration from task attachments.")
		return nil
	default:
		return errors.Errorf("unrecognized volume type: %q", tv.Type)
	}
}

// MarshalJSON overrides the logic for JSON-encoding a TaskVolume object
func (tv *TaskVolume) MarshalJSON() ([]byte, error) {
	result := make(map[string]interface{})

	if len(tv.Type) == 0 {
		tv.Type = HostVolumeType
	}

	result["name"] = tv.Name
	result["type"] = tv.Type

	switch tv.Type {
	case DockerVolumeType:
		result["dockerVolumeConfiguration"] = tv.Volume
	case HostVolumeType:
		result["host"] = tv.Volume
	case EFSVolumeType:
		result["efsVolumeConfiguration"] = tv.Volume
	case FSxWindowsFileServerVolumeType:
		result["fsxWindowsFileServerVolumeConfiguration"] = tv.Volume
	case apiresource.EBSTaskAttach:
		result["ebsVolumeConfiguration"] = tv.Volume
	default:
		return nil, errors.Errorf("unrecognized volume type: %q", tv.Type)
	}

	return json.Marshal(result)
}

func (tv *TaskVolume) unmarshalDockerVolume(data json.RawMessage) error {
	if data == nil {
		return errors.New("invalid volume: empty volume configuration")
	}
	var dockerVolumeConfig taskresourcevolume.DockerVolumeConfig
	err := json.Unmarshal(data, &dockerVolumeConfig)
	if err != nil {
		return err
	}

	tv.Volume = &dockerVolumeConfig
	return nil
}

func (tv *TaskVolume) unmarshalEFSVolume(data json.RawMessage) error {
	if data == nil {
		return errors.New("invalid volume: empty volume configuration")
	}
	var efsVolumeConfig taskresourcevolume.EFSVolumeConfig
	err := json.Unmarshal(data, &efsVolumeConfig)
	if err != nil {
		return err
	}

	tv.Volume = &efsVolumeConfig
	return nil
}

func (tv *TaskVolume) unmarshalFSxWindowsFileServerVolume(data json.RawMessage) error {
	if data == nil {
		return errors.New("invalid volume: empty volume configuration")
	}
	var fsxWindowsFileServerVolumeConfig fsxwindowsfileserver.FSxWindowsFileServerVolumeConfig
	err := json.Unmarshal(data, &fsxWindowsFileServerVolumeConfig)
	if err != nil {
		return err
	}

	tv.Volume = &fsxWindowsFileServerVolumeConfig
	return nil
}

func (tv *TaskVolume) unmarshalHostVolume(data json.RawMessage) error {
	if data == nil {
		return errors.New("invalid volume: empty volume configuration")
	}

	// Default to trying to unmarshal it as a FSHostVolume
	var hostvolume taskresourcevolume.FSHostVolume
	err := json.Unmarshal(data, &hostvolume)
	if err != nil {
		return err
	}
	if hostvolume.FSSourcePath == "" {
		// If the FSSourcePath is empty, that must mean it was not an
		// FSHostVolume (empty path is invalid for that type).
		// Unmarshal it as local docker volume.
		localVolume := &taskresourcevolume.LocalDockerVolume{}
		json.Unmarshal(data, localVolume)
		tv.Volume = localVolume
	} else {
		tv.Volume = &hostvolume
	}
	return nil
}

func (tv *TaskVolume) unmarshalEBSVolume(data json.RawMessage) error {
	if data == nil {
		return errors.New("invalid volume: empty volume configuration")
	}
	var ebsVoumeConfig taskresourcevolume.EBSTaskVolumeConfig
	err := json.Unmarshal(data, &ebsVoumeConfig)
	if err != nil {
		return err
	}

	tv.Volume = &ebsVoumeConfig
	return nil
}

// getEFSVolumeDriverName returns the driver name for creating the EFS volume.
func getEFSVolumeDriverName(cfg *config.Config) string {
	if taskresourcevolume.UseECSVolumePlugin(cfg) {
		return taskresourcevolume.ECSVolumePlugin
	}
	return taskresourcevolume.DockerLocalDriverName
}

// getDockerVolumeResource retrieves docker volume resource from task resource map.
func (task *Task) getDockerVolumeResource() ([]taskresource.TaskResource, bool) {
	task.lock.RLock()
	defer task.lock.RUnlock()

	res, ok := task.ResourcesMapUnsafe[taskresourcetypes.DockerVolumeKey]
	return res, ok
}

// SetPausePIDInVolumeResources sets the pause container pid field in each volume resource.
func (task *Task) SetPausePIDInVolumeResources(pid string) {
	resources, ok := task.getDockerVolumeResource()
	if !ok {
		return
	}

	for _, res := range resources {
		volRes := res.(*taskresourcevolume.VolumeResource)
		volRes.SetPauseContainerPID(pid)
	}
}
