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

import (
	"fmt"

	"github.com/aws/amazon-ecs-agent/agent/config"
	"github.com/aws/amazon-ecs-agent/agent/taskresource/cgroup/control/factory"
	"github.com/opencontainers/runtime-spec/specs-go"

	"github.com/cihub/seelog"
	cgroups "github.com/containerd/cgroups/v3/cgroup1"
	"github.com/pkg/errors"
)

// control is used to implement the cgroup Control interface
type control struct {
	factory.CgroupFactory
}

// New is used to obtain a new cgroup control object
func New() Control {
	if config.CgroupV2 {
		return &controlv2{}
	}
	return newControl(&factory.GlobalCgroupFactory{})
}

// newControl helps setup the cgroup controller
func newControl(cgroupFact factory.CgroupFactory) Control {
	return &control{
		cgroupFact,
	}
}

// Create creates a new cgroup based off the spec post validation
func (c *control) Create(cgroupSpec *Spec) error {
	// Validate incoming spec
	err := validateCgroupSpec(cgroupSpec)
	if err != nil {
		return fmt.Errorf("cgroup create: failed to validate spec: %w", err)
	}

	seelog.Debugf("Creating cgroup cgroupPath=%s", cgroupSpec.Root)
	_, err = c.New(cgroups.Default, cgroups.StaticPath(cgroupSpec.Root), cgroupSpec.Specs)
	if err != nil {
		return fmt.Errorf("cgroup create: unable to create controller: v1: %s", err)
	}

	return nil
}

// Remove is used to delete the cgroup
func (c *control) Remove(cgroupPath string) error {
	seelog.Debugf("Removing cgroup cgroupPath=%s", cgroupPath)

	controller, err := c.Load(cgroups.Default, cgroups.StaticPath(cgroupPath))
	if err != nil {
		// use the %w verb to wrap the error to be unwrapped by errors.Is()
		return fmt.Errorf("cgroup remove: unable to obtain controller: %w", err)
	}

	// Delete cgroup
	err = controller.Delete()
	if err != nil {
		return fmt.Errorf("cgroup remove: unable to delete cgroup: %w", err)
	}
	return nil
}

// Exists is used to verify the existence of a cgroup
func (c *control) Exists(cgroupPath string) bool {
	seelog.Debugf("Checking existence of cgroup cgroupPath=%s", cgroupPath)

	controller, err := c.Load(cgroups.Default, cgroups.StaticPath(cgroupPath))
	if err != nil || controller == nil {
		return false
	}

	return true
}

// Init is used to set up the cgroup root for ecs
func (c *control) Init() error {
	seelog.Debugf("Creating root ecs cgroup cgroupPath=%s", config.DefaultTaskCgroupV1Prefix)

	// Build cgroup spec
	cgroupSpec := &Spec{
		Root:  config.DefaultTaskCgroupV1Prefix,
		Specs: &specs.LinuxResources{},
	}
	err := c.Create(cgroupSpec)
	return err
}

// validateCgroupSpec checks the cgroup spec for valid path and specifications
func validateCgroupSpec(cgroupSpec *Spec) error {
	if cgroupSpec == nil {
		return errors.New("cgroup spec validator: empty cgroup spec")
	}

	if cgroupSpec.Root == "" {
		return errors.New("cgroup spec validator: invalid cgroup root")
	}

	// Validate the linux resource specs
	if cgroupSpec.Specs == nil {
		return errors.New("cgroup spec validator: empty linux resource spec")
	}
	return nil
}
