// Package crypto provides key management and encryption helpers for MCIAS.
//
// Security design:
//   - All random material (keys, nonces, salts) comes from crypto/rand.
//   - AES-256-GCM is used for symmetric encryption; the 256-bit key size
//     provides 128-bit post-quantum security margin.
//   - Ed25519 is used for JWT signing; it has no key-size or parameter
//     malleability issues that affect RSA/ECDSA.
//   - The master key KDF uses Argon2id (separate parameterisation from
//     password hashing) to derive a 256-bit key from a passphrase.
package crypto

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/ed25519"
	"crypto/rand"
	"crypto/x509"
	"encoding/pem"
	"errors"
	"fmt"
	"io"

	"golang.org/x/crypto/argon2"
)

const (
	// aesKeySize is 32 bytes = 256-bit AES key.
	aesKeySize = 32
	// gcmNonceSize is the standard 96-bit GCM nonce.
	gcmNonceSize = 12
	// kdfSaltSize is 32 bytes for the Argon2id salt.
	kdfSaltSize = 32

	// kdfTime and kdfMemory are the Argon2id parameters used for master key
	// derivation. These are separate from password hashing parameters and are
	// chosen to be expensive enough to resist offline attack on the passphrase.
	// Security: OWASP 2023 recommends time=2, memory=64MiB as minimum.
	// We use time=3, memory=64MiB, threads=4 as the operational default for
	// password hashing (configured in mcias.toml).
	// For master key derivation, we hardcode time=3, memory=128MiB, threads=4
	// since this only runs at server startup.
	kdfTime    = 3
	kdfMemory  = 128 * 1024 // 128 MiB in KiB
	kdfThreads = 4
)

// GenerateEd25519KeyPair generates a new Ed25519 key pair using crypto/rand.
// Security: Ed25519 key generation is deterministic given the seed; crypto/rand
// provides the cryptographically-secure seed.
func GenerateEd25519KeyPair() (ed25519.PublicKey, ed25519.PrivateKey, error) {
	pub, priv, err := ed25519.GenerateKey(rand.Reader)
	if err != nil {
		return nil, nil, fmt.Errorf("crypto: generate Ed25519 key pair: %w", err)
	}
	return pub, priv, nil
}

// MarshalPrivateKeyPEM encodes an Ed25519 private key as a PKCS#8 PEM block.
func MarshalPrivateKeyPEM(key ed25519.PrivateKey) ([]byte, error) {
	der, err := x509.MarshalPKCS8PrivateKey(key)
	if err != nil {
		return nil, fmt.Errorf("crypto: marshal private key DER: %w", err)
	}
	return pem.EncodeToMemory(&pem.Block{
		Type:  "PRIVATE KEY",
		Bytes: der,
	}), nil
}

// ParsePrivateKeyPEM decodes a PKCS#8 PEM-encoded Ed25519 private key.
// Returns an error if the PEM block is missing, malformed, or not an Ed25519 key.
func ParsePrivateKeyPEM(pemData []byte) (ed25519.PrivateKey, error) {
	block, _ := pem.Decode(pemData)
	if block == nil {
		return nil, errors.New("crypto: no PEM block found")
	}
	if block.Type != "PRIVATE KEY" {
		return nil, fmt.Errorf("crypto: unexpected PEM block type %q, want %q", block.Type, "PRIVATE KEY")
	}

	key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
	if err != nil {
		return nil, fmt.Errorf("crypto: parse PKCS#8 private key: %w", err)
	}

	ed, ok := key.(ed25519.PrivateKey)
	if !ok {
		return nil, fmt.Errorf("crypto: PEM key is not Ed25519 (got %T)", key)
	}
	return ed, nil
}

// SealAESGCM encrypts plaintext with AES-256-GCM using key.
// Returns ciphertext and nonce separately so both can be stored.
// Security: A fresh random nonce is generated for every call. Nonce reuse
// under the same key would break GCM's confidentiality and authentication
// guarantees, so callers must never reuse nonces manually.
func SealAESGCM(key, plaintext []byte) (ciphertext, nonce []byte, err error) {
	if len(key) != aesKeySize {
		return nil, nil, fmt.Errorf("crypto: AES-GCM key must be %d bytes, got %d", aesKeySize, len(key))
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, nil, fmt.Errorf("crypto: create AES cipher: %w", err)
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, nil, fmt.Errorf("crypto: create GCM: %w", err)
	}

	nonce = make([]byte, gcmNonceSize)
	if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
		return nil, nil, fmt.Errorf("crypto: generate GCM nonce: %w", err)
	}

	ciphertext = gcm.Seal(nil, nonce, plaintext, nil)
	return ciphertext, nonce, nil
}

// OpenAESGCM decrypts and authenticates ciphertext encrypted with SealAESGCM.
// Returns the plaintext, or an error if authentication fails (wrong key, tampered
// ciphertext, or wrong nonce).
func OpenAESGCM(key, nonce, ciphertext []byte) ([]byte, error) {
	if len(key) != aesKeySize {
		return nil, fmt.Errorf("crypto: AES-GCM key must be %d bytes, got %d", aesKeySize, len(key))
	}

	block, err := aes.NewCipher(key)
	if err != nil {
		return nil, fmt.Errorf("crypto: create AES cipher: %w", err)
	}

	gcm, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("crypto: create GCM: %w", err)
	}

	plaintext, err := gcm.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		// Do not expose internal GCM error details; they could reveal key info.
		return nil, errors.New("crypto: AES-GCM authentication failed")
	}
	return plaintext, nil
}

// DeriveKey derives a 256-bit AES key from passphrase and salt using Argon2id.
// The salt must be at least 16 bytes; use NewSalt to generate one.
// Security: Argon2id is the OWASP-recommended KDF for key derivation from
// passphrases. The parameters are hardcoded at compile time and exceed OWASP
// minimums to resist offline dictionary attacks against the passphrase.
func DeriveKey(passphrase string, salt []byte) ([]byte, error) {
	if len(salt) < 16 {
		return nil, fmt.Errorf("crypto: KDF salt must be at least 16 bytes, got %d", len(salt))
	}
	if passphrase == "" {
		return nil, errors.New("crypto: passphrase must not be empty")
	}

	// argon2.IDKey returns keyLen bytes derived from the passphrase and salt.
	// Security: parameters are time=3, memory=128MiB, threads=4, keyLen=32.
	// These exceed OWASP 2023 minimums for key derivation.
	key := argon2.IDKey(
		[]byte(passphrase),
		salt,
		kdfTime,
		kdfMemory,
		kdfThreads,
		aesKeySize,
	)
	return key, nil
}

// NewSalt generates a cryptographically-random 32-byte KDF salt.
func NewSalt() ([]byte, error) {
	salt := make([]byte, kdfSaltSize)
	if _, err := io.ReadFull(rand.Reader, salt); err != nil {
		return nil, fmt.Errorf("crypto: generate salt: %w", err)
	}
	return salt, nil
}

// RandomBytes returns n cryptographically-random bytes.
func RandomBytes(n int) ([]byte, error) {
	b := make([]byte, n)
	if _, err := io.ReadFull(rand.Reader, b); err != nil {
		return nil, fmt.Errorf("crypto: read random bytes: %w", err)
	}
	return b, nil
}
