// Copyright 2020 Nokia
// Licensed under the BSD 3-Clause License.
// SPDX-License-Identifier: BSD-3-Clause

package utils

import (
	"archive/tar"
	"bufio"
	"bytes"
	"context"
	"crypto/tls"
	"errors"
	"fmt"
	"io"
	"io/fs"
	"math"
	"mime"
	"net"
	"net/http"
	"net/url"
	"os"
	"os/exec"
	"os/user"
	"path/filepath"
	"regexp"
	"strconv"
	"strings"
	"time"

	"github.com/bramvdbogaerde/go-scp"
	"github.com/jlaffaye/ftp"
	"github.com/minio/minio-go/v7"
	"github.com/minio/minio-go/v7/pkg/credentials"
	"github.com/pkg/sftp"
	"github.com/steiler/acls"

	"github.com/charmbracelet/log"
	clabconstants "github.com/srl-labs/containerlab/constants"
	"golang.org/x/crypto/ssh"
	"golang.org/x/crypto/ssh/agent"
	"golang.org/x/crypto/ssh/knownhosts"
)

var (
	errNonRegularFile = errors.New("non-regular file")
	errHTTPFetch      = errors.New("failed to fetch http(s) resource")
	errS3Fetch        = errors.New("failed to fetch s3 resource")
	errFTPFetch       = errors.New("failed to fetch ftp resource")
	errSCPFetch       = errors.New("failed to fetch scp resource")
	errSFTPFetch      = errors.New("failed to fetch sftp resource")
)

// FileExists returns true if a file referenced by filename exists & accessible.
func FileExists(filename string) bool {
	f, err := os.Stat(filename)
	if err != nil {
		return false
	}
	return !f.IsDir()
}

// FileOrDirExists returns true if a file or dir referenced by path exists & accessible.
func FileOrDirExists(filename string) bool {
	f, err := os.Stat(filename)

	return err == nil && f != nil
}

// DirExists returns true if a dir referenced by path exists & accessible.
func DirExists(filename string) bool {
	f, err := os.Stat(filename)

	return err == nil && f != nil && f.IsDir()
}

// CopyFile copies a file from src to dst. If src and dst files exist, and are
// the same, then return success. Otherwise, copy the file contents from src to dst.
// mode is the desired target file permissions, e.g. "0644".
func CopyFile(ctx context.Context, src, dst string, mode os.FileMode) (err error) {
	var sfi os.FileInfo
	if !IsDownloadableURL(src) {
		sfi, err = os.Stat(src)
		if err != nil {
			return err
		}

		if !sfi.Mode().IsRegular() {
			// cannot copy non-regular files (e.g., directories,
			// symlinks, devices, etc.)
			return fmt.Errorf(
				"file copy failed: source file %s (%q): %w",
				sfi.Name(),
				sfi.Mode().String(),
				errNonRegularFile,
			)
		}
	}

	dfi, err := os.Stat(dst)
	if err != nil {
		if !errors.Is(err, fs.ErrNotExist) {
			return err
		}
	} else {
		if !(dfi.Mode().IsRegular()) {
			return fmt.Errorf("file copy failed: destination file %s (%q): %w",
				dfi.Name(), dfi.Mode().String(), errNonRegularFile)
		}

		if sfi != nil && os.SameFile(sfi, dfi) {
			return nil
		}
	}

	out, cleanup, err := CreateFileWithPermissions(dst, mode)
	if err != nil {
		return err
	}
	defer cleanup()

	return CopyFileContents(ctx, src, out)
}

// IsHttpURL checks if the url is a downloadable HTTP URL.
// The allowSchemaless toggle when set to true will allow URLs without a schema
// such as "srlinux.dev/clab-srl". This is shortened notion that is used with
// "deploy -t <url>" only.
// Other callers of IsHttpURL should set the toggle to false.
func IsHttpURL(s string, allowSchemaless bool) bool {
	// '-' denotes stdin and not the URL
	if s == "-" {
		return false
	}

	// if schemaless is not allowed and the string does not contain a schema, it is not an URL
	if !allowSchemaless && !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") {
		return false
	}

	// if schemaless is allowed and the string does not contain a schema, but contains a dot
	// in any a non-domain portion then it is not a valid URL
	if allowSchemaless && !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") {
		split := strings.SplitN(s, "/", 2)
		if len(split) > 1 {
			if strings.Contains(split[1], ".") {
				return false
			}
		}
	}

	if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") {
		s = "https://" + s
	}

	u, err := url.ParseRequestURI(s)

	return err == nil && u.Host != ""
}

// IsS3URL checks if the URL is an S3 URL (s3://bucket/key format).
func IsS3URL(s string) bool {
	return strings.HasPrefix(s, "s3://")
}

// IsFTPURL checks if the URL is an FTP URL (ftp://host/path format).
func IsFTPURL(s string) bool {
	return strings.HasPrefix(s, "ftp://")
}

// IsSFTPURL checks if the URL is an SFTP URL (sftp://user@host/path format).
func IsSFTPURL(s string) bool {
	return strings.HasPrefix(s, "sftp://")
}

// IsSCPURL checks if the URL is an SCP URL (scp://user@host/path format).
func IsSCPURL(s string) bool {
	return strings.HasPrefix(s, "scp://")
}

// IsDownloadableURL checks if the path is a URL supported by file download helpers.
func IsDownloadableURL(s string) bool {
	return IsHttpURL(s, false) || IsS3URL(s) || IsFTPURL(s) || IsSFTPURL(s) || IsSCPURL(s)
}

// ParseS3URL parses an S3 URL and returns the bucket and key.
func ParseS3URL(s3URL string) (bucket, key string, err error) {
	if !IsS3URL(s3URL) {
		return "", "", fmt.Errorf("not an S3 URL: %s", s3URL)
	}

	u, err := url.Parse(s3URL)
	if err != nil {
		return "", "", err
	}

	bucket = u.Host
	key = strings.TrimPrefix(u.Path, "/")

	if bucket == "" || key == "" {
		return "", "", fmt.Errorf("invalid S3 URL format: %s", s3URL)
	}

	return bucket, key, nil
}

func copyFileContentsS3(src string) (io.ReadCloser, error) {
	bucket, key, err := ParseS3URL(src)
	if err != nil {
		return nil, err
	}

	// Get region from environment, default to us-east-1
	region := os.Getenv("AWS_REGION")
	if region == "" {
		region = "us-east-1"
	}

	// Create credential chain that mimics AWS SDK behavior
	credProviders := []credentials.Provider{
		&credentials.EnvAWS{},             // 1. Environment variables
		&credentials.FileAWSCredentials{}, // 2. ~/.aws/credentials (default profile)
		&credentials.IAM{
			Client: &http.Client{Timeout: 10 * time.Second},
		}, // 3. IAM role (EC2/ECS/Lambda)
	}

	// Create MinIO client with chained credentials
	client, err := minio.New("s3.amazonaws.com", &minio.Options{
		Creds:  credentials.NewChainCredentials(credProviders),
		Secure: true,
		Region: region,
	})
	if err != nil {
		return nil, fmt.Errorf("failed to create S3 client: %w", err)
	}

	// Get object from S3
	object, err := client.GetObject(context.TODO(), bucket, key, minio.GetObjectOptions{})
	if err != nil {
		return nil, fmt.Errorf("%w: %s: %v", errS3Fetch, src, err)
	}

	// Verify object exists by reading its stats
	_, err = object.Stat()
	if err != nil {
		object.Close()
		return nil, fmt.Errorf(
			"%w: %s: object not found or access denied: %v",
			errS3Fetch,
			src,
			err,
		)
	}

	return object, nil
}

type removeOnCloseFile struct {
	*os.File
}

func (f removeOnCloseFile) Close() error {
	err := f.File.Close()
	_ = os.Remove(f.Name())
	return err
}

func sshDownloadConfig(src string, fetchErr error) (*url.URL, *ssh.ClientConfig, error) {
	u, err := url.Parse(src)
	if err != nil {
		return nil, nil, err
	}

	if u.User == nil || u.User.Username() == "" {
		return nil, nil, fmt.Errorf("%w: %s: missing username", fetchErr, src)
	}

	clientConfig := ssh.ClientConfig{
		User:            u.User.Username(),
		Auth:            []ssh.AuthMethod{},
		HostKeyCallback: customHostKeyCallback(ResolvePath("~/.ssh/known_hosts", "")),
	}

	if socket := os.Getenv("SSH_AUTH_SOCK"); socket != "" {
		conn, err := net.Dial("unix", socket)
		if err != nil {
			log.Error(err)
		} else {
			agentClient := agent.NewClient(conn)
			clientConfig.Auth = append(
				clientConfig.Auth,
				ssh.PublicKeysCallback(agentClient.Signers),
			)
		}
	}

	keyPath := os.Getenv("CLAB_SSH_KEY")
	keyPassphrase := os.Getenv("CLAB_SSH_KEY_PASSPHRASE")
	if keyPath != "" {
		if !FileExists(keyPath) {
			return nil, nil, fmt.Errorf("%w: %s: keyfile %q does not exist", fetchErr, src, keyPath)
		}

		privateKey, err := os.ReadFile(keyPath)
		if err != nil {
			return nil, nil, err
		}

		var signer ssh.Signer
		if keyPassphrase != "" {
			signer, err = ssh.ParsePrivateKeyWithPassphrase(privateKey, []byte(keyPassphrase))
		} else {
			signer, err = ssh.ParsePrivateKey(privateKey)
		}
		if err != nil {
			return nil, nil, err
		}

		clientConfig.Auth = append([]ssh.AuthMethod{ssh.PublicKeys(signer)}, clientConfig.Auth...)
	}

	if pw, ok := u.User.Password(); ok {
		clientConfig.Auth = append([]ssh.AuthMethod{ssh.Password(pw)}, clientConfig.Auth...)
	}

	return u, &clientConfig, nil
}

func sshDownloadAddr(u *url.URL) string {
	port := "22"
	if u.Port() != "" {
		port = u.Port()
	}

	return net.JoinHostPort(u.Hostname(), port)
}

func copyFileContentsSFTP(src string) (io.ReadCloser, error) {
	u, clientConfig, err := sshDownloadConfig(src, errSFTPFetch)
	if err != nil {
		return nil, err
	}

	sshClient, err := ssh.Dial("tcp", sshDownloadAddr(u), clientConfig)
	if err != nil {
		return nil, fmt.Errorf("%w: %s: %v", errSFTPFetch, src, err)
	}
	defer sshClient.Close()

	client, err := sftp.NewClient(sshClient)
	if err != nil {
		return nil, fmt.Errorf("%w: %s: %v", errSFTPFetch, src, err)
	}
	defer client.Close()

	r, err := client.Open(u.Path)
	if err != nil {
		return nil, fmt.Errorf("%w: %s: %v", errSFTPFetch, src, err)
	}
	defer r.Close()

	buf, err := io.ReadAll(r)
	if err != nil {
		return nil, err
	}

	return io.NopCloser(bytes.NewBuffer(buf)), nil
}

func copyFileContentsSCP(ctx context.Context, src string) (io.ReadCloser, error) {
	u, clientConfig, err := sshDownloadConfig(src, errSCPFetch)
	if err != nil {
		return nil, err
	}

	client := scp.NewClient(sshDownloadAddr(u), clientConfig)
	if err := client.Connect(); err != nil {
		return nil, fmt.Errorf("%w: %s: %v", errSCPFetch, src, err)
	}
	defer client.Close()

	f, err := os.CreateTemp("", "clab-scp-*")
	if err != nil {
		return nil, err
	}

	if err := client.CopyFromRemote(ctx, f, u.Path); err != nil {
		_ = f.Close()
		_ = os.Remove(f.Name())
		return nil, fmt.Errorf("%w: %s: %v", errSCPFetch, src, err)
	}

	if _, err := f.Seek(0, io.SeekStart); err != nil {
		_ = f.Close()
		_ = os.Remove(f.Name())
		return nil, err
	}

	return removeOnCloseFile{File: f}, nil
}

func copyFileContentsFTP(src string) (io.ReadCloser, error) {
	u, err := url.Parse(src)
	if err != nil {
		return nil, err
	}

	hostname := u.Hostname()
	port := "21"
	if u.Port() != "" {
		port = u.Port()
	}

	client, err := ftp.Dial(net.JoinHostPort(hostname, port), ftp.DialWithTimeout(5*time.Second))
	if err != nil {
		return nil, fmt.Errorf("%w: %s: %v", errFTPFetch, src, err)
	}
	defer client.Quit()

	if u.User != nil {
		pw, _ := u.User.Password()
		if err := client.Login(u.User.Username(), pw); err != nil {
			return nil, fmt.Errorf("%w: %s: %v", errFTPFetch, src, err)
		}
	}

	r, err := client.Retr(u.Path)
	if err != nil {
		return nil, fmt.Errorf("%w: %s: %v", errFTPFetch, src, err)
	}
	defer r.Close()

	buf, err := io.ReadAll(r)
	if err != nil {
		return nil, err
	}

	return io.NopCloser(bytes.NewBuffer(buf)), nil
}

// CopyFileContents copies the contents of the file named src to the file named
// by dst. The file will be created if it does not already exist. If the
// destination file exists, all it's contents will be replaced by the contents
// of the source file.
// src can be a local path or an http(s), S3, FTP, SFTP, or SCP URL.
func CopyFileContents(ctx context.Context, src string, dst *os.File) (err error) {
	var in io.ReadCloser

	switch {
	case IsHttpURL(src, false):
		client := NewHTTPClient()

		// download using client
		req, err := http.NewRequestWithContext(ctx, http.MethodGet, src, http.NoBody)
		if err != nil {
			return err
		}

		resp, err := client.Do(req)
		if err != nil || resp.StatusCode != 200 {
			return fmt.Errorf("%w: %s", errHTTPFetch, src)
		}

		defer resp.Body.Close()

		in = resp.Body

	case IsS3URL(src):
		in, err = copyFileContentsS3(src)
		if err != nil {
			return err
		}
	case IsFTPURL(src):
		in, err = copyFileContentsFTP(src)
		if err != nil {
			return err
		}
	case IsSFTPURL(src):
		in, err = copyFileContentsSFTP(src)
		if err != nil {
			return err
		}
	case IsSCPURL(src):
		in, err = copyFileContentsSCP(ctx, src)
		if err != nil {
			return err
		}
	default:
		in, err = os.Open(src)
		if err != nil {
			return err
		}
	}
	defer in.Close() // skipcq: GO-S2307

	_, err = io.Copy(dst, in)
	if err != nil {
		return err
	}

	return dst.Sync()
}

// CreateFileWithPermissions creates a file with proper directory structure,
// ownership, and permissions. It returns the file handle and a cleanup function.
// The caller is responsible for calling the cleanup function to close the file.
func CreateFileWithPermissions(filePath string, mode os.FileMode) (*os.File, func(), error) {
	// Create parent directories
	err := os.MkdirAll(filepath.Dir(filePath), 0o750)
	if err != nil {
		return nil, nil, err
	}

	// Create the file
	file, err := os.Create(filePath)
	if err != nil {
		return nil, nil, err
	}

	// Change file ownership to user running Containerlab instead of effective UID
	err = SetUIDAndGID(filePath)
	if err != nil {
		_ = file.Close()
		return nil, nil, err
	}

	// Set file permissions
	err = file.Chmod(mode)
	if err != nil {
		_ = file.Close()
		return nil, nil, err
	}

	cleanup := func() {
		// should only err on repeated calls to close anyway
		_ = file.Close()
	}
	return file, cleanup, nil
}

// CreateFile writes content to a file by path `file`.
func CreateFile(file, content string) (err error) {
	var f *os.File

	f, err = os.Create(file)
	if err != nil {
		return err
	}
	defer f.Close()

	// add newline if missing
	if !strings.HasSuffix(content, "\n") {
		content += "\n"
	}

	_, err = f.WriteString(content)
	if err != nil {
		return err
	}

	// Change file ownership to user running Containerlab instead of effective UID
	err = SetUIDAndGID(file)
	if err != nil {
		return err
	}

	return f.Close()
}

// CreateDirectory creates a directory by a path with a mode/permission specified by perm.
// If directory exists, the function does not do anything.
func CreateDirectory(path string, perm os.FileMode) {
	err := os.MkdirAll(path, perm)
	if err != nil {
		log.Debugf("error while creating a directory path %v: %v", path, err)
	}
}

func ReadFileContent(file string) ([]byte, error) {
	// try to read and return file content, or return an error
	b, err := os.ReadFile(file)
	return b, err
}

// ExpandHome expands `~` char in the path to home path of a current user in provided path p.
// When sudo is used, it expands to home dir of a sudo user.
func ExpandHome(p string) string {
	// current user home dir, used when sudo is not used
	// or when errors occur during sudo user lookup
	curUserHomeDir, _ := os.UserHomeDir()

	userId, isSet := os.LookupEnv("SUDO_UID")
	if !isSet {
		log.Debugf("SUDO_UID env var is not set, using current user home dir: %v", curUserHomeDir)
		p = strings.Replace(p, "~", curUserHomeDir, 1)
		return p
	}

	// lookup user to figure out Home Directory
	u, err := user.LookupId(userId)
	if err != nil {
		log.Debugf("error while looking up user by id using os/user.LookupId %v: %v", userId, err)
		// user.LookupId fails when ActiveDirectory is used, so we try to use getent command
		homedir := lookupUserHomeDirViaGetent(userId)
		if homedir != "" {
			log.Debugf("user home dir %v found using getent command", homedir)
			p = strings.Replace(p, "~", homedir, 1)
			return p
		}
		// fallback to current user home dir if getent command fails
		p = strings.Replace(p, "~", curUserHomeDir, 1)
		return p
	}

	p = strings.Replace(p, "~", u.HomeDir, 1)

	log.Debugf("user home dir %v found using os/user.LookupId", u.HomeDir)

	return p
}

// lookupUserHomeDirViaGetent looks up user's homedir by using `getent passwd` command.
// It is used as a fallback when os/user.LookupId fails, which seems to
// happen when ActiveDirectory is used.
func lookupUserHomeDirViaGetent(userId string) string {
	cmd := exec.Command("getent", "passwd", userId)
	out, err := cmd.Output()
	if err != nil {
		log.Debugf("error while looking up user by id using getent command %v: %v", userId, err)
		return ""
	}

	// output format is `username:x:uid:gid:comment:home:shell`
	// we need to extract home dir
	parts := strings.Split(string(out), ":")
	if len(parts) < 6 {
		log.Debugf(
			"error while looking up user by id using getent command %v: unexpected output format",
			userId,
		)
		return ""
	}

	return parts[5]
}

// ResolvePath resolves path p by expanding ~ to home dir
// or resolving a relative path by joining it with the base path.
// When resolving `~` the function uses the home dir of a sudo user, so that -E sudo
// flag can be omitted.
func ResolvePath(p, base string) string {
	if p == "" {
		return p
	}

	switch p[0] {
	// resolve ~/ path
	case '~':
		p = ExpandHome(p)
	case '/':
		return p
	default:
		// join relative path with the base path
		p = filepath.Join(base, p)
	}
	return p
}

const (
	UndefinedFileName = "undefined"
)

// FilenameForURL extracts a filename from a given url
// returns "undefined" when unsuccessful.
func FilenameForURL(ctx context.Context, rawUrl string) string {
	u, err := url.Parse(rawUrl)
	if err != nil {
		return UndefinedFileName
	}

	// try extracting the filename from "content-disposition" header
	if IsHttpURL(rawUrl, false) {
		client := NewHTTPClient()

		req, err := http.NewRequestWithContext(ctx, http.MethodHead, rawUrl, http.NoBody)
		if err != nil {
			return filepath.Base(u.Path)
		}

		resp, err := client.Do(req)
		if err != nil {
			return filepath.Base(u.Path)
		}

		defer resp.Body.Close()

		if cd := resp.Header.Get("Content-Disposition"); cd != "" {
			if _, params, err := mime.ParseMediaType(cd); err == nil {
				return params["filename"]
			}
		}
	}
	return filepath.Base(u.Path)
}

// FileLines opens a file by the `path` and returns a slice of strings for each line
// excluding lines that start with `commentStr` or are empty.
func FileLines(path, commentStr string) ([]string, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("failed to open file %v: %w", path, err)
	}
	defer f.Close() // skipcq: GO-S2307

	var lines []string

	scanner := bufio.NewScanner(f)
	for scanner.Scan() {
		line := scanner.Text()
		// Skip lines that start with comment char
		if strings.HasPrefix(line, commentStr) || line == "" {
			continue
		}

		lines = append(lines, line)
	}

	return lines, nil
}

// NewHTTPClient creates a new HTTP client with
// insecure skip verify set to true and min TLS version set to 1.2.
func NewHTTPClient() *http.Client {
	// set InsecureSkipVerify to true to allow fetching
	// files form servers with self-signed certificates
	tr := &http.Transport{
		Proxy: http.ProxyFromEnvironment,
		TLSClientConfig: &tls.Config{
			InsecureSkipVerify: true, // skipcq: GSC-G402
			MinVersion:         tls.VersionTLS12,
		},
	}

	return &http.Client{Transport: tr}
}

func GetRealUserIDs() (userUID, userGID int, err error) {
	// Here we check whether SUDO set the SUDO_UID and SUDO_GID variables
	sudoUID, isSudoUIDSet := os.LookupEnv("SUDO_UID")
	if isSudoUIDSet {
		userUID, err = strconv.Atoi(sudoUID)
		if err != nil {
			return -1, -1, fmt.Errorf("unable to convert SUDO_UID %q to int", sudoUID)
		}
		sudoGID, isSudoGIDSet := os.LookupEnv("SUDO_GID")
		if isSudoGIDSet {
			userGID, err = strconv.Atoi(sudoGID)
			if err != nil {
				return -1, -1, fmt.Errorf("unable to convert SUDO_GID %q to int", sudoGID)
			}
		}
		// Otherwise just check for the real UID/GID (instead of the effective UID)
	} else {
		userUID = os.Getuid()
		userGID = os.Getgid()
	}

	return userUID, userGID, nil
}

// AdjustFileACLs takes the given fs path, tries to load the access file acl of that path and adds
// ACL rules:
// rwx for the real UID user and r-x for the real GID group.
func AdjustFileACLs(fsPath string) error {
	userUID, userGID, err := GetRealUserIDs()
	if err != nil {
		return fmt.Errorf("unable to retrieve real user UID and GID: %v", err)
	}

	if userUID == 0 && userGID == 0 {
		// We are running as root without sudo, return early
		return nil
	}

	// create a new ACL instance
	a := &acls.ACL{}
	// load the existing ACL entries of the PosixACLAccess type
	err = a.Load(fsPath, acls.PosixACLAccess)
	if err != nil {
		return err
	}

	// add an entry for the group
	err = a.AddEntry(acls.NewEntry(acls.TAG_ACL_GROUP, uint32(userGID), 5))
	if err != nil {
		return err
	}

	// add an entry for the User
	err = a.AddEntry(acls.NewEntry(acls.TAG_ACL_USER, uint32(userUID), 7))
	if err != nil {
		return err
	}

	// set the mask entry
	err = a.AddEntry(acls.NewEntry(acls.TAG_ACL_MASK, math.MaxUint32, 7))
	if err != nil {
		return err
	}

	// apply the ACL and return the error result
	err = a.Apply(fsPath, acls.PosixACLAccess)
	if err != nil {
		return err
	}

	return a.Apply(fsPath, acls.PosixACLDefault)
}

// SetUIDAndGID changes the UID and GID of the given path recursively to the values taken from
// getRealUserIDs,
// which should reflect the non-root user's UID and GID.
func SetUIDAndGID(fsPath string) error {
	userUID, userGID, err := GetRealUserIDs()
	if err != nil {
		return fmt.Errorf("unable to retrieve real user UID and GID: %v", err)
	}

	if userUID == 0 && userGID == 0 {
		// We are running as root without sudo, return early
		return nil
	}

	err = recursiveChown(fsPath, userUID, userGID)
	if err != nil {
		return err
	}

	return nil
}

// recursiveChown function recursively chowns a path.
func recursiveChown(path string, uid, gid int) error {
	return filepath.Walk(path, func(name string, info os.FileInfo, err error) error {
		if err == nil {
			err = os.Chown(name, uid, gid)
		}

		return err
	})
}

// DownloadFilesInterface describes topology paths needed to materialize embedded/downloaded files.
type DownloadFilesInterface interface {
	ClabTmpDir() string
	DownloadFileTmpAbsPath(nodeName string, filenamePostfix string) string
}

// ProcessDownloadableAndEmbeddedFile materializes embedded content or supported download URLs and
// returns a local path. Local file references are returned unchanged.
func ProcessDownloadableAndEmbeddedFile(
	ctx context.Context,
	nodeName,
	fileRef,
	filenamePostfix string,
	paths DownloadFilesInterface,
) (string, error) {
	isEmbeddedFile := strings.Count(fileRef, "\n") >= 1
	isDownloadableFile := IsDownloadableURL(fileRef)

	if !isEmbeddedFile && !isDownloadableFile {
		return fileRef, nil
	}

	CreateDirectory(paths.ClabTmpDir(), clabconstants.PermissionsDirDefault)

	switch {
	case isEmbeddedFile:
		log.Debugf("%q of node %q is an embedded blob", fileRef, nodeName)

		dstFileAbsPath := paths.DownloadFileTmpAbsPath(nodeName, filenamePostfix)
		if err := CreateFile(dstFileAbsPath, fileRef); err != nil {
			return "", err
		}

		return dstFileAbsPath, nil

	case isDownloadableFile:
		fname := FilenameForURL(ctx, fileRef)
		dstFileAbsPath := paths.DownloadFileTmpAbsPath(nodeName, fname)

		log.Debugf("Fetching %q for node %q storing at %q", fileRef, nodeName, dstFileAbsPath)

		out, cleanup, err := CreateFileWithPermissions(
			dstFileAbsPath,
			clabconstants.PermissionsDirDefault,
		)
		if err != nil {
			return "", err
		}
		defer cleanup()

		if err := CopyFileContents(ctx, fileRef, out); err != nil {
			return "", err
		}

		return dstFileAbsPath, nil
	}

	return fileRef, nil
}

// customHostKeyCallback returns a host-key callback that warns on missing or mismatching
// known_hosts entries but does not block the SSH-based download.
func customHostKeyCallback(knownHostsFiles ...string) ssh.HostKeyCallback {
	var useFiles []string
	for _, file := range knownHostsFiles {
		if !FileExists(file) {
			log.Debugf("known_hosts file %s does not exist.", file)
			continue
		}
		useFiles = append(useFiles, file)
	}

	knownHostsFileCallback, err := knownhosts.New(useFiles...)
	if err != nil {
		log.Debugf("error loading known_hosts files %q", strings.Join(knownHostsFiles, ", "))
		knownHostsFileCallback = func(hostname string, remote net.Addr, key ssh.PublicKey) error {
			return fmt.Errorf("error loading known_hosts files %v", err)
		}
	}

	return func(hostname string, remote net.Addr, key ssh.PublicKey) error {
		err = knownHostsFileCallback(hostname, remote, key)
		if err != nil {
			log.Warnf(
				"error performing host key validation based on %q for hostname %q (%v). continuing anyways",
				strings.Join(knownHostsFiles, ", "),
				hostname,
				err,
			)
		}
		return nil
	}
}

var osRelease string

// GetOSRelease returns the OS release of the host by inspecting /etc/*-release files.
func GetOSRelease() string {
	// return cached result
	if osRelease != "" {
		return osRelease
	}
	osRelease = clabconstants.NotApplicable

	matches, err := filepath.Glob("/etc/*-release")
	if err != nil {
		return osRelease
	}

	re := regexp.MustCompile(`(DISTRIB_DESCRIPTION|PRETTY_NAME)="(.*)"`)

	for _, match := range matches {
		data, err := os.ReadFile(match)
		if err != nil {
			log.Error(err)
		}

		match := re.FindSubmatch(data)
		// [0] = whole line match, [1] = left side of "=", [2] = right side of "="
		if len(match) >= 3 {
			osRelease = string(match[2])
			break
		}
	}

	return osRelease
}

// IsPartialConfigFile returns true if the config file name contains .partial substring (case
// insensitive).
func IsPartialConfigFile(configPath string) bool {
	return strings.Contains(strings.ToUpper(configPath), ".PARTIAL")
}

func FileToTarStream(dstFile string, filePath string) (*bytes.Buffer, error) {
	// Check if file exists and get length
	fileStat, err := os.Stat(filePath)
	if err != nil {
		return nil, fmt.Errorf("cannot get file info for %s: %w", filePath, err)
	}

	// Create tar stream to copy (because Docker can only copy from a tar)
	tarBuf := new(bytes.Buffer)
	tarWriter := tar.NewWriter(tarBuf)

	header, err := tar.FileInfoHeader(fileStat, filepath.Base(dstFile))
	if err != nil {
		return nil, fmt.Errorf("cannot create tar file header for %s: %w", fileStat.Name(), err)
	}
	header.Mode = 0o666
	header.Name = filepath.Base(dstFile)
	if err := tarWriter.WriteHeader(header); err != nil {
		return nil, fmt.Errorf("cannot write tar header for %s: %w", filePath, err)
	}

	fileReader, err := os.Open(filePath)
	if err != nil {
		return nil, fmt.Errorf("cannot open %s: %w", filePath, err)
	}
	defer fileReader.Close()

	_, err = io.Copy(tarWriter, fileReader)
	if err != nil {
		return nil, fmt.Errorf("error reading %s: %w", filePath, err)
	}

	tarWriter.Close()

	return tarBuf, nil
}

func WriteToTempFile(contents string) (string, error) {
	tmpFile, err := os.CreateTemp("", "")
	if err != nil {
		return "", fmt.Errorf("unable to create temporary file: %w", err)
	}
	defer tmpFile.Close()

	_, err = tmpFile.WriteString(contents)
	if err != nil {
		return "", fmt.Errorf("unable to write to temporary file: %w", err)
	}

	return tmpFile.Name(), nil
}
