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

import (
	"errors"
	"net/url"
	"strconv"
)

type Card struct {
	Type      int
	Data      string
	Signature string
}

const (
	CardEncrypted = 1
	CardSigned    = 2
)

type Contact struct {
	ID         string
	Name       string
	UID        string
	Size       int64
	CreateTime int64
	ModifyTime int64
	LabelIDs   []string

	ContactEmails []ContactEmail
	Cards         []Card
}

type ContactEmail struct {
	ID        string
	Name      string
	Email     string
	Type      []string
	Defaults  int
	Order     int
	ContactID string
	LabelIDs  []string
}

var errVerificationFailed = errors.New("signature verification failed")

//================= Public utility functions ======================

func (c *Client) EncryptAndSignCards(cards []Card) ([]Card, error) {
	var err error
	for i := range cards {
		card := &cards[i]
		if isEncryptedCardType(card.Type) {
			if isSignedCardType(card.Type) {
				if card.Signature, err = c.sign(card.Data); err != nil {
					return nil, err
				}
			}

			if card.Data, err = c.encrypt(card.Data, nil); err != nil {
				return nil, err
			}
		} else if isSignedCardType(card.Type) {
			if card.Signature, err = c.sign(card.Data); err != nil {
				return nil, err
			}
		}
	}
	return cards, nil
}

func (c *Client) DecryptAndVerifyCards(cards []Card) ([]Card, error) {
	for i := range cards {
		card := &cards[i]
		if isEncryptedCardType(card.Type) {
			signedCard, err := c.decrypt(card.Data)
			if err != nil {
				return nil, err
			}
			card.Data = signedCard
		}
		if isSignedCardType(card.Type) {
			err := c.verify(card.Data, card.Signature)
			if err != nil {
				return cards, errVerificationFailed
			}
		}
	}
	return cards, nil
}

//====================== READ ===========================

type ContactsListRes struct {
	Res
	Contacts []*Contact
}

// GetContacts gets all contacts.
func (c *Client) GetContacts(page int, pageSize int) (contacts []*Contact, err error) {
	v := url.Values{}
	v.Set("Page", strconv.Itoa(page))
	if pageSize > 0 {
		v.Set("PageSize", strconv.Itoa(pageSize))
	}
	req, err := NewRequest("GET", "/contacts?"+v.Encode(), nil)

	if err != nil {
		return
	}

	var res ContactsListRes
	if err = c.DoJSON(req, &res); err != nil {
		return
	}

	contacts, err = res.Contacts, res.Err()
	return
}

// GetContactByID gets contact details specified by contact ID.
func (c *Client) GetContactByID(id string) (contactDetail Contact, err error) {
	req, err := NewRequest("GET", "/contacts/"+id, nil)

	if err != nil {
		return
	}

	type ContactRes struct {
		Res
		Contact Contact
	}
	var res ContactRes

	if err = c.DoJSON(req, &res); err != nil {
		return
	}

	contactDetail, err = res.Contact, res.Err()
	return
}

// GetContactsForExport gets contacts in vCard format, signed and encrypted.
func (c *Client) GetContactsForExport(page int, pageSize int) (contacts []Contact, err error) {
	v := url.Values{}
	v.Set("Page", strconv.Itoa(page))
	if pageSize > 0 {
		v.Set("PageSize", strconv.Itoa(pageSize))
	}

	req, err := NewRequest("GET", "/contacts/export?"+v.Encode(), nil)

	if err != nil {
		return
	}

	type ContactsDetailsRes struct {
		Res
		Contacts []Contact
	}
	var res ContactsDetailsRes

	if err = c.DoJSON(req, &res); err != nil {
		return
	}

	contacts, err = res.Contacts, res.Err()
	return
}

type ContactsEmailsRes struct {
	Res
	ContactEmails []ContactEmail
	Total         int
}

// GetAllContactsEmails gets all emails from all contacts.
func (c *Client) GetAllContactsEmails(page int, pageSize int) (contactsEmails []ContactEmail, err error) {
	v := url.Values{}
	v.Set("Page", strconv.Itoa(page))
	if pageSize > 0 {
		v.Set("PageSize", strconv.Itoa(pageSize))
	}

	req, err := NewRequest("GET", "/contacts/emails?"+v.Encode(), nil)
	if err != nil {
		return
	}

	var res ContactsEmailsRes
	if err = c.DoJSON(req, &res); err != nil {
		return
	}

	contactsEmails, err = res.ContactEmails, res.Err()
	return
}

// GetContactEmailByEmail gets all emails from all contacts matching a specified email string.
func (c *Client) GetContactEmailByEmail(email string, page int, pageSize int) (contactEmails []ContactEmail, err error) {
	v := url.Values{}
	v.Set("Page", strconv.Itoa(page))
	if pageSize > 0 {
		v.Set("PageSize", strconv.Itoa(pageSize))
	}
	v.Set("Email", email)

	req, err := NewRequest("GET", "/contacts/emails?"+v.Encode(), nil)
	if err != nil {
		return
	}

	var res ContactsEmailsRes
	if err = c.DoJSON(req, &res); err != nil {
		return
	}

	contactEmails, err = res.ContactEmails, res.Err()
	return
}

//============================ CREATE ====================================

type CardsList struct {
	Cards []Card
}

type ContactsCards struct {
	Contacts []CardsList
}

type SingleContactResponse struct {
	Res
	Contact Contact
}

type IndexedContactResponse struct {
	Index    int
	Response SingleContactResponse
}

type AddContactsResponse struct {
	Res
	Responses []IndexedContactResponse
}

type AddContactsReq struct {
	ContactsCards
	Overwrite int
	Groups    int
	Labels    int
}

// AddContacts adds contacts specified by cards. Performs signing and encrypting based on card type.
func (c *Client) AddContacts(cards ContactsCards, overwrite int, groups int, labels int) (res *AddContactsResponse, err error) {
	reqBody := AddContactsReq{
		ContactsCards: cards,
		Overwrite:     overwrite,
		Groups:        groups,
		Labels:        labels,
	}

	req, err := NewJSONRequest("POST", "/contacts", reqBody)
	if err != nil {
		return
	}

	var addContactsRes AddContactsResponse
	if err = c.DoJSON(req, &addContactsRes); err != nil {
		return
	}

	res, err = &addContactsRes, addContactsRes.Err()
	return
}

// ================================= UPDATE =======================================

type UpdateContactResponse struct {
	Res
	Contact Contact
}

type UpdateContactReq struct {
	Cards []Card
}

// UpdateContact updates contact identified by contact ID. Modified contact is specified by cards.
func (c *Client) UpdateContact(id string, cards []Card) (res *UpdateContactResponse, err error) {
	reqBody := UpdateContactReq{
		Cards: cards,
	}
	req, err := NewJSONRequest("PUT", "/contacts/"+id, reqBody)
	if err != nil {
		return
	}
	var updateContactRes UpdateContactResponse
	if err = c.DoJSON(req, &updateContactRes); err != nil {
		return
	}

	res, err = &updateContactRes, updateContactRes.Err()
	return
}

type SingleIDResponse struct {
	Res
	ID string
}

type UpdateContactGroupsResponse struct {
	Res
	Response SingleIDResponse
}

func (c *Client) AddContactGroups(groupID string, contactEmailIDs []string) (res *UpdateContactGroupsResponse, err error) {
	return c.modifyContactGroups(groupID, addContactGroupsAction, contactEmailIDs)
}

func (c *Client) RemoveContactGroups(groupID string, contactEmailIDs []string) (res *UpdateContactGroupsResponse, err error) {
	return c.modifyContactGroups(groupID, removeContactGroupsAction, contactEmailIDs)
}

const (
	removeContactGroupsAction = 0
	addContactGroupsAction    = 1
)

type ModifyContactGroupsReq struct {
	LabelID         string
	Action          int
	ContactEmailIDs []string
}

func (c *Client) modifyContactGroups(groupID string, modifyContactGroupsAction int, contactEmailIDs []string) (res *UpdateContactGroupsResponse, err error) {
	reqBody := ModifyContactGroupsReq{
		LabelID:         groupID,
		Action:          modifyContactGroupsAction,
		ContactEmailIDs: contactEmailIDs,
	}
	req, err := NewJSONRequest("PUT", "/contacts/group", reqBody)
	if err != nil {
		return
	}
	if err = c.DoJSON(req, &res); err != nil {
		return
	}
	err = res.Err()
	return
}

// ================================= DELETE =======================================

type DeleteReq struct {
	IDs []string
}

// DeleteContacts deletes contacts specified by an array of contact IDs.
func (c *Client) DeleteContacts(ids []string) (err error) {
	deleteReq := DeleteReq{
		IDs: ids,
	}

	req, err := NewJSONRequest("PUT", "/contacts/delete", deleteReq)
	if err != nil {
		return
	}

	type DeleteContactsRes struct {
		Res
		Responses []struct {
			ID       string
			Response Res
		}
	}
	var res DeleteContactsRes

	if err = c.DoJSON(req, &res); err != nil {
		return
	}
	if err = res.Err(); err != nil {
		return
	}
	return
}

// DeleteAllContacts deletes all contacts.
func (c *Client) DeleteAllContacts() (err error) {
	req, err := NewRequest("DELETE", "/contacts", nil)
	if err != nil {
		return
	}

	var res Res

	if err = c.DoJSON(req, &res); err != nil {
		return
	}
	if err = res.Err(); err != nil {
		return
	}

	return
}

//===================== Private utility methods =======================

func isSignedCardType(cardType int) bool {
	return (cardType & CardSigned) == CardSigned
}

func isEncryptedCardType(cardType int) bool {
	return (cardType & CardEncrypted) == CardEncrypted
}
