// This file implements the Pre-Obfuscation Layer Protocol with
// XChaCha20-Poly1305 encryption. The protocol applies a reversible transformation
// to plaintext BEFORE it reaches CPU encryption routines, providing defense-in-depth
// against side-channel attacks.
//
// The encryption flow is:
//
//	plaintext -> obfuscate(nonce) -> encrypt -> [nonce || ciphertext || tag]
//
// The decryption flow is:
//
//	[nonce || ciphertext || tag] -> decrypt -> deobfuscate(nonce) -> plaintext
package sigil

import (
	"crypto/rand"
	"crypto/sha256"
	"encoding/binary"
	"errors"
	"io"

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

var (
	// ErrInvalidKey is returned when the encryption key is invalid.
	ErrInvalidKey = errors.New("sigil: invalid key size, must be 32 bytes")
	// ErrCiphertextTooShort is returned when the ciphertext is too short to decrypt.
	ErrCiphertextTooShort = errors.New("sigil: ciphertext too short")
	// ErrDecryptionFailed is returned when decryption or authentication fails.
	ErrDecryptionFailed = errors.New("sigil: decryption failed")
	// ErrNoKeyConfigured is returned when no encryption key has been set.
	ErrNoKeyConfigured = errors.New("sigil: no encryption key configured")
)

// PreObfuscator applies a reversible transformation to data before encryption.
// This ensures that raw plaintext patterns are never sent directly to CPU
// encryption routines, providing defense against side-channel attacks.
//
// Implementations must be deterministic: given the same entropy, the transformation
// must be perfectly reversible: Deobfuscate(Obfuscate(x, e), e) == x
type PreObfuscator interface {
	// Obfuscate transforms plaintext before encryption using the provided entropy.
	// The entropy is typically the encryption nonce, ensuring the transformation
	// is unique per-encryption without additional random generation.
	Obfuscate(data []byte, entropy []byte) []byte

	// Deobfuscate reverses the transformation after decryption.
	// Must be called with the same entropy used during Obfuscate.
	Deobfuscate(data []byte, entropy []byte) []byte
}

// XORObfuscator performs XOR-based obfuscation using an entropy-derived key stream.
//
// The key stream is generated using SHA-256 in counter mode:
//
//	keyStream[i*32:(i+1)*32] = SHA256(entropy || BigEndian64(i))
//
// This provides a cryptographically uniform key stream that decorrelates
// plaintext patterns from the data seen by the encryption routine.
// XOR is symmetric, so obfuscation and deobfuscation use the same operation.
type XORObfuscator struct{}

// Obfuscate XORs the data with a key stream derived from the entropy.
func (x *XORObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
	if len(data) == 0 {
		return data
	}
	return x.transform(data, entropy)
}

// Deobfuscate reverses the XOR transformation (XOR is symmetric).
func (x *XORObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
	if len(data) == 0 {
		return data
	}
	return x.transform(data, entropy)
}

// transform applies XOR with an entropy-derived key stream.
func (x *XORObfuscator) transform(data []byte, entropy []byte) []byte {
	result := make([]byte, len(data))
	keyStream := x.deriveKeyStream(entropy, len(data))
	for i := range data {
		result[i] = data[i] ^ keyStream[i]
	}
	return result
}

// deriveKeyStream creates a deterministic key stream from entropy.
func (x *XORObfuscator) deriveKeyStream(entropy []byte, length int) []byte {
	stream := make([]byte, length)
	h := sha256.New()

	// Generate key stream in 32-byte blocks
	blockNum := uint64(0)
	offset := 0
	for offset < length {
		h.Reset()
		h.Write(entropy)
		var blockBytes [8]byte
		binary.BigEndian.PutUint64(blockBytes[:], blockNum)
		h.Write(blockBytes[:])
		block := h.Sum(nil)

		copyLen := len(block)
		if offset+copyLen > length {
			copyLen = length - offset
		}
		copy(stream[offset:], block[:copyLen])
		offset += copyLen
		blockNum++
	}
	return stream
}

// ShuffleMaskObfuscator provides stronger obfuscation through byte shuffling and masking.
//
// The obfuscation process:
//  1. Generate a mask from entropy using SHA-256 in counter mode
//  2. XOR the data with the mask
//  3. Generate a deterministic permutation using Fisher-Yates shuffle
//  4. Reorder bytes according to the permutation
//
// This provides both value transformation (XOR mask) and position transformation
// (shuffle), making pattern analysis more difficult than XOR alone.
type ShuffleMaskObfuscator struct{}

// Obfuscate shuffles bytes and applies a mask derived from entropy.
func (s *ShuffleMaskObfuscator) Obfuscate(data []byte, entropy []byte) []byte {
	if len(data) == 0 {
		return data
	}

	result := make([]byte, len(data))
	copy(result, data)

	// Generate permutation and mask from entropy
	perm := s.generatePermutation(entropy, len(data))
	mask := s.deriveMask(entropy, len(data))

	// Apply mask first, then shuffle
	for i := range result {
		result[i] ^= mask[i]
	}

	// Shuffle using Fisher-Yates with deterministic seed
	shuffled := make([]byte, len(data))
	for i, p := range perm {
		shuffled[i] = result[p]
	}

	return shuffled
}

// Deobfuscate reverses the shuffle and mask operations.
func (s *ShuffleMaskObfuscator) Deobfuscate(data []byte, entropy []byte) []byte {
	if len(data) == 0 {
		return data
	}

	result := make([]byte, len(data))

	// Generate permutation and mask from entropy
	perm := s.generatePermutation(entropy, len(data))
	mask := s.deriveMask(entropy, len(data))

	// Unshuffle first
	for i, p := range perm {
		result[p] = data[i]
	}

	// Remove mask
	for i := range result {
		result[i] ^= mask[i]
	}

	return result
}

// generatePermutation creates a deterministic permutation from entropy.
func (s *ShuffleMaskObfuscator) generatePermutation(entropy []byte, length int) []int {
	perm := make([]int, length)
	for i := range perm {
		perm[i] = i
	}

	// Use entropy to seed a deterministic shuffle
	h := sha256.New()
	h.Write(entropy)
	h.Write([]byte("permutation"))
	seed := h.Sum(nil)

	// Fisher-Yates shuffle with deterministic randomness
	for i := length - 1; i > 0; i-- {
		h.Reset()
		h.Write(seed)
		var iBytes [8]byte
		binary.BigEndian.PutUint64(iBytes[:], uint64(i))
		h.Write(iBytes[:])
		jBytes := h.Sum(nil)
		j := int(binary.BigEndian.Uint64(jBytes[:8]) % uint64(i+1))
		perm[i], perm[j] = perm[j], perm[i]
	}

	return perm
}

// deriveMask creates a mask byte array from entropy.
func (s *ShuffleMaskObfuscator) deriveMask(entropy []byte, length int) []byte {
	mask := make([]byte, length)
	h := sha256.New()

	blockNum := uint64(0)
	offset := 0
	for offset < length {
		h.Reset()
		h.Write(entropy)
		h.Write([]byte("mask"))
		var blockBytes [8]byte
		binary.BigEndian.PutUint64(blockBytes[:], blockNum)
		h.Write(blockBytes[:])
		block := h.Sum(nil)

		copyLen := len(block)
		if offset+copyLen > length {
			copyLen = length - offset
		}
		copy(mask[offset:], block[:copyLen])
		offset += copyLen
		blockNum++
	}
	return mask
}

// ChaChaPolySigil is a Sigil that encrypts/decrypts data using ChaCha20-Poly1305.
// It applies pre-obfuscation before encryption to ensure raw plaintext never
// goes directly to CPU encryption routines.
//
// The output format is:
// [24-byte nonce][encrypted(obfuscated(plaintext))]
//
// Unlike demo implementations, the nonce is ONLY embedded in the ciphertext,
// not exposed separately in headers.
type ChaChaPolySigil struct {
	Key        []byte
	Obfuscator PreObfuscator
	randReader io.Reader // for testing injection
}

// NewChaChaPolySigil creates a new encryption sigil with the given key.
// The key must be exactly 32 bytes.
func NewChaChaPolySigil(key []byte) (*ChaChaPolySigil, error) {
	if len(key) != 32 {
		return nil, ErrInvalidKey
	}

	keyCopy := make([]byte, 32)
	copy(keyCopy, key)

	return &ChaChaPolySigil{
		Key:        keyCopy,
		Obfuscator: &XORObfuscator{},
		randReader: rand.Reader,
	}, nil
}

// NewChaChaPolySigilWithObfuscator creates a new encryption sigil with custom obfuscator.
func NewChaChaPolySigilWithObfuscator(key []byte, obfuscator PreObfuscator) (*ChaChaPolySigil, error) {
	sigil, err := NewChaChaPolySigil(key)
	if err != nil {
		return nil, err
	}
	if obfuscator != nil {
		sigil.Obfuscator = obfuscator
	}
	return sigil, nil
}

// In encrypts the data with pre-obfuscation.
// The flow is: plaintext -> obfuscate -> encrypt
func (s *ChaChaPolySigil) In(data []byte) ([]byte, error) {
	if s.Key == nil {
		return nil, ErrNoKeyConfigured
	}
	if data == nil {
		return nil, nil
	}

	aead, err := chacha20poly1305.NewX(s.Key)
	if err != nil {
		return nil, err
	}

	// Generate nonce
	nonce := make([]byte, aead.NonceSize())
	reader := s.randReader
	if reader == nil {
		reader = rand.Reader
	}
	if _, err := io.ReadFull(reader, nonce); err != nil {
		return nil, err
	}

	// Pre-obfuscate the plaintext using nonce as entropy
	// This ensures CPU encryption routines never see raw plaintext
	obfuscated := data
	if s.Obfuscator != nil {
		obfuscated = s.Obfuscator.Obfuscate(data, nonce)
	}

	// Encrypt the obfuscated data
	// Output: [nonce | ciphertext | auth tag]
	ciphertext := aead.Seal(nonce, nonce, obfuscated, nil)

	return ciphertext, nil
}

// Out decrypts the data and reverses obfuscation.
// The flow is: decrypt -> deobfuscate -> plaintext
func (s *ChaChaPolySigil) Out(data []byte) ([]byte, error) {
	if s.Key == nil {
		return nil, ErrNoKeyConfigured
	}
	if data == nil {
		return nil, nil
	}

	aead, err := chacha20poly1305.NewX(s.Key)
	if err != nil {
		return nil, err
	}

	minLen := aead.NonceSize() + aead.Overhead()
	if len(data) < minLen {
		return nil, ErrCiphertextTooShort
	}

	// Extract nonce from ciphertext
	nonce := data[:aead.NonceSize()]
	ciphertext := data[aead.NonceSize():]

	// Decrypt
	obfuscated, err := aead.Open(nil, nonce, ciphertext, nil)
	if err != nil {
		return nil, ErrDecryptionFailed
	}

	// Deobfuscate using the same nonce as entropy
	plaintext := obfuscated
	if s.Obfuscator != nil {
		plaintext = s.Obfuscator.Deobfuscate(obfuscated, nonce)
	}

	if len(plaintext) == 0 {
		return []byte{}, nil
	}

	return plaintext, nil
}

// GetNonceFromCiphertext extracts the nonce from encrypted output.
// This is provided for debugging/logging purposes only.
// The nonce should NOT be stored separately in headers.
func GetNonceFromCiphertext(ciphertext []byte) ([]byte, error) {
	nonceSize := chacha20poly1305.NonceSizeX
	if len(ciphertext) < nonceSize {
		return nil, ErrCiphertextTooShort
	}
	nonceCopy := make([]byte, nonceSize)
	copy(nonceCopy, ciphertext[:nonceSize])
	return nonceCopy, nil
}
