// Copyright (c) 2020 Proton Technologies AG
//
// This file is part of ProtonMail Bridge.
//
// ProtonMail Bridge is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// ProtonMail Bridge is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with ProtonMail Bridge.  If not, see <https://www.gnu.org/licenses/>.

// NOTE: Comments in this file refer to a specification in a document called "ProtonMail Encryption logic". It will be referred to via abbreviation PMEL.

package smtp

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"io"
	"math/rand"
	"mime"
	"net/mail"
	"regexp"
	"strconv"
	"strings"
	"time"

	pmcrypto "github.com/ProtonMail/gopenpgp/crypto"
	"github.com/ProtonMail/proton-bridge/internal/bridge"
	"github.com/ProtonMail/proton-bridge/internal/events"
	"github.com/ProtonMail/proton-bridge/pkg/listener"
	"github.com/ProtonMail/proton-bridge/pkg/message"
	"github.com/ProtonMail/proton-bridge/pkg/pmapi"
	goSMTPBackend "github.com/emersion/go-smtp"
	"github.com/pkg/errors"
)

type smtpUser struct {
	panicHandler  panicHandler
	eventListener listener.Listener
	backend       *smtpBackend
	user          bridgeUser
	client        bridge.PMAPIProvider
	storeUser     storeUserProvider
	addressID     string
}

// newSMTPUser returns struct implementing go-smtp/session interface.
func newSMTPUser(
	panicHandler panicHandler,
	eventListener listener.Listener,
	smtpBackend *smtpBackend,
	user bridgeUser,
	addressID string,
) (goSMTPBackend.User, error) {
	// Using client directly is deprecated. Code should be moved to store.
	client := user.GetTemporaryPMAPIClient()

	storeUser := user.GetStore()
	if storeUser == nil {
		return nil, errors.New("user database is not initialized")
	}

	return &smtpUser{
		panicHandler:  panicHandler,
		eventListener: eventListener,
		backend:       smtpBackend,
		user:          user,
		client:        client,
		storeUser:     storeUser,
		addressID:     addressID,
	}, nil
}

// Send sends an email from the given address to the given addresses with the given body.
func (su *smtpUser) Send(from string, to []string, messageReader io.Reader) (err error) { //nolint[funlen]
	// Called from go-smtp in goroutines - we need to handle panics for each function.
	defer su.panicHandler.HandlePanic()

	mailSettings, err := su.client.GetMailSettings()
	if err != nil {
		return err
	}

	var addr *pmapi.Address = su.client.Addresses().ByEmail(from)
	if addr == nil {
		err = errors.New("backend: invalid email address: not owned by user")
		return
	}
	kr := addr.KeyRing()

	var attachedPublicKey string
	var attachedPublicKeyName string
	if mailSettings.AttachPublicKey > 0 {
		attachedPublicKey, err = kr.GetArmoredPublicKey()
		if err != nil {
			return err
		}
		attachedPublicKeyName = "publickey - " + kr.Identities()[0].Name
	}

	message, mimeBody, plainBody, attReaders, err := message.Parse(messageReader, attachedPublicKey, attachedPublicKeyName)
	if err != nil {
		return
	}
	clearBody := message.Body

	externalID := message.Header.Get("Message-Id")
	externalID = strings.Trim(externalID, "<>")

	draftID, parentID := su.handleReferencesHeader(message)

	if err = su.handleSenderAndRecipients(message, addr, from, to); err != nil {
		return err
	}

	message.AddressID = addr.ID

	// Apple Mail Message-Id has to be stored to avoid recovered message after each send.
	// Before it was done only for Apple Mail, but it should work for any client. Also, the client
	// is set up from IMAP and no one can be sure that the same client is used for SMTP as well.
	// Also, user can use more than one client which could break the condition as well.
	// If there is any problem, condition to Apple Mail only should be returned.
	// Note: for that, we would need to refactor a little bit and pass the last client name from
	// the IMAP through the bridge user.
	message.ExternalID = externalID

	// If Outlook does not get a response quickly, it will try to send the message again, leading
	// to sending the same message multiple times. In case we detect the same message is in the
	// sending queue, we wait a minute to finish the first request. If the message is still being
	// sent after the timeout, we return an error back to the client. The UX is not the best,
	// but it's better than sending the message many times. If the message was sent, we simply return
	// nil to indicate it's OK.
	sendRecorderMessageHash := su.backend.sendRecorder.getMessageHash(message)
	isSending, wasSent := su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash)
	if isSending {
		log.Debug("Message is in send queue, waiting")
		time.Sleep(60 * time.Second)
		isSending, wasSent = su.backend.sendRecorder.isSendingOrSent(su.client, sendRecorderMessageHash)
	}
	if isSending {
		log.Debug("Message is still in send queue, returning error")
		return errors.New("message is sending")
	}
	if wasSent {
		log.Debug("Message was already sent")
		return nil
	}

	message, atts, err := su.storeUser.CreateDraft(kr, message, attReaders, attachedPublicKey, attachedPublicKeyName, parentID)
	if err != nil {
		return
	}
	su.backend.sendRecorder.addMessage(sendRecorderMessageHash, message.ID)

	// We always have to create a new draft even if there already is one,
	// because clients don't necessarily save the draft before sending, which
	// can lead to sending the wrong message. Also clients do not necessarily
	// delete the old draft.
	if draftID != "" {
		if err := su.client.DeleteMessages([]string{draftID}); err != nil {
			log.WithError(err).WithField("draftID", draftID).Warn("Original draft cannot be deleted")
		}
	}

	atts = append(atts, message.Attachments...)
	// Decrypt attachment keys, because we will need to re-encrypt them with the recipients' public keys.
	attkeys := make(map[string]*pmcrypto.SymmetricKey)
	attkeysEncoded := make(map[string]pmapi.AlgoKey)

	for _, att := range atts {
		var keyPackets []byte
		if keyPackets, err = base64.StdEncoding.DecodeString(att.KeyPackets); err != nil {
			return errors.Wrap(err, "decoding attachment key packets")
		}
		if attkeys[att.ID], err = kr.DecryptSessionKey(keyPackets); err != nil {
			return errors.Wrap(err, "decrypting attachment session key")
		}
		attkeysEncoded[att.ID] = pmapi.AlgoKey{
			Key:       attkeys[att.ID].GetBase64Key(),
			Algorithm: attkeys[att.ID].Algo,
		}
	}

	plainSharedScheme := 0
	htmlSharedScheme := 0
	mimeSharedType := 0

	plainAddressMap := make(map[string]*pmapi.MessageAddress)
	htmlAddressMap := make(map[string]*pmapi.MessageAddress)
	mimeAddressMap := make(map[string]*pmapi.MessageAddress)

	// PMEL 2.
	settingsPgpScheme := mailSettings.PGPScheme
	settingsSign := (mailSettings.Sign > 0)

	// PMEL 3.
	composeMode := message.MIMEType

	var plainKey, htmlKey, mimeKey *pmcrypto.SymmetricKey
	var plainData, htmlData, mimeData []byte

	containsUnencryptedRecipients := false

	for _, email := range to {
		// PMEL 1.
		contactEmails, err := su.client.GetContactEmailByEmail(email, 0, 1000)
		if err != nil {
			return err
		}
		var contactMeta *ContactMetadata
		var contactKeys []*pmcrypto.KeyRing
		for _, contactEmail := range contactEmails {
			if contactEmail.Defaults == 1 { // WARNING: in doc it says _ignore for now, future feature_
				continue
			}
			contact, err := su.client.GetContactByID(contactEmail.ContactID)
			if err != nil {
				return err
			}
			decryptedCards, err := su.client.DecryptAndVerifyCards(contact.Cards)
			if err != nil {
				return err
			}
			contactMeta, err = GetContactMetadataFromVCards(decryptedCards, email)
			if err != nil {
				return err
			}
			for _, contactRawKey := range contactMeta.Keys {
				contactKey, err := pmcrypto.ReadKeyRing(bytes.NewBufferString(contactRawKey))
				if err != nil {
					return err
				}
				contactKeys = append(contactKeys, contactKey)
			}

			break // We take the first hit where Defaults == 0, see "How to find the right contact" of PMEL
		}

		// PMEL 4.
		apiRawKeyList, isInternal, err := su.client.GetPublicKeysForEmail(email)
		if err != nil {
			err = fmt.Errorf("backend: cannot get recipients' public keys: %v", err)
			return err
		}

		var apiKeys []*pmcrypto.KeyRing
		for _, apiRawKey := range apiRawKeyList {
			var kr *pmcrypto.KeyRing
			if kr, err = pmcrypto.ReadArmoredKeyRing(strings.NewReader(apiRawKey.PublicKey)); err != nil {
				return err
			}
			apiKeys = append(apiKeys, kr)
		}

		sendingInfo, err := generateSendingInfo(su.eventListener, contactMeta, isInternal, composeMode, apiKeys, contactKeys, settingsSign, settingsPgpScheme)
		if !sendingInfo.Encrypt {
			containsUnencryptedRecipients = true
		}
		if err != nil {
			return errors.New("error sending to user " + email + ": " + err.Error())
		}

		var signature int
		if sendingInfo.Sign {
			signature = pmapi.YesSignature
		} else {
			signature = pmapi.NoSignature
		}
		if sendingInfo.Scheme == pmapi.PGPMIMEPackage || sendingInfo.Scheme == pmapi.ClearMIMEPackage {
			if mimeKey == nil {
				if mimeKey, mimeData, err = encryptSymmetric(kr, mimeBody, true); err != nil {
					return err
				}
			}
			if sendingInfo.Scheme == pmapi.PGPMIMEPackage {
				mimeBodyPacket, _, err := createPackets(sendingInfo.PublicKey, mimeKey, map[string]*pmcrypto.SymmetricKey{})
				if err != nil {
					return err
				}
				mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, BodyKeyPacket: mimeBodyPacket, Signature: signature}
			} else {
				mimeAddressMap[email] = &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
			}
			mimeSharedType |= sendingInfo.Scheme
		} else {
			switch sendingInfo.MIMEType {
			case pmapi.ContentTypePlainText:
				if plainKey == nil {
					if plainKey, plainData, err = encryptSymmetric(kr, plainBody, true); err != nil {
						return err
					}
				}
				newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
				if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
					newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, plainKey, attkeys)
					if err != nil {
						return err
					}
				}
				plainAddressMap[email] = newAddress
				plainSharedScheme |= sendingInfo.Scheme
			case pmapi.ContentTypeHTML:
				if htmlKey == nil {
					if htmlKey, htmlData, err = encryptSymmetric(kr, clearBody, true); err != nil {
						return err
					}
				}
				newAddress := &pmapi.MessageAddress{Type: sendingInfo.Scheme, Signature: signature}
				if sendingInfo.Encrypt && sendingInfo.PublicKey != nil {
					newAddress.BodyKeyPacket, newAddress.AttachmentKeyPackets, err = createPackets(sendingInfo.PublicKey, htmlKey, attkeys)
					if err != nil {
						return err
					}
				}
				htmlAddressMap[email] = newAddress
				htmlSharedScheme |= sendingInfo.Scheme
			}
		}
	}

	if containsUnencryptedRecipients {
		dec := new(mime.WordDecoder)
		subject, err := dec.DecodeHeader(message.Header.Get("Subject"))
		if err != nil {
			return errors.New("error decoding subject message " + message.Header.Get("Subject"))
		}
		if !su.continueSendingUnencryptedMail(subject) {
			_ = su.client.DeleteMessages([]string{message.ID})
			return errors.New("sending was canceled by user")
		}
	}

	req := &pmapi.SendMessageReq{}

	plainPkg := buildPackage(plainAddressMap, plainSharedScheme, pmapi.ContentTypePlainText, plainData, plainKey, attkeysEncoded)
	if plainPkg != nil {
		req.Packages = append(req.Packages, plainPkg)
	}
	htmlPkg := buildPackage(htmlAddressMap, htmlSharedScheme, pmapi.ContentTypeHTML, htmlData, htmlKey, attkeysEncoded)
	if htmlPkg != nil {
		req.Packages = append(req.Packages, htmlPkg)
	}

	if len(mimeAddressMap) > 0 {
		pkg := &pmapi.MessagePackage{
			Body:      base64.StdEncoding.EncodeToString(mimeData),
			Addresses: mimeAddressMap,
			MIMEType:  pmapi.ContentTypeMultipartMixed,
			Type:      mimeSharedType,
			BodyKey: pmapi.AlgoKey{
				Key:       mimeKey.GetBase64Key(),
				Algorithm: mimeKey.Algo,
			},
		}
		req.Packages = append(req.Packages, pkg)
	}

	return su.storeUser.SendMessage(message.ID, req)
}

func (su *smtpUser) handleReferencesHeader(m *pmapi.Message) (draftID, parentID string) {
	// Remove the internal IDs from the references header before sending to avoid confusion.
	references := m.Header.Get("References")
	newReferences := []string{}
	for _, reference := range strings.Fields(references) {
		if !strings.Contains(reference, "@protonmail.internalid") {
			newReferences = append(newReferences, reference)
		} else { // internalid is the parentID.
			idMatch := regexp.MustCompile("[a-zA-Z0-9-_=]*@protonmail.internalid").FindString(reference)
			if idMatch != "" {
				lastID := idMatch[0 : len(idMatch)-len("@protonmail.internalid")]
				filter := &pmapi.MessagesFilter{ID: []string{lastID}}
				if su.addressID != "" {
					filter.AddressID = su.addressID
				}
				metadata, _, _ := su.client.ListMessages(filter)
				for _, m := range metadata {
					if isDraft(m) {
						draftID = m.ID
					} else {
						parentID = m.ID
					}
				}
			}
		}
	}

	m.Header["References"] = newReferences

	if parentID == "" && len(newReferences) > 0 {
		externalID := strings.Trim(newReferences[len(newReferences)-1], "<>")
		filter := &pmapi.MessagesFilter{ExternalID: externalID}
		if su.addressID != "" {
			filter.AddressID = su.addressID
		}
		metadata, _, _ := su.client.ListMessages(filter)
		// There can be two or messages with the same external ID and then we cannot
		// be sure which message should be parent. Better to not choose any.
		if len(metadata) == 1 {
			parentID = metadata[0].ID
		}
	}

	return draftID, parentID
}

func isDraft(m *pmapi.Message) bool {
	for _, labelID := range m.LabelIDs {
		if labelID == pmapi.DraftLabel {
			return true
		}
	}
	return false
}

func (su *smtpUser) handleSenderAndRecipients(m *pmapi.Message, addr *pmapi.Address, from string, to []string) (err error) {
	from = pmapi.ConstructAddress(from, addr.Email)

	// Check sender.
	if m.Sender == nil {
		m.Sender = &mail.Address{Address: from}
	} else {
		m.Sender.Address = from
	}

	// Check recipients.
	if len(to) == 0 {
		err = errors.New("backend: no recipient specified")
		return
	}

	// Sanitize ToList because some clients add *Sender* in the *ToList* when only Bcc is filled.
	i := 0
	for _, keep := range m.ToList {
		keepThis := false
		for _, addr := range to {
			if addr == keep.Address {
				keepThis = true
				break
			}
		}
		if keepThis {
			m.ToList[i] = keep
			i++
		}
	}
	m.ToList = m.ToList[:i]

	// Build a map of recipients visible to all.
	// Bcc should be empty when sending a message.
	var recipients []*mail.Address
	recipients = append(recipients, m.ToList...)
	recipients = append(recipients, m.CCList...)
	recipients = append(recipients, m.BCCList...)

	rm := map[string]bool{}
	for _, r := range recipients {
		rm[r.Address] = true
	}

	for _, r := range to {
		if !rm[r] {
			// Recipient is not known, add it to Bcc.
			m.BCCList = append(m.BCCList, &mail.Address{Address: r})
		}
	}

	return nil
}

func (su *smtpUser) continueSendingUnencryptedMail(subject string) bool {
	if !su.backend.shouldReportOutgoingNoEnc() {
		return true
	}

	messageID := strconv.Itoa(rand.Int()) //nolint[gosec]
	ch := make(chan bool)
	su.backend.shouldSendNoEncChannels[messageID] = ch
	su.eventListener.Emit(events.OutgoingNoEncEvent, messageID+":"+subject)

	log.Debug("Waiting for sendingUnencrypted confirmation for ", messageID)

	var res bool
	select {
	case res = <-ch:
		// GUI should always respond in 10 seconds, but let's have safety timeout
		// in case GUI will not respond properly. If GUI didn't respond, we cannot
		// be sure if user even saw the notice: better to not send the e-mail.
		log.Debug("Got sendingUnencrypted for ", messageID, ": ", res)
	case <-time.After(15 * time.Second):
		log.Debug("sendingUnencrypted timeout, not sending ", messageID)
		res = false
	}

	delete(su.backend.shouldSendNoEncChannels, messageID)
	close(ch)

	return res
}

// Logout is called when this User will no longer be used.
func (su *smtpUser) Logout() error {
	log.Debug("SMTP client logged out user ", su.addressID)
	return nil
}
