package crypto

import (
	"bytes"
	"encoding/base64"
	"encoding/json"
	goerrors "errors"
	"io"
	"io/ioutil"
	"regexp"
	"strings"
	"time"

	"github.com/ProtonMail/go-crypto/openpgp/clearsign"
	"github.com/ProtonMail/go-crypto/openpgp/packet"
	"github.com/ProtonMail/gopenpgp/v2/armor"
	"github.com/ProtonMail/gopenpgp/v2/constants"
	"github.com/ProtonMail/gopenpgp/v2/internal"
	"github.com/pkg/errors"
)

// ---- MODELS -----

// PlainMessage stores a plain text / unencrypted message.
type PlainMessage struct {
	// The content of the message
	Data []byte
	// If the content is text or binary
	TextType bool
	// The file's latest modification time
	Time uint32
	// The encrypted message's filename
	Filename string
}

// PGPMessage stores a PGP-encrypted message.
type PGPMessage struct {
	// The content of the message
	Data []byte
}

// PGPSignature stores a PGP-encoded detached signature.
type PGPSignature struct {
	// The content of the signature
	Data []byte
}

// PGPSplitMessage contains a separate session key packet and symmetrically
// encrypted data packet.
type PGPSplitMessage struct {
	DataPacket []byte
	KeyPacket  []byte
}

// A ClearTextMessage is a signed but not encrypted PGP message,
// i.e. the ones beginning with -----BEGIN PGP SIGNED MESSAGE-----.
type ClearTextMessage struct {
	Data      []byte
	Signature []byte
}

// ---- GENERATORS -----

// NewPlainMessage generates a new binary PlainMessage ready for encryption,
// signature, or verification from the unencrypted binary data.
// This will encrypt the message with the binary flag and preserve the file as is.
func NewPlainMessage(data []byte) *PlainMessage {
	return &PlainMessage{
		Data:     clone(data),
		TextType: false,
		Filename: "",
		Time:     uint32(GetUnixTime()),
	}
}

// NewPlainMessageFromFile generates a new binary PlainMessage ready for encryption,
// signature, or verification from the unencrypted binary data.
// This will encrypt the message with the binary flag and preserve the file as is.
// It assigns a filename and a modification time.
func NewPlainMessageFromFile(data []byte, filename string, time uint32) *PlainMessage {
	return &PlainMessage{
		Data:     clone(data),
		TextType: false,
		Filename: filename,
		Time:     time,
	}
}

// NewPlainMessageFromString generates a new text PlainMessage,
// ready for encryption, signature, or verification from an unencrypted string.
// This will encrypt the message with the text flag, canonicalize the line endings
// (i.e. set all of them to \r\n) and strip the trailing spaces for each line.
// This allows seamless conversion to clear text signed messages (see RFC 4880 5.2.1 and 7.1).
func NewPlainMessageFromString(text string) *PlainMessage {
	return &PlainMessage{
		Data:     []byte(internal.Canonicalize(text)),
		TextType: true,
		Filename: "",
		Time:     uint32(GetUnixTime()),
	}
}

// NewPGPMessage generates a new PGPMessage from the unarmored binary data.
func NewPGPMessage(data []byte) *PGPMessage {
	return &PGPMessage{
		Data: clone(data),
	}
}

// NewPGPMessageFromArmored generates a new PGPMessage from an armored string ready for decryption.
func NewPGPMessageFromArmored(armored string) (*PGPMessage, error) {
	encryptedIO, err := internal.Unarmor(armored)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in unarmoring message")
	}

	message, err := ioutil.ReadAll(encryptedIO.Body)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in reading armored message")
	}

	return &PGPMessage{
		Data: message,
	}, nil
}

// NewPGPSplitMessage generates a new PGPSplitMessage from the binary unarmored keypacket,
// datapacket, and encryption algorithm.
func NewPGPSplitMessage(keyPacket []byte, dataPacket []byte) *PGPSplitMessage {
	return &PGPSplitMessage{
		KeyPacket:  clone(keyPacket),
		DataPacket: clone(dataPacket),
	}
}

// NewPGPSplitMessageFromArmored generates a new PGPSplitMessage by splitting an armored message into its
// session key packet and symmetrically encrypted data packet.
func NewPGPSplitMessageFromArmored(encrypted string) (*PGPSplitMessage, error) {
	message, err := NewPGPMessageFromArmored(encrypted)
	if err != nil {
		return nil, err
	}

	return message.SplitMessage()
}

// NewPGPSignature generates a new PGPSignature from the unarmored binary data.
func NewPGPSignature(data []byte) *PGPSignature {
	return &PGPSignature{
		Data: clone(data),
	}
}

// NewPGPSignatureFromArmored generates a new PGPSignature from the armored
// string ready for verification.
func NewPGPSignatureFromArmored(armored string) (*PGPSignature, error) {
	encryptedIO, err := internal.Unarmor(armored)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in unarmoring signature")
	}

	signature, err := ioutil.ReadAll(encryptedIO.Body)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in reading armored signature")
	}

	return &PGPSignature{
		Data: signature,
	}, nil
}

// NewClearTextMessage generates a new ClearTextMessage from data and
// signature.
func NewClearTextMessage(data []byte, signature []byte) *ClearTextMessage {
	return &ClearTextMessage{
		Data:      clone(data),
		Signature: clone(signature),
	}
}

// NewClearTextMessageFromArmored returns the message body and unarmored
// signature from a clearsigned message.
func NewClearTextMessageFromArmored(signedMessage string) (*ClearTextMessage, error) {
	modulusBlock, rest := clearsign.Decode([]byte(signedMessage))
	if len(rest) != 0 {
		return nil, errors.New("gopenpgp: extra data after modulus")
	}

	signature, err := ioutil.ReadAll(modulusBlock.ArmoredSignature.Body)
	if err != nil {
		return nil, errors.Wrap(err, "gopenpgp: error in reading cleartext message")
	}

	return NewClearTextMessage(modulusBlock.Bytes, signature), nil
}

// ---- MODEL METHODS -----

// GetBinary returns the binary content of the message as a []byte.
func (msg *PlainMessage) GetBinary() []byte {
	return msg.Data
}

// GetString returns the content of the message as a string.
func (msg *PlainMessage) GetString() string {
	return sanitizeString(strings.ReplaceAll(string(msg.Data), "\r\n", "\n"))
}

// GetBase64 returns the base-64 encoded binary content of the message as a
// string.
func (msg *PlainMessage) GetBase64() string {
	return base64.StdEncoding.EncodeToString(msg.Data)
}

// NewReader returns a New io.Reader for the binary data of the message.
func (msg *PlainMessage) NewReader() io.Reader {
	return bytes.NewReader(msg.GetBinary())
}

// IsText returns whether the message is a text message.
func (msg *PlainMessage) IsText() bool {
	return msg.TextType
}

// IsBinary returns whether the message is a binary message.
func (msg *PlainMessage) IsBinary() bool {
	return !msg.TextType
}

// getFormattedTime returns the message (latest modification) Time as time.Time.
func (msg *PlainMessage) getFormattedTime() time.Time {
	return time.Unix(int64(msg.Time), 0)
}

// GetBinary returns the unarmored binary content of the message as a []byte.
func (msg *PGPMessage) GetBinary() []byte {
	return msg.Data
}

// NewReader returns a New io.Reader for the unarmored binary data of the
// message.
func (msg *PGPMessage) NewReader() io.Reader {
	return bytes.NewReader(msg.GetBinary())
}

// GetArmored returns the armored message as a string.
func (msg *PGPMessage) GetArmored() (string, error) {
	return armor.ArmorWithType(msg.Data, constants.PGPMessageHeader)
}

// GetArmoredWithCustomHeaders returns the armored message as a string, with
// the given headers. Empty parameters are omitted from the headers.
func (msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (string, error) {
	return armor.ArmorWithTypeAndCustomHeaders(msg.Data, constants.PGPMessageHeader, version, comment)
}

// GetEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
func (msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool) {
	packets := packet.NewReader(bytes.NewReader(msg.Data))
	var err error
	var ids []uint64
	var encryptedKey *packet.EncryptedKey
Loop:
	for {
		var p packet.Packet
		if p, err = packets.Next(); goerrors.Is(err, io.EOF) {
			break
		}
		switch p := p.(type) {
		case *packet.EncryptedKey:
			encryptedKey = p
			ids = append(ids, encryptedKey.KeyId)
		case *packet.SymmetricallyEncrypted,
			*packet.AEADEncrypted,
			*packet.Compressed,
			*packet.LiteralData:
			break Loop
		}
	}
	if len(ids) > 0 {
		return ids, true
	}
	return ids, false
}

// GetHexEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
func (msg *PGPMessage) GetHexEncryptionKeyIDs() ([]string, bool) {
	return getHexKeyIDs(msg.GetEncryptionKeyIDs())
}

// GetHexEncryptionKeyIDsJson returns the key IDs of the keys to which the session key is encrypted as a JSON array.
// If an error occurs it returns nil.
// Helper function for go-mobile clients.
func (msg *PGPMessage) GetHexEncryptionKeyIDsJson() []byte {
	hexIds, ok := msg.GetHexEncryptionKeyIDs()
	if !ok {
		return nil
	}
	hexIdsJson, err := json.Marshal(hexIds)
	if err != nil {
		return nil
	}
	return hexIdsJson
}

// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to.
func (msg *PGPMessage) GetSignatureKeyIDs() ([]uint64, bool) {
	return getSignatureKeyIDs(msg.Data)
}

// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
func (msg *PGPMessage) GetHexSignatureKeyIDs() ([]string, bool) {
	return getHexKeyIDs(msg.GetSignatureKeyIDs())
}

// GetHexSignatureKeyIDsJson returns the key IDs of the keys to which the (readable) signature packets
// are encrypted to as a JSON array. Helper function for go-mobile clients.
func (msg *PGPMessage) GetHexSignatureKeyIDsJson() []byte {
	sigHexSigIds, ok := msg.GetHexSignatureKeyIDs()
	if !ok {
		return nil
	}
	sigHexKeyIdsJSON, err := json.Marshal(sigHexSigIds)
	if err != nil {
		return nil
	}
	return sigHexKeyIdsJSON
}

// GetBinaryDataPacket returns the unarmored binary datapacket as a []byte.
func (msg *PGPSplitMessage) GetBinaryDataPacket() []byte {
	return msg.DataPacket
}

// GetBinaryKeyPacket returns the unarmored binary keypacket as a []byte.
func (msg *PGPSplitMessage) GetBinaryKeyPacket() []byte {
	return msg.KeyPacket
}

// GetBinary returns the unarmored binary joined packets as a []byte.
func (msg *PGPSplitMessage) GetBinary() []byte {
	return append(msg.KeyPacket, msg.DataPacket...)
}

// GetArmored returns the armored message as a string, with joined data and key
// packets.
func (msg *PGPSplitMessage) GetArmored() (string, error) {
	return armor.ArmorWithType(msg.GetBinary(), constants.PGPMessageHeader)
}

// GetPGPMessage joins asymmetric session key packet with the symmetric data
// packet to obtain a PGP message.
func (msg *PGPSplitMessage) GetPGPMessage() *PGPMessage {
	return NewPGPMessage(append(msg.KeyPacket, msg.DataPacket...))
}

// GetNumberOfKeyPackets returns the number of keys packets in this message.
func (msg *PGPSplitMessage) GetNumberOfKeyPackets() (int, error) {
	bytesReader := bytes.NewReader(msg.KeyPacket)
	packets := packet.NewReader(bytesReader)
	var keyPacketCount int
	for {
		p, err := packets.Next()
		if goerrors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return 0, err
		}
		switch p.(type) {
		case *packet.SymmetricKeyEncrypted, *packet.EncryptedKey:
			keyPacketCount += 1
		}
	}
	return keyPacketCount, nil
}

// SplitMessage splits the message into key and data packet(s).
// Parameters are for backwards compatibility and are unused.
func (msg *PGPMessage) SplitMessage() (*PGPSplitMessage, error) {
	bytesReader := bytes.NewReader(msg.Data)
	packets := packet.NewReader(bytesReader)
	splitPoint := int64(0)
Loop:
	for {
		p, err := packets.Next()
		if goerrors.Is(err, io.EOF) {
			break
		}
		if err != nil {
			return nil, err
		}
		switch p.(type) {
		case *packet.SymmetricKeyEncrypted, *packet.EncryptedKey:
			splitPoint = bytesReader.Size() - int64(bytesReader.Len())
		case *packet.SymmetricallyEncrypted, *packet.AEADEncrypted:
			break Loop
		}
	}
	return &PGPSplitMessage{
		KeyPacket:  clone(msg.Data[:splitPoint]),
		DataPacket: clone(msg.Data[splitPoint:]),
	}, nil
}

// SeparateKeyAndData splits the message into key and data packet(s).
// Parameters are for backwards compatibility and are unused.
// Deprecated: use SplitMessage().
func (msg *PGPMessage) SeparateKeyAndData(_ int, _ int) (*PGPSplitMessage, error) {
	return msg.SplitMessage()
}

// GetBinary returns the unarmored binary content of the signature as a []byte.
func (sig *PGPSignature) GetBinary() []byte {
	return sig.Data
}

// GetArmored returns the armored signature as a string.
func (sig *PGPSignature) GetArmored() (string, error) {
	return armor.ArmorWithType(sig.Data, constants.PGPSignatureHeader)
}

// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to.
func (sig *PGPSignature) GetSignatureKeyIDs() ([]uint64, bool) {
	return getSignatureKeyIDs(sig.Data)
}

// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
func (sig *PGPSignature) GetHexSignatureKeyIDs() ([]string, bool) {
	return getHexKeyIDs(sig.GetSignatureKeyIDs())
}

// GetBinary returns the unarmored signed data as a []byte.
func (msg *ClearTextMessage) GetBinary() []byte {
	return msg.Data
}

// GetString returns the unarmored signed data as a string.
func (msg *ClearTextMessage) GetString() string {
	return string(msg.Data)
}

// GetBinarySignature returns the unarmored binary signature as a []byte.
func (msg *ClearTextMessage) GetBinarySignature() []byte {
	return msg.Signature
}

// GetArmored armors plaintext and signature with the PGP SIGNED MESSAGE
// armoring.
func (msg *ClearTextMessage) GetArmored() (string, error) {
	armSignature, err := armor.ArmorWithType(msg.GetBinarySignature(), constants.PGPSignatureHeader)
	if err != nil {
		return "", errors.Wrap(err, "gopenpgp: error in armoring cleartext message")
	}

	str := "-----BEGIN PGP SIGNED MESSAGE-----\r\nHash: SHA512\r\n\r\n"
	str += msg.GetString()
	str += "\r\n"
	str += armSignature

	return str, nil
}

// ---- UTILS -----

// IsPGPMessage checks if data if has armored PGP message format.
func IsPGPMessage(data string) bool {
	re := regexp.MustCompile("^-----BEGIN " + constants.PGPMessageHeader + "-----(?s:.+)-----END " +
		constants.PGPMessageHeader + "-----")
	return re.MatchString(data)
}

func getSignatureKeyIDs(data []byte) ([]uint64, bool) {
	packets := packet.NewReader(bytes.NewReader(data))
	var err error
	var ids []uint64
	var onePassSignaturePacket *packet.OnePassSignature
	var signaturePacket *packet.Signature

Loop:
	for {
		var p packet.Packet
		if p, err = packets.Next(); goerrors.Is(err, io.EOF) {
			break
		}
		switch p := p.(type) {
		case *packet.OnePassSignature:
			onePassSignaturePacket = p
			ids = append(ids, onePassSignaturePacket.KeyId)
		case *packet.Signature:
			signaturePacket = p
			if signaturePacket.IssuerKeyId != nil {
				ids = append(ids, *signaturePacket.IssuerKeyId)
			}
		case *packet.SymmetricallyEncrypted,
			*packet.AEADEncrypted,
			*packet.Compressed,
			*packet.LiteralData:
			break Loop
		}
	}
	if len(ids) > 0 {
		return ids, true
	}
	return ids, false
}

func getHexKeyIDs(keyIDs []uint64, ok bool) ([]string, bool) {
	hexIDs := make([]string, len(keyIDs))

	for i, id := range keyIDs {
		hexIDs[i] = keyIDToHex(id)
	}

	return hexIDs, ok
}
