//go:build linux
// +build linux

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

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"sync"
	"time"

	apicontainer "github.com/aws/amazon-ecs-agent/agent/api/container"
	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/taskresource"
	"github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control"
	resourcestatus "github.com/aws/amazon-ecs-agent/agent/taskresource/status"
	"github.com/aws/amazon-ecs-agent/agent/utils/ioutilwrapper"
	apicontainerstatus "github.com/aws/amazon-ecs-agent/ecs-agent/api/container/status"
	"github.com/aws/amazon-ecs-agent/ecs-agent/api/task/status"
	"github.com/cihub/seelog"
	cgroups "github.com/containerd/cgroups/v3/cgroup1"
	"github.com/opencontainers/runtime-spec/specs-go"
)

const (
	memorySubsystem           = "/memory"
	memoryUseHierarchy        = "memory.use_hierarchy"
	rootReadOnlyPermissions   = os.FileMode(400)
	resourceName              = "cgroup"
	resourceProvisioningError = "CgroupError: Agent could not create task's platform resources"
)

var (
	enableMemoryHierarchy = []byte(strconv.Itoa(1))
)

// CgroupResource represents Cgroup resource
type CgroupResource struct {
	taskARN             string
	control             control.Control
	cgroupRoot          string
	cgroupMountPath     string
	resourceSpec        specs.LinuxResources
	ioutil              ioutilwrapper.IOUtil
	createdAt           time.Time
	desiredStatusUnsafe resourcestatus.ResourceStatus
	knownStatusUnsafe   resourcestatus.ResourceStatus
	// appliedStatus is the status that has been "applied" (e.g., we've called some
	// operation such as 'Create' on the resource) but we don't yet know that the
	// application was successful, which may then change the known status. This is
	// used while progressing resource states in progressTask() of task manager
	appliedStatus       resourcestatus.ResourceStatus
	statusToTransitions map[resourcestatus.ResourceStatus]func() error
	// lock is used for fields that are accessed and updated concurrently
	lock sync.RWMutex
}

// NewCgroupResource is used to return an object that implements the Resource interface
func NewCgroupResource(
	taskARN string,
	control control.Control,
	ioutil ioutilwrapper.IOUtil,
	cgroupRoot string,
	cgroupMountPath string,
	resourceSpec specs.LinuxResources,
) *CgroupResource {
	c := &CgroupResource{
		taskARN:         taskARN,
		control:         control,
		ioutil:          ioutil,
		cgroupRoot:      cgroupRoot,
		cgroupMountPath: cgroupMountPath,
		resourceSpec:    resourceSpec,
	}
	c.initializeResourceStatusToTransitionFunction()
	return c
}

// GetTerminalReason returns an error string to propagate up through to task
// state change messages
func (cgroup *CgroupResource) GetTerminalReason() string {
	// for cgroups we can send up a static string because this is an
	// implementation detail and unrelated to customer resources
	return resourceProvisioningError
}

func (cgroup *CgroupResource) initializeResourceStatusToTransitionFunction() {
	resourceStatusToTransitionFunction := map[resourcestatus.ResourceStatus]func() error{
		resourcestatus.ResourceStatus(CgroupCreated): cgroup.Create,
	}
	cgroup.statusToTransitions = resourceStatusToTransitionFunction
}

func (cgroup *CgroupResource) SetIOUtil(ioutil ioutilwrapper.IOUtil) {
	cgroup.ioutil = ioutil
}

// SetDesiredStatus safely sets the desired status of the resource
func (cgroup *CgroupResource) SetDesiredStatus(status resourcestatus.ResourceStatus) {
	cgroup.lock.Lock()
	defer cgroup.lock.Unlock()

	cgroup.desiredStatusUnsafe = status
}

// GetDesiredStatus safely returns the desired status of the task
func (cgroup *CgroupResource) GetDesiredStatus() resourcestatus.ResourceStatus {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()

	return cgroup.desiredStatusUnsafe
}

// GetName safely returns the name of the resource
func (cgroup *CgroupResource) GetName() string {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()

	return resourceName
}

// DesiredTerminal returns true if the cgroup's desired status is REMOVED
func (cgroup *CgroupResource) DesiredTerminal() bool {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()

	return cgroup.desiredStatusUnsafe == resourcestatus.ResourceStatus(CgroupRemoved)
}

// KnownCreated returns true if the cgroup's known status is CREATED
func (cgroup *CgroupResource) KnownCreated() bool {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()

	return cgroup.knownStatusUnsafe == resourcestatus.ResourceStatus(CgroupCreated)
}

// TerminalStatus returns the last transition state of cgroup
func (cgroup *CgroupResource) TerminalStatus() resourcestatus.ResourceStatus {
	return resourcestatus.ResourceStatus(CgroupRemoved)
}

// NextKnownState returns the state that the resource should
// progress to based on its `KnownState`.
func (cgroup *CgroupResource) NextKnownState() resourcestatus.ResourceStatus {
	return cgroup.GetKnownStatus() + 1
}

// ApplyTransition calls the function required to move to the specified status
func (cgroup *CgroupResource) ApplyTransition(nextState resourcestatus.ResourceStatus) error {
	transitionFunc, ok := cgroup.statusToTransitions[nextState]
	if !ok {
		seelog.Errorf("Cgroup Resource [%s]: unsupported desired state transition [%s]: %s",
			cgroup.taskARN, cgroup.GetName(), cgroup.StatusString(nextState))
		return fmt.Errorf("resource [%s]: transition to %s impossible", cgroup.GetName(),
			cgroup.StatusString(nextState))
	}
	return transitionFunc()
}

// SteadyState returns the transition state of the resource defined as "ready"
func (cgroup *CgroupResource) SteadyState() resourcestatus.ResourceStatus {
	return resourcestatus.ResourceStatus(CgroupCreated)
}

// SetKnownStatus safely sets the currently known status of the resource
func (cgroup *CgroupResource) SetKnownStatus(status resourcestatus.ResourceStatus) {
	cgroup.lock.Lock()
	defer cgroup.lock.Unlock()

	cgroup.knownStatusUnsafe = status
	cgroup.updateAppliedStatusUnsafe(status)
}

// updateAppliedStatusUnsafe updates the resource transitioning status
func (cgroup *CgroupResource) updateAppliedStatusUnsafe(knownStatus resourcestatus.ResourceStatus) {
	if cgroup.appliedStatus == resourcestatus.ResourceStatus(CgroupStatusNone) {
		return
	}

	// Check if the resource transition has already finished
	if cgroup.appliedStatus <= knownStatus {
		cgroup.appliedStatus = resourcestatus.ResourceStatus(CgroupStatusNone)
	}
}

// SetAppliedStatus sets the applied status of resource and returns whether
// the resource is already in a transition
func (cgroup *CgroupResource) SetAppliedStatus(status resourcestatus.ResourceStatus) bool {
	cgroup.lock.Lock()
	defer cgroup.lock.Unlock()

	if cgroup.appliedStatus != resourcestatus.ResourceStatus(CgroupStatusNone) {
		// return false to indicate the set operation failed
		return false
	}

	cgroup.appliedStatus = status
	return true
}

// GetAppliedStatus safely returns the currently applied status of the resource
func (cgroup *CgroupResource) GetAppliedStatus() resourcestatus.ResourceStatus {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()

	return cgroup.appliedStatus
}

// GetKnownStatus safely returns the currently known status of the task
func (cgroup *CgroupResource) GetKnownStatus() resourcestatus.ResourceStatus {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()

	return cgroup.knownStatusUnsafe
}

// StatusString returns the string of the cgroup resource status
func (cgroup *CgroupResource) StatusString(status resourcestatus.ResourceStatus) string {
	return CgroupStatus(status).String()
}

// SetCreatedAt sets the timestamp for resource's creation time
func (cgroup *CgroupResource) SetCreatedAt(createdAt time.Time) {
	if createdAt.IsZero() {
		return
	}
	cgroup.lock.Lock()
	defer cgroup.lock.Unlock()

	cgroup.createdAt = createdAt
}

// GetCreatedAt sets the timestamp for resource's creation time
func (cgroup *CgroupResource) GetCreatedAt() time.Time {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()

	return cgroup.createdAt
}

// Create creates cgroup root for the task
func (cgroup *CgroupResource) Create() error {
	err := cgroup.setupTaskCgroup()
	if err != nil {
		// this error is already formatted in setupTaskCgroup function
		return err
	}
	return nil
}

func (cgroup *CgroupResource) setupTaskCgroup() error {
	cgroupRoot := cgroup.cgroupRoot

	if cgroup.control.Exists(cgroupRoot) {
		seelog.Debugf("Cgroup already exists, skipping creation taskARN=%s cgroupPath=%s cgroupV2=%v", cgroup.taskARN, cgroupRoot, config.CgroupV2)
		return nil
	}

	cgroupSpec := control.Spec{
		Root:  cgroupRoot,
		Specs: &cgroup.resourceSpec,
	}

	seelog.Infof("Creating task cgroup taskARN=%s cgroupPath=%s cgroupV2=%v", cgroup.taskARN, cgroupRoot, config.CgroupV2)
	err := cgroup.control.Create(&cgroupSpec)
	if err != nil {
		return fmt.Errorf("cgroup resource: setup cgroup: unable to create cgroup taskARN=%s cgroupPath=%s cgroupV2=%v err=%s", cgroup.taskARN, cgroupRoot, config.CgroupV2, err)
	}

	if !config.CgroupV2 {
		// enabling cgroup memory hierarchy by doing 'echo 1 > memory.use_hierarchy'
		memoryHierarchyPath := filepath.Join(cgroup.cgroupMountPath, memorySubsystem, cgroupRoot, memoryUseHierarchy)
		err = cgroup.ioutil.WriteFile(memoryHierarchyPath, enableMemoryHierarchy, rootReadOnlyPermissions)
		if err != nil {
			return fmt.Errorf("cgroup resource: setup cgroup: unable to set use hierarchy flag taskARN=%s cgroupPath=%s cgroupV2=%v err=%s", cgroup.taskARN, cgroupRoot, config.CgroupV2, err)
		}
	}

	return nil
}

// Cleanup removes the cgroup root created for the task
func (cgroup *CgroupResource) Cleanup() error {
	err := cgroup.control.Remove(cgroup.cgroupRoot)
	// Explicitly handle cgroup deleted error
	if err != nil {
		if errors.Is(err, cgroups.ErrCgroupDeleted) {
			seelog.Warnf("Cgroup at %s has already been removed: %v", cgroup.cgroupRoot, err)
			return nil
		}
		return fmt.Errorf("resource: cleanup cgroup: unable to remove cgroup at %s: %w", cgroup.cgroupRoot, err)
	}
	return nil
}

// cgroupResourceJSON duplicates CgroupResource fields, only for marshalling and unmarshalling purposes
type cgroupResourceJSON struct {
	CgroupRoot      string               `json:"cgroupRoot"`
	CgroupMountPath string               `json:"cgroupMountPath"`
	CreatedAt       time.Time            `json:"createdAt,omitempty"`
	DesiredStatus   *CgroupStatus        `json:"desiredStatus"`
	KnownStatus     *CgroupStatus        `json:"knownStatus"`
	LinuxSpec       specs.LinuxResources `json:"resourceSpec"`
}

// MarshalJSON marshals CgroupResource object using duplicate struct CgroupResourceJSON
func (cgroup *CgroupResource) MarshalJSON() ([]byte, error) {
	if cgroup == nil {
		return nil, errors.New("cgroup resource is nil")
	}
	return json.Marshal(cgroupResourceJSON{
		cgroup.cgroupRoot,
		cgroup.cgroupMountPath,
		cgroup.GetCreatedAt(),
		func() *CgroupStatus {
			desiredState := cgroup.GetDesiredStatus()
			status := CgroupStatus(desiredState)
			return &status
		}(),
		func() *CgroupStatus {
			knownState := cgroup.GetKnownStatus()
			status := CgroupStatus(knownState)
			return &status
		}(),
		cgroup.resourceSpec,
	})
}

// UnmarshalJSON unmarshals CgroupResource object using duplicate struct CgroupResourceJSON
func (cgroup *CgroupResource) UnmarshalJSON(b []byte) error {
	temp := cgroupResourceJSON{}

	if err := json.Unmarshal(b, &temp); err != nil {
		return err
	}

	cgroup.cgroupRoot = temp.CgroupRoot
	cgroup.cgroupMountPath = temp.CgroupMountPath
	cgroup.resourceSpec = temp.LinuxSpec
	if temp.DesiredStatus != nil {
		cgroup.SetDesiredStatus(resourcestatus.ResourceStatus(*temp.DesiredStatus))
	}
	if temp.KnownStatus != nil {
		cgroup.SetKnownStatus(resourcestatus.ResourceStatus(*temp.KnownStatus))
	}
	return nil
}

// GetCgroupRoot returns cgroup root of the resource
func (cgroup *CgroupResource) GetCgroupRoot() string {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()
	return cgroup.cgroupRoot
}

// GetCgroupMountPath returns cgroup mount path of the resource
func (cgroup *CgroupResource) GetCgroupMountPath() string {
	cgroup.lock.RLock()
	defer cgroup.lock.RUnlock()
	return cgroup.cgroupMountPath
}

// Initialize initializes the resource fileds in cgroup
func (cgroup *CgroupResource) Initialize(
	config *config.Config,
	resourceFields *taskresource.ResourceFields,
	taskKnownStatus status.TaskStatus,
	taskDesiredStatus status.TaskStatus) {
	cgroup.lock.Lock()
	defer cgroup.lock.Unlock()

	cgroup.initializeResourceStatusToTransitionFunction()
	cgroup.ioutil = resourceFields.IOUtil
	cgroup.control = resourceFields.Control
}

func (cgroup *CgroupResource) DependOnTaskNetwork() bool {
	return false
}

func (cgroup *CgroupResource) BuildContainerDependency(containerName string, satisfied apicontainerstatus.ContainerStatus,
	dependent resourcestatus.ResourceStatus) {
}

func (cgroup *CgroupResource) GetContainerDependencies(dependent resourcestatus.ResourceStatus) []apicontainer.ContainerDependency {
	return nil
}
