package jwe

import (
	"crypto/ecdh"
	"crypto/ecdsa"
	"crypto/rsa"
	"fmt"

	"github.com/lestrrat-go/jwx/v3/internal/keyconv"
	"github.com/lestrrat-go/jwx/v3/jwa"
	"github.com/lestrrat-go/jwx/v3/jwe/internal/content_crypt"
	"github.com/lestrrat-go/jwx/v3/jwe/internal/keygen"
	"github.com/lestrrat-go/jwx/v3/jwe/jwebb"
)

// encrypter is responsible for taking various components to encrypt a key.
// its operation is not concurrency safe. You must provide locking yourself
//
//nolint:govet
type encrypter struct {
	apu    []byte
	apv    []byte
	ctalg  jwa.ContentEncryptionAlgorithm
	keyalg jwa.KeyEncryptionAlgorithm
	pubkey any
	rawKey any
	cipher content_crypt.Cipher
}

// newEncrypter creates a new Encrypter instance with all required parameters.
// The content cipher is built internally during construction.
//
// pubkey must be a public key in its "raw" format (i.e. something like
// *rsa.PublicKey, instead of jwk.Key)
//
// You should consider this object immutable once created.
func newEncrypter(keyalg jwa.KeyEncryptionAlgorithm, ctalg jwa.ContentEncryptionAlgorithm, pubkey any, rawKey any, apu, apv []byte) (*encrypter, error) {
	cipher, err := jwebb.CreateContentCipher(ctalg.String())
	if err != nil {
		return nil, fmt.Errorf(`failed to create content cipher: %w`, err)
	}

	return &encrypter{
		apu:    apu,
		apv:    apv,
		ctalg:  ctalg,
		keyalg: keyalg,
		pubkey: pubkey,
		rawKey: rawKey,
		cipher: cipher,
	}, nil
}

func (e *encrypter) EncryptKey(cek []byte) (keygen.ByteSource, error) {
	if ke, ok := e.pubkey.(KeyEncrypter); ok {
		encrypted, err := ke.EncryptKey(cek)
		if err != nil {
			return nil, err
		}
		return keygen.ByteKey(encrypted), nil
	}

	if jwebb.IsDirect(e.keyalg.String()) {
		sharedkey, ok := e.rawKey.([]byte)
		if !ok {
			return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", e.keyalg, e.rawKey)
		}
		return jwebb.KeyEncryptDirect(cek, e.keyalg.String(), sharedkey)
	}

	if jwebb.IsPBES2(e.keyalg.String()) {
		password, ok := e.rawKey.([]byte)
		if !ok {
			return nil, fmt.Errorf("encrypt key: []byte is required as the password for %s (got %T)", e.keyalg, e.rawKey)
		}
		return jwebb.KeyEncryptPBES2(cek, e.keyalg.String(), password)
	}

	if jwebb.IsAESGCMKW(e.keyalg.String()) {
		sharedkey, ok := e.rawKey.([]byte)
		if !ok {
			return nil, fmt.Errorf("encrypt key: []byte is required as the key for %s (got %T)", e.keyalg, e.rawKey)
		}
		return jwebb.KeyEncryptAESGCMKW(cek, e.keyalg.String(), sharedkey)
	}

	if jwebb.IsECDHES(e.keyalg.String()) {
		_, keysize, keywrap, err := jwebb.KeyEncryptionECDHESKeySize(e.keyalg.String(), e.ctalg.String())
		if err != nil {
			return nil, fmt.Errorf(`failed to determine ECDH-ES key size: %w`, err)
		}

		// Use rawKey for ECDH-ES operations - it should contain the actual key material
		keyToUse := e.rawKey
		if keyToUse == nil {
			keyToUse = e.pubkey
		}

		switch key := keyToUse.(type) {
		case *ecdsa.PublicKey:
			// no op
		case ecdsa.PublicKey:
			keyToUse = &key
		case *ecdsa.PrivateKey:
			keyToUse = &key.PublicKey
		case ecdsa.PrivateKey:
			keyToUse = &key.PublicKey
		case *ecdh.PublicKey:
			// no op
		case ecdh.PublicKey:
			keyToUse = &key
		case ecdh.PrivateKey:
			keyToUse = key.PublicKey()
		case *ecdh.PrivateKey:
			keyToUse = key.PublicKey()
		}

		// Determine key type and call appropriate function
		switch key := keyToUse.(type) {
		case *ecdh.PublicKey:
			if key.Curve() == ecdh.X25519() {
				if !keywrap {
					return jwebb.KeyEncryptECDHESX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
				}
				return jwebb.KeyEncryptECDHESKeyWrapX25519(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
			}

			var ecdsaKey *ecdsa.PublicKey
			if err := keyconv.ECDHToECDSA(&ecdsaKey, key); err != nil {
				return nil, fmt.Errorf(`encrypt: failed to convert ECDH public key to ECDSA: %w`, err)
			}
			keyToUse = ecdsaKey
		}

		switch key := keyToUse.(type) {
		case *ecdsa.PublicKey:
			if !keywrap {
				return jwebb.KeyEncryptECDHESECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
			}
			return jwebb.KeyEncryptECDHESKeyWrapECDSA(cek, e.keyalg.String(), e.apu, e.apv, key, keysize, e.ctalg.String())
		default:
			return nil, fmt.Errorf(`encrypt: unsupported key type for ECDH-ES: %T`, keyToUse)
		}
	}

	if jwebb.IsRSA15(e.keyalg.String()) {
		keyToUse := e.rawKey
		if keyToUse == nil {
			keyToUse = e.pubkey
		}

		// Handle rsa.PublicKey by value - convert to pointer
		if pk, ok := keyToUse.(rsa.PublicKey); ok {
			keyToUse = &pk
		}

		var pubkey *rsa.PublicKey
		if err := keyconv.RSAPublicKey(&pubkey, keyToUse); err != nil {
			return nil, fmt.Errorf(`encrypt: failed to convert to RSA public key: %w`, err)
		}

		return jwebb.KeyEncryptRSA15(cek, e.keyalg.String(), pubkey)
	}

	if jwebb.IsRSAOAEP(e.keyalg.String()) {
		keyToUse := e.rawKey
		if keyToUse == nil {
			keyToUse = e.pubkey
		}

		// Handle rsa.PublicKey by value - convert to pointer
		if pk, ok := keyToUse.(rsa.PublicKey); ok {
			keyToUse = &pk
		}

		var pubkey *rsa.PublicKey
		if err := keyconv.RSAPublicKey(&pubkey, keyToUse); err != nil {
			return nil, fmt.Errorf(`encrypt: failed to convert to RSA public key: %w`, err)
		}

		return jwebb.KeyEncryptRSAOAEP(cek, e.keyalg.String(), pubkey)
	}

	if jwebb.IsAESKW(e.keyalg.String()) {
		sharedkey, ok := e.rawKey.([]byte)
		if !ok {
			return nil, fmt.Errorf("[]byte is required as the key to encrypt %s", e.keyalg.String())
		}
		return jwebb.KeyEncryptAESKW(cek, e.keyalg.String(), sharedkey)
	}

	return nil, fmt.Errorf(`unsupported algorithm for key encryption (%s)`, e.keyalg)
}
