package crypto

import (
	"bytes"
	"io"

	"github.com/ProtonMail/go-crypto/openpgp"
	pgpErrors "github.com/ProtonMail/go-crypto/openpgp/errors"
	"github.com/ProtonMail/go-crypto/openpgp/packet"
	"github.com/pkg/errors"
)

// EncryptMessageWithPassword encrypts a PlainMessage to PGPMessage with a
// SymmetricKey.
// * message : The plain data as a PlainMessage.
// * password: A password that will be derived into an encryption key.
// * output  : The encrypted data as PGPMessage.
func EncryptMessageWithPassword(message *PlainMessage, password []byte) (*PGPMessage, error) {
	encrypted, err := passwordEncrypt(message, password)
	if err != nil {
		return nil, err
	}

	return NewPGPMessage(encrypted), nil
}

// DecryptMessageWithPassword decrypts password protected pgp binary messages.
// * encrypted: The encrypted data as PGPMessage.
// * password: A password that will be derived into an encryption key.
// * output: The decrypted data as PlainMessage.
func DecryptMessageWithPassword(message *PGPMessage, password []byte) (*PlainMessage, error) {
	return passwordDecrypt(message.NewReader(), password)
}

// DecryptSessionKeyWithPassword decrypts the binary symmetrically encrypted
// session key packet and returns the session key.
func DecryptSessionKeyWithPassword(keyPacket, password []byte) (*SessionKey, error) {
	keyReader := bytes.NewReader(keyPacket)
	packets := packet.NewReader(keyReader)

	var symKeys []*packet.SymmetricKeyEncrypted
	for {
		var p packet.Packet
		var err error
		if p, err = packets.Next(); err != nil {
			break
		}

		if p, ok := p.(*packet.SymmetricKeyEncrypted); ok {
			symKeys = append(symKeys, p)
		}
	}

	// Try the symmetric passphrase first
	if len(symKeys) != 0 && password != nil {
		for _, s := range symKeys {
			key, cipherFunc, err := s.Decrypt(password)
			if err == nil {
				sk := &SessionKey{
					V6:   s.Version == 6,
					Key:  key,
					Algo: getAlgo(cipherFunc),
				}

				if err = sk.checkSize(); err != nil {
					return nil, errors.Wrap(err, "gopenpgp: unable to decrypt session key with password")
				}

				return sk, nil
			}
		}
	}

	return nil, errors.New("gopenpgp: unable to decrypt any packet")
}

// EncryptSessionKeyWithPassword encrypts the session key with the password and
// returns a binary symmetrically encrypted session key packet.
func EncryptSessionKeyWithPassword(sk *SessionKey, password []byte) ([]byte, error) {
	outbuf := &bytes.Buffer{}

	cf, err := sk.GetCipherFunc()
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key with password")
	}

	if len(password) == 0 {
		return nil, errors.New("gopenpgp: password can't be empty")
	}

	if err = sk.checkSize(); err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key with password")
	}

	config := &packet.Config{
		DefaultCipher: cf,
	}

	err = packet.SerializeSymmetricKeyEncryptedReuseKey(outbuf, sk.Key, password, config)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: unable to encrypt session key with password")
	}
	return outbuf.Bytes(), nil
}

// ----- INTERNAL FUNCTIONS ------

func passwordEncrypt(message *PlainMessage, password []byte) ([]byte, error) {
	var outBuf bytes.Buffer

	config := &packet.Config{
		DefaultCipher: packet.CipherAES256,
		Time:          getTimeGenerator(),
	}

	hints := &openpgp.FileHints{
		IsBinary: message.IsBinary(),
		FileName: message.Filename,
		ModTime:  message.getFormattedTime(),
	}

	encryptWriter, err := openpgp.SymmetricallyEncrypt(&outBuf, password, hints, config)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in encrypting message symmetrically")
	}
	_, err = encryptWriter.Write(message.GetBinary())
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in writing data to message")
	}

	err = encryptWriter.Close()
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in closing writer")
	}

	return outBuf.Bytes(), nil
}

func passwordDecrypt(encryptedIO io.Reader, password []byte) (*PlainMessage, error) {
	firstTimeCalled := true
	var prompt = func(keys []openpgp.Key, symmetric bool) ([]byte, error) {
		if firstTimeCalled {
			firstTimeCalled = false
			return password, nil
		}
		// Re-prompt still occurs if SKESK pasrsing fails (i.e. when decrypted cipher algo is invalid).
		// For most (but not all) cases, inputting a wrong passwords is expected to trigger this error.
		return nil, errors.New("gopenpgp: wrong password in symmetric decryption")
	}

	config := &packet.Config{
		Time: getTimeGenerator(),
	}

	var emptyKeyRing openpgp.EntityList
	md, err := openpgp.ReadMessage(encryptedIO, emptyKeyRing, prompt, config)
	if err != nil {
		// Parsing errors when reading the message are most likely caused by incorrect password, but we cannot know for sure
		return nil, errors.New("gopenpgp: error in reading password protected message: wrong password or malformed message")
	}

	messageBuf := bytes.NewBuffer(nil)
	_, err = io.Copy(messageBuf, md.UnverifiedBody)
	if errors.Is(err, pgpErrors.ErrMDCHashMismatch) {
		// This MDC error may also be triggered if the password is correct, but the encrypted data was corrupted.
		// To avoid confusion, we do not inform the user about the second possibility.
		return nil, errors.New("gopenpgp: wrong password in symmetric decryption")
	}
	if err != nil {
		// Parsing errors after decryption, triggered before parsing the MDC packet, are also usually the result of wrong password
		return nil, errors.New("gopenpgp: error in reading password protected message: wrong password or malformed message")
	}

	return &PlainMessage{
		Data:     messageBuf.Bytes(),
		TextType: !md.LiteralData.IsBinary,
		Filename: md.LiteralData.FileName,
		Time:     md.LiteralData.Time,
	}, nil
}
