package protocol

import (
	"context"
	"crypto/ecdsa"
	"encoding/hex"
	"errors"

	"github.com/golang/protobuf/proto"
	"go.uber.org/zap"

	"github.com/status-im/status-go/eth-node/crypto"
	"github.com/status-im/status-go/eth-node/types"
	"github.com/status-im/status-go/images"
	"github.com/status-im/status-go/protocol/common"
	"github.com/status-im/status-go/protocol/protobuf"
	"github.com/status-im/status-go/protocol/requests"
	v1protocol "github.com/status-im/status-go/protocol/v1"
)

var ErrGroupChatAddedContacts = errors.New("group-chat: can't add members who are not mutual contacts")

func (m *Messenger) validateAddedGroupMembers(members []string) error {
	for _, memberPubkey := range members {
		contactID, err := contactIDFromPublicKeyString(memberPubkey)
		if err != nil {
			return err
		}

		contact, _ := m.allContacts.Load(contactID)
		if contact == nil || !contact.mutual() {
			return ErrGroupChatAddedContacts
		}
	}
	return nil
}

func (m *Messenger) CreateGroupChatWithMembers(ctx context.Context, name string, members []string) (*MessengerResponse, error) {
	var convertedKeyMembers []string
	for _, m := range members {
		k, err := requests.ConvertCompressedToLegacyKey(m)
		if err != nil {
			return nil, err
		}
		convertedKeyMembers = append(convertedKeyMembers, k)

	}
	if err := m.validateAddedGroupMembers(convertedKeyMembers); err != nil {
		return nil, err
	}

	var response MessengerResponse
	logger := m.logger.With(zap.String("site", "CreateGroupChatWithMembers"))
	logger.Info("Creating group chat", zap.String("name", name), zap.Any("members", convertedKeyMembers))
	chat := CreateGroupChat(m.getTimesource())

	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())

	group, err := v1protocol.NewGroupWithCreator(name, chat.Color, clock, m.identity)
	if err != nil {
		return nil, err
	}
	chat.LastClockValue = clock

	chat.updateChatFromGroupMembershipChanges(group)
	chat.Joined = int64(m.getTimesource().GetCurrentTime())

	clock, _ = chat.NextClockAndTimestamp(m.getTimesource())

	// Add members
	if len(convertedKeyMembers) > 0 {
		event := v1protocol.NewMembersAddedEvent(convertedKeyMembers, clock)
		event.ChatID = chat.ID
		err = event.Sign(m.identity)
		if err != nil {
			return nil, err
		}

		err = group.ProcessEvent(event)
		if err != nil {
			return nil, err
		}
	}

	recipients, err := stringSliceToPublicKeys(group.Members())

	if err != nil {
		return nil, err
	}

	encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
	if err != nil {
		return nil, err
	}

	m.allChats.Store(chat.ID, &chat)

	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
		Recipients:  recipients,
	})

	if err != nil {
		return nil, err
	}

	chat.updateChatFromGroupMembershipChanges(group)

	return m.addMessagesAndChat(&chat, buildSystemMessages(chat.MembershipUpdates, m.systemMessagesTranslations), &response)
}

func (m *Messenger) CreateGroupChatFromInvitation(name string, chatID string, adminPK string) (*MessengerResponse, error) {
	var response MessengerResponse
	logger := m.logger.With(zap.String("site", "CreateGroupChatFromInvitation"))
	logger.Info("Creating group chat from invitation", zap.String("name", name))
	chat := CreateGroupChat(m.getTimesource())
	chat.ID = chatID
	chat.Name = name
	chat.InvitationAdmin = adminPK

	response.AddChat(&chat)

	return &response, m.saveChat(&chat)
}

type removeMembersFromGroupChatResponse struct {
	oldRecipients   []*ecdsa.PublicKey
	group           *v1protocol.Group
	encodedProtobuf []byte
}

func (m *Messenger) removeMembersFromGroupChat(ctx context.Context, chat *Chat, members []string) (*removeMembersFromGroupChatResponse, error) {
	chatID := chat.ID
	logger := m.logger.With(zap.String("site", "RemoveMembersFromGroupChat"))
	logger.Info("Removing members form group chat", zap.String("chatID", chatID), zap.Any("members", members))
	group, err := newProtocolGroupFromChat(chat)
	if err != nil {
		return nil, err
	}

	// We save the initial recipients as we want to send updates to also
	// the members kicked out
	oldRecipients, err := stringSliceToPublicKeys(group.Members())
	if err != nil {
		return nil, err
	}

	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())

	for _, member := range members {
		// Remove member
		event := v1protocol.NewMemberRemovedEvent(member, clock)
		event.ChatID = chat.ID
		err = event.Sign(m.identity)
		if err != nil {
			return nil, err
		}

		err = group.ProcessEvent(event)
		if err != nil {
			return nil, err
		}
	}

	encoded, err := m.sender.EncodeMembershipUpdate(group, nil)
	if err != nil {
		return nil, err
	}

	return &removeMembersFromGroupChatResponse{
		oldRecipients:   oldRecipients,
		group:           group,
		encodedProtobuf: encoded,
	}, nil
}

func (m *Messenger) RemoveMembersFromGroupChat(ctx context.Context, chatID string, members []string) (*MessengerResponse, error) {
	var response MessengerResponse

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}

	removeMembersResponse, err := m.removeMembersFromGroupChat(ctx, chat, members)
	if err != nil {
		return nil, err
	}

	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     removeMembersResponse.encodedProtobuf,
		MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
		Recipients:  removeMembersResponse.oldRecipients,
	})
	if err != nil {
		return nil, err
	}

	chat.updateChatFromGroupMembershipChanges(removeMembersResponse.group)

	return m.addMessagesAndChat(chat, buildSystemMessages(chat.MembershipUpdates, m.systemMessagesTranslations), &response)
}

func (m *Messenger) AddMembersToGroupChat(ctx context.Context, chatID string, members []string) (*MessengerResponse, error) {
	if err := m.validateAddedGroupMembers(members); err != nil {
		return nil, err
	}

	var response MessengerResponse
	logger := m.logger.With(zap.String("site", "AddMembersFromGroupChat"))
	logger.Info("Adding members form group chat", zap.String("chatID", chatID), zap.Any("members", members))
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}

	group, err := newProtocolGroupFromChat(chat)
	if err != nil {
		return nil, err
	}

	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
	// Add members
	event := v1protocol.NewMembersAddedEvent(members, clock)
	event.ChatID = chat.ID
	err = event.Sign(m.identity)
	if err != nil {
		return nil, err
	}

	//approve invitations
	for _, member := range members {
		logger.Info("ApproveInvitationByChatIdAndFrom", zap.String("chatID", chatID), zap.Any("member", member))

		groupChatInvitation := &GroupChatInvitation{
			GroupChatInvitation: &protobuf.GroupChatInvitation{
				ChatId: chat.ID,
			},
			From: member,
		}

		groupChatInvitation, err = m.persistence.InvitationByID(groupChatInvitation.ID())
		if err != nil && err != common.ErrRecordNotFound {
			return nil, err
		}
		if groupChatInvitation != nil {
			groupChatInvitation.State = protobuf.GroupChatInvitation_APPROVED

			err := m.persistence.SaveInvitation(groupChatInvitation)
			if err != nil {
				return nil, err
			}
			response.Invitations = append(response.Invitations, groupChatInvitation)
		}
	}

	err = group.ProcessEvent(event)
	if err != nil {
		return nil, err
	}

	recipients, err := stringSliceToPublicKeys(group.Members())
	if err != nil {
		return nil, err
	}

	encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
	if err != nil {
		return nil, err
	}
	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
		Recipients:  recipients,
	})

	if err != nil {
		return nil, err
	}

	chat.updateChatFromGroupMembershipChanges(group)

	return m.addMessagesAndChat(chat, buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations), &response)
}

func (m *Messenger) ChangeGroupChatName(ctx context.Context, chatID string, name string) (*MessengerResponse, error) {
	logger := m.logger.With(zap.String("site", "ChangeGroupChatName"))
	logger.Info("Changing group chat name", zap.String("chatID", chatID), zap.String("name", name))

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}

	group, err := newProtocolGroupFromChat(chat)
	if err != nil {
		return nil, err
	}

	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
	// Change name
	event := v1protocol.NewNameChangedEvent(name, clock)
	event.ChatID = chat.ID
	err = event.Sign(m.identity)
	if err != nil {
		return nil, err
	}

	// Update in-memory group
	err = group.ProcessEvent(event)
	if err != nil {
		return nil, err
	}

	recipients, err := stringSliceToPublicKeys(group.Members())
	if err != nil {
		return nil, err
	}

	encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
	if err != nil {
		return nil, err
	}
	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
		Recipients:  recipients,
	})

	if err != nil {
		return nil, err
	}

	chat.updateChatFromGroupMembershipChanges(group)

	var response MessengerResponse

	return m.addMessagesAndChat(chat, buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations), &response)
}

func (m *Messenger) EditGroupChat(ctx context.Context, chatID string, name string, color string, image images.CroppedImage) (*MessengerResponse, error) {
	logger := m.logger.With(zap.String("site", "EditGroupChat"))
	logger.Info("Editing group chat details", zap.String("chatID", chatID), zap.String("name", name), zap.String("color", color))

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}

	group, err := newProtocolGroupFromChat(chat)
	if err != nil {
		return nil, err
	}

	signAndProcessEvent := func(m *Messenger, event *v1protocol.MembershipUpdateEvent) error {
		err := event.Sign(m.identity)
		if err != nil {
			return err
		}

		err = group.ProcessEvent(*event)
		if err != nil {
			return err
		}

		return nil
	}

	var events []v1protocol.MembershipUpdateEvent

	if chat.Name != name {
		clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
		event := v1protocol.NewNameChangedEvent(name, clock)
		event.ChatID = chat.ID
		err = signAndProcessEvent(m, &event)
		if err != nil {
			return nil, err
		}
		events = append(events, event)
	}

	if chat.Color != color {
		clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
		event := v1protocol.NewColorChangedEvent(color, clock)
		event.ChatID = chat.ID
		err = signAndProcessEvent(m, &event)
		if err != nil {
			return nil, err
		}
		events = append(events, event)
	}

	if len(image.ImagePath) > 0 {
		payload, err := images.OpenAndAdjustImage(image, true)

		if err != nil {
			return nil, err
		}

		// prepare event
		clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
		event := v1protocol.NewImageChangedEvent(payload, clock)
		event.ChatID = chat.ID
		err = signAndProcessEvent(m, &event)
		if err != nil {
			return nil, err
		}
		events = append(events, event)
	}

	recipients, err := stringSliceToPublicKeys(group.Members())
	if err != nil {
		return nil, err
	}

	encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
	if err != nil {
		return nil, err
	}
	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
		Recipients:  recipients,
	})

	if err != nil {
		return nil, err
	}

	chat.updateChatFromGroupMembershipChanges(group)

	var response MessengerResponse

	return m.addMessagesAndChat(chat, buildSystemMessages(events, m.systemMessagesTranslations), &response)
}

func (m *Messenger) SendGroupChatInvitationRequest(ctx context.Context, chatID string, adminPK string,
	message string) (*MessengerResponse, error) {
	logger := m.logger.With(zap.String("site", "SendGroupChatInvitationRequest"))
	logger.Info("Sending group chat invitation request", zap.String("chatID", chatID),
		zap.String("adminPK", adminPK), zap.String("message", message))

	var response MessengerResponse

	// Get chat and clock
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}
	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())

	invitationR := &GroupChatInvitation{
		GroupChatInvitation: &protobuf.GroupChatInvitation{
			Clock:               clock,
			ChatId:              chatID,
			IntroductionMessage: message,
			State:               protobuf.GroupChatInvitation_REQUEST,
		},
		From: types.EncodeHex(crypto.FromECDSAPub(&m.identity.PublicKey)),
	}

	encodedMessage, err := proto.Marshal(invitationR.GetProtobuf())
	if err != nil {
		return nil, err
	}

	spec := common.RawMessage{
		LocalChatID: adminPK,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_GROUP_CHAT_INVITATION,
		ResendType:  common.ResendTypeDataSync,
	}

	pkey, err := hex.DecodeString(adminPK[2:])
	if err != nil {
		return nil, err
	}
	// Safety check, make sure is well formed
	adminpk, err := crypto.UnmarshalPubkey(pkey)
	if err != nil {
		return nil, err
	}

	id, err := m.sender.SendPrivate(ctx, adminpk, &spec)
	if err != nil {
		return nil, err
	}

	spec.ID = types.EncodeHex(id)
	spec.SendCount++
	err = m.persistence.SaveRawMessage(&spec)
	if err != nil {
		return nil, err
	}

	response.Invitations = []*GroupChatInvitation{invitationR}

	err = m.persistence.SaveInvitation(invitationR)
	if err != nil {
		return nil, err
	}

	return &response, nil
}

func (m *Messenger) GetGroupChatInvitations() ([]*GroupChatInvitation, error) {
	return m.persistence.GetGroupChatInvitations()
}

func (m *Messenger) SendGroupChatInvitationRejection(ctx context.Context, invitationRequestID string) (*MessengerResponse, error) {
	logger := m.logger.With(zap.String("site", "SendGroupChatInvitationRejection"))
	logger.Info("Sending group chat invitation reject", zap.String("invitationRequestID", invitationRequestID))

	invitationR, err := m.persistence.InvitationByID(invitationRequestID)
	if err != nil {
		return nil, err
	}

	invitationR.State = protobuf.GroupChatInvitation_REJECTED

	// Get chat and clock
	chat, ok := m.allChats.Load(invitationR.ChatId)
	if !ok {
		return nil, ErrChatNotFound
	}
	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())

	invitationR.Clock = clock

	encodedMessage, err := proto.Marshal(invitationR.GetProtobuf())
	if err != nil {
		return nil, err
	}

	spec := common.RawMessage{
		LocalChatID: invitationR.From,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_GROUP_CHAT_INVITATION,
		ResendType:  common.ResendTypeDataSync,
	}

	pkey, err := hex.DecodeString(invitationR.From[2:])
	if err != nil {
		return nil, err
	}
	// Safety check, make sure is well formed
	userpk, err := crypto.UnmarshalPubkey(pkey)
	if err != nil {
		return nil, err
	}

	id, err := m.sender.SendPrivate(ctx, userpk, &spec)
	if err != nil {
		return nil, err
	}

	spec.ID = types.EncodeHex(id)
	spec.SendCount++
	err = m.persistence.SaveRawMessage(&spec)
	if err != nil {
		return nil, err
	}

	var response MessengerResponse

	response.Invitations = []*GroupChatInvitation{invitationR}

	err = m.persistence.SaveInvitation(invitationR)
	if err != nil {
		return nil, err
	}

	return &response, nil
}

func (m *Messenger) AddAdminsToGroupChat(ctx context.Context, chatID string, members []string) (*MessengerResponse, error) {
	var response MessengerResponse
	logger := m.logger.With(zap.String("site", "AddAdminsToGroupChat"))
	logger.Info("Add admins to group chat", zap.String("chatID", chatID), zap.Any("members", members))

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}

	group, err := newProtocolGroupFromChat(chat)
	if err != nil {
		return nil, err
	}

	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
	// Add members
	event := v1protocol.NewAdminsAddedEvent(members, clock)
	event.ChatID = chat.ID
	err = event.Sign(m.identity)
	if err != nil {
		return nil, err
	}

	err = group.ProcessEvent(event)
	if err != nil {
		return nil, err
	}

	recipients, err := stringSliceToPublicKeys(group.Members())
	if err != nil {
		return nil, err
	}

	encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
	if err != nil {
		return nil, err
	}
	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
		Recipients:  recipients,
	})

	if err != nil {
		return nil, err
	}

	chat.updateChatFromGroupMembershipChanges(group)
	return m.addMessagesAndChat(chat, buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations), &response)
}

// Kept only for backward compatibility (auto-join), explicit join has been removed
func (m *Messenger) ConfirmJoiningGroup(ctx context.Context, chatID string) (*MessengerResponse, error) {
	var response MessengerResponse

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}

	_, err := m.Join(chat)
	if err != nil {
		return nil, err
	}

	group, err := newProtocolGroupFromChat(chat)
	if err != nil {
		return nil, err
	}
	clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
	event := v1protocol.NewMemberJoinedEvent(
		clock,
	)
	event.ChatID = chat.ID
	err = event.Sign(m.identity)
	if err != nil {
		return nil, err
	}

	err = group.ProcessEvent(event)
	if err != nil {
		return nil, err
	}

	recipients, err := stringSliceToPublicKeys(group.Members())
	if err != nil {
		return nil, err
	}

	encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
	if err != nil {
		return nil, err
	}
	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
		Recipients:  recipients,
	})
	if err != nil {
		return nil, err
	}

	chat.updateChatFromGroupMembershipChanges(group)
	chat.Joined = int64(m.getTimesource().GetCurrentTime())

	return m.addMessagesAndChat(chat, buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations), &response)
}

func (m *Messenger) leaveGroupChat(ctx context.Context, response *MessengerResponse, chatID string, remove bool, shouldBeSynced bool) (*MessengerResponse, error) {
	chat, ok := m.allChats.Load(chatID)
	if !ok {
		return nil, ErrChatNotFound
	}

	amIMember := chat.HasMember(common.PubkeyToHex(&m.identity.PublicKey))

	if amIMember {
		chat.RemoveMember(common.PubkeyToHex(&m.identity.PublicKey))

		group, err := newProtocolGroupFromChat(chat)
		if err != nil {
			return nil, err
		}
		clock, _ := chat.NextClockAndTimestamp(m.getTimesource())
		event := v1protocol.NewMemberRemovedEvent(
			contactIDFromPublicKey(&m.identity.PublicKey),
			clock,
		)
		event.ChatID = chat.ID
		err = event.Sign(m.identity)
		if err != nil {
			return nil, err
		}

		err = group.ProcessEvent(event)
		if err != nil {
			return nil, err
		}

		recipients, err := stringSliceToPublicKeys(group.Members())
		if err != nil {
			return nil, err
		}

		encodedMessage, err := m.sender.EncodeMembershipUpdate(group, nil)
		if err != nil {
			return nil, err
		}

		// shouldBeSynced is false if we got here because a synced client has already
		// sent the leave group message. In that case we don't need to send it again.
		if shouldBeSynced {
			_, err = m.dispatchMessage(ctx, common.RawMessage{
				LocalChatID: chat.ID,
				Payload:     encodedMessage,
				MessageType: protobuf.ApplicationMetadataMessage_MEMBERSHIP_UPDATE_MESSAGE,
				Recipients:  recipients,
			})
			if err != nil {
				return nil, err
			}
		}

		chat.updateChatFromGroupMembershipChanges(group)
		response.AddMessages(buildSystemMessages([]v1protocol.MembershipUpdateEvent{event}, m.systemMessagesTranslations))
		err = m.persistence.SaveMessages(response.Messages())
		if err != nil {
			return nil, err
		}
	}

	if remove {
		chat.Active = false
	}

	if remove && shouldBeSynced {
		err := m.syncChatRemoving(ctx, chat.ID, m.dispatchMessage)
		if err != nil {
			return nil, err
		}
	}

	response.AddChat(chat)

	return response, m.saveChat(chat)
}

func (m *Messenger) LeaveGroupChat(ctx context.Context, chatID string, remove bool) (*MessengerResponse, error) {
	_, err := m.DismissAllActivityCenterNotificationsFromChatID(ctx, chatID, m.GetCurrentTimeInMillis())
	if err != nil {
		return nil, err
	}
	var response MessengerResponse
	return m.leaveGroupChat(ctx, &response, chatID, remove, true)
}

// Decline all pending group invites from a user
func (m *Messenger) DeclineAllPendingGroupInvitesFromUser(ctx context.Context, response *MessengerResponse, userPublicKey string) (*MessengerResponse, error) {

	// Decline group invites from active chats
	chats, err := m.persistence.Chats()
	if err != nil {
		return nil, err
	}

	for _, chat := range chats {
		if chat.ChatType == ChatTypePrivateGroupChat &&
			chat.ReceivedInvitationAdmin == userPublicKey &&
			chat.Joined == 0 && chat.Active {
			response, err = m.leaveGroupChat(ctx, response, chat.ID, true, true)
			if err != nil {
				return nil, err
			}
		}
	}

	// Decline group invites from activity center notifications
	notifications, err := m.AcceptActivityCenterNotificationsForInvitesFromUser(ctx, userPublicKey, m.GetCurrentTimeInMillis())
	if err != nil {
		return nil, err
	}

	for _, notification := range notifications {
		response, err = m.leaveGroupChat(ctx, response, notification.ChatID, true, true)
		if err != nil {
			return nil, err
		}
	}
	return response, nil
}