//go:build linux

//go:generate ../../../tools/readme_config_includer/generator
package systemd

import (
	"context"
	_ "embed"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	"github.com/coreos/go-systemd/v22/dbus"

	"github.com/influxdata/telegraf"
	"github.com/influxdata/telegraf/plugins/secretstores"
)

const systemdMinimumVersion = 250

// Required to be a variable to mock the version in tests
var getSystemdVersion = getSystemdMajorVersion

//go:embed sample.conf
var sampleConfig string

type Systemd struct {
	Path   string          `toml:"path"`
	Prefix string          `toml:"prefix"`
	Log    telegraf.Logger `toml:"-"`
}

func (*Systemd) SampleConfig() string {
	return sampleConfig
}

func (s *Systemd) Init() error {
	version, err := getSystemdVersion()
	if err != nil {
		return fmt.Errorf("unable to detect systemd version: %w", err)
	}
	s.Log.Debugf("Found systemd version %d...", version)
	if version < systemdMinimumVersion {
		return fmt.Errorf("systemd version %d below minimum version %d", version, systemdMinimumVersion)
	}

	// By default the credentials directory is passed in by systemd
	// via the "CREDENTIALS_DIRECTORY" environment variable.
	defaultPath := os.Getenv("CREDENTIALS_DIRECTORY")
	if defaultPath == "" {
		s.Log.Warn("CREDENTIALS_DIRECTORY environment variable undefined. Make sure credentials are setup correctly!")
		if s.Path == "" {
			return errors.New("'path' required without CREDENTIALS_DIRECTORY")
		}
	}

	// Use default path if no explicit was specified. This should be the common case.
	if s.Path == "" {
		s.Path = defaultPath
	}
	s.Path, err = filepath.Abs(s.Path)
	if err != nil {
		return fmt.Errorf("cannot determine absolute path of %q: %w", s.Path, err)
	}

	// Check if we can access the target directory
	if _, err := os.Stat(s.Path); err != nil {
		return fmt.Errorf("accessing credentials directory %q failed: %w", s.Path, err)
	}
	return nil
}

func (s *Systemd) Get(key string) ([]byte, error) {
	secretFile, err := filepath.Abs(filepath.Join(s.Path, s.Prefix+key))
	if err != nil {
		return nil, err
	}
	if filepath.Dir(secretFile) != s.Path {
		return nil, fmt.Errorf("invalid directory detected for key %q", key)
	}
	value, err := os.ReadFile(secretFile)
	if err != nil {
		return nil, fmt.Errorf("cannot read the secret's value: %w", err)
	}
	return value, nil
}

func (s *Systemd) List() ([]string, error) {
	secretFiles, err := os.ReadDir(s.Path)
	if err != nil {
		return nil, fmt.Errorf("cannot read files: %w", err)
	}
	secrets := make([]string, 0, len(secretFiles))
	for _, entry := range secretFiles {
		key := strings.TrimPrefix(entry.Name(), s.Prefix)
		secrets = append(secrets, key)
	}
	return secrets, nil
}

func (*Systemd) Set(_, _ string) error {
	return errors.New("secret store does not support creating secrets")
}

func (s *Systemd) GetResolver(key string) (telegraf.ResolveFunc, error) {
	resolver := func() ([]byte, bool, error) {
		s, err := s.Get(key)
		return s, false, err
	}
	return resolver, nil
}

func getSystemdMajorVersion() (int, error) {
	ctx := context.Background()
	conn, err := dbus.NewWithContext(ctx)
	if err != nil {
		return 0, err
	}
	defer conn.Close()

	fullVersion, err := conn.GetManagerProperty("Version")
	if err != nil {
		return 0, err
	}
	fullVersion = strings.Trim(fullVersion, "\"")
	major, _, _ := strings.Cut(fullVersion, ".")
	major, _, _ = strings.Cut(major, "-")
	return strconv.Atoi(major)
}

func init() {
	secretstores.Add("systemd", func(_ string) telegraf.SecretStore {
		return &Systemd{Prefix: "telegraf."}
	})
}
