package cryptokit

import (
	"bytes"
	"errors"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
)

const maxOpenPGPMessageBytes = 64 << 20

type limitedBuffer struct {
	buffer bytes.Buffer
	limit  int
}

func (b *limitedBuffer) Write(value []byte) (int, error) {
	if b.buffer.Len() < b.limit {
		remaining := b.limit - b.buffer.Len()
		if len(value) > remaining {
			_, _ = b.buffer.Write(value[:remaining])
		} else {
			_, _ = b.buffer.Write(value)
		}
	}
	return len(value), nil
}

// OpenPGP support is delegated to the system GnuPG binary. This keeps Aegis
// compatible with the user's existing OpenPGP installation, including RSA,
// Ed25519 and X25519 keys, without embedding another keyring. Key material is
// imported into a temporary 0700 GNUPGHOME and removed after each operation.

func gpgBinary() (string, error) {
	path, err := exec.LookPath("gpg")
	if err != nil {
		return "", errors.New("gpg is required for OpenPGP operations")
	}
	return path, nil
}

func withGPG(operation func(home string) error) error {
	if _, err := gpgBinary(); err != nil {
		return err
	}
	home, err := os.MkdirTemp("", "aegis-gpg-")
	if err != nil {
		return fmt.Errorf("create temporary OpenPGP home: %w", err)
	}
	defer os.RemoveAll(home)
	if err := os.Chmod(home, 0o700); err != nil {
		return fmt.Errorf("protect temporary OpenPGP home: %w", err)
	}
	return operation(home)
}

func runGPG(home string, args []string, input []byte, outputLimit int) ([]byte, string, error) {
	binary, err := gpgBinary()
	if err != nil {
		return nil, "", err
	}
	base := []string{
		"--batch", "--no-tty", "--no-options", "--no-auto-check-trustdb",
		"--homedir", home,
	}
	cmd := exec.Command(binary, append(base, args...)...)
	cmd.Stdin = bytes.NewReader(input)
	var stdout limitedBuffer
	var stderr bytes.Buffer
	if outputLimit <= 0 {
		outputLimit = maxOpenPGPMessageBytes
	}
	stdout.limit = outputLimit + 1
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr
	if err := cmd.Run(); err != nil {
		return stdout.buffer.Bytes(), stderr.String(), fmt.Errorf("gpg %s: %w: %s", args[0], err, cleanGPGError(stderr.String()))
	}
	if stdout.buffer.Len() > outputLimit {
		return nil, stderr.String(), fmt.Errorf("OpenPGP output exceeds the %d MiB limit", outputLimit/(1<<20))
	}
	return stdout.buffer.Bytes(), stderr.String(), nil
}

func cleanGPGError(value string) string {
	value = strings.TrimSpace(value)
	if len(value) > 1000 {
		return value[:1000] + "..."
	}
	return value
}

func importOpenPGP(home, keyMaterial string) error {
	if strings.TrimSpace(keyMaterial) == "" {
		return errors.New("OpenPGP key material is required")
	}
	_, stderr, err := runGPG(home, []string{"--import"}, []byte(keyMaterial), 1<<20)
	if err != nil {
		return fmt.Errorf("import OpenPGP key: %w", err)
	}
	if strings.Contains(stderr, "no valid OpenPGP data found") {
		return errors.New("no valid OpenPGP key found")
	}
	return nil
}

func listFingerprints(home string) ([]string, error) {
	output, _, err := runGPG(home, []string{"--with-colons", "--list-keys"}, nil, 1<<20)
	if err != nil {
		return nil, fmt.Errorf("list OpenPGP keys: %w", err)
	}
	var fingerprints []string
	for _, line := range strings.Split(string(output), "\n") {
		fields := strings.Split(line, ":")
		if len(fields) > 9 && fields[0] == "fpr" && fields[9] != "" {
			fingerprints = append(fingerprints, fields[9])
		}
	}
	if len(fingerprints) == 0 {
		return nil, errors.New("no usable OpenPGP key found")
	}
	return fingerprints, nil
}

func writeGPGFile(home, name string, content []byte) (string, error) {
	path := filepath.Join(home, name)
	file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
	if err != nil {
		return "", fmt.Errorf("create temporary OpenPGP file: %w", err)
	}
	if _, err := file.Write(content); err != nil {
		_ = file.Close()
		return "", fmt.Errorf("write temporary OpenPGP file: %w", err)
	}
	if err := file.Close(); err != nil {
		return "", fmt.Errorf("close temporary OpenPGP file: %w", err)
	}
	return path, nil
}

// SignOpenPGPDetached returns an ASCII-armored detached signature. The input
// must contain a private signing key supplied by the user.
func SignOpenPGPDetached(message []byte, privateKeyArmor string) (string, error) {
	var signature []byte
	err := withGPG(func(home string) error {
		if err := importOpenPGP(home, privateKeyArmor); err != nil {
			return err
		}
		output, _, err := runGPG(home, []string{"--armor", "--detach-sign", "--output", "-"}, message, 1<<20)
		if err != nil {
			return fmt.Errorf("sign OpenPGP message: %w", err)
		}
		signature = output
		return nil
	})
	return string(signature), err
}

// VerifyOpenPGPDetached verifies an ASCII-armored or binary detached
// signature using public or private OpenPGP key material supplied by the user.
func VerifyOpenPGPDetached(message []byte, signature, publicKeyArmor string) error {
	return withGPG(func(home string) error {
		if err := importOpenPGP(home, publicKeyArmor); err != nil {
			return err
		}
		signaturePath, err := writeGPGFile(home, "signature.asc", []byte(signature))
		if err != nil {
			return err
		}
		messagePath, err := writeGPGFile(home, "message.bin", message)
		if err != nil {
			return err
		}
		if _, _, err := runGPG(home, []string{"--verify", signaturePath, messagePath}, nil, 1<<20); err != nil {
			return fmt.Errorf("verify OpenPGP signature: %w", err)
		}
		return nil
	})
}

// EncryptOpenPGP encrypts a message to every entity in the supplied armored
// public key ring. If privateSignerArmor is non-empty, it also signs the
// message with the supplied private key. The result is ASCII armored.
func EncryptOpenPGP(message []byte, recipientKeyArmor, privateSignerArmor string) (string, error) {
	var encrypted []byte
	err := withGPG(func(home string) error {
		if err := importOpenPGP(home, recipientKeyArmor); err != nil {
			return err
		}
		recipientFingerprints, err := listFingerprints(home)
		if err != nil {
			return err
		}
		args := []string{"--armor", "--trust-model", "always", "--encrypt", "--output", "-"}
		for _, fingerprint := range recipientFingerprints {
			args = append(args, "--recipient", fingerprint)
		}
		if strings.TrimSpace(privateSignerArmor) != "" {
			if err := importOpenPGP(home, privateSignerArmor); err != nil {
				return err
			}
			signerFingerprints, err := listFingerprints(home)
			if err != nil {
				return err
			}
			args = append(args, "--sign", "--local-user", signerFingerprints[0])
		}
		output, _, err := runGPG(home, args, message, maxOpenPGPMessageBytes)
		if err != nil {
			return fmt.Errorf("encrypt OpenPGP message: %w", err)
		}
		encrypted = output
		return nil
	})
	return string(encrypted), err
}

// DecryptOpenPGP decrypts an ASCII-armored or binary OpenPGP message with a
// user-provided private key. A bad embedded signature is rejected.
func DecryptOpenPGP(message []byte, privateKeyArmor string) ([]byte, error) {
	var plaintext []byte
	err := withGPG(func(home string) error {
		if err := importOpenPGP(home, privateKeyArmor); err != nil {
			return err
		}
		output, status, err := runGPG(home, []string{"--status-fd", "2", "--decrypt", "--output", "-"}, message, maxOpenPGPMessageBytes)
		if strings.Contains(status, "[GNUPG:] BADSIG") || strings.Contains(status, "[GNUPG:] ERRSIG") {
			return errors.New("authenticate OpenPGP message: invalid signature")
		}
		if err != nil {
			return fmt.Errorf("decrypt OpenPGP message: %w", err)
		}
		plaintext = output
		return nil
	})
	return plaintext, err
}
