//  Copyright 2019 Google Inc. All Rights Reserved.
//
//  Licensed under the Apache License, Version 2.0 (the "License");
//  you may not use this file except in compliance with the License.
//  You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
//  Unless required by applicable law or agreed to in writing, software
//  distributed under the License is distributed on an "AS IS" BASIS,
//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//  See the License for the specific language governing permissions and
//  limitations under the License.

// Package util contains common functions for use in the osconfig agent.
package util

import (
	"bytes"
	"context"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"math/rand"
	"os"
	"os/exec"
	"path/filepath"
	"runtime"
	"strconv"
	"strings"
	"time"

	"github.com/GoogleCloudPlatform/osconfig/clog"
)

// Logger holds log functions.
type Logger struct {
	Debugf   func(string, ...any)
	Infof    func(string, ...any)
	Warningf func(string, ...any)
	Errorf   func(string, ...any)
	Fatalf   func(string, ...any)
}

// SanitizePath ensures that relative path does not contains ".." to avoid directory traversal attacks.
// As well run filepath.Clean to remove redundant path segments.
func SanitizePath(path string) string {
	sanitized := strings.ReplaceAll(path, "../", "")

	return filepath.Clean(sanitized)
}

// NormPath transforms a windows path into an extended-length path as described in
// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
// when not running on windows it will just return the input path.
func NormPath(path string) (string, error) {
	if strings.HasPrefix(path, `\\?\`) {
		return path, nil
	}

	path, err := filepath.Abs(path)
	if err != nil {
		return "", err
	}

	if runtime.GOOS != "windows" {
		return path, nil
	}

	return `\\?\` + strings.ReplaceAll(path, "/", `\`), nil
}

// Exists check for the existence of a file
func Exists(name string) bool {
	if strings.TrimSpace(name) == "" {
		return false
	}
	if _, err := os.Stat(name); err != nil {
		return false
	}
	return true
}

// AtomicWriteFileStream attempts to atomically write data from the provided reader to the path
// checking the checksum if provided.
func AtomicWriteFileStream(r io.Reader, checksum, path string, mode os.FileMode) (string, error) {
	path, err := NormPath(path)
	if err != nil {
		return "", err
	}

	tmp, err := TempFile(filepath.Dir(path), filepath.Base(path), mode)
	if err != nil {
		return "", fmt.Errorf("unable to create temp file: %v", err)
	}

	tmpName := tmp.Name()
	// Make sure we cleanup on any errors.
	defer func() {
		if err != nil {
			tmp.Close()
			os.Remove(tmpName)
		}
	}()

	hasher := sha256.New()
	if _, err = io.Copy(io.MultiWriter(tmp, hasher), r); err != nil {
		return "", err
	}

	computed := hex.EncodeToString(hasher.Sum(nil))
	if checksum != "" && !strings.EqualFold(checksum, computed) {
		return "", fmt.Errorf("got %q for checksum, expected %q", computed, checksum)
	}

	if err := tmp.Close(); err != nil {
		return "", err
	}

	return computed, os.Rename(tmpName, path)
}

// CommandRunner will execute the commands and return the results of that
// execution.
type CommandRunner interface {
	Run(ctx context.Context, command *exec.Cmd) ([]byte, []byte, error)
}

// DefaultRunner is a default CommandRunner.
type DefaultRunner struct{}

// Run takes precreated exec.Cmd and returns the stdout and stderr.
func (r *DefaultRunner) Run(ctx context.Context, cmd *exec.Cmd) ([]byte, []byte, error) {
	clog.Debugf(ctx, "Running %q with args %q\n", cmd.Path, cmd.Args[1:])
	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	err := cmd.Run()
	clog.DebugStructured(
		ctx,
		struct {
			Command  string
			Args     []string
			ExitCode any
			Stdout   string
			Stderr   string
		}{
			Command:  cmd.Path,
			Args:     cmd.Args[1:],
			ExitCode: cmd.ProcessState.ExitCode(),
			Stdout:   stdout.String(),
			Stderr:   stderr.String(),
		},
		"%s %q exit code: %d, output:\n%s", cmd.Path, cmd.Args[1:], cmd.ProcessState.ExitCode(), strings.ReplaceAll(stdout.String(), "\n", "\n "))
	return stdout.Bytes(), stderr.Bytes(), err
}

// TempFile is a little bit like ioutil.TempFile but takes FileMode in
// order to work nicely on Windows where File.Chmod is not supported.
func TempFile(dir string, pattern string, mode os.FileMode) (f *os.File, err error) {
	r := strconv.Itoa(rand.New(rand.NewSource(time.Now().UnixNano())).Intn(99999))
	name := filepath.Join(dir, pattern+r+".tmp")
	return os.OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_EXCL, mode)
}

// AtomicWrite attempts to atomically write a file.
func AtomicWrite(path string, content []byte, mode os.FileMode) (err error) {
	path, err = NormPath(path)
	if err != nil {
		return err
	}

	tmp, err := TempFile(filepath.Dir(path), filepath.Base(path), mode)
	if err != nil {
		return fmt.Errorf("unable to create temp file: %v", err)
	}

	tmpName := tmp.Name()
	// Make sure we cleanup on any errors.
	defer func() {
		if err != nil {
			tmp.Close()
			os.Remove(tmpName)
		}
	}()

	if _, err := tmp.Write(content); err != nil {
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	return os.Rename(tmpName, path)
}
