// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 Sumner Evans
//
// 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"
	"crypto/rand"
	"encoding/base64"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"strings"
	"time"

	"github.com/rs/zerolog"
	"github.com/tidwall/gjson"
	"go.mau.fi/util/exslices"
	"go.mau.fi/util/random"
	"maunium.net/go/mautrix/bridgev2"
	"maunium.net/go/mautrix/bridgev2/networkid"

	"go.mau.fi/mautrix-telegram/pkg/connector/ids"
	"go.mau.fi/mautrix-telegram/pkg/gotd/bin"
	"go.mau.fi/mautrix-telegram/pkg/gotd/crypto"
	"go.mau.fi/mautrix-telegram/pkg/gotd/tg"
)

var (
	_ bridgev2.PushableNetworkAPI = (*TelegramClient)(nil)
	_ bridgev2.PushParsingNetwork = (*TelegramConnector)(nil)
)

var PushAppSandbox = false

type PushCustomData struct {
	MessageID int `json:"msg_id,string"`

	ChannelID int64 `json:"channel_id,string"`
	TopicID   int   `json:"top_msg_id,string"`
	ChatID    int64 `json:"chat_id,string"`
	FromID    int64 `json:"from_id,string"`

	ChatFromBroadcastID int64 `json:"chat_from_broadcast_id,string"`
	ChatFromGroupID     int64 `json:"chat_from_group_id,string"`
	ChatFromID          int64 `json:"chat_from_id,string"`
}

type Aps struct {
	Alert    Alert  `json:"alert"`
	ThreadID string `json:"thread-id"`
}

type Alert struct {
	Title string `json:"title"`
	Body  string `json:"body"`
}

type PushNotificationData struct {
	Aps *Aps `json:"aps"` // Only present for APNs

	LocKey  string         `json:"loc_key"`
	LocArgs []string       `json:"loc_args"`
	Custom  PushCustomData `json:"custom"`
	Sound   string         `json:"sound"`
	UserID  int            `json:"user_id,string"`
}

var PushMessageFormats = map[string]string{
	"AUTH_REGION":                    "New login from unrecognized device %[1]v, location: %[2]v",
	"AUTH_UNKNOWN":                   "New login from unrecognized device %[1]v",
	"CHANNEL_MESSAGES":               "%[1]v posted an album",
	"CHANNEL_MESSAGE_AUDIO":          "%[1]v posted a voice message",
	"CHANNEL_MESSAGE_CONTACT":        "%[1]v posted a contact %[2]v",
	"CHANNEL_MESSAGE_DOC":            "%[1]v posted a file",
	"CHANNEL_MESSAGE_DOCS":           "%[1]v posted %[2]v files",
	"CHANNEL_MESSAGE_FWDS":           "%[1]v posted %[2]v forwarded messages",
	"CHANNEL_MESSAGE_GAME":           "%[1]v invited you to play %[2]v",
	"CHANNEL_MESSAGE_GAME_SCORE":     "%[1]v scored %[3]v in game %[2]v",
	"CHANNEL_MESSAGE_GEO":            "%[1]v posted a location",
	"CHANNEL_MESSAGE_GEOLIVE":        "%[1]v posted a live location",
	"CHANNEL_MESSAGE_GIF":            "%[1]v posted a GIF",
	"CHANNEL_MESSAGE_GIVEAWAY":       "%[1]v posted a giveaway of %[2]vx %[3]vm Premium subscriptions",
	"CHANNEL_MESSAGE_GIVEAWAY_STARS": "%[1]v posted a giveaway of %[3]v stars %[2]v",
	"CHANNEL_MESSAGE_NOTEXT":         "%[1]v posted a message",
	"CHANNEL_MESSAGE_PAID_MEDIA":     "%[1]v posted a paid post for %[2]v star",
	"CHANNEL_MESSAGE_PHOTO":          "%[1]v posted a photo",
	"CHANNEL_MESSAGE_PHOTOS":         "%[1]v posted %[2]v photos",
	"CHANNEL_MESSAGE_PLAYLIST":       "%[1]v posted %[2]v music files",
	"CHANNEL_MESSAGE_POLL":           "%[1]v posted a poll %[2]v",
	"CHANNEL_MESSAGE_QUIZ":           "%[1]v posted a quiz %[2]v",
	"CHANNEL_MESSAGE_ROUND":          "%[1]v posted a video message",
	"CHANNEL_MESSAGE_STICKER":        "%[1]v posted a %[2]v sticker",
	"CHANNEL_MESSAGE_STORY":          "%[1]v shared a story",
	"CHANNEL_MESSAGE_TEXT":           "%[1]v: %[2]v",
	"CHANNEL_MESSAGE_VIDEO":          "%[1]v posted a video",
	"CHANNEL_MESSAGE_VIDEOS":         "%[1]v posted %[2]v videos",
	"CHAT_ADD_MEMBER":                "%[1]v invited %[3]v to the group %[2]v",
	"CHAT_ADD_YOU":                   "%[1]v invited you to the group %[2]v",
	"CHAT_CREATED":                   "%[1]v invited you to the group %[2]v",
	"CHAT_DELETE_MEMBER":             "%[1]v removed %[3]v from the group %[2]v",
	"CHAT_DELETE_YOU":                "%[1]v removed you from the group %[2]v",
	"CHAT_JOINED":                    "%[1]v joined the group %[2]v",
	"CHAT_LEFT":                      "%[1]v left the group %[2]v",
	"CHAT_MESSAGES":                  "%[1]v sent an album to the group %[2]v",
	"CHAT_MESSAGE_AUDIO":             "%[1]v sent a voice message to the group %[2]v",
	"CHAT_MESSAGE_CONTACT":           "%[1]v shared a contact %[3]v in the group %[2]v",
	"CHAT_MESSAGE_DOC":               "%[1]v sent a file to the group %[2]v",
	"CHAT_MESSAGE_DOCS":              "%[1]v sent %[3]v files to the group %[2]v",
	"CHAT_MESSAGE_FWDS":              "%[1]v forwarded %[3]v messages to the group %[2]v",
	"CHAT_MESSAGE_GAME":              "%[1]v invited the group %[2]v to play %[3]v",
	"CHAT_MESSAGE_GAME_SCORE":        "%[1]v scored %[4]v in game %[3]v in the group %[2]v",
	"CHAT_MESSAGE_GEO":               "%[1]v sent a location to the group %[2]v",
	"CHAT_MESSAGE_GEOLIVE":           "%[1]v shared a live location with the group %[2]v",
	"CHAT_MESSAGE_GIF":               "%[1]v sent a GIF to the group %[2]v",
	"CHAT_MESSAGE_GIVEAWAY":          "%[1]v sent a giveaway of %[3]vx %[4]vm Premium subscriptions to the group %[2]v",
	"CHAT_MESSAGE_GIVEAWAY_STARS":    "%[1]v sent a giveaway of %[4]v stars %[3]v to the group %[2]v",
	"CHAT_MESSAGE_INVOICE":           "%[1]v sent an invoice to the group %[2]v for %[3]v",
	"CHAT_MESSAGE_NOTEXT":            "%[1]v sent a message to the group %[2]v",
	"CHAT_MESSAGE_PAID_MEDIA":        "%[1]v posted a paid post in %[2]v group for %[3]v star",
	"CHAT_MESSAGE_PHOTO":             "%[1]v sent a photo to the group %[2]v",
	"CHAT_MESSAGE_PHOTOS":            "%[1]v sent %[3]v photos to the group %[2]v",
	"CHAT_MESSAGE_PLAYLIST":          "%[1]v sent %[3]v music files to the group %[2]v",
	"CHAT_MESSAGE_POLL":              "%[1]v sent a poll %[3]v to the group %[2]v",
	"CHAT_MESSAGE_QUIZ":              "%[1]v sent a quiz %[3]v to the group %[2]v",
	"CHAT_MESSAGE_ROUND":             "%[1]v sent a video message to the group %[2]v",
	"CHAT_MESSAGE_STICKER":           "%[1]v sent a %[3]v sticker to the group %[2]v",
	"CHAT_MESSAGE_STORY":             "%[1]v shared a story to the group",
	"CHAT_MESSAGE_TEXT":              "%[1]v @ %[2]v: %[3]v",
	"CHAT_MESSAGE_VIDEO":             "%[1]v sent a video to the group %[2]v",
	"CHAT_MESSAGE_VIDEOS":            "%[1]v sent %[3]v videos to the group %[2]v",
	"CHAT_PHOTO_EDITED":              "%[1]v changed the group photo for %[2]v",
	"CHAT_REACT_AUDIO":               "%[1]v: %[3]v to your voice message in %[2]v",
	"CHAT_REACT_CONTACT":             "%[1]v: %[3]v to your contact %[4]v in %[2]v",
	"CHAT_REACT_DOC":                 "%[1]v: %[3]v to your file in %[2]v",
	"CHAT_REACT_GAME":                "%[1]v: %[3]v to your game in %[2]v",
	"CHAT_REACT_GEO":                 "%[1]v: %[3]v to your map in %[2]v",
	"CHAT_REACT_GEOLIVE":             "%[1]v: %[3]v to your live location in %[2]v",
	"CHAT_REACT_GIF":                 "%[1]v: %[3]v to your GIF in %[2]v",
	"CHAT_REACT_GIVEAWAY":            "%[1]v reacted %[3]v in group %[2]v to your giveaway",
	"CHAT_REACT_INVOICE":             "%[1]v: %[3]v to your invoice in %[2]v",
	"CHAT_REACT_NOTEXT":              "%[1]v: %[3]v to your message in %[2]v",
	"CHAT_REACT_PAID_MEDIA":          "%[1]v reacted %[3]v in group %[2]v to your paid post for %[4]v star",
	"CHAT_REACT_PHOTO":               "%[1]v: %[3]v to your photo in %[2]v",
	"CHAT_REACT_POLL":                "%[1]v: %[3]v to your poll %[4]v in %[2]v",
	"CHAT_REACT_QUIZ":                "%[1]v: %[3]v to your quiz %[4]v in %[2]v",
	"CHAT_REACT_ROUND":               "%[1]v: %[3]v to your video message in %[2]v",
	"CHAT_REACT_STICKER":             "%[1]v: %[3]v to your %[4]v sticker in %[2]v",
	"CHAT_REACT_TEXT":                "%[1]v: %[3]v in %[2]v to your \"%[4]v\"",
	"CHAT_REACT_VIDEO":               "%[1]v: %[3]v to your video in %[2]v",
	"CHAT_REQ_JOINED":                "%[2]v|%[1]v was accepted into the group",
	"CHAT_RETURNED":                  "%[1]v returned to the group %[2]v",
	"CHAT_TITLE_EDITED":              "%[1]v renamed the group %[2]v",
	"CHAT_VOICECHAT_END":             "%[1]v ended a voice chat in the group %[2]v",
	"CHAT_VOICECHAT_INVITE":          "%[1]v invited %[3]v to a voice chat in the group %[2]v",
	"CHAT_VOICECHAT_INVITE_YOU":      "%[1]v invited you to a voice chat in the group %[2]v",
	"CHAT_VOICECHAT_START":           "%[1]v started a voice chat in the group %[2]v",
	"CONTACT_JOINED":                 "%[1]v joined Telegram!",
	"ENCRYPTED_MESSAGE":              "You have a new message",
	"ENCRYPTION_ACCEPT":              "You have a new message",
	"ENCRYPTION_REQUEST":             "You have a new message",
	"LOCKED_MESSAGE":                 "You have a new message",
	"MESSAGES":                       "%[1]v sent you an album",
	"MESSAGE_AUDIO":                  "%[1]v sent you a voice message",
	"MESSAGE_CONTACT":                "%[1]v shared a contact %[2]v with you",
	"MESSAGE_DOC":                    "%[1]v sent you a file",
	"MESSAGE_DOCS":                   "%[1]v sent you %[2]v files",
	"MESSAGE_FWDS":                   "%[1]v forwarded you %[2]v messages",
	"MESSAGE_GAME":                   "%[1]v invited you to play %[2]v",
	"MESSAGE_GAME_SCORE":             "%[1]v scored %[3]v in game %[2]v",
	"MESSAGE_GEO":                    "%[1]v sent you a location",
	"MESSAGE_GEOLIVE":                "%[1]v sent you a live location",
	"MESSAGE_GIF":                    "%[1]v sent you a GIF",
	"MESSAGE_GIFTCODE":               "%[1]v sent you a Gift Code for %[2]v of Telegram Premium",
	"MESSAGE_GIVEAWAY":               "%[1]v sent you a giveaway of %[2]vx %[3]vm Premium subscriptions",
	"MESSAGE_GIVEAWAY_STARS":         "%[1]v sent you a giveaway of %[3]v stars %[2]v",
	"MESSAGE_INVOICE":                "%[1]v sent you an invoice for %[2]v",
	"MESSAGE_NOTEXT":                 "%[1]v sent you a message",
	"MESSAGE_PAID_MEDIA":             "%[1]v sent you a paid post for %[2]v star",
	"MESSAGE_PHOTO":                  "%[1]v sent you a photo",
	"MESSAGE_PHOTOS":                 "%[1]v sent you %[2]v photos",
	"MESSAGE_PHOTO_SECRET":           "%[1]v sent you a self-destructing photo",
	"MESSAGE_PLAYLIST":               "%[1]v sent you %[2]v music files",
	"MESSAGE_POLL":                   "%[1]v sent you a poll %[2]v",
	"MESSAGE_QUIZ":                   "%[1]v sent you a quiz %[2]v",
	"MESSAGE_RECURRING_PAY":          "You were charged %[2]v by %[1]v",
	"MESSAGE_ROUND":                  "%[1]v sent you a video message",
	"MESSAGE_SAME_WALLPAPER":         "%[1]v set a same wallpaper for this chat",
	"MESSAGE_SCREENSHOT":             "%[1]v took a screenshot",
	"MESSAGE_STARGIFT":               "%[1]v sent you a Gift worth %[2]v Stars",
	"MESSAGE_STICKER":                "%[1]v sent you a %[2]v sticker",
	"MESSAGE_STORY":                  "%[1]v shared a story with you",
	"MESSAGE_STORY_MENTION":          "%[1]v mentioned you in a story",
	"MESSAGE_TEXT":                   "%[1]v: %[2]v",
	"MESSAGE_VIDEO":                  "%[1]v sent you a video",
	"MESSAGE_VIDEOS":                 "%[1]v sent you %[2]v videos",
	"MESSAGE_VIDEO_SECRET":           "%[1]v sent you a self-destructing video",
	"MESSAGE_WALLPAPER":              "%[1]v set a new wallpaper for this chat",
	"PHONE_CALL_MISSED":              "You missed a call from %[1]v",
	"PHONE_CALL_REQUEST":             "%[1]v is calling you!",
	"PINNED_AUDIO":                   "%[1]v pinned a voice message in the group %[2]v",
	"PINNED_CONTACT":                 "%[1]v pinned a contact %[3]v in the group %[2]v",
	"PINNED_DOC":                     "%[1]v pinned a file in the group %[2]v",
	"PINNED_GAME":                    "%[1]v pinned a game in the group %[2]v",
	"PINNED_GAME_SCORE":              "%[1]v pinned a game score in the group %[2]v",
	"PINNED_GEO":                     "%[1]v pinned a map in the group %[2]v",
	"PINNED_GEOLIVE":                 "%[1]v pinned a live location in the group %[2]v",
	"PINNED_GIF":                     "%[1]v pinned a GIF in the group %[2]v",
	"PINNED_GIVEAWAY":                "%[1]v pinned a giveaway in the group %[2]v",
	"PINNED_INVOICE":                 "%[1]v pinned an invoice in the group %[2]v",
	"PINNED_NOTEXT":                  "%[1]v pinned a message in the group %[2]v",
	"PINNED_PAID_MEDIA":              "%[1]v pinned a paid post for %[3]v star in the group %[2]v",
	"PINNED_PHOTO":                   "%[1]v pinned a photo in the group %[2]v",
	"PINNED_POLL":                    "%[1]v pinned a poll %[3]v in the group %[2]v",
	"PINNED_QUIZ":                    "%[1]v pinned a quiz %[3]v in the group %[2]v",
	"PINNED_ROUND":                   "%[1]v pinned a video message in the group %[2]v",
	"PINNED_STICKER":                 "%[1]v pinned a %[3]v sticker in the group %[2]v",
	"PINNED_TEXT":                    "%[1]v pinned \"%[3]v\" in the group %[2]v",
	"PINNED_VIDEO":                   "%[1]v pinned a video in the group %[2]v",
	"REACT_AUDIO":                    "%[1]v: %[2]v to your voice message",
	"REACT_CONTACT":                  "%[1]v: %[2]v to your contact %[3]v",
	"REACT_DOC":                      "%[1]v: %[2]v to your file",
	"REACT_GAME":                     "%[1]v: %[2]v to your game",
	"REACT_GEO":                      "%[1]v: %[2]v to your map",
	"REACT_GEOLIVE":                  "%[1]v: %[2]v to your live location",
	"REACT_GIF":                      "%[1]v: %[2]v to your GIF",
	"REACT_GIVEAWAY":                 "%[1]v reacted %[2]v to your giveaway",
	"REACT_HIDDEN":                   "New reaction to your message",
	"REACT_INVOICE":                  "%[1]v: %[2]v to your invoice",
	"REACT_NOTEXT":                   "%[1]v: %[2]v to your message",
	"REACT_PHOTO":                    "%[1]v: %[2]v to your photo",
	"REACT_POLL":                     "%[1]v: %[2]v to your poll %[3]v",
	"REACT_QUIZ":                     "%[1]v: %[2]v to your quiz %[3]v",
	"REACT_ROUND":                    "%[1]v: %[2]v to your video message",
	"REACT_STICKER":                  "%[1]v: %[2]v to your %[3]v sticker",
	"REACT_STORY":                    "%[1]v: %[2]v to your story",
	"REACT_STORY_HIDDEN":             "New reaction to your story",
	"REACT_TEXT":                     "%[1]v: %[2]v to your \"%[3]v\"",
	"REACT_VIDEO":                    "%[1]v: %[2]v to your video",
	"STORY_HIDDEN_AUTHOR":            "A new story was posted",
	"STORY_NOTEXT":                   "%[1]v posted a story",
}

var FullSyncOnConnectBackground = false

func (t *TelegramClient) ConnectBackground(ctx context.Context, params *bridgev2.ConnectBackgroundParams) error {
	data, _ := params.ExtraData.(*PushNotificationData)
	var relatedPortal *bridgev2.Portal
	var sender *bridgev2.Ghost
	var messageID networkid.MessageID
	var messageText, notificationText, notificationTitle string
	if data != nil {
		if data.Aps != nil {
			notificationTitle = data.Aps.Alert.Title
			notificationText = data.Aps.Alert.Body
			if data.LocKey == "" {
				messageText = data.Aps.Alert.Body
			}
		}
		tpl, ok := PushMessageFormats[data.LocKey]
		if ok && (len(data.LocArgs) > 0 || !strings.Contains(tpl, "%[1]")) {
			notificationText = fmt.Sprintf(tpl, exslices.CastToAny(data.LocArgs)...)
		}
		switch data.LocKey {
		case "MESSAGE_TEXT", "CHANNEL_MESSAGE_TEXT":
			messageText = data.LocArgs[1]
		case "CHAT_MESSAGE_TEXT":
			messageText = data.LocArgs[2]
		}
		var err error
		if data.Custom.ChannelID != 0 {
			relatedPortal, err = t.main.Bridge.GetPortalByKey(ctx, t.makePortalKeyFromID(ids.PeerTypeChannel, data.Custom.ChannelID, data.Custom.TopicID))
		} else if data.Custom.ChatID != 0 {
			relatedPortal, err = t.main.Bridge.GetPortalByKey(ctx, t.makePortalKeyFromID(ids.PeerTypeChat, data.Custom.ChatID, 0))
		} else if data.Custom.FromID != 0 {
			relatedPortal, err = t.main.Bridge.GetPortalByKey(ctx, t.makePortalKeyFromID(ids.PeerTypeUser, data.Custom.FromID, 0))
		}
		if err != nil {
			return fmt.Errorf("failed to get related portal: %w", err)
		}
		if data.Custom.ChatFromBroadcastID != 0 {
			sender, err = t.main.Bridge.GetGhostByID(ctx, ids.MakeChannelUserID(data.Custom.FromID))
		} else if data.Custom.ChatFromGroupID != 0 {
			sender, err = t.main.Bridge.GetGhostByID(ctx, ids.MakeChannelUserID(data.Custom.ChatFromGroupID))
		} else if data.Custom.ChatFromID != 0 {
			sender, err = t.main.Bridge.GetGhostByID(ctx, ids.MakeUserID(data.Custom.ChatFromID))
		} else if data.Custom.FromID != 0 {
			sender, err = t.main.Bridge.GetGhostByID(ctx, ids.MakeUserID(data.Custom.FromID))
		}
		if err != nil {
			return fmt.Errorf("failed to get sender: %w", err)
		}
		if relatedPortal != nil && data.Custom.MessageID != 0 {
			messageID = ids.MakeMessageID(relatedPortal.PortalKey, data.Custom.MessageID)
		}
	}
	notifs, ok := t.main.Bridge.Matrix.(bridgev2.MatrixConnectorWithNotifications)
	if ok {
		notifs.DisplayNotification(ctx, &bridgev2.DirectNotificationData{
			Portal:    relatedPortal,
			Sender:    sender,
			Message:   messageText,
			MessageID: messageID,

			FormattedNotification: notificationText,
			FormattedTitle:        notificationTitle,
		})
	}
	if FullSyncOnConnectBackground {
		t.Connect(ctx)
		defer t.Disconnect()
		// TODO is it possible to safely only sync one chat?
		select {
		case <-time.After(20 * time.Second):
		case <-ctx.Done():
		}
	}
	return nil
}

func (tg *TelegramConnector) ParsePushNotification(ctx context.Context, data json.RawMessage) (networkid.UserLoginID, any, error) {
	val := gjson.GetBytes(data, "p")
	if val.Type != gjson.String {
		return "", nil, fmt.Errorf("missing or invalid p field")
	}
	valBytes, err := base64.RawURLEncoding.DecodeString(val.Str)
	if err != nil {
		return "", nil, fmt.Errorf("failed to base64 decode p field: %w", err)
	}
	var em crypto.EncryptedMessage
	err = em.DecodeWithoutCopy(&bin.Buffer{Buf: valBytes})
	if err != nil {
		return "", nil, fmt.Errorf("failed to decode auth key and message ID: %w", err)
	}
	userIDs, err := tg.Bridge.DB.UserLogin.GetAllUserIDsWithLogins(ctx)
	if err != nil {
		return "", nil, fmt.Errorf("failed to get users with logins: %w", err)
	}
	var matchingAuthKey *crypto.AuthKey
	var userLoginID networkid.UserLoginID
UserLoop:
	for _, userID := range userIDs {
		user, err := tg.Bridge.GetExistingUserByMXID(ctx, userID)
		if err != nil {
			return "", nil, fmt.Errorf("failed to get user %s: %w", userID, err)
		}
		for _, login := range user.GetUserLogins() {
			key := login.Metadata.(*UserLoginMetadata).PushEncryptionKey
			if len(key) != 256 {
				continue
			}
			authKey := crypto.Key(key).WithID()
			if authKey.ID == em.AuthKeyID {
				matchingAuthKey = &authKey
				userLoginID = login.ID
				break UserLoop
			}
		}
	}
	if matchingAuthKey == nil {
		return "", nil, fmt.Errorf("no matching auth key found")
	}
	c := crypto.NewClientCipher(rand.Reader)
	plaintext, err := c.DecryptRaw(*matchingAuthKey, &em)
	if err != nil {
		return userLoginID, nil, fmt.Errorf("failed to decrypt payload: %w", err)
	} else if len(plaintext) < 4 {
		return userLoginID, nil, fmt.Errorf("decrypted payload too short (expected >4, got %d)", len(plaintext))
	}
	jsonLength := binary.LittleEndian.Uint32(plaintext[0:4])
	if len(plaintext) < int(jsonLength)+4 {
		return userLoginID, nil, fmt.Errorf("decrypted payload too short (expected 4+%d, got %d)", jsonLength, len(plaintext))
	}
	jsonData := plaintext[4 : jsonLength+4]
	var pmd PushNotificationData
	err = json.Unmarshal(jsonData, &pmd)
	if err != nil {
		zerolog.Ctx(ctx).Debug().Str("raw_data", base64.StdEncoding.EncodeToString(plaintext)).Msg("Decrypted non-JSON push data")
		return userLoginID, nil, fmt.Errorf("failed to unmarshal decrypted payload: %w", err)
	}
	if pmd.Aps != nil {
		// APNs notifications don't have custom data nested in a separate key, they have it at the top level
		err = json.Unmarshal(jsonData, &pmd.Custom)
		if err != nil {
			return userLoginID, nil, fmt.Errorf("failed to unmarshal APNs data into custom field: %w", err)
		}
	}
	return userLoginID, &pmd, nil
}

func (t *TelegramClient) RegisterPushNotifications(ctx context.Context, pushType bridgev2.PushType, token string) error {
	meta := t.metadata
	if meta.PushEncryptionKey == nil {
		meta.PushEncryptionKey = random.Bytes(256)
		err := t.userLogin.Save(ctx)
		if err != nil {
			return fmt.Errorf("failed to save push encryption key: %w", err)
		}
	}
	var tokenType int
	switch pushType {
	case bridgev2.PushTypeWeb:
		tokenType = 10
	case bridgev2.PushTypeFCM:
		tokenType = 2
	case bridgev2.PushTypeAPNs:
		tokenType = 1
	default:
		return fmt.Errorf("unsupported push type %s", pushType)
	}
	_, err := t.client.API().AccountRegisterDevice(ctx, &tg.AccountRegisterDeviceRequest{
		NoMuted:    true,
		TokenType:  tokenType,
		Token:      token,
		AppSandbox: PushAppSandbox,
		Secret:     meta.PushEncryptionKey,
		OtherUIDs:  nil, // TODO set properly
	})
	return err
}

func (t *TelegramClient) GetPushConfigs() *bridgev2.PushConfig {
	return &bridgev2.PushConfig{Native: true}
}
