//go:generate mockgen -destination mock_synctree/mock_synctree.go github.com/anyproto/any-sync/commonspace/object/tree/synctree SyncTree,HeadNotifiable,SyncClient,RequestFactory
package synctree

import (
	"context"
	"errors"
	"slices"
	"time"

	anystore "github.com/anyproto/any-store"
	"go.uber.org/zap"

	"github.com/anyproto/any-sync/app/logger"
	"github.com/anyproto/any-sync/commonspace/headsync/headstorage"
	"github.com/anyproto/any-sync/commonspace/object/acl/list"
	"github.com/anyproto/any-sync/commonspace/object/tree/objecttree"
	"github.com/anyproto/any-sync/commonspace/object/tree/synctree/updatelistener"
	"github.com/anyproto/any-sync/commonspace/object/tree/treestorage"
	"github.com/anyproto/any-sync/commonspace/spacestorage"
	"github.com/anyproto/any-sync/commonspace/sync/syncdeps"
	"github.com/anyproto/any-sync/commonspace/syncstatus"
	"github.com/anyproto/any-sync/net/peer"
	"github.com/anyproto/any-sync/nodeconf"
	"github.com/anyproto/any-sync/util/slice"
)

var (
	ErrSyncTreeClosed  = errors.New("sync tree is closed")
	ErrSyncTreeDeleted = errors.New("sync tree is deleted")
)

type HeadNotifiable interface {
	UpdateHeads(id string, heads []string)
}

type ListenerSetter interface {
	SetListener(listener updatelistener.UpdateListener)
}

type peerSendableObjectTree interface {
	objecttree.ObjectTree
	syncdeps.ObjectSyncHandler
	AddRawChangesFromPeer(ctx context.Context, peerId string, changesPayload objecttree.RawChangesPayload) (res objecttree.AddResult, err error)
}

type SyncTree interface {
	peerSendableObjectTree
	ListenerSetter
	SyncWithPeer(ctx context.Context, p peer.Peer) (err error)
}

// SyncTree sends head updates to sync service and also sends new changes to update listener
type syncTree struct {
	syncdeps.ObjectSyncHandler
	objecttree.ObjectTree
	syncClient     SyncClient
	syncStatus     syncstatus.StatusUpdater
	listener       updatelistener.UpdateListener
	statsCollector *TreeStatsCollector
	onClose        func(id string)
	isClosed       bool
	isDeleted      bool
	buildTime      time.Duration
}

var log = logger.NewNamed("common.commonspace.synctree")

type ResponsiblePeersGetter interface {
	GetResponsiblePeers(ctx context.Context) (peers []peer.Peer, err error)
}

type BuildDeps struct {
	SpaceId            string
	SyncClient         SyncClient
	Configuration      nodeconf.NodeConf
	HeadNotifiable     HeadNotifiable
	Listener           updatelistener.UpdateListener
	AclList            list.AclList
	SpaceStorage       spacestorage.SpaceStorage
	TreeStorage        objecttree.Storage
	OnClose            func(id string)
	SyncStatus         syncstatus.StatusUpdater
	PeerGetter         ResponsiblePeersGetter
	BuildObjectTree    objecttree.BuildObjectTreeFunc
	ValidateObjectTree objecttree.ValidatorFunc
	StatsCollector     *TreeStatsCollector
}

var newTreeGetter = func(deps BuildDeps, treeId string) treeGetter {
	return treeRemoteGetter{deps: deps, treeId: treeId}
}

func BuildSyncTreeOrGetRemote(ctx context.Context, id string, deps BuildDeps) (t SyncTree, err error) {
	var (
		remoteGetter = newTreeGetter(deps, id)
		peerId       string
	)
	deps.TreeStorage, peerId, err = remoteGetter.getTree(ctx)
	if err != nil {
		return
	}
	return buildSyncTree(ctx, peerId, deps)
}

func PutSyncTree(ctx context.Context, payload treestorage.TreeStorageCreatePayload, deps BuildDeps) (t SyncTree, err error) {
	err = checkTreeDeleted(ctx, payload.RootRawChange.Id, deps.SpaceStorage)
	if err != nil {
		return
	}
	deps.TreeStorage, err = deps.SpaceStorage.CreateTreeStorage(ctx, payload)
	if err != nil {
		return
	}
	return buildSyncTree(ctx, peer.CtxResponsiblePeers, deps)
}

func buildSyncTree(ctx context.Context, peerId string, deps BuildDeps) (t SyncTree, err error) {
	buildStart := time.Now()
	objTree, err := deps.BuildObjectTree(deps.TreeStorage, deps.AclList)
	if err != nil {
		return
	}
	syncClient := deps.SyncClient
	syncTree := &syncTree{
		ObjectTree:     objTree,
		syncClient:     syncClient,
		onClose:        deps.OnClose,
		listener:       deps.Listener,
		syncStatus:     deps.SyncStatus,
		statsCollector: deps.StatsCollector,
		buildTime:      time.Since(buildStart),
	}
	syncHandler := NewSyncHandler(syncTree, syncClient, deps.SpaceId)
	syncTree.ObjectSyncHandler = syncHandler
	t = syncTree
	syncTree.Lock()
	syncTree.afterBuild()
	syncTree.Unlock()

	if peerId != "" && !objecttree.IsEmptyTree(objTree) {
		headUpdate, err := syncTree.syncClient.CreateHeadUpdate(objTree, "", nil)
		if err != nil {
			return nil, err
		}
		log.Debug("broadcast after build", zap.String("treeId", objTree.Id()), zap.Strings("heads", objTree.Heads()))
		// send to everybody, because everybody should know that the node or client got new tree
		broadcastErr := syncTree.syncClient.Broadcast(ctx, headUpdate)
		if broadcastErr != nil {
			log.Warn("failed to broadcast head update", zap.Error(broadcastErr))
		}
		if peerId != peer.CtxResponsiblePeers {
			deps.SyncStatus.ObjectReceive(peerId, syncTree.Id(), syncTree.Heads())
		}
	}
	if syncTree.statsCollector != nil {
		syncTree.statsCollector.Register(syncTree)
	}
	return
}

func (s *syncTree) SetListener(listener updatelistener.UpdateListener) {
	// this should be called under lock
	s.listener = listener
}

func (s *syncTree) IterateFrom(id string, convert objecttree.ChangeConvertFunc, iterate objecttree.ChangeIterateFunc) (err error) {
	if err = s.checkAlive(); err != nil {
		return
	}
	return s.ObjectTree.IterateFrom(id, convert, iterate)
}

func (s *syncTree) IterateRoot(convert objecttree.ChangeConvertFunc, iterate objecttree.ChangeIterateFunc) (err error) {
	if err = s.checkAlive(); err != nil {
		return
	}
	return s.ObjectTree.IterateRoot(convert, iterate)
}

func (s *syncTree) AddContent(ctx context.Context, content objecttree.SignableChangeContent) (res objecttree.AddResult, err error) {
	return s.AddContentWithValidator(ctx, content, nil)
}

func (s *syncTree) AddContentWithValidator(ctx context.Context, content objecttree.SignableChangeContent, validate func(change objecttree.StorageChange) error) (res objecttree.AddResult, err error) {
	if err = s.checkAlive(); err != nil {
		return
	}
	res, err = s.ObjectTree.AddContentWithValidator(ctx, content, validate)
	if err != nil {
		return
	}
	s.syncStatus.HeadsChange(s.Id(), res.Heads)
	headUpdate, err := s.syncClient.CreateHeadUpdate(s, "", res.RawChanges())
	if err != nil {
		return
	}
	broadcastErr := s.syncClient.Broadcast(ctx, headUpdate)
	if broadcastErr != nil {
		log.Warn("failed to broadcast head update", zap.Error(broadcastErr))
	}
	return
}

func (s *syncTree) AddRawChangesFromPeer(ctx context.Context, peerId string, changesPayload objecttree.RawChangesPayload) (res objecttree.AddResult, err error) {
	if s.hasHeads(s, changesPayload.NewHeads) {
		s.syncStatus.HeadsApply(peerId, s.Id(), s.Heads(), true)
		log.Debug("heads already applied", zap.String("treeId", s.Id()), zap.Strings("heads", changesPayload.NewHeads))
		return objecttree.AddResult{
			OldHeads: changesPayload.NewHeads,
			Heads:    changesPayload.NewHeads,
			Mode:     objecttree.Nothing,
		}, nil
	}
	prevHeads := s.Heads()
	res, err = s.AddRawChanges(ctx, changesPayload)
	if err != nil || res.Mode == objecttree.Nothing {
		return
	}
	headUpdate, err := s.syncClient.CreateHeadUpdate(s, peerId, res.RawChanges())
	if err != nil {
		return res, err
	}
	broadcastErr := s.syncClient.Broadcast(ctx, headUpdate)
	if broadcastErr != nil {
		log.Warn("failed to broadcast head update", zap.Error(broadcastErr))
	}
	curHeads := s.Heads()
	allAdded := true
	for _, head := range changesPayload.NewHeads {
		if !slices.Contains(curHeads, head) {
			allAdded = false
			break
		}
	}
	if !slice.UnsortedEquals(prevHeads, curHeads) {
		s.syncStatus.HeadsApply(peerId, s.Id(), curHeads, allAdded)
	}
	return
}

func (s *syncTree) hasHeads(ot objecttree.ObjectTree, heads []string) bool {
	return slice.UnsortedEquals(ot.Heads(), heads) || ot.HasChanges(heads...)
}

func (s *syncTree) AddRawChanges(ctx context.Context, changesPayload objecttree.RawChangesPayload) (res objecttree.AddResult, err error) {
	if err = s.checkAlive(); err != nil {
		return
	}
	return s.ObjectTree.AddRawChangesWithUpdater(ctx, changesPayload, func(tree objecttree.ObjectTree, md objecttree.Mode) error {
		if s.listener != nil {
			switch md {
			case objecttree.Nothing:
				return nil
			case objecttree.Append:
				return s.listener.Update(s)
			case objecttree.Rebuild:
				return s.listener.Rebuild(s)
			}
		}
		return nil
	})
}

func (s *syncTree) Delete() (err error) {
	log.Debug("deleting sync tree", zap.String("id", s.Id()))
	defer func() {
		log.Debug("deleted sync tree", zap.Error(err), zap.String("id", s.Id()))
	}()
	s.Lock()
	defer s.Unlock()
	if err = s.checkAlive(); err != nil {
		return
	}
	err = s.ObjectTree.Delete()
	if err != nil {
		return
	}
	s.isDeleted = true
	return
}

func (s *syncTree) TryClose(objectTTL time.Duration) (bool, error) {
	if !s.TryLock() {
		return false, nil
	}
	log.Debug("closing sync tree", zap.String("id", s.Id()))
	return true, s.close()
}

func (s *syncTree) Close() (err error) {
	log.Debug("closing sync tree", zap.String("id", s.Id()))
	s.Lock()
	return s.close()
}

func (s *syncTree) close() (err error) {
	defer func() {
		if s.statsCollector != nil {
			s.statsCollector.Unregister(s)
		}
	}()
	defer s.Unlock()
	defer func() {
		log.Debug("closed sync tree", zap.Error(err), zap.String("id", s.Id()))
	}()
	if s.isClosed {
		err = ErrSyncTreeClosed
		return
	}
	s.onClose(s.Id())
	s.isClosed = true
	return
}

func (s *syncTree) checkAlive() (err error) {
	if s.isClosed {
		err = ErrSyncTreeClosed
	}
	if s.isDeleted {
		err = ErrSyncTreeDeleted
	}
	return
}

func (s *syncTree) SyncWithPeer(ctx context.Context, p peer.Peer) (err error) {
	s.Lock()
	defer s.Unlock()
	req, err := s.syncClient.CreateFullSyncRequest(p.Id(), s)
	if err != nil {
		return
	}
	return s.syncClient.QueueRequest(ctx, req)
}

func (s *syncTree) afterBuild() {
	if s.listener != nil {
		s.listener.Rebuild(s)
	}
}

func checkTreeDeleted(ctx context.Context, treeId string, spaceStorage spacestorage.SpaceStorage) error {
	status, err := spaceStorage.HeadStorage().GetEntry(ctx, treeId)
	if err != nil {
		if errors.Is(err, anystore.ErrDocNotFound) {
			return nil
		}
		return err
	}
	if status.DeletedStatus != headstorage.DeletedStatusNotDeleted {
		return spacestorage.ErrTreeStorageAlreadyDeleted
	}
	return nil
}
