// mautrix-signal - A Matrix-Signal puppeting bridge.
// Copyright (C) 2024 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program 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 Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

package connector

import (
	"context"
	"encoding/base64"
	"fmt"
	"strings"
	"time"

	"github.com/google/uuid"
	"github.com/rs/zerolog"
	"go.mau.fi/util/exzerolog"
	"go.mau.fi/util/jsontime"
	"go.mau.fi/util/ptr"
	"maunium.net/go/mautrix/bridgev2"
	"maunium.net/go/mautrix/bridgev2/database"
	"maunium.net/go/mautrix/bridgev2/networkid"
	"maunium.net/go/mautrix/bridgev2/simplevent"
	"maunium.net/go/mautrix/bridgev2/status"
	"maunium.net/go/mautrix/event"

	"go.mau.fi/mautrix-signal/pkg/libsignalgo"
	"go.mau.fi/mautrix-signal/pkg/signalid"
	"go.mau.fi/mautrix-signal/pkg/signalmeow"
	"go.mau.fi/mautrix-signal/pkg/signalmeow/events"
	signalpb "go.mau.fi/mautrix-signal/pkg/signalmeow/protobuf"
	"go.mau.fi/mautrix-signal/pkg/signalmeow/types"
)

func (s *SignalClient) handleSignalEvent(rawEvt events.SignalEvent) bool {
	switch evt := rawEvt.(type) {
	case *events.ChatEvent:
		return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, &Bv2ChatEvent{ChatEvent: evt, s: s}).Success
	case *events.DecryptionError:
		return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapDecryptionError(evt)).Success
	case *events.Receipt:
		return s.handleSignalReceipt(evt)
	case *events.ReadSelf:
		return s.handleSignalReadSelf(evt)
	case *events.DeleteForMe:
		return s.handleSignalDeleteForMe(evt)
	case *events.MessageRequestResponse:
		return s.handleSignalMessageRequestResponse(evt)
	case *events.Call:
		return s.Main.Bridge.QueueRemoteEvent(s.UserLogin, s.wrapCallEvent(evt)).Success
	case *events.ContactList:
		s.handleSignalContactList(evt)
	case *events.ACIFound:
		s.handleSignalACIFound(evt)
	case *events.QueueEmpty:
		s.queueEmptyWaiter.Set()
	case *events.LoggedOut:
		s.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateBadCredentials, Message: evt.Error.Error()})
	default:
		s.UserLogin.Log.Warn().Type("event_type", evt).Msg("Unrecognized signalmeow event type")
	}
	return true
}

func (s *SignalClient) wrapCallEvent(evt *events.Call) bridgev2.RemoteMessage {
	return &simplevent.Message[*events.Call]{
		EventMeta: simplevent.EventMeta{
			Type: bridgev2.RemoteEventMessage,
			LogContext: func(c zerolog.Context) zerolog.Context {
				c = c.Stringer("sender_id", evt.Info.Sender)
				c = c.Uint64("message_ts", evt.Timestamp)
				return c
			},
			PortalKey:    s.makePortalKey(evt.Info.ChatID),
			CreatePortal: true,
			Sender:       s.makeEventSender(evt.Info.Sender),
			Timestamp:    time.UnixMilli(int64(evt.Timestamp)),
		},
		Data: evt,
		ID:   signalid.MakeMessageID(evt.Info.Sender, evt.Timestamp),

		ConvertMessageFunc: convertCallEvent,
	}
}

func convertCallEvent(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, data *events.Call) (*bridgev2.ConvertedMessage, error) {
	content := &event.MessageEventContent{
		MsgType: event.MsgNotice,
	}
	if data.IsRinging {
		content.Body = "Incoming call"
		if userID, _, _ := signalid.ParsePortalID(portal.ID); !userID.IsEmpty() {
			content.MsgType = event.MsgText
		}
		content.BeeperActionMessage = &event.BeeperActionMessage{
			Type: event.BeeperActionMessageCall,
		}
	} else {
		content.Body = "Call ended"
	}
	return &bridgev2.ConvertedMessage{
		Parts: []*bridgev2.ConvertedMessagePart{{
			Type:    event.EventMessage,
			Content: content,
		}},
	}, nil
}

func (s *SignalClient) wrapDecryptionError(evt *events.DecryptionError) bridgev2.RemoteMessage {
	return &simplevent.Message[*events.DecryptionError]{
		EventMeta: simplevent.EventMeta{
			Type: bridgev2.RemoteEventMessage,
			LogContext: func(c zerolog.Context) zerolog.Context {
				c = c.Stringer("sender_id", evt.Sender)
				c = c.Uint64("message_ts", evt.Timestamp)
				return c
			},
			PortalKey:    s.makePortalKey(evt.Sender.String()),
			CreatePortal: true,
			Sender:       s.makeEventSender(evt.Sender),
			Timestamp:    time.UnixMilli(int64(evt.Timestamp)),
			StreamOrder:  int64(evt.Timestamp),
		},
		Data: evt,
		// TODO use main message id and edit it if it later becomes decryptable?
		ID: "decrypterr|" + signalid.MakeMessageID(evt.Sender, evt.Timestamp),

		ConvertMessageFunc: convertDecryptionError,
	}
}

func convertDecryptionError(_ context.Context, _ *bridgev2.Portal, _ bridgev2.MatrixAPI, _ *events.DecryptionError) (*bridgev2.ConvertedMessage, error) {
	return &bridgev2.ConvertedMessage{
		Parts: []*bridgev2.ConvertedMessagePart{{
			Type: event.EventMessage,
			Content: &event.MessageEventContent{
				MsgType: event.MsgNotice,
				Body:    "Message couldn't be decrypted. It may have been in this chat or a group chat. Please check your Signal app.",
			},
		}},
	}, nil
}

type Bv2ChatEvent struct {
	*events.ChatEvent
	s *SignalClient
}

var (
	_ bridgev2.RemoteMessage              = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteEdit                 = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteEventWithTimestamp   = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteReaction             = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteReactionRemove       = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteMessageRemove        = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteTyping               = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemotePreHandler           = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteChatInfoChange       = (*Bv2ChatEvent)(nil)
	_ bridgev2.RemoteEventWithStreamOrder = (*Bv2ChatEvent)(nil)
)

func (evt *Bv2ChatEvent) GetType() bridgev2.RemoteEventType {
	switch innerEvt := evt.Event.(type) {
	case *signalpb.DataMessage:
		switch {
		case innerEvt.Body != nil, innerEvt.Attachments != nil, innerEvt.Contact != nil, innerEvt.Sticker != nil,
			innerEvt.Payment != nil, innerEvt.GiftBadge != nil, innerEvt.PollCreate != nil, innerEvt.PollVote != nil,
			innerEvt.GetRequiredProtocolVersion() > uint32(signalpb.DataMessage_CURRENT),
			innerEvt.GetFlags()&uint32(signalpb.DataMessage_EXPIRATION_TIMER_UPDATE) != 0:
			return bridgev2.RemoteEventMessage
		case innerEvt.Reaction != nil:
			if innerEvt.Reaction.GetRemove() {
				return bridgev2.RemoteEventReactionRemove
			}
			return bridgev2.RemoteEventReaction
		case innerEvt.Delete != nil:
			return bridgev2.RemoteEventMessageRemove
		case innerEvt.GetGroupV2().GetGroupChange() != nil:
			return bridgev2.RemoteEventChatInfoChange
		}
	case *signalpb.EditMessage:
		return bridgev2.RemoteEventEdit
	case *signalpb.TypingMessage:
		return bridgev2.RemoteEventTyping
	}
	return bridgev2.RemoteEventUnknown
}

func (evt *Bv2ChatEvent) GetChatInfoChange(ctx context.Context) (*bridgev2.ChatInfoChange, error) {
	dm, _ := evt.Event.(*signalpb.DataMessage)
	gv2 := dm.GetGroupV2()
	if gv2 == nil || gv2.GroupChange == nil {
		return nil, fmt.Errorf("GetChatInfoChange() called for non-GroupChange event")
	}
	groupChange, err := evt.s.Client.DecryptGroupChange(ctx, gv2)
	if err != nil {
		return nil, fmt.Errorf("failed to decrypt group change: %w", err)
	}
	// XXX: is this ID compatible with types.GroupIdentifier?
	return evt.s.groupChangeToChatInfoChange(ctx, types.GroupIdentifier(evt.Info.ChatID), gv2.GetRevision(), groupChange)
}

func (evt *Bv2ChatEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal) {
	dataMsg, ok := evt.Event.(*signalpb.DataMessage)
	if !ok || dataMsg.GroupV2 == nil {
		return
	}
	portalRev := portal.Metadata.(*signalid.PortalMetadata).Revision
	if evt.Info.GroupRevision > portalRev {
		toRevision := evt.Info.GroupRevision
		if dataMsg.GetGroupV2().GetGroupChange() != nil {
			toRevision--
		}
		evt.s.catchUpGroup(ctx, portal, portalRev, toRevision, dataMsg.GetTimestamp())
	}
}

func (evt *Bv2ChatEvent) GetTimeout() time.Duration {
	if evt.Event.(*signalpb.TypingMessage).GetAction() == signalpb.TypingMessage_STARTED {
		return 15 * time.Second
	} else {
		return 0
	}
}

func (evt *Bv2ChatEvent) GetPortalKey() networkid.PortalKey {
	return evt.s.makePortalKey(evt.Info.ChatID)
}

func (evt *Bv2ChatEvent) ShouldCreatePortal() bool {
	return evt.GetType() == bridgev2.RemoteEventMessage || evt.GetType() == bridgev2.RemoteEventChatInfoChange
}

func (evt *Bv2ChatEvent) AddLogContext(c zerolog.Context) zerolog.Context {
	c = c.Stringer("sender_id", evt.Info.Sender)
	switch innerEvt := evt.Event.(type) {
	case *signalpb.DataMessage:
		c = c.Uint64("message_ts", innerEvt.GetTimestamp())
		switch {
		case innerEvt.Reaction != nil:
			c = c.Uint64("reaction_target_ts", innerEvt.Reaction.GetTargetSentTimestamp())
		case innerEvt.Delete != nil:
			c = c.Uint64("delete_target_ts", innerEvt.Delete.GetTargetSentTimestamp())
		}
	case *signalpb.EditMessage:
		c = c.
			Uint64("edit_target_ts", innerEvt.GetTargetSentTimestamp()).
			Uint64("edit_ts", innerEvt.GetDataMessage().GetTimestamp())
	}
	return c
}

func (evt *Bv2ChatEvent) GetSender() bridgev2.EventSender {
	return evt.s.makeEventSender(evt.Info.Sender)
}

func (evt *Bv2ChatEvent) GetID() networkid.MessageID {
	ts := evt.getDataMsgTimestamp()
	if ts == 0 {
		return ""
	}
	return signalid.MakeMessageID(evt.Info.Sender, ts)
}

func (evt *Bv2ChatEvent) getDataMsgTimestamp() uint64 {
	switch innerEvt := evt.Event.(type) {
	case *signalpb.DataMessage:
		return innerEvt.GetTimestamp()
	case *signalpb.EditMessage:
		return innerEvt.GetDataMessage().GetTimestamp()
	default:
		return 0
	}
}

func (evt *Bv2ChatEvent) GetTimestamp() time.Time {
	ts := evt.getDataMsgTimestamp()
	if ts == 0 {
		return time.Now()
	}
	return time.UnixMilli(int64(ts))
}

func (evt *Bv2ChatEvent) GetTargetMessage() networkid.MessageID {
	var targetAuthorACI uuid.UUID
	var targetSentTS uint64
	switch innerEvt := evt.Event.(type) {
	case *signalpb.DataMessage:
		switch {
		case innerEvt.Reaction != nil:
			targetAuthorACI, _ = signalmeow.ParseStringOrBinaryUUID(innerEvt.Reaction.GetTargetAuthorAci(), innerEvt.Reaction.GetTargetAuthorAciBinary())
			targetSentTS = innerEvt.Reaction.GetTargetSentTimestamp()
		case innerEvt.Delete != nil:
			targetSentTS = innerEvt.Delete.GetTargetSentTimestamp()
		default:
			return ""
		}
	case *signalpb.EditMessage:
		targetSentTS = innerEvt.GetTargetSentTimestamp()
	default:
		return ""
	}
	if targetAuthorACI == uuid.Nil {
		targetAuthorACI = evt.Info.Sender
	}
	return signalid.MakeMessageID(targetAuthorACI, targetSentTS)
}

func (evt *Bv2ChatEvent) GetReactionEmoji() (string, networkid.EmojiID) {
	dataMsg, ok := evt.Event.(*signalpb.DataMessage)
	if !ok || dataMsg.Reaction == nil {
		panic(fmt.Errorf("GetReactionEmoji() called for non-reaction event"))
	}
	return dataMsg.GetReaction().GetEmoji(), ""
}

func (evt *Bv2ChatEvent) GetRemovedEmojiID() networkid.EmojiID {
	return ""
}

func (evt *Bv2ChatEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
	dataMsg, ok := evt.Event.(*signalpb.DataMessage)
	if !ok {
		return nil, fmt.Errorf("ConvertMessage() called for non-DataMessage event")
	}
	converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, dataMsg, nil)
	if converted.Disappear.Type != "" {
		evtTS := evt.GetTimestamp()
		if !dataMsg.GetIsViewOnce() {
			portal.UpdateDisappearingSetting(ctx, converted.Disappear, bridgev2.UpdateDisappearingSettingOpts{
				Sender:     intent,
				Timestamp:  evtTS,
				Implicit:   true,
				Save:       true,
				SendNotice: true,
			})
		}
		if evt.Info.Sender == evt.s.Client.Store.ACI {
			converted.Disappear.DisappearAt = evtTS.Add(converted.Disappear.Timer)
		}
	}
	return converted, nil
}

func (evt *Bv2ChatEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (*bridgev2.ConvertedEdit, error) {
	editMsg, ok := evt.Event.(*signalpb.EditMessage)
	if !ok {
		return nil, fmt.Errorf("ConvertEdit() called for non-EditMessage event")
	}
	// TODO tell converter about existing parts to avoid reupload?
	converted := evt.s.Main.MsgConv.ToMatrix(ctx, evt.s.Client, portal, evt.Info.Sender, intent, editMsg.GetDataMessage(), nil)
	// TODO can anything other than the text be edited?
	editPart := converted.Parts[len(converted.Parts)-1].ToEditPart(existing[len(existing)-1])
	editPart.Part.EditCount++
	editPart.Part.ID = signalid.MakeMessageID(evt.Info.Sender, editMsg.GetDataMessage().GetTimestamp())
	return &bridgev2.ConvertedEdit{
		ModifiedParts: []*bridgev2.ConvertedEditPart{editPart},
	}, nil
}

func (evt *Bv2ChatEvent) GetStreamOrder() int64 {
	return int64(evt.Info.ServerTimestamp)
}

type Bv2Receipt struct {
	Type   signalpb.ReceiptMessage_Type
	Chat   networkid.PortalKey
	Sender bridgev2.EventSender

	LastTS time.Time
	LastID networkid.MessageID
	IDs    []networkid.MessageID
}

func (b *Bv2Receipt) GetType() bridgev2.RemoteEventType {
	switch b.Type {
	case signalpb.ReceiptMessage_READ:
		return bridgev2.RemoteEventReadReceipt
	case signalpb.ReceiptMessage_DELIVERY:
		return bridgev2.RemoteEventDeliveryReceipt
	default:
		return bridgev2.RemoteEventUnknown
	}
}

func (b *Bv2Receipt) GetPortalKey() networkid.PortalKey {
	return b.Chat
}

func (b *Bv2Receipt) AddLogContext(c zerolog.Context) zerolog.Context {
	return c.
		Str("sender_id", string(b.Sender.Sender)).
		Stringer("receipt_type", b.Type).
		Array("message_ids", exzerolog.ArrayOfStrs(b.IDs))
}

func (b *Bv2Receipt) GetSender() bridgev2.EventSender {
	return b.Sender
}

func (b *Bv2Receipt) GetLastReceiptTarget() networkid.MessageID {
	return b.LastID
}

func (b *Bv2Receipt) GetReceiptTargets() []networkid.MessageID {
	return b.IDs
}

func (b *Bv2Receipt) GetReadUpTo() time.Time {
	return time.Time{}
}

var _ bridgev2.RemoteReceipt = (*Bv2Receipt)(nil)

func convertReceipts[T any](ctx context.Context, input []T, getMessageFunc func(ctx context.Context, msgID T) (*database.Message, error)) map[networkid.PortalKey]*Bv2Receipt {
	log := zerolog.Ctx(ctx)
	receipts := make(map[networkid.PortalKey]*Bv2Receipt)
	for _, msgID := range input {
		msg, err := getMessageFunc(ctx, msgID)
		if err != nil {
			log.Err(err).Any("message_id", msgID).Msg("Failed to get target message for receipt")
		} else if msg == nil {
			log.Debug().Any("message_id", msgID).Msg("Got receipt for unknown message")
		} else {
			receiptEvt, ok := receipts[msg.Room]
			if !ok {
				receiptEvt = &Bv2Receipt{Chat: msg.Room}
				receipts[msg.Room] = receiptEvt
			}
			receiptEvt.IDs = append(receiptEvt.IDs, msg.ID)
			if receiptEvt.LastTS.Before(msg.Timestamp) {
				receiptEvt.LastTS = msg.Timestamp
				receiptEvt.LastID = msg.ID
			}
		}
	}
	return receipts
}

func (s *SignalClient) dispatchReceipts(sender uuid.UUID, receiptType signalpb.ReceiptMessage_Type, receipts map[networkid.PortalKey]*Bv2Receipt) bool {
	evtSender := s.makeEventSender(sender)
	for chat, receiptEvt := range receipts {
		receiptEvt.Chat = chat
		receiptEvt.Sender = evtSender
		receiptEvt.Type = receiptType
		if !s.Main.Bridge.QueueRemoteEvent(s.UserLogin, receiptEvt).Success {
			return false
		}
	}
	return true
}

func (s *SignalClient) handleSignalReceipt(evt *events.Receipt) bool {
	log := s.UserLogin.Log.With().
		Str("action", "handle signal receipt").
		Stringer("sender_id", evt.Sender).
		Stringer("receipt_type", evt.Content.GetType()).
		Logger()
	ctx := log.WithContext(context.TODO())
	receipts := convertReceipts(ctx, evt.Content.Timestamp, func(ctx context.Context, msgTS uint64) (*database.Message, error) {
		return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(s.Client.Store.ACI, msgTS))
	})
	return s.dispatchReceipts(evt.Sender, evt.Content.GetType(), receipts)
}

func (s *SignalClient) handleSignalReadSelf(evt *events.ReadSelf) bool {
	log := s.UserLogin.Log.With().
		Str("action", "handle signal read self").
		Logger()
	ctx := log.WithContext(context.TODO())
	receipts := convertReceipts(ctx, evt.Messages, func(ctx context.Context, msgInfo *signalpb.SyncMessage_Read) (*database.Message, error) {
		aciUUID, err := signalmeow.ParseStringOrBinaryUUID(msgInfo.GetSenderAci(), msgInfo.GetSenderAciBinary())
		if err != nil {
			return nil, err
		}
		return s.Main.Bridge.DB.Message.GetFirstPartByID(ctx, s.UserLogin.ID, signalid.MakeMessageID(aciUUID, msgInfo.GetTimestamp()))
	})
	return s.dispatchReceipts(s.Client.Store.ACI, signalpb.ReceiptMessage_READ, receipts)
}

func (s *SignalClient) conversationIDToPortalKey(ctx context.Context, cid *signalpb.ConversationIdentifier) (networkid.PortalKey, bool) {
	log := zerolog.Ctx(ctx)
	switch ident := cid.GetIdentifier().(type) {
	case *signalpb.ConversationIdentifier_ThreadServiceId:
		serviceID, err := libsignalgo.ServiceIDFromString(ident.ThreadServiceId)
		if err != nil {
			log.Err(err).Str("chat_id", ident.ThreadServiceId).Msg("Failed to parse delete for me conversation ID")
			return networkid.PortalKey{}, false
		}
		return s.makeDMPortalKey(serviceID), true
	case *signalpb.ConversationIdentifier_ThreadServiceIdBinary:
		serviceID, err := libsignalgo.ServiceIDFromBytes(ident.ThreadServiceIdBinary)
		if err != nil {
			log.Err(err).Hex("chat_id", ident.ThreadServiceIdBinary).Msg("Failed to parse delete for me conversation ID")
			return networkid.PortalKey{}, false
		}
		return s.makeDMPortalKey(serviceID), true
	case *signalpb.ConversationIdentifier_ThreadGroupId:
		if len(ident.ThreadGroupId) != libsignalgo.GroupIdentifierLength {
			log.Error().
				Str("chat_id", base64.StdEncoding.EncodeToString(ident.ThreadGroupId)).
				Msg("Invalid group ID length in delete for me conversation")
			return networkid.PortalKey{}, false
		}
		return s.makePortalKey((*libsignalgo.GroupIdentifier)(ident.ThreadGroupId).String()), true
	case *signalpb.ConversationIdentifier_ThreadE164:
		log.Warn().Str("chat_id", ident.ThreadE164).Msg("Unsupported E164 conversation ID in delete for me")
		return networkid.PortalKey{}, false
	default:
		log.Warn().
			Type("chat_id_type", ident).
			Msg("Unsupported conversation ID protobuf type in delete for me")
		return networkid.PortalKey{}, false
	}
}

func (s *SignalClient) addressableMessageToID(ctx context.Context, portalKey networkid.PortalKey, am *signalpb.AddressableMessage) networkid.MessageID {
	log := zerolog.Ctx(ctx)
	switch typedAuthor := am.GetAuthor().(type) {
	case *signalpb.AddressableMessage_AuthorServiceId:
		serviceID, err := libsignalgo.ServiceIDFromString(typedAuthor.AuthorServiceId)
		if err != nil {
			log.Err(err).
				Object("portal_key", portalKey).
				Str("author_service_id", typedAuthor.AuthorServiceId).
				Msg("Failed to parse delete for me message author service ID")
			return ""
		} else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
			log.Warn().
				Object("portal_key", portalKey).
				Str("author_service_id", typedAuthor.AuthorServiceId).
				Msg("Dropping delete for me message with unsupported service ID type")
			return ""
		}
		return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
	case *signalpb.AddressableMessage_AuthorServiceIdBinary:
		serviceID, err := libsignalgo.ServiceIDFromBytes(typedAuthor.AuthorServiceIdBinary)
		if err != nil {
			log.Err(err).
				Object("portal_key", portalKey).
				Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
				Msg("Failed to parse delete for me message author service ID")
			return ""
		} else if serviceID.Type != libsignalgo.ServiceIDTypeACI {
			log.Warn().
				Object("portal_key", portalKey).
				Hex("author_service_id_binary", typedAuthor.AuthorServiceIdBinary).
				Msg("Dropping delete for me message with unsupported service ID type")
			return ""
		}
		return signalid.MakeMessageID(serviceID.UUID, am.GetSentTimestamp())
	case *signalpb.AddressableMessage_AuthorE164:
		log.Warn().
			Object("portal_key", portalKey).
			Str("author_e164", typedAuthor.AuthorE164).
			Msg("Dropping delete for me message with unsupported E164 author")
		return ""
	default:
		log.Warn().
			Object("portal_key", portalKey).
			Type("author_type", typedAuthor).
			Msg("Dropping delete for me message with unrecognized author protobuf type")
		return ""
	}
}

func (s *SignalClient) handleSignalDeleteForMe(evt *events.DeleteForMe) bool {
	log := s.UserLogin.Log.With().
		Str("action", "handle signal delete for me").
		Logger()
	ctx := log.WithContext(s.Main.Bridge.BackgroundCtx)
	for _, conv := range evt.GetConversationDeletes() {
		if !conv.GetIsFullDelete() {
			// Non-full deletes might mean clearing chats?
			continue
		}
		portalKey, ok := s.conversationIDToPortalKey(ctx, conv.GetConversation())
		if !ok {
			continue
		}

		res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
			EventMeta: simplevent.EventMeta{
				Type:        bridgev2.RemoteEventChatDelete,
				PortalKey:   portalKey,
				Timestamp:   time.UnixMilli(int64(evt.Timestamp)),
				StreamOrder: int64(evt.Timestamp),
			},
			OnlyForMe: true,
		})
		if !res.Success {
			return false
		}
	}
	for _, conv := range evt.GetLocalOnlyConversationDeletes() {
		portalKey, ok := s.conversationIDToPortalKey(ctx, conv.GetConversation())
		if !ok {
			continue
		}

		res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
			EventMeta: simplevent.EventMeta{
				Type:        bridgev2.RemoteEventChatDelete,
				PortalKey:   portalKey,
				Timestamp:   time.UnixMilli(int64(evt.Timestamp)),
				StreamOrder: int64(evt.Timestamp),
			},
			OnlyForMe: true,
		})
		if !res.Success {
			return false
		}
	}
	for _, conv := range evt.GetMessageDeletes() {
		portalKey, ok := s.conversationIDToPortalKey(ctx, conv.GetConversation())
		if !ok {
			continue
		}
		for _, msg := range conv.GetMessages() {
			msgID := s.addressableMessageToID(ctx, portalKey, msg)
			if msgID == "" {
				continue
			}
			res := s.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
				EventMeta: simplevent.EventMeta{
					Type:        bridgev2.RemoteEventMessageRemove,
					PortalKey:   portalKey,
					Timestamp:   time.UnixMilli(int64(evt.Timestamp)),
					StreamOrder: int64(evt.Timestamp),
				},
				OnlyForMe:     true,
				TargetMessage: msgID,
			})
			if !res.Success {
				return false
			}
		}
	}
	return true
}

func (s *SignalClient) handleSignalMessageRequestResponse(evt *events.MessageRequestResponse) bool {
	if evt.Type != signalpb.SyncMessage_MessageRequestResponse_ACCEPT {
		// TODO do we need to do anything with blocks/deletes here or are they sent as normal delete events?
		return true
	}
	var portalKey networkid.PortalKey
	if evt.GroupID != nil {
		portalKey = s.makePortalKey(evt.GroupID.String())
	} else if evt.ThreadACI != uuid.Nil {
		portalKey = s.makeDMPortalKey(libsignalgo.NewACIServiceID(evt.ThreadACI))
	} else {
		return true
	}
	res := s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
		EventMeta: simplevent.EventMeta{
			Type:        bridgev2.RemoteEventChatInfoChange,
			PortalKey:   portalKey,
			Timestamp:   time.UnixMilli(int64(evt.Timestamp)),
			StreamOrder: int64(evt.Timestamp),
			LogContext: func(c zerolog.Context) zerolog.Context {
				return c.Str("action", "unmark message request").Str("source", "sync message")
			},
		},
		ChatInfoChange: &bridgev2.ChatInfoChange{
			ChatInfo: &bridgev2.ChatInfo{
				MessageRequest: ptr.Ptr(false),
			},
		},
	})
	return res.Success
}

func (s *SignalClient) handleSignalACIFound(evt *events.ACIFound) {
	log := s.UserLogin.Log.With().
		Str("action", "handle aci found").
		Stringer("aci", evt.ACI).
		Stringer("pni", evt.PNI).
		Logger()
	ctx := log.WithContext(context.TODO())
	pniPortalKey := s.makeDMPortalKey(evt.PNI)
	aciPortalKey := s.makeDMPortalKey(evt.ACI)
	result, portal, err := s.Main.Bridge.ReIDPortal(ctx, pniPortalKey, aciPortalKey)
	if err != nil {
		log.Err(err).Msg("Failed to re-ID portal")
	} else if result == bridgev2.ReIDResultSourceReIDd || result == bridgev2.ReIDResultTargetDeletedAndSourceReIDd {
		// If the source portal is re-ID'd, we need to sync metadata and participants.
		// If the source is deleted, then it doesn't matter, any existing target will already be correct
		info, err := s.GetChatInfo(ctx, portal)
		if err != nil {
			log.Err(err).Msg("Failed to get chat info to update portal after re-ID")
		} else {
			portal.UpdateInfo(ctx, info, s.UserLogin, nil, time.Time{})
		}
	}
}

func (s *SignalClient) handleSignalContactList(evt *events.ContactList) {
	log := s.UserLogin.Log.With().Str("action", "handle contact list").Logger()
	ctx := log.WithContext(context.TODO())
	for _, contact := range evt.Contacts {
		if contact.ACI == uuid.Nil {
			continue
		}
		if !evt.IsFromDB {
			fullContact, err := s.Client.ContactByACI(ctx, contact.ACI)
			if err != nil {
				log.Err(err).Msg("Failed to get full contact info from store")
				continue
			}
			fullContact.ContactAvatar = contact.ContactAvatar
			contact = fullContact
		}
		ghost, err := s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(contact.ACI))
		if err != nil {
			log.Err(err).Msg("Failed to get ghost to update contact info")
			continue
		}
		userInfo, err := s.contactToUserInfo(ctx, contact)
		if err != nil {
			log.Err(err).Msg("Failed to convert contact info")
			continue
		}
		ghost.UpdateInfo(ctx, userInfo)
		if contact.ACI == s.Client.Store.ACI {
			s.updateRemoteProfile(ctx, true)
		}
		if ptr.Val(contact.Whitelisted) {
			portal, err := s.Main.Bridge.GetExistingPortalByKey(ctx, s.makeDMPortalKey(libsignalgo.NewACIServiceID(contact.ACI)))
			if err != nil {
				log.Err(err).Msg("Failed to get existing portal to update contact info")
				continue
			} else if portal != nil && portal.MessageRequest {
				s.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
					EventMeta: simplevent.EventMeta{
						Type: bridgev2.RemoteEventChatInfoChange,
						LogContext: func(c zerolog.Context) zerolog.Context {
							return c.Str("action", "unmark message request").Str("source", "contact list")
						},
						PortalKey: portal.PortalKey,
					},
					ChatInfoChange: &bridgev2.ChatInfoChange{
						ChatInfo: &bridgev2.ChatInfo{
							MessageRequest: ptr.Ptr(false),
						},
					},
				})
			}
		}
	}
	s.UserLogin.Metadata.(*signalid.UserLoginMetadata).LastContactSync = jsontime.UnixMilliNow()
	err := s.UserLogin.Save(ctx)
	if err != nil {
		log.Err(err).Msg("Failed to update last contact sync time")
	}
}

func (s *SignalClient) updateRemoteProfile(ctx context.Context, resendState bool) {
	var err error
	if s.Ghost == nil {
		s.Ghost, err = s.Main.Bridge.GetGhostByID(ctx, signalid.MakeUserID(s.Client.Store.ACI))
		if err != nil {
			zerolog.Ctx(ctx).Err(err).Msg("Failed to get ghost for remote profile update")
			return
		}
	}
	changed := false
	if s.UserLogin.RemoteProfile.Name != s.Ghost.Name {
		s.UserLogin.RemoteProfile.Name = s.Ghost.Name
		changed = true
	}
	if s.UserLogin.RemoteProfile.Avatar != s.Ghost.AvatarMXC {
		s.UserLogin.RemoteProfile.Avatar = s.Ghost.AvatarMXC
		changed = true
	}
	if len(s.Ghost.Identifiers) > 0 && strings.HasPrefix(s.Ghost.Identifiers[0], "tel:") {
		phone := strings.TrimPrefix(s.Ghost.Identifiers[0], "tel:")
		if s.UserLogin.RemoteProfile.Phone != phone {
			s.UserLogin.RemoteProfile.Phone = phone
			changed = true
		}
	}
	if changed {
		err = s.UserLogin.Save(ctx)
		if err != nil {
			zerolog.Ctx(ctx).Err(err).Msg("Failed to save updated remote profile")
		}
		if resendState {
			// TODO this has potential race conditions
			s.UserLogin.BridgeState.Send(s.UserLogin.BridgeState.GetPrevUnsent())
		}
	}
}
