// Unless explicitly stated otherwise all files in this repository are licensed
// under the Apache License Version 2.0.
// This product includes software developed at Datadog (https://www.datadoghq.com/).
// Copyright 2016-present Datadog, Inc.

//go:build linux

package cgroups

import (
	"fmt"
	"path/filepath"
	"regexp"
	"strings"
	"sync"
	"time"
)

const (
	// ContainerRegexpStr defines the regexp used to match container IDs
	// ([0-9a-f]{64}) is standard container id used pretty much everywhere
	// ([0-9a-f]{32}-\d+) is container id used by AWS ECS
	// ([0-9a-f]{8}(-[0-9a-f]{4}){4}$) is container id used by Garden
	ContainerRegexpStr = "([0-9a-f]{64})|([0-9a-f]{32}-\\d+)|([0-9a-f]{8}(-[0-9a-f]{4}){4}$)"
)

// Reader is the main interface to scrape data from cgroups
// Calling RefreshCgroups() with your cache toleration is mandatory to retrieve accurate data
// All Reader methods support concurrent calls
type Reader struct {
	pidMapperID            string
	hostPrefix             string
	procPath               string
	cgroupVersion          int
	cgroupV1BaseController string
	readerFilter           ReaderFilter
	impl                   readerImpl

	cgroups         map[string]Cgroup
	cgroupByInode   map[uint64]Cgroup
	cgroupsLock     sync.RWMutex
	scrapeTimestmap time.Time
}

type readerImpl interface {
	parseCgroups() (map[string]Cgroup, error)
}

// ReaderFilter allows to filter cgroups based on their path + folder name
type ReaderFilter func(path, name string) (string, error)

// DefaultFilter matches all cgroup folders and use folder name as identifier
func DefaultFilter(path, _ string) (string, error) {
	return path, nil
}

// ContainerRegexp defines the regexp used to match container ids
// First part is usual containerid (opencontainers standard)
// Second part is PCF/Garden regexp. We currently assume no suffix ($) to avoid matching pod UIDs
var ContainerRegexp = regexp.MustCompile(ContainerRegexpStr)

// ContainerFilter returns a filter that will match cgroup folders containing a container id
func ContainerFilter(_, name string) (string, error) {
	match := ContainerRegexp.FindString(name)

	// With systemd cgroup driver, there may be a `.mount` cgroup on top of the normal one
	// While existing, no process is attached to it and thus holds no stats
	if match != "" {
		if strings.HasSuffix(name, ".mount") || strings.HasPrefix(name, "crio-conmon-") {
			return "", nil
		}

		return match, nil
	}

	return "", nil
}

// ReaderOption allows to customize reader behavior (Builder-style)
type ReaderOption func(*Reader)

// WithHostPrefix sets where hosts path are mounted (if not running on-host)
func WithHostPrefix(hostPrefix string) ReaderOption {
	return func(r *Reader) {
		r.hostPrefix = hostPrefix
	}
}

// WithProcPath sets where /proc is currently mounted.
// If set, hostPrefix is not added to this path.
// Default to `$hostPrefix/proc` if empty.
func WithProcPath(fullPath string) ReaderOption {
	return func(r *Reader) {
		r.procPath = fullPath
	}
}

// WithReaderFilter sets the filter used to select interesting cgroup folders
// and provides an identifier for them.
func WithReaderFilter(rf ReaderFilter) ReaderOption {
	return func(r *Reader) {
		r.readerFilter = rf
	}
}

// WithCgroupV1BaseController sets which controller is used to select cgroups
// it then assumes that, if being, used other controllers uses the same relative path.
// Default to "memory" if not set.
func WithCgroupV1BaseController(controller string) ReaderOption {
	return func(r *Reader) {
		r.cgroupV1BaseController = controller
	}
}

// WithPIDMapper allows to force the selection of a specific PID mapper
func WithPIDMapper(pidMapperID string) ReaderOption {
	return func(r *Reader) {
		r.pidMapperID = pidMapperID
	}
}

// NewReader returns a new cgroup reader with given options
func NewReader(opts ...ReaderOption) (*Reader, error) {
	r := &Reader{}
	for _, opt := range opts {
		opt(r)
	}

	if err := r.init(); err != nil {
		return nil, err
	}
	return r, nil
}

func (r *Reader) init() error {
	if r.procPath == "" {
		r.procPath = filepath.Join(r.hostPrefix, "/proc")
	}

	cgroupMounts, err := discoverCgroupMountPoints(r.hostPrefix, r.procPath)
	if err != nil {
		return err
	}

	if r.readerFilter == nil {
		r.readerFilter = DefaultFilter
	}

	if isCgroup1(cgroupMounts) {
		r.cgroupVersion = 1

		r.impl, err = newReaderV1(r.procPath, cgroupMounts, r.cgroupV1BaseController, r.readerFilter, r.pidMapperID)
		if err != nil {
			return err
		}
	} else if isCgroup2(cgroupMounts) {
		r.cgroupVersion = 2

		r.impl, err = newReaderV2(r.procPath, cgroupMounts[cgroupV2Key], r.readerFilter, r.pidMapperID)
		if err != nil {
			return err
		}
	} else {
		return fmt.Errorf("unable to detect cgroup version from detected mount points: %v", cgroupMounts)
	}

	return nil
}

// CgroupVersion returns the detected cgroup version
func (r *Reader) CgroupVersion() int {
	return r.cgroupVersion
}

// ListCgroups returns list of known cgroups
func (r *Reader) ListCgroups() []Cgroup {
	r.cgroupsLock.RLock()
	defer r.cgroupsLock.RUnlock()

	cgroups := make([]Cgroup, 0, len(r.cgroups))
	for _, cg := range r.cgroups {
		cgroups = append(cgroups, cg)
	}

	return cgroups
}

// GetCgroup returns cgroup for a given id, or nil if not found.
func (r *Reader) GetCgroup(id string) Cgroup {
	r.cgroupsLock.RLock()
	defer r.cgroupsLock.RUnlock()

	return r.cgroups[id]
}

// GetCgroupByInode returns the cgroup for the given inode.
func (r *Reader) GetCgroupByInode(inode uint64) Cgroup {
	r.cgroupsLock.RLock()
	defer r.cgroupsLock.RUnlock()

	return r.cgroupByInode[inode]
}

// RefreshCgroups triggers a refresh if data are older than cacheValidity. 0 to always refesh.
func (r *Reader) RefreshCgroups(cacheValidity time.Duration) error {
	r.cgroupsLock.Lock()
	defer r.cgroupsLock.Unlock()

	// Refresh not required
	if r.scrapeTimestmap.Add(cacheValidity).After(time.Now()) {
		return nil
	}

	newCgroups, err := r.impl.parseCgroups()
	if err != nil {
		return err
	}

	r.cgroupByInode = make(map[uint64]Cgroup, len(newCgroups))
	for _, cg := range newCgroups {
		if inode := cg.Inode(); inode != unknownInode {
			r.cgroupByInode[inode] = cg
		}
	}

	r.scrapeTimestmap = time.Now()
	r.cgroups = newCgroups
	return nil
}
