// mautrix-telegram - A Matrix-Telegram puppeting bridge.
// Copyright (C) 2025 Sumner Evans
// Copyright (C) 2026 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"
	"fmt"

	"github.com/rs/zerolog"
	"maunium.net/go/mautrix/bridgev2"
	"maunium.net/go/mautrix/bridgev2/database"
	"maunium.net/go/mautrix/bridgev2/networkid"
	"maunium.net/go/mautrix/bridgev2/simplevent"

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

func (tc *TelegramClient) syncChats(ctx context.Context, takeoutID int64, onLogin, restart bool) error {
	if takeoutID != 0 && !tc.main.Config.Takeout.DialogSync {
		return nil
	}
	logWith := zerolog.Ctx(ctx).With().Str("loop", "chat sync")
	if onLogin {
		logWith = logWith.Bool("on_login", true)
	}
	if takeoutID != 0 {
		logWith = logWith.Int64("takeout_id", takeoutID)
	}
	log := logWith.Logger()

	if !tc.syncChatsLock.TryLock() {
		log.Warn().Msg("Waiting for chat sync lock")
		tc.syncChatsLock.Lock()
		log.Debug().Msg("Acquired chat sync lock after waiting")
	}
	defer tc.syncChatsLock.Unlock()

	if restart {
		tc.metadata.DialogSyncCount = 0
		tc.metadata.DialogSyncComplete = false
		tc.metadata.DialogSyncCursor = ""
	} else if tc.metadata.DialogSyncComplete {
		log.Debug().Msg("Dialogs already synced")
		return nil
	}

	isFullSync := true
	updateLimit := subtractLimit(tc.main.Config.Sync.UpdateLimit, tc.metadata.DialogSyncCount)
	if onLogin && tc.main.Config.Takeout.DialogSync {
		updateLimit = tc.main.Config.Sync.LoginLimit
		isFullSync = false
	}
	createLimit := subtractLimit(tc.main.Config.Sync.CreateLimit, tc.metadata.DialogSyncCount)

	var req tg.MessagesGetDialogsRequest
	isFirst := true
	if tc.metadata.DialogSyncCursor != "" {
		isFirst = false
		var err error
		req.OffsetPeer, _, err = tc.inputPeerForPortalID(ctx, tc.metadata.DialogSyncCursor)
		if err != nil {
			return fmt.Errorf("failed to get input peer for pagination: %w", err)
		}
	} else {
		req.OffsetPeer = &tg.InputPeerEmpty{}
	}
	var wrappedReq bin.Object
	if takeoutID != 0 {
		wrappedReq = &tg.InvokeWithTakeoutRequest{TakeoutID: takeoutID, Query: &req}
	} else {
		wrappedReq = &req
	}
	for updateLimit < 0 || updateLimit > 0 {
		if updateLimit < 0 {
			req.Limit = 100
		} else {
			req.Limit = min(100, updateLimit)
		}
		log.Info().
			Stringer("request", &req).
			Int("update_limit", updateLimit).
			Int("create_limit", createLimit).
			Msg("Fetching dialogs")
		dialogs, err := APICallWithUpdates(ctx, tc, func() (tg.ModifiedMessagesDialogs, error) {
			var dialogs tg.MessagesDialogsBox
			retry := true
			attempts := 0
			var err error
			for retry && attempts < 5 {
				retry, err = tgerr.FloodWait(ctx, tc.client.Invoke(ctx, wrappedReq, &dialogs))
				attempts++
			}
			if err != nil {
				return nil, err
			} else if modified, ok := dialogs.Dialogs.AsModified(); !ok {
				return nil, fmt.Errorf("unexpected response type: %T", dialogs.Dialogs)
			} else {
				return modified, nil
			}
		})
		if err != nil {
			return fmt.Errorf("failed to get dialogs: %w", err)
		} else if len(dialogs.GetDialogs()) == 0 {
			log.Debug().Msg("No more dialogs found (empty response)")
			break
		}

		if isFirst {
			// This is the first fetch of dialogs, reset the pinned dialogs based on the list.
			if err = tc.resetPinnedDialogs(ctx, dialogs.GetDialogs()); err != nil {
				return fmt.Errorf("failed to save pinned dialogs: %w", err)
			}
		}
		isFirst = false

		dialogList := dialogs.GetDialogs()
		if updateLimit > 0 && len(dialogList) > updateLimit {
			dialogList = dialogList[:updateLimit]
		}
		err = tc.handleDialogs(ctx, dialogList, dialogs, createLimit)
		if err != nil {
			return fmt.Errorf("failed to handle dialogs: %w", err)
		}
		updateLimit = subtractLimit(updateLimit, len(dialogList))
		createLimit = subtractLimit(createLimit, len(dialogList))

		cursorPortalKey := tc.makePortalKeyFromPeer(dialogList[len(dialogList)-1].GetPeer(), 0)
		if tc.metadata.DialogSyncCursor == cursorPortalKey.ID {
			log.Debug().Msg("No more dialogs found (last dialog is same as old cursor)")
			break
		}
		tc.metadata.DialogSyncCursor = cursorPortalKey.ID
		tc.metadata.DialogSyncCount += len(dialogList)
		if err = tc.userLogin.Save(ctx); err != nil {
			return fmt.Errorf("failed to save user login to update cursor: %w", err)
		}

		req.OffsetPeer, _, err = tc.inputPeerForPortalID(ctx, cursorPortalKey.ID)
		if err != nil {
			return fmt.Errorf("failed to get input peer for pagination: %w", err)
		}
	}
	if isFullSync {
		tc.metadata.DialogSyncComplete = true
		tc.metadata.DialogSyncCursor = ""
		tc.metadata.DialogSyncCount = 0
		if err := tc.userLogin.Save(ctx); err != nil {
			return fmt.Errorf("failed to save user login after successful sync: %w", err)
		}
	}
	log.Info().Msg("Finished dialog sync")
	return nil
}

func subtractLimit(limit, count int) int {
	if limit < 0 {
		return limit
	}
	limit -= count
	if limit < 0 {
		return 0
	}
	return limit
}

func (tc *TelegramClient) resetPinnedDialogs(ctx context.Context, dialogs []tg.DialogClass) error {
	tc.metadata.PinnedDialogs = nil
	for _, dialog := range dialogs {
		if dialog.GetPinned() {
			portalKey := tc.makePortalKeyFromPeer(dialog.GetPeer(), 0)
			tc.metadata.PinnedDialogs = append(tc.metadata.PinnedDialogs, portalKey.ID)
		}
	}
	return tc.userLogin.Save(ctx)
}

func (tc *TelegramClient) handleDialogs(ctx context.Context, dialogList []tg.DialogClass, meta tg.ModifiedMessagesDialogs, createLimit int) error {
	log := zerolog.Ctx(ctx)

	users := map[int64]tg.UserClass{}
	for _, user := range meta.GetUsers() {
		users[user.GetID()] = user
	}
	chats := map[int64]tg.ChatClass{}
	for _, chat := range meta.GetChats() {
		chats[chat.GetID()] = chat
	}
	messages := map[networkid.MessageID]tg.MessageClass{}
	for _, message := range meta.GetMessages() {
		messages[ids.GetMessageIDFromMessage(message)] = message
	}

	for i, d := range dialogList {
		dialog, ok := d.(*tg.Dialog)
		if !ok {
			continue
		}

		log := log.With().
			Stringer("peer", dialog.Peer).
			Int("top_message", dialog.TopMessage).
			Logger()
		log.Debug().Msg("Syncing dialog")

		portalKey := tc.makePortalKeyFromPeer(dialog.GetPeer(), 0)
		portal, err := tc.main.Bridge.GetPortalByKey(ctx, portalKey)
		if err != nil {
			return err
		}
		if dialog.UnreadCount == 0 && !dialog.UnreadMark {
			portal.Metadata.(*PortalMetadata).ReadUpTo = dialog.TopMessage
		}

		var chatInfo *bridgev2.ChatInfo
		switch peer := dialog.Peer.(type) {
		case *tg.PeerUser:
			switch user := users[peer.UserID].(type) {
			case *tg.User:
				if user.GetDeleted() {
					log.Debug().Int64("user_id", peer.UserID).Msg("Not syncing portal because user is deleted")
					continue
				}
				chatInfo, err = tc.getDMChatInfo(ctx, peer.UserID)
				if err != nil {
					return fmt.Errorf("failed to get dm info for %d: %w", peer.UserID, err)
				}
			default:
				log.Debug().
					Int64("user_id", peer.UserID).
					Type("user_type", user).
					Msg("Not syncing portal because user type is unsupported")
				continue
			}
		case *tg.PeerChat:
			switch chat := chats[peer.ChatID].(type) {
			case *tg.Chat:
				// Need to get full chat info to get the member list
				chatInfo, err = tc.GetChatInfo(ctx, portal)
				if err != nil {
					return fmt.Errorf("failed to get chat info for %s: %w", portalKey, err)
				}
			case *tg.ChatForbidden:
				log.Debug().
					Int64("chat_id", peer.ChatID).
					Msg("Not syncing portal because chat is forbidden")
				continue
			default:
				log.Debug().
					Int64("chat_id", peer.ChatID).
					Type("chat_type", chat).
					Msg("Not syncing portal because chat type is unsupported")
				continue
			}
		case *tg.PeerChannel:
			switch channel := chats[peer.ChannelID].(type) {
			case *tg.Channel:
				var mfm *memberFetchMeta
				chatInfo, mfm, err = tc.wrapChatInfo(portal.ID, channel)
				if err != nil {
					return fmt.Errorf("failed to get chat info for %s: %w", portalKey, err)
				}
				err = tc.fillChannelMembers(ctx, mfm, chatInfo.Members)
				if err != nil {
					log.Err(err).Msg("Failed to get channel members")
				}
			case *tg.ChannelForbidden:
				log.Debug().
					Int64("channel_id", peer.ChannelID).
					Msg("Not syncing portal because channel is forbidden")
				continue
			default:
				log.Debug().
					Int64("channel_id", peer.ChannelID).
					Type("channel_type", channel).
					Msg("Not syncing portal because channel type is unsupported")
				continue
			}
		}

		if portal.MXID == "" {
			// Check what the latest message is
			topMessage := messages[ids.MakeMessageID(dialog.Peer, dialog.TopMessage)]
			if topMessage == nil {
				if dialog.TopMessage == 0 {
					log.Debug().Msg("Not syncing portal because there are no messages")
					continue
				}
				log.Warn().Msg("TopMessage of dialog not in messages map")
			} else if topMessage.TypeID() == tg.MessageServiceTypeID {
				action := topMessage.(*tg.MessageService).Action
				if action.TypeID() == tg.MessageActionContactSignUpTypeID || action.TypeID() == tg.MessageActionHistoryClearTypeID {
					log.Debug().Str("action_type", action.TypeName()).Msg("Not syncing portal because it's a contact sign up or history clear")
					continue
				}
			}

			if createLimit >= 0 && i >= createLimit {
				continue
			}
		}

		tc.fillUserLocalMeta(chatInfo, dialog)

		res := tc.main.Bridge.QueueRemoteEvent(tc.userLogin, &simplevent.ChatResync{
			ChatInfo: chatInfo,
			EventMeta: simplevent.EventMeta{
				Type: bridgev2.RemoteEventChatResync,
				LogContext: func(c zerolog.Context) zerolog.Context {
					return c.Str("update", "sync")
				},
				PortalKey:    portalKey,
				CreatePortal: true,
			},
			CheckNeedsBackfillFunc: func(ctx context.Context, latestMessage *database.Message) (bool, error) {
				if latestMessage == nil {
					return true, nil
				}
				_, latestMessageID, err := ids.ParseMessageID(latestMessage.ID)
				if err != nil {
					panic(err)
				}
				return dialog.TopMessage > latestMessageID, nil
			},
		})
		if err = resultToError(res); err != nil {
			return err
		}

		// Generate a read receipt from the last known read message id
		res = tc.main.Bridge.QueueRemoteEvent(tc.userLogin, &simplevent.Receipt{
			EventMeta: simplevent.EventMeta{
				Type:      bridgev2.RemoteEventReadReceipt,
				PortalKey: portalKey,
				Sender:    tc.mySender(),
			},
			LastTarget:          ids.MakeMessageID(portalKey, dialog.ReadInboxMaxID),
			ReadUpToStreamOrder: int64(dialog.ReadInboxMaxID),
		})
		if err = resultToError(res); err != nil {
			return err
		}
	}
	return nil
}
