package protocol

import (
	"context"
	"time"

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

	"github.com/status-im/status-go/multiaccounts/accounts"
	multiaccountscommon "github.com/status-im/status-go/multiaccounts/common"
	"github.com/status-im/status-go/multiaccounts/settings"
	"github.com/status-im/status-go/protocol/common"
	"github.com/status-im/status-go/protocol/communities"
	"github.com/status-im/status-go/protocol/protobuf"
)

const (
	BackupContactsPerBatch = 20
)

// backupTickerInterval is how often we should check for backups
var backupTickerInterval = 120 * time.Second

// backupIntervalSeconds is the amount of seconds we should allow between
// backups
var backupIntervalSeconds uint64 = 28800

type CommunitySet struct {
	Joined  []*communities.Community
	Deleted []*communities.Community
}

func (m *Messenger) backupEnabled() (bool, error) {
	return m.settings.BackupEnabled()
}

func (m *Messenger) lastBackup() (uint64, error) {
	return m.settings.LastBackup()
}

func (m *Messenger) startBackupLoop() {
	ticker := time.NewTicker(backupTickerInterval)
	go func() {
		for {
			select {
			case <-ticker.C:
				if !m.Online() {
					continue
				}

				enabled, err := m.backupEnabled()
				if err != nil {
					m.logger.Error("failed to fetch backup enabled")
					continue
				}
				if !enabled {
					m.logger.Debug("backup not enabled, skipping")
					continue
				}

				lastBackup, err := m.lastBackup()
				if err != nil {
					m.logger.Error("failed to fetch last backup time")
					continue
				}

				now := time.Now().Unix()
				if uint64(now) <= backupIntervalSeconds+lastBackup {
					m.logger.Debug("not backing up")
					continue
				}
				m.logger.Debug("backing up data")

				ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
				defer cancel()
				_, err = m.BackupData(ctx)
				if err != nil {
					m.logger.Error("failed to backup data", zap.Error(err))
				}
			case <-m.quit:
				ticker.Stop()
				return
			}
		}
	}()
}

func (m *Messenger) BackupData(ctx context.Context) (uint64, error) {
	clock, chat := m.getLastClockWithRelatedChat()
	contactsToBackup := m.backupContacts(ctx)
	communitiesToBackup, err := m.backupCommunities(ctx, clock)
	if err != nil {
		return 0, err
	}
	chatsToBackup := m.backupChats(ctx, clock)
	if err != nil {
		return 0, err
	}
	profileToBackup, err := m.backupProfile(ctx, clock)
	if err != nil {
		return 0, err
	}
	_, settings, errors := m.prepareSyncSettingsMessages(clock, true)
	if len(errors) != 0 {
		// return just the first error, the others have been logged
		return 0, errors[0]
	}

	keypairsToBackup, err := m.backupKeypairs()
	if err != nil {
		return 0, err
	}

	woAccountsToBackup, err := m.backupWatchOnlyAccounts()
	if err != nil {
		return 0, err
	}

	backupDetailsOnly := func() *protobuf.Backup {
		return &protobuf.Backup{
			Clock: clock,
			ChatsDetails: &protobuf.FetchingBackedUpDataDetails{
				DataNumber:  uint32(0),
				TotalNumber: uint32(len(chatsToBackup)),
			},
			ContactsDetails: &protobuf.FetchingBackedUpDataDetails{
				DataNumber:  uint32(0),
				TotalNumber: uint32(len(contactsToBackup)),
			},
			CommunitiesDetails: &protobuf.FetchingBackedUpDataDetails{
				DataNumber:  uint32(0),
				TotalNumber: uint32(len(communitiesToBackup)),
			},
			ProfileDetails: &protobuf.FetchingBackedUpDataDetails{
				DataNumber:  uint32(0),
				TotalNumber: uint32(len(profileToBackup)),
			},
			SettingsDetails: &protobuf.FetchingBackedUpDataDetails{
				DataNumber:  uint32(0),
				TotalNumber: uint32(len(settings)),
			},
			KeypairDetails: &protobuf.FetchingBackedUpDataDetails{
				DataNumber:  uint32(0),
				TotalNumber: uint32(len(keypairsToBackup)),
			},
			WatchOnlyAccountDetails: &protobuf.FetchingBackedUpDataDetails{
				DataNumber:  uint32(0),
				TotalNumber: uint32(len(woAccountsToBackup)),
			},
		}
	}

	// Update contacts messages encode and dispatch
	for i, d := range contactsToBackup {
		pb := backupDetailsOnly()
		pb.ContactsDetails.DataNumber = uint32(i + 1)
		pb.Contacts = d.Contacts
		err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
		if err != nil {
			return 0, err
		}
	}

	// Update communities messages encode and dispatch
	for i, d := range communitiesToBackup {
		pb := backupDetailsOnly()
		pb.CommunitiesDetails.DataNumber = uint32(i + 1)
		pb.Communities = d.Communities
		err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
		if err != nil {
			return 0, err
		}
	}

	// Update profile messages encode and dispatch
	for i, d := range profileToBackup {
		pb := backupDetailsOnly()
		pb.ProfileDetails.DataNumber = uint32(i + 1)
		pb.Profile = d.Profile
		err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
		if err != nil {
			return 0, err
		}
	}

	// Update chats encode and dispatch
	for i, d := range chatsToBackup {
		pb := backupDetailsOnly()
		pb.ChatsDetails.DataNumber = uint32(i + 1)
		pb.Chats = d.Chats
		err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
		if err != nil {
			return 0, err
		}
	}

	// Update settings messages encode and dispatch
	for i, d := range settings {
		pb := backupDetailsOnly()
		pb.SettingsDetails.DataNumber = uint32(i + 1)
		pb.Setting = d
		err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
		if err != nil {
			return 0, err
		}
	}

	// Update keypairs messages encode and dispatch
	for i, d := range keypairsToBackup {
		pb := backupDetailsOnly()
		pb.KeypairDetails.DataNumber = uint32(i + 1)
		pb.Keypair = d.Keypair
		err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
		if err != nil {
			return 0, err
		}
	}

	// Update watch only messages encode and dispatch
	for i, d := range woAccountsToBackup {
		pb := backupDetailsOnly()
		pb.WatchOnlyAccountDetails.DataNumber = uint32(i + 1)
		pb.WatchOnlyAccount = d.WatchOnlyAccount
		err = m.encodeAndDispatchBackupMessage(ctx, pb, chat.ID)
		if err != nil {
			return 0, err
		}
	}

	chat.LastClockValue = clock
	err = m.saveChat(chat)
	if err != nil {
		return 0, err
	}

	clockInSeconds := clock / 1000
	err = m.settings.SetLastBackup(clockInSeconds)
	if err != nil {
		return 0, err
	}
	if m.config.messengerSignalsHandler != nil {
		m.config.messengerSignalsHandler.BackupPerformed(clockInSeconds)
	}

	return clockInSeconds, nil
}

func (m *Messenger) encodeAndDispatchBackupMessage(ctx context.Context, message *protobuf.Backup, chatID string) error {
	encodedMessage, err := proto.Marshal(message)
	if err != nil {
		return err
	}

	_, err = m.dispatchMessage(ctx, common.RawMessage{
		LocalChatID:         chatID,
		Payload:             encodedMessage,
		SkipEncryptionLayer: true,
		SendOnPersonalTopic: true,
		MessageType:         protobuf.ApplicationMetadataMessage_BACKUP,
	})

	return err
}

func (m *Messenger) backupContacts(ctx context.Context) []*protobuf.Backup {
	var contacts []*protobuf.SyncInstallationContactV2
	m.allContacts.Range(func(contactID string, contact *Contact) (shouldContinue bool) {
		syncContact := m.buildSyncContactMessage(contact)
		if syncContact != nil {
			contacts = append(contacts, syncContact)
		}
		return true
	})

	var backupMessages []*protobuf.Backup
	for i := 0; i < len(contacts); i += BackupContactsPerBatch {
		j := i + BackupContactsPerBatch
		if j > len(contacts) {
			j = len(contacts)
		}

		contactsToAdd := contacts[i:j]

		backupMessage := &protobuf.Backup{
			Contacts: contactsToAdd,
		}
		backupMessages = append(backupMessages, backupMessage)
	}

	return backupMessages
}

func (m *Messenger) retrieveAllCommunities() (*CommunitySet, error) {
	joinedCs, err := m.communitiesManager.JoinedAndPendingCommunitiesWithRequests()
	if err != nil {
		return nil, err
	}

	deletedCs, err := m.communitiesManager.DeletedCommunities()
	if err != nil {
		return nil, err
	}

	return &CommunitySet{
		Joined:  joinedCs,
		Deleted: deletedCs,
	}, nil
}

func (m *Messenger) backupCommunities(ctx context.Context, clock uint64) ([]*protobuf.Backup, error) {
	communitySet, err := m.retrieveAllCommunities()
	if err != nil {
		return nil, err
	}

	var backupMessages []*protobuf.Backup
	combinedCs := append(communitySet.Joined, communitySet.Deleted...)

	for _, c := range combinedCs {
		_, beingImported := m.importingCommunities[c.IDString()]
		if !beingImported {
			backupMessage, err := m.backupCommunity(c, clock)
			if err != nil {
				return nil, err
			}

			backupMessages = append(backupMessages, backupMessage)
		}
	}

	return backupMessages, nil
}

func (m *Messenger) backupCommunity(community *communities.Community, clock uint64) (*protobuf.Backup, error) {
	communityId := community.ID()
	settings, err := m.communitiesManager.GetCommunitySettingsByID(communityId)
	if err != nil {
		return nil, err
	}

	syncControlNode, err := m.communitiesManager.GetSyncControlNode(communityId)
	if err != nil {
		return nil, err
	}

	syncMessage, err := community.ToSyncInstallationCommunityProtobuf(clock, settings, syncControlNode)
	if err != nil {
		return nil, err
	}

	err = m.propagateSyncInstallationCommunityWithHRKeys(syncMessage, community)
	if err != nil {
		return nil, err
	}

	return &protobuf.Backup{
		Communities: []*protobuf.SyncInstallationCommunity{syncMessage},
	}, nil
}

func (m *Messenger) backupChats(ctx context.Context, clock uint64) []*protobuf.Backup {
	var oneToOneAndGroupChats []*protobuf.SyncChat
	m.allChats.Range(func(chatID string, chat *Chat) bool {
		if !chat.OneToOne() && !chat.PrivateGroupChat() {
			return true
		}
		syncChat := protobuf.SyncChat{
			Clock:    clock,
			Id:       chatID,
			ChatType: uint32(chat.ChatType),
			Active:   chat.Active,
		}
		chatMuteTill, _ := time.Parse(time.RFC3339, chat.MuteTill.Format(time.RFC3339))
		if chat.Muted && chatMuteTill.Equal(time.Time{}) {
			// Only set Muted if it is "permanently" muted
			syncChat.Muted = true
		}
		if chat.PrivateGroupChat() {
			syncChat.Name = chat.Name // The Name is only useful in the case of a group chat

			syncChat.MembershipUpdateEvents = make([]*protobuf.MembershipUpdateEvents, len(chat.MembershipUpdates))
			for i, membershipUpdate := range chat.MembershipUpdates {
				syncChat.MembershipUpdateEvents[i] = &protobuf.MembershipUpdateEvents{
					Clock:      membershipUpdate.ClockValue,
					Type:       uint32(membershipUpdate.Type),
					Members:    membershipUpdate.Members,
					Name:       membershipUpdate.Name,
					Signature:  membershipUpdate.Signature,
					ChatId:     membershipUpdate.ChatID,
					From:       membershipUpdate.From,
					RawPayload: membershipUpdate.RawPayload,
					Color:      membershipUpdate.Color,
				}
			}
		}
		oneToOneAndGroupChats = append(oneToOneAndGroupChats, &syncChat)
		return true
	})

	var backupMessages []*protobuf.Backup
	backupMessage := &protobuf.Backup{
		Chats: oneToOneAndGroupChats,
	}
	backupMessages = append(backupMessages, backupMessage)
	return backupMessages
}

func (m *Messenger) buildSyncContactMessage(contact *Contact) *protobuf.SyncInstallationContactV2 {
	var ensName string
	if contact.ENSVerified {
		ensName = contact.EnsName
	}

	var customizationColor uint32
	if len(contact.CustomizationColor) != 0 {
		customizationColor = multiaccountscommon.ColorToIDFallbackToBlue(contact.CustomizationColor)
	}

	oneToOneChat, ok := m.allChats.Load(contact.ID)
	muted := false
	if ok {
		muted = oneToOneChat.Muted
	}

	return &protobuf.SyncInstallationContactV2{
		LastUpdatedLocally:        contact.LastUpdatedLocally,
		LastUpdated:               contact.LastUpdated,
		Id:                        contact.ID,
		DisplayName:               contact.DisplayName,
		EnsName:                   ensName,
		CustomizationColor:        customizationColor,
		LocalNickname:             contact.LocalNickname,
		Added:                     contact.added(),
		Blocked:                   contact.Blocked,
		Muted:                     muted,
		HasAddedUs:                contact.hasAddedUs(),
		Removed:                   contact.Removed,
		ContactRequestLocalState:  int64(contact.ContactRequestLocalState),
		ContactRequestRemoteState: int64(contact.ContactRequestRemoteState),
		ContactRequestRemoteClock: int64(contact.ContactRequestRemoteClock),
		ContactRequestLocalClock:  int64(contact.ContactRequestLocalClock),
		VerificationStatus:        int64(contact.VerificationStatus),
		TrustStatus:               int64(contact.TrustStatus),
	}
}

func (m *Messenger) backupProfile(ctx context.Context, clock uint64) ([]*protobuf.Backup, error) {
	displayName, err := m.settings.DisplayName()
	if err != nil {
		return nil, err
	}

	displayNameClock, err := m.settings.GetSettingLastSynced(settings.DisplayName)
	if err != nil {
		return nil, err
	}

	if m.account == nil {
		return nil, nil
	}

	keyUID := m.account.KeyUID
	images, err := m.multiAccounts.GetIdentityImages(keyUID)
	if err != nil {
		return nil, err
	}

	pictureProtos := make([]*protobuf.SyncProfilePicture, len(images))
	for i, image := range images {
		p := &protobuf.SyncProfilePicture{}
		p.Name = image.Name
		p.Payload = image.Payload
		p.Width = uint32(image.Width)
		p.Height = uint32(image.Height)
		p.FileSize = uint32(image.FileSize)
		p.ResizeTarget = uint32(image.ResizeTarget)
		if image.Clock == 0 {
			p.Clock = clock
		} else {
			p.Clock = image.Clock
		}
		pictureProtos[i] = p
	}

	ensUsernameDetails, err := m.getEnsUsernameDetails()
	if err != nil {
		return nil, err
	}
	ensUsernameDetailProtos := make([]*protobuf.SyncEnsUsernameDetail, len(ensUsernameDetails))
	for i, ensUsernameDetail := range ensUsernameDetails {
		ensUsernameDetailProtos[i] = &protobuf.SyncEnsUsernameDetail{
			Username: ensUsernameDetail.Username,
			Clock:    ensUsernameDetail.Clock,
			Removed:  ensUsernameDetail.Removed,
			ChainId:  ensUsernameDetail.ChainID,
		}
	}

	profileShowcasePreferences, err := m.GetProfileShowcasePreferences()
	if err != nil {
		return nil, err
	}

	backupMessage := &protobuf.Backup{
		Profile: &protobuf.BackedUpProfile{
			KeyUid:                     keyUID,
			DisplayName:                displayName,
			Pictures:                   pictureProtos,
			DisplayNameClock:           displayNameClock,
			EnsUsernameDetails:         ensUsernameDetailProtos,
			ProfileShowcasePreferences: ToProfileShowcasePreferencesProto(profileShowcasePreferences),
		},
	}

	backupMessages := []*protobuf.Backup{backupMessage}

	return backupMessages, nil
}

func (m *Messenger) backupKeypairs() ([]*protobuf.Backup, error) {
	keypairs, err := m.settings.GetAllKeypairs()
	if err != nil {
		return nil, err
	}

	var backupMessages []*protobuf.Backup
	for _, kp := range keypairs {

		kp.SyncedFrom = accounts.SyncedFromBackup
		keypair, err := m.prepareSyncKeypairMessage(kp)
		if err != nil {
			return nil, err
		}

		backupMessage := &protobuf.Backup{
			Keypair: keypair,
		}

		backupMessages = append(backupMessages, backupMessage)
	}

	return backupMessages, nil
}

func (m *Messenger) backupWatchOnlyAccounts() ([]*protobuf.Backup, error) {
	accounts, err := m.settings.GetAllWatchOnlyAccounts()
	if err != nil {
		return nil, err
	}

	var backupMessages []*protobuf.Backup
	for _, acc := range accounts {

		backupMessage := &protobuf.Backup{}
		backupMessage.WatchOnlyAccount = m.prepareSyncAccountMessage(acc)

		backupMessages = append(backupMessages, backupMessage)
	}

	return backupMessages, nil
}