package cryptokit

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

const maxYubiCryptMessageBytes = 64 << 20

type limitedBuffer struct {
	buffer bytes.Buffer
	limit  int
}

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

var yubiCryptCandidates = []string{
	"/home/gabriel1/bin/yubicrypt",
	"/home/gabriel1/bin/yubicrpt-cli",
	"/home/gabriel1/Projects/yubicrpt-cli/yubicrypt",
	"/home/gabriel1/Projects/yubicrpt-cli/yubicrpt-cli",
	"/usr/local/bin/yubicrypt",
	"/usr/bin/yubicrypt",
}

// FindYubiCryptCLI returns the optional yubicrypt-cli executable. An explicit
// AEGIS_YUBICRYPT_CLI path takes precedence over the standard locations.
func FindYubiCryptCLI() (string, error) {
	if configured := strings.TrimSpace(os.Getenv("AEGIS_YUBICRYPT_CLI")); configured != "" {
		if isExecutableBinary(configured) {
			return configured, nil
		}
		return "", fmt.Errorf("AEGIS_YUBICRYPT_CLI is not executable: %s", configured)
	}
	for _, candidate := range yubiCryptCandidates {
		if isExecutableBinary(candidate) {
			return candidate, nil
		}
	}
	if path, err := exec.LookPath("yubicrypt"); err == nil {
		return path, nil
	}
	return "", errors.New("yubicrypt-cli executable not found; set AEGIS_YUBICRYPT_CLI or install yubicrypt")
}

func isExecutableBinary(path string) bool {
	info, err := os.Stat(path)
	return err == nil && info.Mode().IsRegular() && info.Mode()&0o111 != 0
}

func runYubiCrypt(args []string, input []byte) ([]byte, error) {
	executable, err := FindYubiCryptCLI()
	if err != nil {
		return nil, err
	}
	command := exec.Command(executable, args...)
	command.Stdin = bytes.NewReader(input)
	var stdout limitedBuffer
	var stderr bytes.Buffer
	stdout.limit = maxYubiCryptMessageBytes + 1
	command.Stdout = &stdout
	command.Stderr = &stderr
	if err := command.Run(); err != nil {
		return nil, fmt.Errorf("yubicrypt %s: %w: %s", firstCommandArg(args), err, cleanCommandError(stderr.String()))
	}
	if stdout.buffer.Len() > maxYubiCryptMessageBytes {
		return nil, errors.New("yubicrypt output exceeds the 64 MiB limit")
	}
	return stdout.buffer.Bytes(), nil
}

func firstCommandArg(args []string) string {
	if len(args) == 0 {
		return "operation"
	}
	return args[0]
}

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

func withYubiTemp(operation func(directory string) error) error {
	directory, err := os.MkdirTemp("", "aegis-yubicrypt-")
	if err != nil {
		return fmt.Errorf("create temporary YubiCrypt directory: %w", err)
	}
	defer os.RemoveAll(directory)
	if err := os.Chmod(directory, 0o700); err != nil {
		return fmt.Errorf("protect temporary YubiCrypt directory: %w", err)
	}
	return operation(directory)
}

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

func checkYubiMessageSize(message []byte) error {
	if len(message) > maxYubiCryptMessageBytes {
		return errors.New("message exceeds the 64 MiB limit")
	}
	return nil
}

func requireYubiPIN(pin string) ([]byte, error) {
	pin = strings.TrimRight(pin, "\r\n")
	if pin == "" {
		return nil, errors.New("YubiKey PIV PIN is required")
	}
	return []byte(pin + "\n"), nil
}

// SignYubiCrypt signs with the YubiKey PIV slot 9c. The PIN is sent through
// stdin to yubicrypt-cli and is not placed in command arguments or logs.
func SignYubiCrypt(message []byte, pin string) ([]byte, error) {
	if err := checkYubiMessageSize(message); err != nil {
		return nil, err
	}
	pinInput, err := requireYubiPIN(pin)
	if err != nil {
		return nil, err
	}
	var result []byte
	err = withYubiTemp(func(directory string) error {
		messagePath, err := writeYubiTemp(directory, "message.bin", message)
		if err != nil {
			return err
		}
		result, err = runYubiCrypt([]string{
			"sign", "--quiet", "--pin-stdin", "--input", messagePath, "--output", "-",
		}, pinInput)
		if err != nil {
			return fmt.Errorf("YubiCrypt signing failed: %w", err)
		}
		return nil
	})
	return result, err
}

// VerifyYubiCrypt verifies a yubicrypt signature block and returns the
// original message only after the CLI has authenticated it.
func VerifyYubiCrypt(signedMessage []byte) ([]byte, error) {
	if err := checkYubiMessageSize(signedMessage); err != nil {
		return nil, err
	}
	var result []byte
	err := withYubiTemp(func(directory string) error {
		signedPath, err := writeYubiTemp(directory, "signed.yc", signedMessage)
		if err != nil {
			return err
		}
		result, err = runYubiCrypt([]string{
			"verify", "--quiet", "--input", signedPath, "--message-output", "-",
		}, nil)
		if err != nil {
			return fmt.Errorf("YubiCrypt verification failed: %w", err)
		}
		return nil
	})
	return result, err
}
