// Package seal implements the master key sealing mechanism (REQ-147,
// D-241, C-35). The secrets master key (32 random bytes) is sealed
// (encrypted) with a key derived from an OIDC ID token exchange at
// unseal time. The raw master key never touches disk; the sealed blob
// (salt + ciphertext) is stored at ClusterDir()/master.key.sealed (0600).
//
// Shamir 3-of-5 recovery: at seal time, 5 shards are generated; the
// operator stores them offline. If the IdP is permanently lost, the
// master key can be recovered with any 3 of the 5 shards. No backdoor.
//
// For the mTLS-only offline path (no OIDC), the seal key is derived
// from the cluster's own CA (the operator holds the CA, not a password).
package seal

import (
	"crypto/aes"
	"crypto/cipher"
	"crypto/hmac"
	"crypto/rand"
	"crypto/sha256"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"os"

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

// SealedBlob is the on-disk format for the sealed master key.
// Salt is used with the OIDC token sub (or CA fingerprint) to derive
// the unwrapping key via HKDF-SHA256.
type SealedBlob struct {
	Salt       []byte `json:"salt"`
	Nonce      []byte `json:"nonce"`
	Ciphertext []byte `json:"ciphertext"`
	// Mode indicates how the seal key was derived: "oidc" or "ca".
	Mode string `json:"mode"`
	// Hint is a non-secret hint for recovery (e.g. the OIDC issuer URL
	// or the CA fingerprint). Used to identify which seal key to use.
	Hint string `json:"hint"`
}

// Seal encrypts the master key with a key derived from the OIDC token
// subject + salt. The seal key = HKDF-SHA256(oidcSub, salt, info="orca-master-key-seal").
// Returns the sealed blob (to store on disk) + 5 Shamir shards (to
// print for offline recovery).
func Seal(masterKey []byte, oidcSub string, issuerHint string) (*SealedBlob, [][]byte, error) {
	if len(masterKey) != 32 {
		return nil, nil, fmt.Errorf("seal: master key must be 32 bytes, got %d", len(masterKey))
	}
	if oidcSub == "" {
		return nil, nil, fmt.Errorf("seal: oidc sub is empty")
	}
	salt := make([]byte, 32)
	if _, err := rand.Read(salt); err != nil {
		return nil, nil, fmt.Errorf("seal: salt rand: %w", err)
	}
	nonce := make([]byte, 12)
	if _, err := rand.Read(nonce); err != nil {
		return nil, nil, fmt.Errorf("seal: nonce rand: %w", err)
	}
	sealKey := deriveSealKey(oidcSub, salt)
	block, err := aes.NewCipher(sealKey)
	if err != nil {
		return nil, nil, fmt.Errorf("seal: aes: %w", err)
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, nil, fmt.Errorf("seal: gcm: %w", err)
	}
	ciphertext := aead.Seal(nil, nonce, masterKey, []byte("orca-seal"))
	blob := &SealedBlob{
		Salt:       salt,
		Nonce:      nonce,
		Ciphertext: ciphertext,
		Mode:       "oidc",
		Hint:       issuerHint,
	}
	// Generate 5 Shamir shards for recovery.
	shards, err := ShamirSplit(masterKey, 5, 3)
	if err != nil {
		return nil, nil, fmt.Errorf("seal: shamir: %w", err)
	}
	return blob, shards, nil
}

// Unseal decrypts the sealed master key using the OIDC token subject.
// The seal key = HKDF-SHA256(oidcSub, salt, info="orca-master-key-seal").
func Unseal(blob *SealedBlob, oidcSub string) ([]byte, error) {
	if blob.Mode != "oidc" {
		return nil, fmt.Errorf("seal: blob mode is %q, not oidc", blob.Mode)
	}
	sealKey := deriveSealKey(oidcSub, blob.Salt)
	block, err := aes.NewCipher(sealKey)
	if err != nil {
		return nil, fmt.Errorf("seal: aes: %w", err)
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("seal: gcm: %w", err)
	}
	masterKey, err := aead.Open(nil, blob.Nonce, blob.Ciphertext, []byte("orca-seal"))
	if err != nil {
		return nil, fmt.Errorf("seal: decrypt (wrong sub or corrupted): %w", err)
	}
	return masterKey, nil
}

// UnsealWithShamir recovers the master key from a quorum of Shamir
// shards (3 of 5). Used when the IdP is permanently lost (C-35).
func UnsealWithShamir(blob *SealedBlob, shards [][]byte) ([]byte, error) {
	if len(shards) < 3 {
		return nil, fmt.Errorf("seal: need at least 3 shards, got %d", len(shards))
	}
	masterKey, err := ShamirCombine(shards[:3])
	if err != nil {
		return nil, fmt.Errorf("seal: shamir combine: %w", err)
	}
	if len(masterKey) != 32 {
		return nil, fmt.Errorf("seal: recovered key is %d bytes, want 32", len(masterKey))
	}
	return masterKey, nil
}

// SealWithCA encrypts the master key using a key derived from the
// cluster CA fingerprint (mTLS-only offline path, D-241). The seal
// key = HKDF-SHA256(caFingerprint, salt, info="orca-master-key-seal-ca").
func SealWithCA(masterKey []byte, caFingerprint string) (*SealedBlob, error) {
	if len(masterKey) != 32 {
		return nil, fmt.Errorf("seal: master key must be 32 bytes, got %d", len(masterKey))
	}
	salt := make([]byte, 32)
	if _, err := rand.Read(salt); err != nil {
		return nil, fmt.Errorf("seal: salt rand: %w", err)
	}
	nonce := make([]byte, 12)
	if _, err := rand.Read(nonce); err != nil {
		return nil, fmt.Errorf("seal: nonce rand: %w", err)
	}
	sealKey := deriveCASealKey(caFingerprint, salt)
	block, err := aes.NewCipher(sealKey)
	if err != nil {
		return nil, fmt.Errorf("seal: aes: %w", err)
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("seal: gcm: %w", err)
	}
	ciphertext := aead.Seal(nil, nonce, masterKey, []byte("orca-seal-ca"))
	return &SealedBlob{
		Salt:       salt,
		Nonce:      nonce,
		Ciphertext: ciphertext,
		Mode:       "ca",
		Hint:       caFingerprint,
	}, nil
}

// UnsealWithCA decrypts using the CA fingerprint.
func UnsealWithCA(blob *SealedBlob, caFingerprint string) ([]byte, error) {
	if blob.Mode != "ca" {
		return nil, fmt.Errorf("seal: blob mode is %q, not ca", blob.Mode)
	}
	sealKey := deriveCASealKey(caFingerprint, blob.Salt)
	block, err := aes.NewCipher(sealKey)
	if err != nil {
		return nil, fmt.Errorf("seal: aes: %w", err)
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return nil, fmt.Errorf("seal: gcm: %w", err)
	}
	masterKey, err := aead.Open(nil, blob.Nonce, blob.Ciphertext, []byte("orca-seal-ca"))
	if err != nil {
		return nil, fmt.Errorf("seal: decrypt (wrong CA or corrupted): %w", err)
	}
	return masterKey, nil
}

// deriveSealKey derives a 32-byte AES key from the OIDC subject + salt
// via HKDF-SHA256.
func deriveSealKey(oidcSub string, salt []byte) []byte {
	hk := hkdf.New(sha256.New, []byte(oidcSub), salt, []byte("orca-master-key-seal"))
	key := make([]byte, 32)
	hk.Read(key)
	return key
}

// deriveCASealKey derives a 32-byte AES key from the CA fingerprint +
// salt via HKDF-SHA256.
func deriveCASealKey(caFingerprint string, salt []byte) []byte {
	hk := hkdf.New(sha256.New, []byte(caFingerprint), salt, []byte("orca-master-key-seal-ca"))
	key := make([]byte, 32)
	hk.Read(key)
	return key
}

// SaveSealed writes the sealed blob to disk at 0600.
func SaveSealed(path string, blob *SealedBlob) error {
	data, err := json.MarshalIndent(blob, "", "  ")
	if err != nil {
		return fmt.Errorf("seal: marshal: %w", err)
	}
	tmp := path + ".tmp"
	if err := os.WriteFile(tmp, data, 0o600); err != nil {
		return fmt.Errorf("seal: write tmp: %w", err)
	}
	return os.Rename(tmp, path)
}

// LoadSealed reads the sealed blob from disk.
func LoadSealed(path string) (*SealedBlob, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return nil, fmt.Errorf("seal: read: %w", err)
	}
	var blob SealedBlob
	if err := json.Unmarshal(data, &blob); err != nil {
		return nil, fmt.Errorf("seal: parse: %w", err)
	}
	return &blob, nil
}

// EncodeShard base64-encodes a shard for display/storage.
func EncodeShard(shard []byte) string {
	return base64.StdEncoding.EncodeToString(shard)
}

// DecodeShard base64-decodes a shard.
func DecodeShard(s string) ([]byte, error) {
	return base64.StdEncoding.DecodeString(s)
}

// VerifySealedKey verifies that a candidate master key matches the
// sealed blob (by re-sealing and comparing). Used after unseal to
// confirm correctness before use.
func VerifySealedKey(blob *SealedBlob, masterKey []byte, oidcSub string) bool {
	sealKey := deriveSealKey(oidcSub, blob.Salt)
	block, err := aes.NewCipher(sealKey)
	if err != nil {
		return false
	}
	aead, err := cipher.NewGCM(block)
	if err != nil {
		return false
	}
	ct := aead.Seal(nil, blob.Nonce, masterKey, []byte("orca-seal"))
	return hmac.Equal(ct, blob.Ciphertext)
}

// ensure binary import is used (for shard encoding).
var _ = binary.BigEndian
