package crypto

import (
	"context"
	"crypto/aes"
	"crypto/cipher"
	"crypto/rand"
	"errors"
	"fmt"
	"io"
	"sync"
)

// KeyStore tracks which key versions exist and which one is the current write
// ("active") version. The key material itself is never stored here — only the
// version metadata (see crypto_keys table). It is consulted on Encrypt to pick
// the active version and by the re-encrypt job to discover the target version.
type KeyStore interface {
	// ActiveVersion returns the version that new ciphertext must be sealed with.
	ActiveVersion(ctx context.Context) (int, error)
}

// staticKeyStore is a fixed-version store, used when no DB is wired (tests,
// degraded boot). It always reports the same active version.
type staticKeyStore struct{ v int }

// NewStaticKeyStore returns a KeyStore that always reports version v as active.
func NewStaticKeyStore(v int) KeyStore { return staticKeyStore{v: v} }

func (s staticKeyStore) ActiveVersion(context.Context) (int, error) { return s.v, nil }

// KeyedEncryptor seals/opens secrets with a versioned key. The on-disk format
// is unchanged from the legacy Encryptor — nonce(12) || ciphertext || tag —
// because the key version travels in a separate DB column (key_version), not in
// the ciphertext. This keeps every existing blob forward-compatible: a v1 blob
// is still openable as long as version 1 is recorded for its row.
type KeyedEncryptor struct {
	provider Provider
	store    KeyStore

	mu   sync.Mutex
	gcms map[int]cipher.AEAD // version -> AEAD, lazily built from provider keys
}

// NewKeyedEncryptor builds a KeyedEncryptor over a key provider and key store.
// store may be nil → a static version-1 store is used (back-compat with the
// single-key world).
func NewKeyedEncryptor(provider Provider, store KeyStore) *KeyedEncryptor {
	if store == nil {
		store = NewStaticKeyStore(1)
	}
	return &KeyedEncryptor{provider: provider, store: store, gcms: map[int]cipher.AEAD{}}
}

// gcmFor returns (and caches) the AEAD for a version, resolving the key bytes
// via the provider on first use.
func (e *KeyedEncryptor) gcmFor(ctx context.Context, version int) (cipher.AEAD, error) {
	e.mu.Lock()
	defer e.mu.Unlock()
	if g, ok := e.gcms[version]; ok {
		return g, nil
	}
	key, err := e.provider.KeyMaterial(ctx, version)
	if err != nil {
		return nil, fmt.Errorf("resolve key v%d: %w", version, err)
	}
	if len(key) != 32 {
		return nil, fmt.Errorf("key v%d must be 32 bytes (got %d)", version, len(key))
	}
	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("aes new cipher v%d: %w", version, err)
	}
	g, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("new gcm v%d: %w", version, err)
	}
	e.gcms[version] = g
	return g, nil
}

// ActiveVersion reports the version new ciphertext is sealed with.
func (e *KeyedEncryptor) ActiveVersion(ctx context.Context) (int, error) {
	return e.store.ActiveVersion(ctx)
}

// EncryptVersion seals plaintext with the active version and returns the
// ciphertext together with the version it was sealed under (to persist into the
// row's key_version column).
func (e *KeyedEncryptor) EncryptVersion(ctx context.Context, plaintext []byte) ([]byte, int, error) {
	version, err := e.store.ActiveVersion(ctx)
	if err != nil {
		return nil, 0, fmt.Errorf("active key version: %w", err)
	}
	ct, err := e.sealWith(ctx, version, plaintext)
	if err != nil {
		return nil, 0, err
	}
	return ct, version, nil
}

// SealWithVersion encrypts plaintext under an explicit version. The re-encrypt
// job uses this to re-seal an old-version blob onto the new active version.
func (e *KeyedEncryptor) SealWithVersion(ctx context.Context, version int, plaintext []byte) ([]byte, error) {
	return e.sealWith(ctx, version, plaintext)
}

// sealWith encrypts with a specific version (used by EncryptVersion and the
// re-encrypt job which re-seals onto the new active version).
func (e *KeyedEncryptor) sealWith(ctx context.Context, version int, plaintext []byte) ([]byte, error) {
	g, err := e.gcmFor(ctx, version)
	if err != nil {
		return nil, err
	}
	nonce := make([]byte, g.NonceSize())
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, fmt.Errorf("rand nonce: %w", err)
	}
	return g.Seal(nonce, nonce, plaintext, nil), nil
}

// DecryptVersion opens a ciphertext that was sealed under the given version.
func (e *KeyedEncryptor) DecryptVersion(ctx context.Context, ciphertext []byte, version int) ([]byte, error) {
	g, err := e.gcmFor(ctx, version)
	if err != nil {
		return nil, err
	}
	if len(ciphertext) < g.NonceSize() {
		return nil, errors.New("ciphertext too short")
	}
	nonce, ct := ciphertext[:g.NonceSize()], ciphertext[g.NonceSize():]
	pt, err := g.Open(nil, nonce, ct, nil)
	if err != nil {
		return nil, fmt.Errorf("gcm open v%d: %w", version, err)
	}
	return pt, nil
}

// DecryptAny opens a ciphertext without knowing its key version: it tries the
// active version first, then every lower version down to 1, returning the first
// that authenticates. AES-GCM's auth tag guarantees only the correct key opens a
// blob, so trying versions is safe and lets the read path stay version-agnostic
// (callers need not thread key_version through every SELECT). Version count is
// tiny (1-2 in practice), so the loop is cheap. A version whose key material is
// not configured simply fails to build its AEAD and is skipped.
func (e *KeyedEncryptor) DecryptAny(ctx context.Context, ciphertext []byte) ([]byte, error) {
	active, err := e.store.ActiveVersion(ctx)
	if err != nil || active < 1 {
		active = 1
	}
	var lastErr error
	for v := active; v >= 1; v-- {
		pt, derr := e.DecryptVersion(ctx, ciphertext, v)
		if derr == nil {
			return pt, nil
		}
		lastErr = derr
	}
	if lastErr == nil {
		lastErr = errors.New("decrypt: no key version available")
	}
	return nil, lastErr
}
