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

import (
	"fmt"
	"strings"

	"github.com/ProtonMail/proton-bridge/pkg/pmapi"
	"github.com/pkg/errors"
)

// createMailbox creates the mailbox via the API.
// The store mailbox is created later by processing an event.
func (store *Store) createMailbox(name string) error {
	defer store.eventLoop.pollNow()

	log.WithField("name", name).Debug("Creating mailbox")

	if store.hasMailbox(name) {
		return fmt.Errorf("mailbox %v already exists", name)
	}

	color := store.leastUsedColor()

	var exclusive int
	switch {
	case strings.HasPrefix(name, UserLabelsPrefix):
		name = strings.TrimPrefix(name, UserLabelsPrefix)
		exclusive = 0
	case strings.HasPrefix(name, UserFoldersPrefix):
		name = strings.TrimPrefix(name, UserFoldersPrefix)
		exclusive = 1
	default:
		// Ideally we would throw an error here, but then Outlook for
		// macOS keeps trying to make an IMAP Drafts folder and popping
		// up the error to the user.
		store.log.WithField("name", name).
			Warn("Ignoring creation of new mailbox in IMAP root")
		return nil
	}

	_, err := store.api.CreateLabel(&pmapi.Label{
		Name:      name,
		Color:     color,
		Exclusive: exclusive,
		Type:      pmapi.LabelTypeMailbox,
	})
	return err
}

// allAddressesHaveMailbox returns whether each address has a mailbox with the given labelID.
func (store *Store) allAddressesHaveMailbox(labelID string) bool {
	store.lock.RLock()
	defer store.lock.RUnlock()

	for _, a := range store.addresses {
		addressHasMailbox := false
		for _, m := range a.mailboxes {
			if m.labelID == labelID {
				addressHasMailbox = true
				break
			}
		}
		if !addressHasMailbox {
			return false
		}
	}
	return true
}

// hasMailbox returns whether there is at least one address which has a mailbox with the given name.
func (store *Store) hasMailbox(name string) bool {
	mailbox, _ := store.getMailbox(name)
	return mailbox != nil
}

// getMailbox returns the first mailbox with the given name.
func (store *Store) getMailbox(name string) (*Mailbox, error) {
	store.lock.RLock()
	defer store.lock.RUnlock()

	for _, a := range store.addresses {
		for _, m := range a.mailboxes {
			if m.labelName == name {
				return m, nil
			}
		}
	}
	return nil, fmt.Errorf("mailbox %s does not exist", name)
}

// leastUsedColor returns the least used color to be used for a newly created folder or label.
func (store *Store) leastUsedColor() string {
	store.lock.RLock()
	defer store.lock.RUnlock()

	usage := map[string]int{}
	for _, a := range store.addresses {
		for _, m := range a.mailboxes {
			if m.color != "" {
				usage[m.color]++
			}
		}
	}

	leastUsed := pmapi.LabelColors[0]
	for _, color := range pmapi.LabelColors {
		if usage[leastUsed] > usage[color] {
			leastUsed = color
		}
	}
	return leastUsed
}

// updateMailbox updates the mailbox via the API.
// The store mailbox is updated later by processing an event.
func (store *Store) updateMailbox(labelID, newName, color string) error {
	defer store.eventLoop.pollNow()

	_, err := store.api.UpdateLabel(&pmapi.Label{
		ID:    labelID,
		Name:  newName,
		Color: color,
	})
	return err
}

// deleteMailbox deletes the mailbox via the API.
// The store mailbox is deleted later by processing an event.
func (store *Store) deleteMailbox(labelID, addressID string) error {
	defer store.eventLoop.pollNow()

	if pmapi.IsSystemLabel(labelID) {
		var err error
		switch labelID {
		case pmapi.SpamLabel:
			err = store.api.EmptyFolder(pmapi.SpamLabel, addressID)
		case pmapi.TrashLabel:
			err = store.api.EmptyFolder(pmapi.TrashLabel, addressID)
		default:
			err = fmt.Errorf("cannot empty mailbox %v", labelID)
		}
		return err
	}
	return store.api.DeleteLabel(labelID)
}

func (store *Store) createLabelsIfMissing(affectedLabelIDs map[string]bool) error {
	newLabelIDs := []string{}
	for labelID := range affectedLabelIDs {
		if pmapi.IsSystemLabel(labelID) || store.allAddressesHaveMailbox(labelID) {
			continue
		}
		newLabelIDs = append(newLabelIDs, labelID)
	}
	if len(newLabelIDs) == 0 {
		return nil
	}

	labels, err := store.api.ListLabels()
	if err != nil {
		return err
	}
	for _, newLabelID := range newLabelIDs {
		for _, label := range labels {
			if label.ID != newLabelID {
				continue
			}
			if err := store.createOrUpdateMailboxEvent(label); err != nil {
				return err
			}
		}
	}
	return nil
}

// createOrUpdateMailboxEvent creates or updates the mailbox in the store.
// This is called from the event loop.
func (store *Store) createOrUpdateMailboxEvent(label *pmapi.Label) error {
	store.lock.Lock()
	defer store.lock.Unlock()

	if label.Type != pmapi.LabelTypeMailbox {
		return nil
	}

	if err := store.createOrUpdateMailboxCountsBuckets([]*pmapi.Label{label}); err != nil {
		return errors.Wrap(err, "cannot update counts")
	}

	for _, a := range store.addresses {
		if err := a.createOrUpdateMailboxEvent(label); err != nil {
			return err
		}
	}
	return nil
}

// deleteMailboxEvent deletes the mailbox in the store.
// This is called from the event loop.
func (store *Store) deleteMailboxEvent(labelID string) error {
	store.lock.Lock()
	defer store.lock.Unlock()

	_ = store.removeMailboxCount(labelID)

	for _, a := range store.addresses {
		if err := a.deleteMailboxEvent(labelID); err != nil {
			return err
		}
	}
	return nil
}
