////////////////////////////////////////////////////////////////////////////////
// Copyright © 2022 xx foundation                                             //
//                                                                            //
// Use of this source code is governed by a license that can be found in the  //
// LICENSE file.                                                              //
////////////////////////////////////////////////////////////////////////////////

//go:build js && wasm

package main

import (
	"crypto/ed25519"
	"encoding/json"
	"strconv"
	"strings"
	"syscall/js"
	"time"

	"github.com/hack-pad/go-indexeddb/idb"
	"github.com/pkg/errors"
	jww "github.com/spf13/jwalterweatherman"

	"gitlab.com/elixxir/client/v4/bindings"
	"gitlab.com/elixxir/client/v4/channels"
	"gitlab.com/elixxir/client/v4/cmix/rounds"
	cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast"
	cryptoChannel "gitlab.com/elixxir/crypto/channel"
	"gitlab.com/elixxir/crypto/message"
	"gitlab.com/elixxir/wasm-utils/utils"
	"gitlab.com/elixxir/xxdk-wasm/indexedDb/impl"
	"gitlab.com/xx_network/primitives/id"
)

// wasmModel implements [channels.EventModel] interface, which uses the channels
// system passed an object that adheres to in order to get events on the
// channel.
// NOTE: This model is NOT thread safe - it is the responsibility of the
// caller to ensure that its methods are called sequentially.
type wasmModel struct {
	db     *idb.Database
	cipher cryptoChannel.Cipher
	cbs    bindings.ChannelUICallbacks
}

// JoinChannel is called whenever a channel is joined locally.
func (w *wasmModel) JoinChannel(channel *cryptoBroadcast.Channel) {
	parentErr := errors.New("failed to JoinChannel")

	// Build object
	newChannel := Channel{
		ID:          channel.ReceptionID.Marshal(),
		Name:        channel.Name,
		Description: channel.Description,
	}

	// Convert to jsObject
	newChannelJson, err := json.Marshal(&newChannel)
	if err != nil {
		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
			"Unable to marshal Channel: %+v", err))
		return
	}
	channelObj, err := utils.JsonToJS(newChannelJson)
	if err != nil {
		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
			"Unable to marshal Channel: %+v", err))
		return
	}

	_, err = impl.Put(w.db, channelStoreName, channelObj)
	if err != nil {
		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
			"Unable to put Channel: %+v", err))
	}
}

// LeaveChannel is called whenever a channel is left locally.
func (w *wasmModel) LeaveChannel(channelID *id.ID) {
	parentErr := errors.New("failed to LeaveChannel")

	// Delete the channel from storage
	err := impl.Delete(w.db, channelStoreName, js.ValueOf(channelID.String()))
	if err != nil {
		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
			"Unable to delete Channel: %+v", err))
		return
	}

	// Clean up lingering data
	err = w.deleteMsgByChannel(channelID)
	if err != nil {
		jww.ERROR.Printf("%+v", errors.WithMessagef(parentErr,
			"Deleting Channel's Message data failed: %+v", err))
		return
	}
	jww.DEBUG.Printf("Successfully deleted channel: %s", channelID)
}

// deleteMsgByChannel is a private helper that uses messageStoreChannelIndex
// to delete all Message with the given Channel ID.
func (w *wasmModel) deleteMsgByChannel(channelID *id.ID) error {
	parentErr := errors.New("failed to deleteMsgByChannel")

	// Prepare the Transaction
	txn, err := w.db.Transaction(idb.TransactionReadWrite, messageStoreName)
	if err != nil {
		return errors.WithMessagef(parentErr,
			"Unable to create Transaction: %+v", err)
	}
	store, err := txn.ObjectStore(messageStoreName)
	if err != nil {
		return errors.WithMessagef(parentErr,
			"Unable to get ObjectStore: %+v", err)
	}
	index, err := store.Index(messageStoreChannelIndex)
	if err != nil {
		return errors.WithMessagef(parentErr,
			"Unable to get Index: %+v", err)
	}

	// Set up the operation
	keyRange, err := idb.NewKeyRangeOnly(impl.EncodeBytes(channelID.Marshal()))
	cursorRequest, err := index.OpenCursorRange(keyRange, idb.CursorNext)
	if err != nil {
		return errors.WithMessagef(parentErr, "Unable to open Cursor: %+v", err)
	}

	// Perform the operation
	err = impl.SendCursorRequest(cursorRequest,
		func(cursor *idb.CursorWithValue) error {
			_, err := cursor.Delete()
			return err
		})
	if err != nil {
		return errors.WithMessagef(parentErr,
			"Unable to delete Message data: %+v", err)
	}
	return nil
}

// ReceiveMessage is called whenever a message is received on a given channel.
//
// It may be called multiple times on the same message; it is incumbent on the
// user of the API to filter such called by message ID.
func (w *wasmModel) ReceiveMessage(channelID *id.ID, messageID message.ID,
	nickname, text string, pubKey ed25519.PublicKey, dmToken uint32,
	codeset uint8, timestamp time.Time, lease time.Duration, round rounds.Round,
	mType channels.MessageType, status channels.SentStatus, hidden bool) uint64 {
	textBytes := []byte(text)
	var err error

	// Handle encryption, if it is present
	if w.cipher != nil {
		textBytes, err = w.cipher.Encrypt([]byte(text))
		if err != nil {
			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
			return 0
		}
	}

	channelIDBytes := channelID.Marshal()

	msgToInsert := buildMessage(
		channelIDBytes, messageID.Bytes(), nil, nickname,
		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
		false, hidden, status)

	uuid, err := w.upsertMessage(msgToInsert)
	if err != nil {
		jww.ERROR.Printf("Failed to receive Message: %+v", err)
		return 0
	}

	w.sendReceiveMessageUpdate(uuid, channelID, false)
	return uuid
}

// ReceiveReply is called whenever a message is received that is a reply on a
// given channel. It may be called multiple times on the same message; it is
// incumbent on the user of the API to filter such called by message ID.
//
// Messages may arrive our of order, so a reply, in theory, can arrive before
// the initial message. As a result, it may be important to buffer replies.
func (w *wasmModel) ReceiveReply(channelID *id.ID, messageID,
	replyTo message.ID, nickname, text string, pubKey ed25519.PublicKey,
	dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration,
	round rounds.Round, mType channels.MessageType, status channels.SentStatus,
	hidden bool) uint64 {
	textBytes := []byte(text)
	var err error

	// Handle encryption, if it is present
	if w.cipher != nil {
		textBytes, err = w.cipher.Encrypt([]byte(text))
		if err != nil {
			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
			return 0
		}
	}

	channelIDBytes := channelID.Marshal()

	msgToInsert := buildMessage(channelIDBytes, messageID.Bytes(),
		replyTo.Bytes(), nickname, textBytes, pubKey, dmToken, codeset,
		timestamp, lease, round.ID, mType, hidden, false, status)

	uuid, err := w.upsertMessage(msgToInsert)
	if err != nil {
		jww.ERROR.Printf("Failed to receive reply: %+v", err)
		return 0
	}

	w.sendReceiveMessageUpdate(uuid, channelID, false)
	return uuid
}

// ReceiveReaction is called whenever a reaction to a message is received on a
// given channel. It may be called multiple times on the same reaction; it is
// incumbent on the user of the API to filter such called by message ID.
//
// Messages may arrive our of order, so a reply, in theory, can arrive before
// the initial message. As a result, it may be important to buffer reactions.
func (w *wasmModel) ReceiveReaction(channelID *id.ID, messageID,
	reactionTo message.ID, nickname, reaction string, pubKey ed25519.PublicKey,
	dmToken uint32, codeset uint8, timestamp time.Time, lease time.Duration,
	round rounds.Round, mType channels.MessageType, status channels.SentStatus,
	hidden bool) uint64 {
	textBytes := []byte(reaction)
	var err error

	// Handle encryption, if it is present
	if w.cipher != nil {
		textBytes, err = w.cipher.Encrypt([]byte(reaction))
		if err != nil {
			jww.ERROR.Printf("Failed to encrypt Message: %+v", err)
			return 0
		}
	}

	channelIDBytes := channelID.Marshal()
	msgToInsert := buildMessage(
		channelIDBytes, messageID.Bytes(), reactionTo.Bytes(), nickname,
		textBytes, pubKey, dmToken, codeset, timestamp, lease, round.ID, mType,
		false, hidden, status)

	uuid, err := w.upsertMessage(msgToInsert)
	if err != nil {
		jww.ERROR.Printf("Failed to receive reaction: %+v", err)
		return 0
	}
	w.sendReceiveMessageUpdate(uuid, channelID, false)
	return uuid
}

// UpdateFromUUID is called whenever a message at the UUID is modified.
//
// messageID, timestamp, round, pinned, and hidden are all nillable and may be
// updated based upon the UUID at a later date. If a nil value is passed, then
// make no update.
//
// Returns an error if the message cannot be updated. It must return
// channels.NoMessageErr if the message does not exist.
func (w *wasmModel) UpdateFromUUID(uuid uint64, messageID *message.ID,
	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
	status *channels.SentStatus) error {
	parentErr := "failed to UpdateFromUUID"

	// Convert messageID to the key generated by json.Marshal
	key := js.ValueOf(uuid)

	// Use the key to get the existing Message
	msgObj, err := impl.Get(w.db, messageStoreName, key)
	if err != nil {
		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
			return errors.WithMessage(channels.NoMessageErr, parentErr)
		}
		return errors.WithMessage(err, parentErr)
	}

	currentMsg, err := valueToMessage(msgObj)
	if err != nil {
		return errors.WithMessagef(err,
			"%s Failed to marshal Message", parentErr)
	}

	_, err = w.updateMessage(currentMsg, messageID, timestamp,
		round, pinned, hidden, status)
	if err != nil {
		return errors.WithMessage(err, parentErr)
	}
	return nil
}

// UpdateFromMessageID is called whenever a message with the message ID is
// modified.
//
// The API needs to return the UUID of the modified message that can be
// referenced at a later time.
//
// timestamp, round, pinned, and hidden are all nillable and may be updated
// based upon the UUID at a later date. If a nil value is passed, then make
// no update.
//
// Returns an error if the message cannot be updated. It must return
// channels.NoMessageErr if the message does not exist.
func (w *wasmModel) UpdateFromMessageID(messageID message.ID,
	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
	status *channels.SentStatus) (uint64, error) {
	parentErr := "failed to UpdateFromMessageID"

	msgObj, err := impl.GetIndex(w.db, messageStoreName,
		messageStoreMessageIndex, impl.EncodeBytes(messageID.Marshal()))
	if err != nil {
		if strings.Contains(err.Error(), impl.ErrDoesNotExist) {
			return 0, errors.WithMessage(channels.NoMessageErr, parentErr)
		}
		return 0, errors.WithMessage(err, parentErr)
	}

	currentMsg, err := valueToMessage(msgObj)
	if err != nil {
		return 0, errors.WithMessagef(err,
			"%s Failed to marshal Message", parentErr)
	}

	uuid, err := w.updateMessage(currentMsg, &messageID, timestamp,
		round, pinned, hidden, status)
	if err != nil {
		return 0, errors.WithMessage(err, parentErr)
	}
	return uuid, nil
}

// buildMessage is a private helper that converts typical [channels.EventModel]
// inputs into a basic Message structure for insertion into storage.
//
// NOTE: ID is not set inside this function because we want to use the
// autoincrement key by default. If you are trying to overwrite an existing
// message, then you need to set it manually yourself.
func buildMessage(channelID, messageID, parentID []byte, nickname string,
	text []byte, pubKey ed25519.PublicKey, dmToken uint32, codeset uint8,
	timestamp time.Time, lease time.Duration, round id.Round,
	mType channels.MessageType, pinned, hidden bool,
	status channels.SentStatus) *Message {
	return &Message{
		MessageID:       messageID,
		Nickname:        nickname,
		ChannelID:       channelID,
		ParentMessageID: parentID,
		Timestamp:       timestamp,
		Lease:           strconv.FormatInt(int64(lease), 10),
		Status:          uint8(status),
		Hidden:          hidden,
		Pinned:          pinned,
		Text:            text,
		Type:            uint16(mType),
		Round:           uint64(round),
		// User Identity Info
		Pubkey:         pubKey,
		DmToken:        dmToken,
		CodesetVersion: codeset,
	}
}

// updateMessage is a helper for updating a stored message.
func (w *wasmModel) updateMessage(currentMsg *Message, messageID *message.ID,
	timestamp *time.Time, round *rounds.Round, pinned, hidden *bool,
	status *channels.SentStatus) (uint64, error) {

	if status != nil {
		currentMsg.Status = uint8(*status)
	}
	if messageID != nil {
		currentMsg.MessageID = messageID.Bytes()
	}

	if round != nil {
		currentMsg.Round = uint64(round.ID)
	}

	if timestamp != nil {
		currentMsg.Timestamp = *timestamp
	}

	if pinned != nil {
		currentMsg.Pinned = *pinned
	}

	if hidden != nil {
		currentMsg.Hidden = *hidden
	}

	// Store the updated Message
	uuid, err := w.upsertMessage(currentMsg)
	if err != nil {
		return 0, err
	}
	w.sendReceiveMessageUpdate(uuid, (*id.ID)(currentMsg.ChannelID), true)
	return uuid, nil
}

// upsertMessage is a helper function that will update an existing record
// if Message.ID is specified. Otherwise, it will perform an insert.
func (w *wasmModel) upsertMessage(msg *Message) (uint64, error) {
	// Convert to jsObject
	newMessageJson, err := json.Marshal(msg)
	if err != nil {
		return 0, errors.Errorf("Unable to marshal Message: %+v", err)
	}
	messageObj, err := utils.JsonToJS(newMessageJson)
	if err != nil {
		return 0, errors.Errorf("Unable to marshal Message: %+v", err)
	}

	// Store message to database
	msgIdObj, err := impl.Put(w.db, messageStoreName, messageObj)
	if err != nil {
		return 0, errors.Errorf("Unable to put Message: %+v\n%s",
			err, newMessageJson)
	}

	uuid := msgIdObj.Int()
	jww.DEBUG.Printf("Successfully stored message %d", uuid)
	return uint64(uuid), nil
}

// GetMessage returns the message with the given [channel.MessageID].
func (w *wasmModel) GetMessage(
	messageID message.ID) (channels.ModelMessage, error) {
	msgIDStr := impl.EncodeBytes(messageID.Marshal())

	resultObj, err := impl.GetIndex(w.db, messageStoreName,
		messageStoreMessageIndex, msgIDStr)
	if err != nil {
		return channels.ModelMessage{}, err
	}

	lookupResult, err := valueToMessage(resultObj)
	if err != nil {
		return channels.ModelMessage{}, err
	}

	var channelId *id.ID
	if lookupResult.ChannelID != nil {
		channelId, err = id.Unmarshal(lookupResult.ChannelID)
		if err != nil {
			return channels.ModelMessage{}, err
		}
	}

	var parentMsgId message.ID
	if lookupResult.ParentMessageID != nil {
		parentMsgId, err = message.UnmarshalID(lookupResult.ParentMessageID)
		if err != nil {
			return channels.ModelMessage{}, err
		}
	}

	lease := time.Duration(0)
	if len(lookupResult.Lease) > 0 {
		leaseInt, err := strconv.ParseInt(lookupResult.Lease, 10, 64)
		if err != nil {
			return channels.ModelMessage{}, err
		}
		lease = time.Duration(leaseInt)
	}

	return channels.ModelMessage{
		UUID:            lookupResult.ID,
		Nickname:        lookupResult.Nickname,
		MessageID:       messageID,
		ChannelID:       channelId,
		ParentMessageID: parentMsgId,
		Timestamp:       lookupResult.Timestamp,
		Lease:           lease,
		Status:          channels.SentStatus(lookupResult.Status),
		Hidden:          lookupResult.Hidden,
		Pinned:          lookupResult.Pinned,
		Content:         lookupResult.Text,
		Type:            channels.MessageType(lookupResult.Type),
		Round:           id.Round(lookupResult.Round),
		PubKey:          lookupResult.Pubkey,
		CodesetVersion:  lookupResult.CodesetVersion,
	}, nil
}

// DeleteMessage removes a message with the given messageID from storage.
func (w *wasmModel) DeleteMessage(messageID message.ID) error {
	err := impl.DeleteIndex(w.db, messageStoreName,
		messageStoreMessageIndex, pkeyName, impl.EncodeBytes(messageID.Marshal()))
	if err != nil {
		return err
	}

	eventData, err := json.Marshal(bindings.MessageDeletedJson{
		MessageID: messageID,
	})
	if err != nil {
		jww.WARN.Printf("couldn't marshal MessageDeleted: %s, %+v",
			messageID, err)
	} else {
		go w.cbs.EventUpdate(bindings.MessageDeleted, eventData)
	}
	return nil
}

// MuteUser is called whenever a user is muted or unmuted.
func (w *wasmModel) MuteUser(
	channelID *id.ID, pubKey ed25519.PublicKey, unmute bool) {
	eventData, err := json.Marshal(bindings.UserMutedJson{
		ChannelID: channelID,
		PubKey:    pubKey,
		Unmute:    unmute,
	})
	if err != nil {
		jww.WARN.Printf("couldn't marshal UserMuted: %s, %+v",
			pubKey, err)
	} else {
		go w.cbs.EventUpdate(bindings.UserMuted, eventData)
	}
}

func (w *wasmModel) sendReceiveMessageUpdate(uuid uint64, channelID *id.ID,
	update bool) {
	eventMsg := bindings.MessageReceivedJson{
		Uuid:      int64(uuid),
		ChannelID: channelID,
		Update:    update,
	}
	eventData, err := json.Marshal(eventMsg)
	if err != nil {
		jww.WARN.Printf("couldn't marshal MessageReceive: %v, %+v",
			eventMsg, err)
	} else {
		go w.cbs.EventUpdate(bindings.MessageReceived, eventData)
	}
}

// valueToMessage is a helper for converting js.Value to Message.
func valueToMessage(msgObj js.Value) (*Message, error) {
	resultMsg := &Message{}
	return resultMsg, json.Unmarshal([]byte(utils.JsToJson(msgObj)), resultMsg)
}

// valueToFile is a helper for converting js.Value to File.
func valueToFile(fileObj js.Value) (*File, error) {
	resultFile := &File{}
	return resultFile, json.Unmarshal([]byte(utils.JsToJson(fileObj)), resultFile)
}