package protocol

import (
	"context"
	"errors"
	"time"

	"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/encryption/multidevice"
	"github.com/status-im/status-go/protocol/protobuf"
	"github.com/status-im/status-go/protocol/requests"
)

type InstallationIDProvider interface {
	GetInstallationID() string
	Validate() error
}

func (m *Messenger) EnableInstallationAndSync(request *requests.EnableInstallationAndSync) (*MessengerResponse, error) {
	if err := request.Validate(); err != nil {
		return nil, err
	}

	installation, err := m.EnableInstallation(request.InstallationID)
	if err != nil {
		return nil, err
	}

	response := &MessengerResponse{}
	response.AddInstallation(installation)

	pairResponse, err := m.SendPairInstallation(context.Background(), request.InstallationID, nil)
	if err != nil {
		return nil, err
	}

	if err = m.SyncDevices(context.Background(), "", "", nil); err != nil {
		return nil, err
	}

	if err = m.deleteNotification(pairResponse, request.InstallationID); err != nil {
		return nil, err
	}

	if err = pairResponse.Merge(response); err != nil {
		return nil, err
	}

	return pairResponse, nil
}

func (m *Messenger) EnableInstallationAndPair(request InstallationIDProvider) (*MessengerResponse, error) {
	if err := request.Validate(); err != nil {
		return nil, err
	}

	myIdentity := crypto.CompressPubkey(&m.identity.PublicKey)
	timestamp := time.Now().UnixNano()
	installationID := request.GetInstallationID()

	installation := &multidevice.Installation{
		ID:        installationID,
		Enabled:   true,
		Version:   2,
		Timestamp: timestamp,
	}

	_, err := m.encryptor.AddInstallation(myIdentity, timestamp, installation, true)
	if err != nil {
		return nil, err
	}
	i, ok := m.allInstallations.Load(installationID)
	if !ok {
		i = installation
	} else {
		i.Enabled = true
	}
	m.allInstallations.Store(installationID, i)
	response, err := m.SendPairInstallation(context.Background(), request.GetInstallationID(), nil)
	if err != nil {
		return nil, err
	}

	notification := &ActivityCenterNotification{
		ID:             types.FromHex(installationID),
		Type:           ActivityCenterNotificationTypeNewInstallationCreated,
		InstallationID: m.installationID, // Put our own installation ID, as we're the initiator of the pairing
		Timestamp:      m.getTimesource().GetCurrentTime(),
		Read:           false,
		Deleted:        false,
		UpdatedAt:      m.GetCurrentTimeInMillis(),
	}

	err = m.addActivityCenterNotification(response, notification, nil)
	if err != nil {
		return nil, err
	}
	return response, err
}

// SendPairInstallation sends a pair installation message
func (m *Messenger) SendPairInstallation(ctx context.Context, targetInstallationID string, rawMessageHandler RawMessageHandler) (*MessengerResponse, error) {
	var err error
	var response MessengerResponse

	installation, ok := m.allInstallations.Load(m.installationID)
	if !ok {
		return nil, errors.New("no installation found")
	}

	if installation.InstallationMetadata == nil {
		return nil, errors.New("no installation metadata")
	}

	clock, chat := m.getLastClockWithRelatedChat()

	pairMessage := &protobuf.SyncPairInstallation{
		Clock:                clock,
		Name:                 installation.InstallationMetadata.Name,
		InstallationId:       installation.ID,
		DeviceType:           installation.InstallationMetadata.DeviceType,
		Version:              installation.Version,
		TargetInstallationId: targetInstallationID,
	}
	encodedMessage, err := proto.Marshal(pairMessage)
	if err != nil {
		return nil, err
	}

	if rawMessageHandler == nil {
		rawMessageHandler = m.dispatchPairInstallationMessage
	}
	_, err = rawMessageHandler(ctx, common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_SYNC_PAIR_INSTALLATION,
		ResendType:  common.ResendTypeDataSync,
	})
	if err != nil {
		return nil, err
	}

	response.AddChat(chat)

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

// SyncDevices sends all public chats and contacts to paired devices
// TODO remove use of photoPath in contacts
func (m *Messenger) SyncDevices(ctx context.Context, ensName, photoPath string, rawMessageHandler RawMessageHandler) (err error) {
	if rawMessageHandler == nil {
		rawMessageHandler = m.dispatchMessage
	}

	myID := contactIDFromPublicKey(&m.identity.PublicKey)

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

	if _, err = m.sendContactUpdate(ctx, myID, displayName, ensName, photoPath, m.account.GetCustomizationColor(), rawMessageHandler); err != nil {
		return err
	}

	m.allChats.Range(func(chatID string, chat *Chat) bool {
		if !chat.shouldBeSynced() {
			return true

		}
		err = m.syncChat(ctx, chat, rawMessageHandler)
		return err == nil
	})
	if err != nil {
		return err
	}

	m.allContacts.Range(func(contactID string, contact *Contact) bool {
		if contact.ID == myID {
			return true
		}
		if contact.LocalNickname != "" || contact.added() || contact.hasAddedUs() || contact.Blocked {
			if err = m.syncContact(ctx, contact, rawMessageHandler); err != nil {
				return false
			}
		}
		return true
	})

	cs, err := m.communitiesManager.JoinedAndPendingCommunitiesWithRequests()
	if err != nil {
		return err
	}
	for _, c := range cs {
		if err = m.syncCommunity(ctx, c, rawMessageHandler); err != nil {
			return err
		}
	}

	bookmarks, err := m.browserDatabase.GetBookmarks()
	if err != nil {
		return err
	}
	for _, b := range bookmarks {
		if err = m.SyncBookmark(ctx, b, rawMessageHandler); err != nil {
			return err
		}
	}

	trustedUsers, err := m.verificationDatabase.GetAllTrustStatus()
	if err != nil {
		return err
	}
	for id, ts := range trustedUsers {
		if err = m.SyncTrustedUser(ctx, id, ts, rawMessageHandler); err != nil {
			return err
		}
	}

	verificationRequests, err := m.verificationDatabase.GetVerificationRequests()
	if err != nil {
		return err
	}
	for i := range verificationRequests {
		if err = m.SyncVerificationRequest(ctx, &verificationRequests[i], rawMessageHandler); err != nil {
			return err
		}
	}

	err = m.syncSettings(rawMessageHandler)
	if err != nil {
		return err
	}

	err = m.syncProfilePicturesFromDatabase(rawMessageHandler)
	if err != nil {
		return err
	}

	if err = m.syncLatestContactRequests(ctx, rawMessageHandler); err != nil {
		return err
	}

	// we have to sync deleted keypairs as well
	keypairs, err := m.settings.GetAllKeypairs()
	if err != nil {
		return err
	}

	for _, kp := range keypairs {
		err = m.syncKeypair(kp, rawMessageHandler)
		if err != nil {
			return err
		}
	}

	// we have to sync deleted watch only accounts as well
	woAccounts, err := m.settings.GetAllWatchOnlyAccounts()
	if err != nil {
		return err
	}

	for _, woAcc := range woAccounts {
		err = m.syncWalletAccount(woAcc, rawMessageHandler)
		if err != nil {
			return err
		}
	}

	savedAddresses, err := m.savedAddressesManager.GetRawSavedAddresses()
	if err != nil {
		return err
	}

	for i := range savedAddresses {
		sa := savedAddresses[i]

		err = m.syncSavedAddress(ctx, sa, rawMessageHandler)
		if err != nil {
			return err
		}
	}

	if err = m.syncEnsUsernameDetails(ctx, rawMessageHandler); err != nil {
		return err
	}

	if err = m.syncDeleteForMeMessage(ctx, rawMessageHandler); err != nil {
		return err
	}

	err = m.syncAccountsPositions(rawMessageHandler)
	if err != nil {
		return err
	}

	err = m.syncProfileShowcasePreferences(context.Background(), rawMessageHandler)
	if err != nil {
		return err
	}

	return nil
}

func (m *Messenger) syncProfilePictures(rawMessageHandler RawMessageHandler, identityImages []*images.IdentityImage) error {
	if !m.hasPairedDevices() {
		return nil
	}

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	pictures := make([]*protobuf.SyncProfilePicture, len(identityImages))
	clock, chat := m.getLastClockWithRelatedChat()
	for i, image := range identityImages {
		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
		}
		pictures[i] = p
	}

	message := &protobuf.SyncProfilePictures{}
	message.KeyUid = m.account.KeyUID
	message.Pictures = pictures

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

	rawMessage := common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_SYNC_PROFILE_PICTURES,
		ResendType:  common.ResendTypeDataSync,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	chat.LastClockValue = clock
	return m.saveChat(chat)
}

func (m *Messenger) syncLatestContactRequests(ctx context.Context, rawMessageHandler RawMessageHandler) error {
	latestContactRequests, err := m.persistence.LatestContactRequests()

	if err != nil {
		return err
	}

	for _, r := range latestContactRequests {
		if r.ContactRequestState == common.ContactRequestStateAccepted || r.ContactRequestState == common.ContactRequestStateDismissed {
			accepted := r.ContactRequestState == common.ContactRequestStateAccepted
			err = m.syncContactRequestDecision(ctx, r.MessageID, r.ContactID, accepted, rawMessageHandler)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

func (m *Messenger) syncContactRequestDecision(ctx context.Context, requestID, contactId string, accepted bool, rawMessageHandler RawMessageHandler) error {
	m.logger.Info("syncContactRequestDecision", zap.Any("from", requestID))
	if !m.hasPairedDevices() {
		return nil
	}

	clock, chat := m.getLastClockWithRelatedChat()

	var status protobuf.SyncContactRequestDecision_DecisionStatus
	if accepted {
		status = protobuf.SyncContactRequestDecision_ACCEPTED
	} else {
		status = protobuf.SyncContactRequestDecision_DECLINED
	}

	message := &protobuf.SyncContactRequestDecision{
		RequestId:      requestID,
		ContactId:      contactId,
		Clock:          clock,
		DecisionStatus: status,
	}

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

	rawMessage := common.RawMessage{
		LocalChatID: chat.ID,
		Payload:     encodedMessage,
		MessageType: protobuf.ApplicationMetadataMessage_SYNC_CONTACT_REQUEST_DECISION,
		ResendType:  common.ResendTypeDataSync,
	}

	_, err = rawMessageHandler(ctx, rawMessage)
	if err != nil {
		return err
	}

	return nil
}

func (m *Messenger) getLastClockWithRelatedChat() (uint64, *Chat) {
	chatID := contactIDFromPublicKey(&m.identity.PublicKey)

	chat, ok := m.allChats.Load(chatID)
	if !ok {
		chat = OneToOneFromPublicKey(&m.identity.PublicKey, m.getTimesource())
		// We don't want to show the chat to the user
		chat.Active = false
	}

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

	return clock, chat
}

func (m *Messenger) syncProfilePicturesFromDatabase(rawMessageHandler RawMessageHandler) error {
	keyUID := m.account.KeyUID
	identityImages, err := m.multiAccounts.GetIdentityImages(keyUID)
	if err != nil {
		return err
	}
	return m.syncProfilePictures(rawMessageHandler, identityImages)
}

func (m *Messenger) InitInstallations() error {
	installations, err := m.encryptor.GetOurInstallations(&m.identity.PublicKey)
	if err != nil {
		return err
	}

	for _, installation := range installations {
		m.allInstallations.Store(installation.ID, installation)
	}

	err = m.setInstallationHostname()
	if err != nil {
		return err
	}

	if m.telemetryClient != nil {
		installation, ok := m.allInstallations.Load(m.installationID)
		if ok {
			m.telemetryClient.SetDeviceType(installation.InstallationMetadata.DeviceType)
		}
	}

	return nil
}

func (m *Messenger) Installations() []*multidevice.Installation {
	installations := make([]*multidevice.Installation, m.allInstallations.Len())

	var i = 0
	m.allInstallations.Range(func(installationID string, installation *multidevice.Installation) (shouldContinue bool) {
		installations[i] = installation
		i++
		return true
	})
	return installations
}

func (m *Messenger) setInstallationMetadata(id string, data *multidevice.InstallationMetadata) error {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return errors.New("no installation found")
	}

	installation.InstallationMetadata = data
	return m.encryptor.SetInstallationMetadata(m.IdentityPublicKey(), id, data)
}

func (m *Messenger) SetInstallationMetadata(id string, data *multidevice.InstallationMetadata) error {
	return m.setInstallationMetadata(id, data)
}

func (m *Messenger) SetInstallationName(id string, name string) error {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return errors.New("no installation found")
	}

	installation.InstallationMetadata.Name = name
	return m.encryptor.SetInstallationName(m.IdentityPublicKey(), id, name)
}

// EnableInstallation enables an installation and returns the installation
func (m *Messenger) EnableInstallation(id string) (*multidevice.Installation, error) {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return nil, errors.New("no installation found")
	}

	err := m.encryptor.EnableInstallation(&m.identity.PublicKey, id)
	if err != nil {
		return nil, err
	}
	installation.Enabled = true
	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allInstallations.Store(id, installation)
	return installation, nil
}

func (m *Messenger) DisableInstallation(id string) error {
	installation, ok := m.allInstallations.Load(id)
	if !ok {
		return errors.New("no installation found")
	}

	err := m.encryptor.DisableInstallation(&m.identity.PublicKey, id)
	if err != nil {
		return err
	}
	installation.Enabled = false
	// TODO(samyoul) remove storing of an updated reference pointer?
	m.allInstallations.Store(id, installation)
	return nil
}