// Package secretbox encrypts member-owned secrets for persistence.
package secretbox

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"encoding/base64"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"strings"
)

const prefix = "enc:v1:"

var (
	ErrInvalidConfig      = errors.New("invalid secretbox configuration")
	ErrInvalidCiphertext  = errors.New("invalid encrypted secret")
	ErrUnsupportedVersion = errors.New("unsupported encrypted secret version")
)

// Codec is an AES-256-GCM codec for the current encryption key.
type Codec struct {
	keyID string
	aead  cipher.AEAD
	rand  io.Reader
}

// New constructs a codec from a key ID and a base64-encoded 32-byte key.
func New(keyID, encodedKey string) (*Codec, error) {
	keyID = strings.TrimSpace(keyID)
	encodedKey = strings.TrimSpace(encodedKey)
	if keyID == "" || strings.Contains(keyID, ":") || encodedKey == "" {
		return nil, fmt.Errorf("%w: key ID and key are required", ErrInvalidConfig)
	}
	key, err := base64.StdEncoding.DecodeString(encodedKey)
	if err != nil {
		return nil, fmt.Errorf("%w: key must be base64: %v", ErrInvalidConfig, err)
	}
	if len(key) != 32 {
		return nil, fmt.Errorf("%w: decoded key must be 32 bytes", ErrInvalidConfig)
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
	}
	return &Codec{keyID: keyID, aead: aead, rand: rand.Reader}, nil
}

// Encrypt encrypts plaintext and binds it to its member, field, and provider.
func (c *Codec) Encrypt(uid int64, field, provider, plaintext string) (string, error) {
	if c == nil || c.aead == nil {
		return "", ErrInvalidConfig
	}
	nonce := make([]byte, c.aead.NonceSize())
	if _, err := io.ReadFull(c.rand, nonce); err != nil {
		return "", fmt.Errorf("generate encryption nonce: %w", err)
	}
	sealed := c.aead.Seal(nil, nonce, []byte(plaintext), additionalData(uid, field, provider))
	payload := append(nonce, sealed...)
	return prefix + c.keyID + ":" + base64.RawURLEncoding.EncodeToString(payload), nil
}

// Decrypt decrypts encrypted values. Unprefixed values are legacy plaintext.
func (c *Codec) Decrypt(uid int64, field, provider, value string) (string, error) {
	if !strings.HasPrefix(value, "enc:") {
		return value, nil
	}
	if !strings.HasPrefix(value, prefix) {
		return "", ErrUnsupportedVersion
	}
	if c == nil || c.aead == nil {
		return "", ErrInvalidConfig
	}
	rest := strings.TrimPrefix(value, prefix)
	keyID, payloadText, ok := strings.Cut(rest, ":")
	if !ok || keyID == "" || payloadText == "" {
		return "", ErrInvalidCiphertext
	}
	if keyID != c.keyID {
		return "", fmt.Errorf("%w: unknown key ID %q", ErrInvalidCiphertext, keyID)
	}
	payload, err := base64.RawURLEncoding.DecodeString(payloadText)
	if err != nil || len(payload) < c.aead.NonceSize()+c.aead.Overhead() {
		return "", ErrInvalidCiphertext
	}
	nonce := payload[:c.aead.NonceSize()]
	ciphertext := payload[c.aead.NonceSize():]
	plaintext, err := c.aead.Open(nil, nonce, ciphertext, additionalData(uid, field, provider))
	if err != nil {
		return "", fmt.Errorf("%w: authentication failed", ErrInvalidCiphertext)
	}
	return string(plaintext), nil
}

// IsEncrypted reports whether a value uses the encrypted-value namespace.
func IsEncrypted(value string) bool {
	return strings.HasPrefix(value, "enc:")
}

func additionalData(uid int64, field, provider string) []byte {
	fieldBytes := []byte(field)
	providerBytes := []byte(provider)
	data := make([]byte, 8+4+len(fieldBytes)+4+len(providerBytes))
	binary.BigEndian.PutUint64(data, uint64(uid))
	offset := 8
	binary.BigEndian.PutUint32(data[offset:], uint32(len(fieldBytes)))
	offset += 4
	copy(data[offset:], fieldBytes)
	offset += len(fieldBytes)
	binary.BigEndian.PutUint32(data[offset:], uint32(len(providerBytes)))
	offset += 4
	copy(data[offset:], providerBytes)
	return data
}
