package mlkem768

import (
	"bytes"
	"crypto/mlkem"
	"crypto/sha256"
	"crypto/sha3"
	"errors"
	"fmt"
	"io"

	"git.tswf.io/infra/go-synapse-backupper/pkg/domain/crypto"
)

// suiteID is the scheme identifier for ML-KEM-768.
const suiteID uint16 = 0x0006

// ErrDecapsulationFailed is returned when ciphertext decapsulation fails,
// typically due to a tampered or invalid ciphertext.
var ErrDecapsulationFailed = errors.New("decapsulation failed")

// DefaultRegistry is the package-level registry for ML-KEM-768.
var DefaultRegistry = crypto.NewRegistry()

// kemAdapter wraps the Go stdlib crypto/mlkem implementation to satisfy
// the pkg/domain/crypto.KEM interface.
type kemAdapter struct{}

// New creates a new KEM adapter instance.
func New() crypto.KEM {
	return &kemAdapter{}
}

// SchemeID returns the ML-KEM-768 scheme identifier (0x0006).
func (k *kemAdapter) SchemeID() uint16 {
	return suiteID
}

// GenerateKeyPair generates a new ML-KEM-768 key pair.
//
// Note: the rand parameter is part of the KEM interface contract but is
// ignored here because the stdlib crypto/mlkem.GenerateKey768 uses
// crypto/rand internally.
func (k *kemAdapter) GenerateKeyPair(
	rand io.Reader,
) (
	crypto.RecipientPub,
	crypto.RecipientPriv,
	error,
) {
	dk, err := mlkem.GenerateKey768()
	if err != nil {
		return nil, nil, err
	}

	ek := dk.EncapsulationKey()
	rawPub := ek.Bytes()
	keyID := computeKeyID(rawPub)

	pub := &pubKey{
		key:   ek,
		keyID: keyID,
	}
	priv := &privKey{
		key:   dk,
		keyID: keyID,
	}

	return pub, priv, nil
}

// Encapsulate generates a shared secret and ciphertext for the given public key.
//
// Note: the rand parameter is part of the KEM interface contract but is
// ignored here because the stdlib (*EncapsulationKey768).Encapsulate uses
// crypto/rand internally.
//
// CRITICAL: the stdlib returns (sharedKey, ciphertext) but this adapter
// swaps the order to (ciphertext, sharedSecret) to match the domain KEM
// interface contract.
func (k *kemAdapter) Encapsulate(
	pub crypto.RecipientPub,
	rand io.Reader,
) (
	ciphertext []byte,
	sharedSecret []byte,
	err error,
) {
	p, ok := pub.(*pubKey)
	if !ok {
		raw := pub.Raw()
		ek, parseErr := mlkem.NewEncapsulationKey768(raw)
		if parseErr != nil {
			return nil, nil, fmt.Errorf("invalid public key for ML-KEM-768: %w", parseErr)
		}
		p = &pubKey{key: ek, keyID: computeKeyID(raw)}
	}

	// stdlib returns (sharedKey, ciphertext); we swap to (ciphertext, sharedSecret).
	ss, ct := p.key.Encapsulate()

	return ct, ss, nil
}

// LoadPriv loads an ML-KEM-768 private key from raw bytes.
func (k *kemAdapter) LoadPriv(
	raw []byte,
) (
	crypto.RecipientPriv,
	error,
) {
	dk, err := mlkem.NewDecapsulationKey768(raw)
	if err != nil {
		return nil, fmt.Errorf("invalid ML-KEM-768 private key: %w", err)
	}
	pubRaw := dk.EncapsulationKey().Bytes()
	return &privKey{key: dk, keyID: computeKeyID(pubRaw)}, nil
}

// Decapsulate recovers the shared secret from a ciphertext using the private key.
// For tampered ciphertexts, it returns ErrDecapsulationFailed.
func (k *kemAdapter) Decapsulate(
	priv crypto.RecipientPriv,
	ciphertext []byte,
) (
	sharedSecret []byte,
	err error,
) {
	p, ok := priv.(*privKey)
	if !ok {
		return nil, fmt.Errorf("invalid private key type for ML-KEM-768")
	}

	ss, err := p.key.Decapsulate(ciphertext)
	if err != nil {
		return nil, errors.Join(ErrDecapsulationFailed, err)
	}

	// crypto/mlkem.Decapsulate implements implicit rejection: it returns a
	// pseudorandom shared secret instead of an error for invalid ciphertexts.
	// Perform explicit rejection by recomputing the implicit-rejection value
	// Kout = SHAKE256(z || ciphertext) and comparing it with the result.
	// If they match, the ciphertext was invalid.
	seed := p.key.Bytes()
	z := seed[32:]
	shake := sha3.NewSHAKE256()
	// sha3.ShakeHash.Write/Read never return an error; explicitly ignore to satisfy errcheck.
	_, _ = shake.Write(z)
	_, _ = shake.Write(ciphertext)
	computedKout := make([]byte, mlkem.SharedKeySize)
	_, _ = shake.Read(computedKout)

	if bytes.Equal(ss, computedKout) {
		return nil, ErrDecapsulationFailed
	}

	return ss, nil
}

// pubKey wraps *mlkem.EncapsulationKey768 to satisfy crypto.RecipientPub.
type pubKey struct {
	key   *mlkem.EncapsulationKey768
	keyID []byte
}

func (p *pubKey) SchemeID() uint16 { return suiteID }
func (p *pubKey) KeyID() []byte    { return p.keyID }
func (p *pubKey) Raw() []byte      { return p.key.Bytes() }

// privKey wraps *mlkem.DecapsulationKey768 to satisfy crypto.RecipientPriv.
type privKey struct {
	key   *mlkem.DecapsulationKey768
	keyID []byte
}

func (p *privKey) SchemeID() uint16 { return suiteID }
func (p *privKey) KeyID() []byte    { return p.keyID }
func (p *privKey) Raw() []byte      { return p.key.Bytes() }

// computeKeyID derives the first 8 bytes of SHA-256 over the first 8 bytes of raw key material.
func computeKeyID(raw []byte) []byte {
	h := sha256.Sum256(raw[:8])
	return h[:8]
}

// init registers the ML-KEM-768 factory under suiteID 0x0006.
func init() {
	_ = DefaultRegistry.Register(
		suiteID,
		func() crypto.KEM {
			return New()
		},
	)
}
