// Copyright 2020 New Relic Corporation. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

package sysinfo

import (
	"bufio"
	"bytes"
	"errors"
	"fmt"
	"io"
	"os"
	"regexp"
	"runtime"
	"strings"
)

var (
	// ErrDockerNotFound is returned if a Docker ID is not found in
	// /proc/self/cgroup
	ErrDockerNotFound = errors.New("Docker ID not found")
)

// DockerID attempts to detect Docker.
func DockerID() (string, error) {
	if "linux" != runtime.GOOS {
		return "", ErrFeatureUnsupported
	}

	f, err := os.Open("/proc/self/cgroup")
	if err != nil {
		return "", err
	}
	defer f.Close()
	id, err := parseDockerID(f)

	// Attempt mountinfo file lookup if DockerID not found in cgroup file
	if err == ErrDockerNotFound {
		f, err := os.Open("/proc/self/mountinfo")
		if err != nil {
			return "", err
		}
		defer f.Close()
		return parseDockerIDMountInfo(f)
	}
	return id, err
}

var (
	// The DockerID must be a 64-character lowercase hex string
	// be greedy and match anything 64-characters or longer to spot invalid IDs
	dockerIDLength   = 64
	dockerIDRegexRaw = fmt.Sprintf("[0-9a-f]{%d,}", dockerIDLength)
	dockerIDRegex    = regexp.MustCompile(dockerIDRegexRaw)
)

func parseDockerIDMountInfo(r io.Reader) (string, error) {
	// Each Line in the mountinfo file starts with a set of IDs before showing the path file we actually want
	// 	1. Mount ID
	//	2. Parent ID
	//	3. Major and minor device numbers
	// 	4. Path to ContainerID

	scanner := bufio.NewScanner(r)
	for scanner.Scan() {
		line := scanner.Text()
		if strings.Contains(line, "/docker/containers/") {
			id := dockerIDRegex.FindString(line)
			if err := validateDockerID(id); err != nil {
				return "", err
			}
			return id, nil
		}
	}
	return "", ErrDockerNotFound
}

func parseDockerID(r io.Reader) (string, error) {
	// Each line in the cgroup file consists of three colon delimited fields.
	//   1. hierarchy ID  - we don't care about this
	//   2. subsystems    - comma separated list of cgroup subsystem names
	//   3. control group - control group to which the process belongs
	//
	// Example
	//   5:cpuacct,cpu,cpuset:/daemons
	var id string

	for scanner := bufio.NewScanner(r); scanner.Scan(); {
		line := scanner.Bytes()
		cols := bytes.SplitN(line, []byte(":"), 3)

		if len(cols) < 3 {
			continue
		}

		//  We're only interested in the cpu subsystem.
		if !isCPUCol(cols[1]) {
			continue
		}

		id = dockerIDRegex.FindString(string(cols[2]))

		if err := validateDockerID(id); err != nil {
			// We can stop searching at this point, the CPU
			// subsystem should only occur once, and its cgroup is
			// not docker or not a format we accept.
			return "", err
		}
		return id, nil
	}
	return "", ErrDockerNotFound
}

func isCPUCol(col []byte) bool {
	// Sometimes we have multiple subsystems in one line, as in this example
	// from:
	// https://source.datanerd.us/newrelic/cross_agent_tests/blob/master/docker_container_id/docker-1.1.2-native-driver-systemd.txt
	//
	// 3:cpuacct,cpu:/system.slice/docker-67f98c9e6188f9c1818672a15dbe46237b6ee7e77f834d40d41c5fb3c2f84a2f.scope
	splitCSV := func(r rune) bool { return r == ',' }
	subsysCPU := []byte("cpu")

	for _, subsys := range bytes.FieldsFunc(col, splitCSV) {
		if bytes.Equal(subsysCPU, subsys) {
			return true
		}
	}
	return false
}

func isHex(r rune) bool {
	return ('0' <= r && r <= '9') || ('a' <= r && r <= 'f')
}

func validateDockerID(id string) error {
	if len(id) != 64 {
		return fmt.Errorf("%s is not %d characters long", id, dockerIDLength)
	}

	for _, c := range id {
		if !isHex(c) {
			return fmt.Errorf("Character: %c is not hex in string %s", c, id)
		}
	}

	return nil
}
