// Package helper contains several functions with a simple interface to extend usability and compatibility with gomobile
package helper

import (
	"github.com/ProtonMail/gopenpgp/v2/crypto"
	"github.com/pkg/errors"
)

// EncryptMessageWithPassword encrypts a string with a passphrase using AES256.
func EncryptMessageWithPassword(password []byte, plaintext string) (ciphertext string, err error) {
	var pgpMessage *crypto.PGPMessage

	var message = crypto.NewPlainMessageFromString(plaintext)

	if pgpMessage, err = crypto.EncryptMessageWithPassword(message, password); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to encrypt message with password")
	}

	if ciphertext, err = pgpMessage.GetArmored(); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to armor ciphertext")
	}

	return ciphertext, nil
}

// DecryptMessageWithPassword decrypts an armored message with a random token.
// The algorithm is derived from the armoring.
func DecryptMessageWithPassword(password []byte, ciphertext string) (plaintext string, err error) {
	var message *crypto.PlainMessage
	var pgpMessage *crypto.PGPMessage

	if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to unarmor ciphertext")
	}

	if message, err = crypto.DecryptMessageWithPassword(pgpMessage, password); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to decrypt message with password")
	}

	return message.GetString(), nil
}

// EncryptMessageArmored generates an armored PGP message given a plaintext and
// an armored public key.
func EncryptMessageArmored(key, plaintext string) (string, error) {
	return encryptMessageArmored(key, crypto.NewPlainMessageFromString(plaintext))
}

// EncryptSignMessageArmored generates an armored signed PGP message given a
// plaintext and an armored public key a private key and its passphrase.
func EncryptSignMessageArmored(
	publicKey, privateKey string, passphrase []byte, plaintext string,
) (ciphertext string, err error) {
	var privateKeyObj, unlockedKeyObj *crypto.Key
	var publicKeyRing, privateKeyRing *crypto.KeyRing
	var pgpMessage *crypto.PGPMessage

	var message = crypto.NewPlainMessageFromString(plaintext)

	if publicKeyRing, err = createPublicKeyRing(publicKey); err != nil {
		return "", err
	}

	if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to read key")
	}

	if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to unlock key")
	}
	defer unlockedKeyObj.ClearPrivateParams()

	if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to create new keyring")
	}

	if pgpMessage, err = publicKeyRing.Encrypt(message, privateKeyRing); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to encrypt message")
	}

	if ciphertext, err = pgpMessage.GetArmored(); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to armor ciphertext")
	}

	return ciphertext, nil
}

// DecryptMessageArmored decrypts an armored PGP message given a private key
// and its passphrase.
func DecryptMessageArmored(
	privateKey string, passphrase []byte, ciphertext string,
) (string, error) {
	message, err := decryptMessageArmored(privateKey, passphrase, ciphertext)
	if err != nil {
		return "", err
	}

	return message.GetString(), nil
}

// DecryptVerifyMessageArmored decrypts an armored PGP message given a private
// key and its passphrase and verifies the embedded signature. Returns the
// plain data or an error on signature verification failure.
func DecryptVerifyMessageArmored(
	publicKey, privateKey string, passphrase []byte, ciphertext string,
) (plaintext string, err error) {
	var privateKeyObj, unlockedKeyObj *crypto.Key
	var publicKeyRing, privateKeyRing *crypto.KeyRing
	var pgpMessage *crypto.PGPMessage
	var message *crypto.PlainMessage

	if publicKeyRing, err = createPublicKeyRing(publicKey); err != nil {
		return "", err
	}

	if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to unarmor private key")
	}

	if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to unlock private key")
	}
	defer unlockedKeyObj.ClearPrivateParams()

	if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to create new keyring")
	}

	if pgpMessage, err = crypto.NewPGPMessageFromArmored(ciphertext); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to unarmor ciphertext")
	}

	if message, err = privateKeyRing.Decrypt(pgpMessage, publicKeyRing, crypto.GetUnixTime()); err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to decrypt message")
	}

	return message.GetString(), nil
}

// DecryptVerifyAttachment decrypts and verifies an attachment split into the
// keyPacket, dataPacket and an armored (!) signature, given a publicKey, and a
// privateKey with its passphrase. Returns the plain data or an error on
// signature verification failure.
func DecryptVerifyAttachment(
	publicKey, privateKey string,
	passphrase, keyPacket, dataPacket []byte,
	armoredSignature string,
) (plainData []byte, err error) {
	// We decrypt the attachment
	message, err := decryptAttachment(privateKey, passphrase, keyPacket, dataPacket)
	if err != nil {
		return nil, err
	}

	// We verify the signature
	var check bool
	if check, err = verifyDetachedArmored(publicKey, message, armoredSignature); err != nil {
		return nil, err
	}
	if !check {
		return nil, errors.New("gopenpgp: unable to verify attachment")
	}

	return message.GetBinary(), nil
}

// EncryptBinaryMessageArmored generates an armored PGP message given a binary data and
// an armored public key.
func EncryptBinaryMessageArmored(key string, data []byte) (string, error) {
	return encryptMessageArmored(key, crypto.NewPlainMessage(data))
}

// DecryptBinaryMessageArmored decrypts an armored PGP message given a private key
// and its passphrase.
func DecryptBinaryMessageArmored(privateKey string, passphrase []byte, ciphertext string) ([]byte, error) {
	message, err := decryptMessageArmored(privateKey, passphrase, ciphertext)
	if err != nil {
		return nil, err
	}

	return message.GetBinary(), nil
}

// encryptSignArmoredDetached takes a public key for encryption,
// a private key and its passphrase for signature, and the plaintext data
// Returns an armored ciphertext and a detached armored encrypted signature.
func encryptSignArmoredDetached(
	publicKey, privateKey string,
	passphrase, plainData []byte,
) (ciphertextArmored, encryptedSignatureArmored string, err error) {
	var message = crypto.NewPlainMessage(plainData)

	// We encrypt and signcrypt
	ciphertext, encryptedSignatureArmored, err := encryptSignObjDetached(publicKey, privateKey, passphrase, message)
	if err != nil {
		return "", "", err
	}

	// We armor the ciphertext and signature
	ciphertextArmored, err = ciphertext.GetArmored()
	if err != nil {
		return "", "", errors.Wrap(err, "gopenpgp: unable to armor the ciphertext")
	}

	return ciphertextArmored, encryptedSignatureArmored, nil
}

// DecryptVerifyArmoredDetached decrypts an armored pgp message
// and verify a detached armored encrypted signature
// given a publicKey, and a privateKey with its passphrase.
// Returns the plain data or an error on
// signature verification failure.
func DecryptVerifyArmoredDetached(
	publicKey, privateKey string,
	passphrase []byte,
	ciphertextArmored string,
	encryptedSignatureArmored string,
) (plainData []byte, err error) {
	// Some type casting
	ciphertext, err := crypto.NewPGPMessageFromArmored(ciphertextArmored)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to unarmor ciphertext")
	}

	// We decrypt and verify the encrypted signature
	message, err := decryptVerifyObjDetached(publicKey, privateKey, passphrase, ciphertext, encryptedSignatureArmored)
	if err != nil {
		return nil, err
	}
	return message.GetBinary(), nil
}

// encryptSignBinaryDetached takes a public key for encryption,
// a private key and its passphrase for signature, and the plaintext data
// Returns encrypted binary data and a detached armored encrypted signature.
func encryptSignBinaryDetached(
	publicKey, privateKey string,
	passphrase, plainData []byte,
) (encryptedData []byte, encryptedSignatureArmored string, err error) {
	// Some type casting
	message := crypto.NewPlainMessage(plainData)

	// We encrypt and signcrypt
	ciphertext, encryptedSignatureArmored, err := encryptSignObjDetached(publicKey, privateKey, passphrase, message)
	if err != nil {
		return nil, "", err
	}

	// We get the encrypted data
	encryptedData = ciphertext.GetBinary()
	return encryptedData, encryptedSignatureArmored, nil
}

// DecryptVerifyBinaryDetached decrypts binary encrypted data
// and verify a detached armored encrypted signature
// given a publicKey, and a privateKey with its passphrase.
// Returns the plain data or an error on
// signature verification failure.
func DecryptVerifyBinaryDetached(
	publicKey, privateKey string,
	passphrase []byte,
	encryptedData []byte,
	encryptedSignatureArmored string,
) (plainData []byte, err error) {
	// Some type casting
	ciphertext := crypto.NewPGPMessage(encryptedData)

	// We decrypt and verify the encrypted signature
	message, err := decryptVerifyObjDetached(publicKey, privateKey, passphrase, ciphertext, encryptedSignatureArmored)
	if err != nil {
		return nil, err
	}
	return message.GetBinary(), nil
}

// EncryptAttachmentWithKey encrypts a binary file
// Using a given armored public key.
func EncryptAttachmentWithKey(
	publicKey string,
	filename string,
	plainData []byte,
) (message *crypto.PGPSplitMessage, err error) {
	publicKeyRing, err := createPublicKeyRing(publicKey)
	if err != nil {
		return nil, err
	}
	return EncryptAttachment(plainData, filename, publicKeyRing)
}

// DecryptAttachmentWithKey decrypts a binary file
// Using a given armored private key and its passphrase.
func DecryptAttachmentWithKey(
	privateKey string,
	passphrase, keyPacket, dataPacket []byte,
) (attachment []byte, err error) {
	message, err := decryptAttachment(privateKey, passphrase, keyPacket, dataPacket)
	if err != nil {
		return nil, err
	}
	return message.GetBinary(), nil
}

// EncryptSessionKey encrypts a session key
// using a given armored public key.
func EncryptSessionKey(
	publicKey string,
	sessionKey *crypto.SessionKey,
) (encryptedSessionKey []byte, err error) {
	publicKeyRing, err := createPublicKeyRing(publicKey)
	if err != nil {
		return nil, err
	}
	encryptedSessionKey, err = publicKeyRing.EncryptSessionKey(sessionKey)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to encrypt sessionKey")
	}
	return encryptedSessionKey, nil
}

// DecryptSessionKey decrypts a session key
// using a given armored private key
// and its passphrase.
func DecryptSessionKey(
	privateKey string,
	passphrase, encryptedSessionKey []byte,
) (sessionKey *crypto.SessionKey, err error) {
	privateKeyObj, err := crypto.NewKeyFromArmored(privateKey)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to read armored key")
	}

	privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to unlock private key")
	}

	defer privateKeyUnlocked.ClearPrivateParams()

	privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring")
	}

	sessionKey, err = privateKeyRing.DecryptSessionKey(encryptedSessionKey)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to decrypt session key")
	}

	return sessionKey, nil
}

func encryptMessageArmored(key string, message *crypto.PlainMessage) (string, error) {
	ciphertext, err := encryptMessage(key, message)
	if err != nil {
		return "", err
	}

	ciphertextArmored, err := ciphertext.GetArmored()
	if err != nil {
		return "", errors.Wrap(err, "gopenpgp: unable to armor ciphertext")
	}

	return ciphertextArmored, nil
}

func decryptMessageArmored(privateKey string, passphrase []byte, ciphertextArmored string) (*crypto.PlainMessage, error) {
	ciphertext, err := crypto.NewPGPMessageFromArmored(ciphertextArmored)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to parse ciphertext")
	}

	return decryptMessage(privateKey, passphrase, ciphertext)
}

func encryptMessage(key string, message *crypto.PlainMessage) (*crypto.PGPMessage, error) {
	publicKeyRing, err := createPublicKeyRing(key)
	if err != nil {
		return nil, err
	}

	ciphertext, err := publicKeyRing.Encrypt(message, nil)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to encrypt message")
	}

	return ciphertext, nil
}

func decryptMessage(privateKey string, passphrase []byte, ciphertext *crypto.PGPMessage) (*crypto.PlainMessage, error) {
	privateKeyObj, err := crypto.NewKeyFromArmored(privateKey)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to parse the private key")
	}

	privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to unlock key")
	}

	defer privateKeyUnlocked.ClearPrivateParams()

	privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to create the private key ring")
	}

	message, err := privateKeyRing.Decrypt(ciphertext, nil, 0)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to decrypt message")
	}

	return message, nil
}

func signDetached(privateKey string, passphrase []byte, message *crypto.PlainMessage) (detachedSignature *crypto.PGPSignature, err error) {
	privateKeyObj, err := crypto.NewKeyFromArmored(privateKey)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to parse private key")
	}

	privateKeyUnlocked, err := privateKeyObj.Unlock(passphrase)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to unlock key")
	}

	defer privateKeyUnlocked.ClearPrivateParams()

	privateKeyRing, err := crypto.NewKeyRing(privateKeyUnlocked)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring")
	}

	detachedSignature, err = privateKeyRing.SignDetached(message)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to sign message")
	}

	return detachedSignature, nil
}

func verifyDetachedArmored(publicKey string, message *crypto.PlainMessage, armoredSignature string) (check bool, err error) {
	var detachedSignature *crypto.PGPSignature

	// We unarmor the signature
	if detachedSignature, err = crypto.NewPGPSignatureFromArmored(armoredSignature); err != nil {
		return false, errors.Wrap(err, "gopenpgp: unable to unarmor signature")
	}
	// we verify the signature
	return verifyDetached(publicKey, message, detachedSignature)
}

func verifyDetached(publicKey string, message *crypto.PlainMessage, detachedSignature *crypto.PGPSignature) (check bool, err error) {
	var publicKeyRing *crypto.KeyRing

	// We prepare the public key for signature verification
	publicKeyRing, err = createPublicKeyRing(publicKey)
	if err != nil {
		return false, err
	}

	// We verify the signature
	if publicKeyRing.VerifyDetached(message, detachedSignature, crypto.GetUnixTime()) != nil {
		return false, nil
	}
	return true, nil
}

func decryptAttachment(
	privateKey string,
	passphrase, keyPacket, dataPacket []byte,
) (message *crypto.PlainMessage, err error) {
	var privateKeyObj, unlockedKeyObj *crypto.Key
	var privateKeyRing *crypto.KeyRing

	packets := crypto.NewPGPSplitMessage(keyPacket, dataPacket)

	// prepare the private key for decryption
	if privateKeyObj, err = crypto.NewKeyFromArmored(privateKey); err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to parse private key")
	}
	if unlockedKeyObj, err = privateKeyObj.Unlock(passphrase); err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to unlock private key")
	}
	defer unlockedKeyObj.ClearPrivateParams()

	if privateKeyRing, err = crypto.NewKeyRing(unlockedKeyObj); err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring")
	}

	if message, err = privateKeyRing.DecryptAttachment(packets); err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to decrypt attachment")
	}

	return message, nil
}

func createPublicKeyRing(publicKey string) (*crypto.KeyRing, error) {
	publicKeyObj, err := crypto.NewKeyFromArmored(publicKey)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to parse public key")
	}

	if publicKeyObj.IsPrivate() {
		publicKeyObj, err = publicKeyObj.ToPublic()
		if err != nil {
			return nil, errors.Wrap(err, "gopenpgp: unable to extract public key from private key")
		}
	}

	publicKeyRing, err := crypto.NewKeyRing(publicKeyObj)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to create new keyring")
	}

	return publicKeyRing, nil
}

func encryptSignObjDetached(
	publicKey, privateKey string,
	passphrase []byte,
	message *crypto.PlainMessage,
) (ciphertext *crypto.PGPSplitMessage, encryptedSignatureArmored string, err error) {
	// We generate the session key
	sessionKey, err := crypto.GenerateSessionKey()
	if err != nil {
		return nil, "", errors.Wrap(err, "gopenpgp: unable to create new session key")
	}

	// We encrypt the message with the session key
	messageDataPacket, err := sessionKey.Encrypt(message)
	if err != nil {
		return nil, "", errors.Wrap(err, "gopenpgp: unable to encrypt message")
	}

	// We sign the message
	detachedSignature, err := signDetached(privateKey, passphrase, message)
	if err != nil {
		return nil, "", errors.Wrap(err, "gopenpgp: unable to sign message")
	}

	// We encrypt the signature with the session key
	signaturePlaintext := crypto.NewPlainMessage(detachedSignature.GetBinary())
	signatureDataPacket, err := sessionKey.Encrypt(signaturePlaintext)
	if err != nil {
		return nil, "", errors.Wrap(err, "gopenpgp: unable to encrypt detached signature")
	}

	// We encrypt the session key
	keyPacket, err := EncryptSessionKey(publicKey, sessionKey)
	if err != nil {
		return nil, "", errors.Wrap(err, "gopenpgp: unable to encrypt the session key")
	}

	// We join the key packets and datapackets
	ciphertext = crypto.NewPGPSplitMessage(keyPacket, messageDataPacket)
	encryptedSignature := crypto.NewPGPSplitMessage(keyPacket, signatureDataPacket)
	encryptedSignatureArmored, err = encryptedSignature.GetArmored()
	if err != nil {
		return nil, "", errors.Wrap(err, "gopenpgp: unable to armor encrypted signature")
	}
	return ciphertext, encryptedSignatureArmored, nil
}

func decryptVerifyObjDetached(
	publicKey, privateKey string,
	passphrase []byte,
	ciphertext *crypto.PGPMessage,
	encryptedSignatureArmored string,
) (message *crypto.PlainMessage, err error) {
	// We decrypt the message
	if message, err = decryptMessage(privateKey, passphrase, ciphertext); err != nil {
		return nil, err
	}

	encryptedSignature, err := crypto.NewPGPMessageFromArmored(encryptedSignatureArmored)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to parse encrypted signature")
	}

	// We decrypt the signature
	signatureMessage, err := decryptMessage(privateKey, passphrase, encryptedSignature)
	if err != nil {
		return nil, err
	}
	detachedSignature := crypto.NewPGPSignature(signatureMessage.GetBinary())

	// We verify the signature
	var check bool
	if check, err = verifyDetached(publicKey, message, detachedSignature); err != nil {
		return nil, err
	}
	if !check {
		return nil, errors.New("gopenpgp: unable to verify message")
	}

	return message, nil
}
