package config

import (
	"bufio"
	"bytes"
	"context"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"errors"
	"fmt"
	"io"
	"os"
	"os/exec"
	"strings"

	"golang.org/x/crypto/nacl/secretbox"

	"github.com/rclone/rclone/fs"
	"github.com/rclone/rclone/fs/config/obscure"
)

var (
	// Key to use for password en/decryption.
	// When nil, no encryption will be used for saving.
	configKey []byte

	// PasswordPromptOutput is output of prompt for password
	PasswordPromptOutput = os.Stderr

	// PassConfigKeyForDaemonization if set to true, the configKey
	// is obscured with obscure.Obscure and saved to a temp file
	// when it is calculated from the password. The path of that
	// temp file is then written to the environment variable
	// `_RCLONE_CONFIG_KEY_FILE`. If `_RCLONE_CONFIG_KEY_FILE` is
	// present, password prompt is skipped and
	// `RCLONE_CONFIG_PASS` ignored. For security reasons, the
	// temp file is deleted once the configKey is successfully
	// loaded. This can be used to pass the configKey to a child
	// process.
	PassConfigKeyForDaemonization = false
)

// IsEncrypted returns true if the config file is encrypted
func IsEncrypted() bool {
	return len(configKey) > 0
}

// Decrypt will automatically decrypt a reader
func Decrypt(b io.ReadSeeker) (io.Reader, error) {
	ctx := context.Background()
	ci := fs.GetConfig(ctx)
	var usingPasswordCommand bool
	var usingEnvPassword bool

	// Find first non-empty line
	r := bufio.NewReader(b)
	for {
		line, _, err := r.ReadLine()
		if err != nil {
			if err == io.EOF {
				if _, err := b.Seek(0, io.SeekStart); err != nil {
					return nil, err
				}
				return b, nil
			}
			return nil, err
		}
		l := strings.TrimSpace(string(line))
		if len(l) == 0 || strings.HasPrefix(l, ";") || strings.HasPrefix(l, "#") {
			continue
		}
		// First non-empty or non-comment must be ENCRYPT_V0
		if l == "RCLONE_ENCRYPT_V0:" {
			break
		}
		if strings.HasPrefix(l, "RCLONE_ENCRYPT_V") {
			return nil, errors.New("unsupported configuration encryption - update rclone for support")
		}
		// Restore non-seekable plain-text stream to its original state
		if _, err := b.Seek(0, io.SeekStart); err != nil {
			return io.MultiReader(strings.NewReader(l+"\n"), r), nil
		}
		return b, nil
	}

	if len(configKey) == 0 {
		pass, err := GetPasswordCommand(ctx)
		if err != nil {
			return nil, err
		}
		if pass != "" {
			usingPasswordCommand = true
			err = SetConfigPassword(pass)
			if err != nil {
				return nil, fmt.Errorf("incorrect password: %w", err)
			}
			if len(configKey) == 0 {
				return nil, errors.New("unable to decrypt configuration: incorrect password")
			}
		} else {
			usingPasswordCommand = false

			envPassword := os.Getenv("RCLONE_CONFIG_PASS")

			if envPassword != "" {
				usingEnvPassword = true
				err := SetConfigPassword(envPassword)
				if err != nil {
					fs.Errorf(nil, "Using RCLONE_CONFIG_PASS returned: %v", err)
				} else {
					fs.Debugf(nil, "Using RCLONE_CONFIG_PASS password.")
				}
			} else {
				usingEnvPassword = false
			}
		}
	}

	// Encrypted content is base64 encoded.
	dec := base64.NewDecoder(base64.StdEncoding, r)
	box, err := io.ReadAll(dec)
	if err != nil {
		return nil, fmt.Errorf("failed to load base64 encoded data: %w", err)
	}
	if len(box) < 24+secretbox.Overhead {
		return nil, errors.New("configuration data too short")
	}

	var out []byte
	for {
		if envKeyFile := os.Getenv("_RCLONE_CONFIG_KEY_FILE"); len(envKeyFile) > 0 {
			fs.Debugf(nil, "attempting to obtain configKey from temp file %s", envKeyFile)
			obscuredKey, err := os.ReadFile(envKeyFile)
			if err != nil {
				errRemove := os.Remove(envKeyFile)
				if errRemove != nil {
					return nil, fmt.Errorf("unable to read obscured config key and unable to delete the temp file: %w", err)
				}
				return nil, fmt.Errorf("unable to read obscured config key: %w", err)
			}
			errRemove := os.Remove(envKeyFile)
			if errRemove != nil {
				return nil, fmt.Errorf("unable to delete temp file with configKey: %w", errRemove)
			}
			configKey = []byte(obscure.MustReveal(string(obscuredKey)))
			fs.Debugf(nil, "using _RCLONE_CONFIG_KEY_FILE for configKey")
		} else if len(configKey) == 0 {
			if usingPasswordCommand {
				return nil, errors.New("using --password-command derived password, unable to decrypt configuration")
			}
			if usingEnvPassword {
				return nil, errors.New("using RCLONE_CONFIG_PASS env password, unable to decrypt configuration")
			}
			if !ci.AskPassword {
				return nil, errors.New("unable to decrypt configuration and not allowed to ask for password - set RCLONE_CONFIG_PASS to your configuration password")
			}
			getConfigPassword("Enter configuration password:")
		}

		// Nonce is first 24 bytes of the ciphertext
		var nonce [24]byte
		copy(nonce[:], box[:24])
		var key [32]byte
		copy(key[:], configKey[:32])

		// Attempt to decrypt
		var ok bool
		out, ok = secretbox.Open(nil, box[24:], &nonce, &key)
		if ok {
			break
		}

		// Retry
		fs.Errorf(nil, "Couldn't decrypt configuration, most likely wrong password.")
		configKey = nil
	}
	return bytes.NewReader(out), nil
}

// GetPasswordCommand gets the password using the --password-command setting
//
// If the --password-command flag was not in use it returns "", nil
func GetPasswordCommand(ctx context.Context) (pass string, err error) {
	ci := fs.GetConfig(ctx)
	if len(ci.PasswordCommand) == 0 {
		return "", nil
	}

	var stdout bytes.Buffer
	var stderr bytes.Buffer

	cmd := exec.Command(ci.PasswordCommand[0], ci.PasswordCommand[1:]...)

	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	cmd.Stdin = os.Stdin

	err = cmd.Run()
	if err != nil {
		// One does not always get the stderr returned in the wrapped error.
		fs.Errorf(nil, "Using --password-command returned: %v", err)
		if ers := strings.TrimSpace(stderr.String()); ers != "" {
			fs.Errorf(nil, "--password-command stderr: %s", ers)
		}
		return pass, fmt.Errorf("password command failed: %w", err)
	}
	pass = strings.Trim(stdout.String(), "\r\n")
	if pass == "" {
		return pass, errors.New("--password-command returned empty string")
	}
	return pass, nil
}

// Encrypt the config file
func Encrypt(src io.Reader, dst io.Writer) error {
	if len(configKey) == 0 {
		_, err := io.Copy(dst, src)
		return err
	}

	_, _ = fmt.Fprintln(dst, "# Encrypted rclone configuration File")
	_, _ = fmt.Fprintln(dst, "")
	_, _ = fmt.Fprintln(dst, "RCLONE_ENCRYPT_V0:")

	// Generate new nonce and write it to the start of the ciphertext
	var nonce [24]byte
	n, _ := rand.Read(nonce[:])
	if n != 24 {
		return fmt.Errorf("nonce short read: %d", n)
	}
	enc := base64.NewEncoder(base64.StdEncoding, dst)
	_, err := enc.Write(nonce[:])
	if err != nil {
		return fmt.Errorf("failed to write config file: %w", err)
	}

	var key [32]byte
	copy(key[:], configKey[:32])

	data, err := io.ReadAll(src)
	if err != nil {
		return err
	}
	b := secretbox.Seal(nil, data, &nonce, &key)
	_, err = enc.Write(b)
	if err != nil {
		return fmt.Errorf("failed to write config file: %w", err)
	}
	return enc.Close()
}

// getConfigPassword will query the user for a password the
// first time it is required.
func getConfigPassword(q string) {
	if len(configKey) != 0 {
		return
	}
	for {
		password := GetPassword(q)
		err := SetConfigPassword(password)
		if err == nil {
			return
		}
		_, _ = fmt.Fprintln(os.Stderr, "Error:", err)
	}
}

// SetConfigPassword will set the configKey to the hash of
// the password. If the length of the password is
// zero after trimming+normalization, an error is returned.
func SetConfigPassword(password string) error {
	password, err := checkPassword(password)
	if err != nil {
		return err
	}
	// Create SHA256 has of the password
	sha := sha256.New()
	_, err = sha.Write([]byte("[" + password + "][rclone-config]"))
	if err != nil {
		return err
	}
	configKey = sha.Sum(nil)
	if PassConfigKeyForDaemonization {
		tempFile, err := os.CreateTemp("", "rclone")
		if err != nil {
			return fmt.Errorf("cannot create temp file to store configKey: %w", err)
		}
		_, err = tempFile.WriteString(obscure.MustObscure(string(configKey)))
		if err != nil {
			errRemove := os.Remove(tempFile.Name())
			if errRemove != nil {
				return fmt.Errorf("error writing configKey to temp file and also error deleting it: %w", err)
			}
			return fmt.Errorf("error writing configKey to temp file: %w", err)
		}
		err = tempFile.Close()
		if err != nil {
			errRemove := os.Remove(tempFile.Name())
			if errRemove != nil {
				return fmt.Errorf("error closing temp file with configKey and also error deleting it: %w", err)
			}
			return fmt.Errorf("error closing temp file with configKey: %w", err)
		}
		fs.Debugf(nil, "saving configKey to temp file")
		err = os.Setenv("_RCLONE_CONFIG_KEY_FILE", tempFile.Name())
		if err != nil {
			errRemove := os.Remove(tempFile.Name())
			if errRemove != nil {
				return fmt.Errorf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE and unable to delete the temp file: %w", err)
			}
			return fmt.Errorf("unable to set environment variable _RCLONE_CONFIG_KEY_FILE: %w", err)
		}
	}
	return nil
}

// ClearConfigPassword sets the current the password to empty
func ClearConfigPassword() {
	configKey = nil
}

// changeConfigPassword will query the user twice
// for a password. If the same password is entered
// twice the key is updated.
//
// This will use --password-command if configured to read the password.
func changeConfigPassword() {
	// Set RCLONE_PASSWORD_CHANGE to "1" when calling the --password-command tool
	_ = os.Setenv("RCLONE_PASSWORD_CHANGE", "1")
	defer func() {
		_ = os.Unsetenv("RCLONE_PASSWORD_CHANGE")
	}()
	pass, err := GetPasswordCommand(context.Background())
	if err != nil {
		fmt.Printf("Failed to read new password with --password-command: %v\n", err)
		return
	}
	if pass == "" {
		pass = ChangePassword("NEW configuration")
	} else {
		fmt.Printf("Read password using --password-command\n")
	}
	err = SetConfigPassword(pass)
	if err != nil {
		fmt.Printf("Failed to set config password: %v\n", err)
		return
	}
}

// ChangeConfigPasswordAndSave will query the user twice
// for a password. If the same password is entered
// twice the key is updated.
//
// This will use --password-command if configured to read the password.
//
// It will then save the config
func ChangeConfigPasswordAndSave() {
	changeConfigPassword()
	SaveConfig()
}

// RemoveConfigPasswordAndSave will clear the config password and save
// the unencrypted config file.
func RemoveConfigPasswordAndSave() {
	configKey = nil
	SaveConfig()
}
