// Package crypto provides NaCl sealed-box encryption for GitHub Actions secrets.
// The CLI encrypts secret values with the server's public key using box.SealAnonymous;
// this package generates the keypair and decrypts the sealed boxes.
//
// Configuration:
//   - SECRET_ENCRYPTION_KEY: base64-encoded 32-byte private key for
//     load-balanced deployments. When set, all server instances use the same
//     keypair.
//   - When unset, a new keypair is generated at startup (single-node mode only).
//
// Failure behavior:
//   - If SECRET_ENCRYPTION_KEY is set but invalid (wrong length, bad base64),
//     the server panics at startup with a descriptive error.
//   - In multi-instance mode, all instances MUST share the same key; otherwise
//     secrets encrypted against one instance cannot be decrypted by another.
package crypto

import (
	"crypto/rand"
	"encoding/base64"
	"fmt"

	"golang.org/x/crypto/curve25519"
	"golang.org/x/crypto/nacl/box"
)

// SecretBox is one tenant's NaCl sealed-box keypair.
type SecretBox struct {
	publicKey  [32]byte
	privateKey [32]byte
}

// NewSecretBox loads a configured private key or generates an ephemeral
// keypair when keyB64 is empty.
func NewSecretBox(keyB64 string) (*SecretBox, error) {
	if keyB64 == "" {
		pub, priv, err := box.GenerateKey(rand.Reader)
		if err != nil {
			return nil, fmt.Errorf("failed to generate NaCl keypair: %w", err)
		}
		return &SecretBox{publicKey: *pub, privateKey: *priv}, nil
	}

	keyBytes, err := base64.StdEncoding.DecodeString(keyB64)
	if err != nil {
		return nil, fmt.Errorf("SECRET_ENCRYPTION_KEY is not valid base64: %w", err)
	}
	if len(keyBytes) != 32 {
		return nil, fmt.Errorf("SECRET_ENCRYPTION_KEY must be 32 bytes, got %d", len(keyBytes))
	}

	privArray := [32]byte(keyBytes)
	var pubArray [32]byte
	curve25519.ScalarBaseMult(&pubArray, &privArray)

	return &SecretBox{publicKey: pubArray, privateKey: privArray}, nil
}

// PublicKeyBase64 returns the base64-encoded 32-byte Curve25519 public key.
func (s *SecretBox) PublicKeyBase64() string {
	return base64.StdEncoding.EncodeToString(s.publicKey[:])
}

// PublicKeyID returns a stable identifier for the current public key.
func (s *SecretBox) PublicKeyID() string {
	return base64.RawStdEncoding.EncodeToString(s.publicKey[:8])
}

// EncryptSecret encrypts a plaintext value using NaCl sealed box and returns base64-encoded ciphertext.
func (s *SecretBox) EncryptSecret(plaintext string) (string, error) {
	return EncryptWithPublicKey(s.PublicKeyBase64(), plaintext)
}

// EncryptWithPublicKey encrypts plaintext exactly as an Actions client does,
// without requiring access to the tenant's private key.
func EncryptWithPublicKey(publicKeyBase64, plaintext string) (string, error) {
	keyBytes, err := base64.StdEncoding.DecodeString(publicKeyBase64)
	if err != nil {
		return "", fmt.Errorf("crypto: public key is not valid base64: %w", err)
	}
	if len(keyBytes) != 32 {
		return "", fmt.Errorf("crypto: public key must be 32 bytes, got %d", len(keyBytes))
	}
	publicKey := [32]byte(keyBytes)
	encrypted, err := box.SealAnonymous(nil, []byte(plaintext), &publicKey, rand.Reader)
	if err != nil {
		return "", fmt.Errorf("crypto: encrypt failed: %w", err)
	}
	return base64.StdEncoding.EncodeToString(encrypted), nil
}

// DecryptSecret decrypts a base64-encoded NaCl sealed box and returns the plaintext.
func (s *SecretBox) DecryptSecret(encryptedBase64 string) (string, error) {
	encrypted, err := base64.StdEncoding.DecodeString(encryptedBase64)
	if err != nil {
		return "", fmt.Errorf("crypto: base64 decode failed: %w", err)
	}

	decrypted, ok := box.OpenAnonymous(nil, encrypted, &s.publicKey, &s.privateKey)
	if !ok {
		return "", fmt.Errorf("crypto: failed to open sealed box")
	}

	return string(decrypted), nil
}
