// Copyright 2024 ChainSafe Systems (ON)
// SPDX-License-Identifier: LGPL-3.0-only

package sync

import (
	"errors"
	"fmt"
	"sync"
	"time"

	"github.com/ChainSafe/gossamer/config"
	"github.com/ChainSafe/gossamer/dot/network"
	"github.com/ChainSafe/gossamer/dot/peerset"
	"github.com/ChainSafe/gossamer/dot/state"
	"github.com/ChainSafe/gossamer/dot/types"
	"github.com/ChainSafe/gossamer/internal/log"
	"github.com/ChainSafe/gossamer/lib/common"
	"github.com/ChainSafe/gossamer/lib/grandpa/warpsync"
	lrucache "github.com/ChainSafe/gossamer/lib/utils/lru-cache"
	"github.com/libp2p/go-libp2p/core/peer"
	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promauto"
)

const (
	waitPeersDefaultTimeout = 10 * time.Second
	minPeersDefault         = 1
	blockRequestTimeout     = 20 * time.Second
)

var (
	isSyncedGauge = promauto.NewGauge(prometheus.GaugeOpts{
		Namespace: "gossamer_network_syncer",
		Name:      "is_synced",
		Help:      "bool representing whether the node is synced to the head of the chain",
	})

	logger = log.NewFromGlobal(log.AddContext("pkg", "sync"))
)

type BlockOrigin byte

const (
	networkInitialSync BlockOrigin = iota
	networkBroadcast
)

type EpochState interface {
	SetEpochDataRaw(epoch uint64, raw *types.EpochDataRaw) error
	StoreCurrentEpoch(epoch uint64) error
	StoreConfigData(epoch uint64, info *types.ConfigData) error
}

type GrandpaState interface {
	GetCurrentSetID() (uint64, error)
	GetAuthorities(uint64) ([]types.GrandpaVoter, error)
	SetAuthorities(setID uint64, authorities []types.GrandpaVoter) error
	GetAuthoritiesChangesFromBlock(uint) ([]uint, error)
}

type Network interface {
	AllConnectedPeersIDs() []peer.ID
	ReportPeer(change peerset.ReputationChange, p peer.ID)
	BlockAnnounceHandshake(*types.Header) error
	GetRequestResponseProtocol(subprotocol string, requestTimeout time.Duration,
		maxResponseSize uint64) *network.RequestResponseProtocol
	GossipMessageExcluding(network.NotificationsMessage, peer.ID)
}

type Change struct {
	who peer.ID
	rep peerset.ReputationChange
}

type Strategy interface {
	OnBlockAnnounce(from peer.ID, msg *network.BlockAnnounceMessage) (repChange *Change, err error)
	OnBlockAnnounceHandshake(from peer.ID, msg *network.BlockAnnounceHandshake) error
	NextActions() ([]*SyncTask, error)
	Process(results []*SyncTaskResult) (done bool, repChanges []Change, blocks []peer.ID, err error)
	ShowStatus()
	IsSynced() bool
	Result() any
}

type SyncService struct {
	mu                 sync.Mutex
	wg                 sync.WaitGroup
	network            Network
	blockState         state.BlockState
	grandpaState       GrandpaState
	epochState         EpochState
	storageState       StorageState
	transactionState   TransactionState
	finalityGadget     FinalityGadget
	babeVerifier       BabeVerifier
	blockImportHandler BlockImportHandler
	telemetry          Telemetry
	badBlocks          []string
	peers              *peerViewSet

	syncStrategy    config.SyncMode
	currentStrategy Strategy

	workerPool        *syncWorkerPool
	waitPeersDuration time.Duration
	minPeers          int
	slotDuration      time.Duration

	seenBlockSyncRequests *lrucache.LRUCache[common.Hash, uint]

	stopCh chan struct{}
}

func NewSyncService(logLvl log.Level, cfgs ...ServiceConfig) *SyncService {
	logger.Patch(log.SetLevel(logLvl))

	svc := &SyncService{
		minPeers:              minPeersDefault,
		waitPeersDuration:     waitPeersDefaultTimeout,
		stopCh:                make(chan struct{}),
		seenBlockSyncRequests: lrucache.NewLRUCache[common.Hash, uint](100),
		peers:                 NewPeerViewSet(),
	}

	for _, cfg := range cfgs {
		cfg(svc)
	}

	switch svc.syncStrategy {
	case config.FullSync:
		svc.useFullSyncStrategy()
	case config.WarpSync:
		svc.useWarpSyncStrategy()
	}

	return svc
}

func (s *SyncService) useWarpSyncStrategy() {
	warpSyncProvider := warpsync.NewWarpSyncProofProvider(s.blockState, s.grandpaState)

	warpSyncCfg := &WarpSyncConfig{
		Telemetry:        s.telemetry,
		BadBlocks:        s.badBlocks,
		WarpSyncProvider: warpSyncProvider,
		RequestMaker: s.network.GetRequestResponseProtocol(network.WarpSyncID,
			blockRequestTimeout, network.MaxBlockResponseSize),
		BlockState: s.blockState,
		Peers:      s.peers,
	}

	s.currentStrategy = NewWarpSyncStrategy(warpSyncCfg)
	s.syncStrategy = config.WarpSync
}

func (s *SyncService) useFullSyncStrategy() {
	syncCfg := &FullSyncConfig{
		BlockState:         s.blockState,
		StorageState:       s.storageState,
		TransactionState:   s.transactionState,
		FinalityGadget:     s.finalityGadget,
		BabeVerifier:       s.babeVerifier,
		BlockImportHandler: s.blockImportHandler,
		Telemetry:          s.telemetry,
		BadBlocks:          s.badBlocks,
		RequestMaker: s.network.GetRequestResponseProtocol(network.SyncID,
			blockRequestTimeout, network.MaxBlockResponseSize),
		Peers: s.peers,
	}

	s.currentStrategy = NewFullSyncStrategy(syncCfg)
	s.syncStrategy = config.FullSync
}

func (s *SyncService) waitWorkers() {
	bestBlockHeader, err := s.blockState.BestBlockHeader()
	if err != nil {
		panic(fmt.Sprintf("failed to get highest finalised header: %v", err))
	}

	for {
		total := s.workerPool.totalWorkers()
		if total >= s.minPeers {
			return
		}

		err = s.network.BlockAnnounceHandshake(bestBlockHeader)
		if err != nil && !errors.Is(err, network.ErrNoPeersConnected) {
			logger.Criticalf("waiting workers: %s", err.Error())
			break
		}

		waitPeersTimer := time.NewTimer(s.waitPeersDuration)
		select {
		case <-waitPeersTimer.C:
			waitPeersTimer.Reset(s.waitPeersDuration)

		case <-s.stopCh:
			return
		}
	}
}

func (s *SyncService) Start() error {
	s.wg.Add(1)
	go s.runSyncEngine()
	return nil
}

func (s *SyncService) Stop() error {
	close(s.stopCh)
	s.wg.Wait()
	return nil
}

func (s *SyncService) HandleBlockAnnounceHandshake(from peer.ID, msg *network.BlockAnnounceHandshake) error {
	logger.Debugf("receiving a block announce handshake from %s", from.String())
	if err := s.workerPool.fromBlockAnnounceHandshake(from); err != nil {
		return err
	}

	s.mu.Lock()
	defer s.mu.Unlock()

	return s.currentStrategy.OnBlockAnnounceHandshake(from, msg)
}

func (s *SyncService) HandleBlockAnnounce(from peer.ID, msg *network.BlockAnnounceMessage) error {
	s.mu.Lock()
	defer s.mu.Unlock()

	repChange, err := s.currentStrategy.OnBlockAnnounce(from, msg)

	if repChange != nil {
		s.network.ReportPeer(repChange.rep, repChange.who)
	}

	if err != nil {
		return fmt.Errorf("while handling block announce: %w", err)
	}
	return nil
}

func (s *SyncService) OnConnectionClosed(who peer.ID) {
	logger.Tracef("removing peer worker: %s", who.String())
	s.workerPool.removeWorker(who)
}

func (s *SyncService) IsSynced() bool {
	s.mu.Lock()
	defer s.mu.Unlock()

	return s.currentStrategy.IsSynced()
}

func (s *SyncService) HighestBlock() uint {
	highestBlock, err := s.blockState.BestBlockNumber()
	if err != nil {
		logger.Warnf("failed to get the highest block: %s", err)
		return 0
	}
	return highestBlock
}

func (s *SyncService) runSyncEngine() {
	defer s.wg.Done()
	s.waitWorkers()

	logger.Infof("starting sync engine with strategy: %s", s.syncStrategy)

	for {
		select {
		case <-s.stopCh:
			return
		case <-time.After(s.slotDuration):
		}

		s.runStrategy()

		if s.IsSynced() {
			isSyncedGauge.Set(1)
		} else {
			isSyncedGauge.Set(0)
		}
	}
}

func (s *SyncService) runStrategy() {
	s.mu.Lock()
	defer s.mu.Unlock()

	logger.Tracef("running strategy: %s", s.syncStrategy)

	tasks, err := s.currentStrategy.NextActions()
	if err != nil {
		logger.Criticalf("current sync strategy next actions failed with: %s", err.Error())
		return
	}

	logger.Tracef("amount of tasks to process: %d", len(tasks))
	if len(tasks) == 0 {
		return
	}

	results := s.workerPool.submitRequests(tasks)
	done, repChanges, peersToIgnore, err := s.currentStrategy.Process(results)

	if err != nil {
		logger.Criticalf("current sync strategy failed with: %s", err.Error())
		return
	}

	for _, change := range repChanges {
		s.network.ReportPeer(change.rep, change.who)
	}

	for _, block := range peersToIgnore {
		s.workerPool.ignorePeerAsWorker(block)
	}

	s.currentStrategy.ShowStatus()

	// TODO: why not use s.currentStrategy.IsSynced()?
	if done {
		switch s.syncStrategy {
		case config.WarpSync:
			logger.Info("Switching sync strategy: warp sync -> state sync")
			// Switch to state sync when warp sync finishes
			stateSyncCfg := &StateSyncStrategyConfig{
				Telemetry:  s.telemetry,
				BadBlocks:  s.badBlocks,
				BlockState: s.blockState,
				Peers:      s.peers,
				ReqMaker: s.network.GetRequestResponseProtocol(network.StateSyncID,
					blockRequestTimeout, network.MaxBlockResponseSize),
				BlockReqMaker: s.network.GetRequestResponseProtocol(network.SyncID,
					blockRequestTimeout, network.MaxBlockResponseSize),
				StateStorage:       s.storageState,
				GrandpaState:       s.grandpaState,
				EpochState:         s.epochState,
				FinalityGadget:     s.finalityGadget,
				TransactionState:   s.transactionState,
				BlockImportHandler: s.blockImportHandler,
				WarpSyncResult:     s.currentStrategy.Result().(warpsync.WarpSyncVerificationResult),
			}

			s.currentStrategy = NewStateSyncStrategy(stateSyncCfg)
			s.syncStrategy = config.StateSync

		case config.StateSync:
			logger.Info("Switching sync strategy: state sync -> full sync")
			// Switch to full sync when state sync finishes
			s.useFullSyncStrategy()
		case config.FullSync:
			logger.Errorf("Full sync strategy should not finish")
		}
	}
}
