// 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/>.

package imap

import (
	"errors"
	"fmt"
	"net/mail"
	"strings"
	"sync"
	"time"

	"github.com/ProtonMail/proton-bridge/internal/imap/uidplus"
	"github.com/ProtonMail/proton-bridge/pkg/message"
	"github.com/ProtonMail/proton-bridge/pkg/parallel"
	"github.com/ProtonMail/proton-bridge/pkg/pmapi"
	"github.com/emersion/go-imap"
	"github.com/sirupsen/logrus"
)

// UpdateMessagesFlags alters flags for the specified message(s).
//
// If the Backend implements Updater, it must notify the client immediately
// via a message update.
func (im *imapMailbox) UpdateMessagesFlags(uid bool, seqSet *imap.SeqSet, operation imap.FlagsOp, flags []string) error {
	log.WithFields(logrus.Fields{
		"flags":     flags,
		"operation": operation,
	}).Debug("Updating message flags")

	// Called from go-imap in goroutines - we need to handle panics for each function.
	defer im.panicHandler.HandlePanic()

	messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
	if err != nil || len(messageIDs) == 0 {
		return err
	}

	for _, f := range flags {
		switch f {
		case imap.SeenFlag:
			switch operation {
			case imap.SetFlags, imap.AddFlags:
				_ = im.storeMailbox.MarkMessagesRead(messageIDs)
			case imap.RemoveFlags:
				_ = im.storeMailbox.MarkMessagesUnread(messageIDs)
			}
		case imap.FlaggedFlag:
			switch operation {
			case imap.SetFlags, imap.AddFlags:
				_ = im.storeMailbox.MarkMessagesStarred(messageIDs)
			case imap.RemoveFlags:
				_ = im.storeMailbox.MarkMessagesUnstarred(messageIDs)
			}
		case imap.DeletedFlag:
			if operation == imap.RemoveFlags {
				break // Nothing to do, no message has the \Deleted flag.
			}
			_ = im.storeMailbox.DeleteMessages(messageIDs)
		case imap.AnsweredFlag, imap.DraftFlag, imap.RecentFlag:
			// Not supported.
		case message.AppleMailJunkFlag, message.ThunderbirdJunkFlag:
			storeMailbox, err := im.storeAddress.GetMailbox(pmapi.SpamLabel)
			if err != nil {
				return err
			}

			// Handle custom junk flags for Apple Mail and Thunderbird.
			switch operation {
			// No label removal is necessary because Spam and Inbox are both exclusive labels so the backend
			// will automatically take care of label removal.
			case imap.SetFlags, imap.AddFlags:
				_ = storeMailbox.LabelMessages(messageIDs)
			case imap.RemoveFlags:
				_ = storeMailbox.UnlabelMessages(messageIDs)
			}
		}
	}

	return nil
}

// CopyMessages copies the specified message(s) to the end of the specified
// destination mailbox. The flags and internal date of the message(s) SHOULD
// be preserved, and the Recent flag SHOULD be set, in the copy.
func (im *imapMailbox) CopyMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
	// Called from go-imap in goroutines - we need to handle panics for each function.
	defer im.panicHandler.HandlePanic()

	return im.labelMessages(uid, seqSet, targetLabel, false)
}

// MoveMessages adds dest's label and removes this mailbox' label from each message.
//
// This should not be used until MOVE extension has option to send UIDPLUS
// responses.
func (im *imapMailbox) MoveMessages(uid bool, seqSet *imap.SeqSet, targetLabel string) error {
	// Called from go-imap in goroutines - we need to handle panics for each function.
	defer im.panicHandler.HandlePanic()

	return im.labelMessages(uid, seqSet, targetLabel, true)
}

func (im *imapMailbox) labelMessages(uid bool, seqSet *imap.SeqSet, targetLabel string, move bool) error {
	messageIDs, err := im.apiIDsFromSeqSet(uid, seqSet)
	if err != nil || len(messageIDs) == 0 {
		return err
	}

	// It is needed to get UID list before LabelingMessages because
	// messages can be removed from source during labeling (e.g. folder1 -> folder2).
	sourceSeqSet := im.storeMailbox.GetUIDList(messageIDs)

	targetStoreMailbox, err := im.storeAddress.GetMailbox(targetLabel)
	if err != nil {
		return err
	}

	// Label messages first to not lose them. If message is only in trash and we unlabel
	// it, it will be removed completely and we cannot label it back.
	if err := targetStoreMailbox.LabelMessages(messageIDs); err != nil {
		return err
	}
	if move {
		if err := im.storeMailbox.UnlabelMessages(messageIDs); err != nil {
			return err
		}
	}

	targetSeqSet := targetStoreMailbox.GetUIDList(messageIDs)
	return uidplus.CopyResponse(targetStoreMailbox.UIDValidity(), sourceSeqSet, targetSeqSet)
}

// SearchMessages searches messages. The returned list must contain UIDs if
// uid is set to true, or sequence numbers otherwise.
func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { //nolint[gocyclo]
	// Called from go-imap in goroutines - we need to handle panics for each function.
	defer im.panicHandler.HandlePanic()

	if criteria.Not != nil || criteria.Or[0] != nil {
		return nil, errors.New("unsupported search query")
	}

	if criteria.Body != "" || criteria.Text != "" {
		log.Warn("Body and Text criteria not applied.")
	}

	var apiIDs []string
	if criteria.SeqSet != nil {
		apiIDs, err = im.apiIDsFromSeqSet(false, criteria.SeqSet)
	} else {
		apiIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(1, 0)
	}
	if err != nil {
		return nil, err
	}

	var apiIDsFromUID []string
	if criteria.Uid != nil {
		if apiIDs, err := im.apiIDsFromSeqSet(true, criteria.Uid); err == nil {
			apiIDsFromUID = append(apiIDsFromUID, apiIDs...)
		}
	}

	// Apply filters.
	for _, apiID := range apiIDs {
		// Filter on UIDs.
		if len(apiIDsFromUID) > 0 && !isStringInList(apiIDsFromUID, apiID) {
			continue
		}

		// Get message.
		storeMessage, err := im.storeMailbox.GetMessage(apiID)
		if err != nil {
			log.Warnf("search messages: cannot get message %q from db: %v", apiID, err)
			continue
		}
		m := storeMessage.Message()

		// Filter addresses.
		if criteria.From != "" && !addressMatch([]*mail.Address{m.Sender}, criteria.From) {
			continue
		}
		if criteria.To != "" && !addressMatch(m.ToList, criteria.To) {
			continue
		}
		if criteria.Cc != "" && !addressMatch(m.CCList, criteria.Cc) {
			continue
		}
		if criteria.Bcc != "" && !addressMatch(m.BCCList, criteria.Bcc) {
			continue
		}

		// Filter strings.
		if criteria.Subject != "" && !strings.Contains(strings.ToLower(m.Subject), strings.ToLower(criteria.Subject)) {
			continue
		}
		if criteria.Keyword != "" && !hasKeyword(m, criteria.Keyword) {
			continue
		}
		if criteria.Unkeyword != "" && hasKeyword(m, criteria.Unkeyword) {
			continue
		}
		if criteria.Header[0] != "" {
			h := message.GetHeader(m)
			if val := h.Get(criteria.Header[0]); val == "" {
				continue // Field is not in header.
			} else if criteria.Header[1] != "" && !strings.Contains(strings.ToLower(val), strings.ToLower(criteria.Header[1])) {
				continue // Field is in header, second criteria is non-zero and field value not matched (case insensitive).
			}
		}

		// Filter flags.
		if criteria.Flagged && !isStringInList(m.LabelIDs, pmapi.StarredLabel) {
			continue
		}
		if criteria.Unflagged && isStringInList(m.LabelIDs, pmapi.StarredLabel) {
			continue
		}
		if criteria.Seen && m.Unread == 1 {
			continue
		}
		if criteria.Unseen && m.Unread == 0 {
			continue
		}
		if criteria.Deleted {
			continue
		}
		// if criteria.Undeleted { // All messages matches this criteria }
		if criteria.Draft && (m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) {
			continue
		}
		if criteria.Undraft && !(m.Has(pmapi.FlagSent) || m.Has(pmapi.FlagReceived)) {
			continue
		}
		if criteria.Answered && !(m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) {
			continue
		}
		if criteria.Unanswered && (m.Has(pmapi.FlagReplied) || m.Has(pmapi.FlagRepliedAll)) {
			continue
		}
		if criteria.Recent && m.Has(pmapi.FlagOpened) { // opened means not recent
			continue
		}
		if criteria.Old && !m.Has(pmapi.FlagOpened) {
			continue
		}
		if criteria.New && !(!m.Has(pmapi.FlagOpened) && m.Unread == 1) {
			continue
		}

		// Filter internal date.
		if !criteria.Before.IsZero() {
			if truncated := criteria.Before.Truncate(24 * time.Hour); m.Time > truncated.Unix() {
				continue
			}
		}
		if !criteria.Since.IsZero() {
			if truncated := criteria.Since.Truncate(24 * time.Hour); m.Time < truncated.Unix() {
				continue
			}
		}
		if !criteria.On.IsZero() {
			truncated := criteria.On.Truncate(24 * time.Hour)
			if m.Time < truncated.Unix() || m.Time > truncated.Add(24*time.Hour).Unix() {
				continue
			}
		}
		if !(criteria.SentBefore.IsZero() && criteria.SentSince.IsZero() && criteria.SentOn.IsZero()) {
			if t, err := m.Header.Date(); err == nil && !t.IsZero() {
				// Filter header date.
				if !criteria.SentBefore.IsZero() {
					if truncated := criteria.SentBefore.Truncate(24 * time.Hour); t.Unix() > truncated.Unix() {
						continue
					}
				}
				if !criteria.SentSince.IsZero() {
					if truncated := criteria.SentSince.Truncate(24 * time.Hour); t.Unix() < truncated.Unix() {
						continue
					}
				}
				if !criteria.SentOn.IsZero() {
					truncated := criteria.SentOn.Truncate(24 * time.Hour)
					if t.Unix() < truncated.Unix() || t.Unix() > truncated.Add(24*time.Hour).Unix() {
						continue
					}
				}
			}
		}

		// Filter size (only if size was already calculated).
		if m.Size > 0 {
			if criteria.Larger != 0 && m.Size <= int64(criteria.Larger) {
				continue
			}
			if criteria.Smaller != 0 && m.Size >= int64(criteria.Smaller) {
				continue
			}
		}

		// Add the ID to response.
		var id uint32
		if isUID {
			id, err = storeMessage.UID()
			if err != nil {
				return nil, err
			}
		} else {
			id, err = storeMessage.SequenceNumber()
			if err != nil {
				return nil, err
			}
		}
		ids = append(ids, id)
	}

	return ids, nil
}

// ListMessages returns a list of messages. seqset must be interpreted as UIDs
// if uid is set to true and as message sequence numbers otherwise. See RFC
// 3501 section 6.4.5 for a list of items that can be requested.
//
// Messages must be sent to msgResponse. When the function returns, msgResponse must be closed.
func (im *imapMailbox) ListMessages(isUID bool, seqSet *imap.SeqSet, items []string, msgResponse chan<- *imap.Message) (err error) { //nolint[funlen]
	defer func() {
		close(msgResponse)
		if err != nil {
			log.Errorf("cannot list messages (%v, %v, %v): %v", isUID, seqSet, items, err)
		}
		// Called from go-imap in goroutines - we need to handle panics for each function.
		im.panicHandler.HandlePanic()
	}()

	var markAsReadIDs []string
	markAsReadMutex := &sync.Mutex{}

	l := log.WithField("cmd", "ListMessages")

	apiIDs, err := im.apiIDsFromSeqSet(isUID, seqSet)
	if err != nil {
		err = fmt.Errorf("list messages seq: %v", err)
		l.WithField("seq", seqSet).Error(err)
		return err
	}

	// From RFC: UID range of 559:* always includes the UID of the last message
	// in the mailbox, even if 559 is higher than any assigned UID value.
	// See: https://tools.ietf.org/html/rfc3501#page-61
	if isUID && seqSet.Dynamic() && len(apiIDs) == 0 {
		l.Debug("Requesting empty UID dynamic fetch, adding latest message")
		apiID, err := im.storeMailbox.GetLatestAPIID()
		if err != nil {
			return nil
		}
		apiIDs = []string{apiID}
	}

	input := make([]interface{}, len(apiIDs))
	for i, apiID := range apiIDs {
		input[i] = apiID
	}

	processCallback := func(value interface{}) (interface{}, error) {
		apiID := value.(string)

		storeMessage, err := im.storeMailbox.GetMessage(apiID)
		if err != nil {
			err = fmt.Errorf("list message from db: %v", err)
			l.WithField("apiID", apiID).Error(err)
			return nil, err
		}

		msg, err := im.getMessage(storeMessage, items)
		if err != nil {
			err = fmt.Errorf("list message build: %v", err)
			l.WithField("metaID", storeMessage.ID()).Error(err)
			return nil, err
		}

		if storeMessage.Message().Unread == 1 {
			for section := range msg.Body {
				// Peek means get messages without marking them as read.
				// If client does not only ask for peek, we have to mark them as read.
				if !section.Peek {
					markAsReadMutex.Lock()
					markAsReadIDs = append(markAsReadIDs, storeMessage.ID())
					markAsReadMutex.Unlock()
					msg.Flags = append(msg.Flags, imap.SeenFlag)
					break
				}
			}
		}

		return msg, nil
	}

	collectCallback := func(idx int, value interface{}) error {
		msg := value.(*imap.Message)
		msgResponse <- msg
		return nil
	}

	err = parallel.RunParallel(fetchMessagesWorkers, input, processCallback, collectCallback)
	if err != nil {
		return err
	}

	if len(markAsReadIDs) > 0 {
		if err := im.storeMailbox.MarkMessagesRead(markAsReadIDs); err != nil {
			l.Warnf("Cannot mark messages as read: %v", err)
		}
	}
	return nil
}

// apiIDsFromSeqSet takes an IMAP sequence set (which can contain either
// sequence numbers or UIDs) and returns all known API IDs in this range.
func (im *imapMailbox) apiIDsFromSeqSet(uid bool, seqSet *imap.SeqSet) ([]string, error) {
	apiIDs := []string{}
	for _, seq := range seqSet.Set {
		var newAPIIDs []string
		var err error
		if uid {
			newAPIIDs, err = im.storeMailbox.GetAPIIDsFromUIDRange(seq.Start, seq.Stop)
		} else {
			newAPIIDs, err = im.storeMailbox.GetAPIIDsFromSequenceRange(seq.Start, seq.Stop)
		}
		if err != nil {
			return []string{}, err
		}
		apiIDs = append(apiIDs, newAPIIDs...)
	}
	if len(apiIDs) == 0 {
		log.Debugf("Requested empty message list: %v %v", uid, seqSet)
	}
	return apiIDs, nil
}

func isAddressInList(addrs []*mail.Address, query string) bool { //nolint[deadcode]
	for _, addr := range addrs {
		if strings.Contains(addr.Address, query) || strings.Contains(addr.Name, query) {
			return true
		}
	}
	return false
}

func isStringInList(list []string, s string) bool {
	for _, v := range list {
		if v == s {
			return true
		}
	}
	return false
}

func addressMatch(addresses []*mail.Address, criteria string) bool {
	for _, addr := range addresses {
		if strings.Contains(strings.ToLower(addr.String()), strings.ToLower(criteria)) {
			return true
		}
	}
	return false
}

func hasKeyword(m *pmapi.Message, keyword string) bool {
	for _, v := range message.GetHeader(m) {
		if strings.Contains(strings.ToLower(strings.Join(v, " ")), strings.ToLower(keyword)) {
			return true
		}
	}
	return false
}
