package chezmoi

import (
	"log/slog"
	"os"
	"os/exec"
	"runtime"

	"chezmoi.io/chezmoi/internal/chezmoierrors"
	"chezmoi.io/chezmoi/internal/chezmoilog"
)

// A GPGEncryption uses gpg for encryption and decryption. See
// https://gnupg.org/.
type GPGEncryption struct {
	Command    string   `json:"command"    mapstructure:"command"    yaml:"command"`
	Args       []string `json:"args"       mapstructure:"args"       yaml:"args"`
	Recipient  string   `json:"recipient"  mapstructure:"recipient"  yaml:"recipient"`
	Recipients []string `json:"recipients" mapstructure:"recipients" yaml:"recipients"`
	Symmetric  bool     `json:"symmetric"  mapstructure:"symmetric"  yaml:"symmetric"`
	Suffix     string   `json:"suffix"     mapstructure:"suffix"     yaml:"suffix"`
}

// Decrypt implements Encryption.Decrypt.
func (e *GPGEncryption) Decrypt(ciphertext []byte) ([]byte, error) {
	var plaintext []byte
	if err := withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())
		if err := os.WriteFile(ciphertextAbsPath.String(), ciphertext, 0o600); err != nil {
			return err
		}
		plaintextAbsPath := tempDirAbsPath.JoinString("plaintext")

		args := e.decryptArgs(plaintextAbsPath, ciphertextAbsPath)
		if err := e.run(args); err != nil {
			return err
		}

		var err error
		plaintext, err = os.ReadFile(plaintextAbsPath.String())
		return err
	}); err != nil {
		return nil, err
	}
	return plaintext, nil
}

// DecryptToFile implements Encryption.DecryptToFile.
func (e *GPGEncryption) DecryptToFile(plaintextAbsPath AbsPath, ciphertext []byte) error {
	return withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())
		if err := os.WriteFile(ciphertextAbsPath.String(), ciphertext, 0o600); err != nil {
			return err
		}
		args := e.decryptArgs(plaintextAbsPath, ciphertextAbsPath)
		return e.run(args)
	})
}

// Encrypt implements Encryption.Encrypt.
func (e *GPGEncryption) Encrypt(plaintext []byte) ([]byte, error) {
	var ciphertext []byte
	if err := withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
		plaintextAbsPath := tempDirAbsPath.JoinString("plaintext")
		if err := os.WriteFile(plaintextAbsPath.String(), plaintext, 0o600); err != nil {
			return err
		}
		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())

		args := e.encryptArgs(plaintextAbsPath, ciphertextAbsPath)
		if err := e.run(args); err != nil {
			return err
		}

		var err error
		ciphertext, err = os.ReadFile(ciphertextAbsPath.String())
		return err
	}); err != nil {
		return nil, err
	}
	return ciphertext, nil
}

// EncryptFile implements Encryption.EncryptFile.
func (e *GPGEncryption) EncryptFile(plaintextAbsPath AbsPath) ([]byte, error) {
	var ciphertext []byte
	if err := withPrivateTempDir(func(tempDirAbsPath AbsPath) error {
		ciphertextAbsPath := tempDirAbsPath.JoinString("ciphertext" + e.EncryptedSuffix())

		args := e.encryptArgs(plaintextAbsPath, ciphertextAbsPath)
		if err := e.run(args); err != nil {
			return err
		}

		var err error
		ciphertext, err = os.ReadFile(ciphertextAbsPath.String())
		return err
	}); err != nil {
		return nil, err
	}
	return ciphertext, nil
}

// EncryptedSuffix implements Encryption.EncryptedSuffix.
func (e *GPGEncryption) EncryptedSuffix() string {
	return e.Suffix
}

// decryptArgs returns the arguments for decryption.
func (e *GPGEncryption) decryptArgs(plaintextAbsPath, ciphertextAbsPath AbsPath) []string {
	args := []string{"--output", plaintextAbsPath.String()}
	args = append(args, e.Args...)
	args = append(args, "--decrypt", ciphertextAbsPath.String())
	return args
}

// encryptArgs returns the arguments for encryption.
func (e *GPGEncryption) encryptArgs(plaintextAbsPath, ciphertextAbsPath AbsPath) []string {
	args := []string{
		"--armor",
		"--output", ciphertextAbsPath.String(),
	}
	if e.Symmetric {
		args = append(args, "--symmetric")
	} else {
		if e.Recipient != "" {
			args = append(args, "--recipient", e.Recipient)
		}
		for _, recipient := range e.Recipients {
			args = append(args, "--recipient", recipient)
		}
	}
	args = append(args, e.Args...)
	if !e.Symmetric {
		args = append(args, "--encrypt")
	}
	args = append(args, plaintextAbsPath.String())
	return args
}

// run runs the command with args.
func (e *GPGEncryption) run(args []string) error {
	cmd := exec.Command(e.Command, args...)
	cmd.Stdin = os.Stdin
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return chezmoilog.LogCmdRun(slog.Default(), cmd)
}

// withPrivateTempDir creates a private temporary and calls f.
func withPrivateTempDir(f func(tempDirAbsPath AbsPath) error) (err error) {
	var tempDir string
	if tempDir, err = os.MkdirTemp("", "chezmoi-encryption"); err != nil {
		return err
	}
	defer chezmoierrors.CombineFunc(&err, func() error {
		return os.RemoveAll(tempDir)
	})
	if runtime.GOOS != "windows" {
		if err := os.Chmod(tempDir, 0o700); err != nil {
			return err
		}
	}

	return f(NewAbsPath(tempDir))
}
