package cmd

import (
	"errors"
	"fmt"
	"io"
	"os"
	"runtime"
	"strings"
)

const (
	maxRuntimeTokenFileBytes        = int64(16 << 10)
	maxRuntimeTokenFileOpenAttempts = 3
)

func readRuntimeTokenFile(
	scope string,
	tokenFile string,
	lstat func(string) (os.FileInfo, error),
	open func(string) (*os.File, error),
) (string, error) {
	scope = strings.TrimSpace(scope)
	if scope == "" {
		scope = "runtime"
	}
	for attempt := 0; attempt < maxRuntimeTokenFileOpenAttempts; attempt++ {
		before, err := lstat(tokenFile)
		if err != nil || !before.Mode().IsRegular() || before.Mode()&os.ModeSymlink != 0 {
			return "", fmt.Errorf("%s token file is unavailable", scope)
		}
		if runtime.GOOS != "windows" && before.Mode().Perm()&^0o600 != 0 {
			return "", fmt.Errorf("%s token file permissions are invalid", scope)
		}

		file, err := open(tokenFile)
		if err != nil {
			if errors.Is(err, os.ErrNotExist) &&
				attempt+1 < maxRuntimeTokenFileOpenAttempts {
				continue
			}
			return "", fmt.Errorf("%s token file is unavailable", scope)
		}
		after, statErr := file.Stat()
		if statErr != nil || !after.Mode().IsRegular() || !os.SameFile(before, after) {
			_ = file.Close()
			if statErr == nil && after.Mode().IsRegular() &&
				attempt+1 < maxRuntimeTokenFileOpenAttempts {
				continue
			}
			return "", fmt.Errorf("%s token file changed while opening", scope)
		}

		value, readErr := io.ReadAll(io.LimitReader(file, maxRuntimeTokenFileBytes+1))
		_ = file.Close()
		if readErr != nil {
			return "", fmt.Errorf("%s token file is unavailable", scope)
		}
		if int64(len(value)) > maxRuntimeTokenFileBytes {
			return "", fmt.Errorf("%s token file exceeds 16 KiB", scope)
		}
		tokenValue := strings.TrimSpace(string(value))
		if tokenValue == "" {
			return "", fmt.Errorf("%s token file is empty", scope)
		}
		for _, character := range tokenValue {
			if character <= 0x20 || character >= 0x7f {
				return "", fmt.Errorf("%s token file contains invalid characters", scope)
			}
		}
		return tokenValue, nil
	}
	return "", fmt.Errorf("%s token file changed while opening", scope)
}
