package node

import (
	"context"
	"errors"
	"time"

	"github.com/ethereum/go-ethereum/eth/downloader"
	"github.com/ethereum/go-ethereum/les"
	"github.com/status-im/status-go/geth/log"
)

// errors
var (
	ErrStartAborted = errors.New("node synchronization timeout before starting")
	ErrSyncAborted  = errors.New("node synchronization timeout before completion")
)

// SyncPoll provides a structure that allows us to check the status of
// ethereum node synchronization.
type SyncPoll struct {
	downloader *downloader.Downloader
}

// NewSyncPoll returns a new instance of SyncPoll.
func NewSyncPoll(leth *les.LightEthereum) *SyncPoll {
	return &SyncPoll{
		downloader: leth.Downloader(),
	}
}

// Poll checks for the status of blockchain synchronization and returns an error
// if the blockchain failed to start synchronizing or fails to complete, within
// the time provided by the passed in context.
func (n *SyncPoll) Poll(ctx context.Context) error {
	if err := n.pollSyncStart(ctx); err != nil {
		return err
	}

	if err := n.waitSyncCompleted(ctx); err != nil {
		return err
	}

	return nil
}

func (n *SyncPoll) pollSyncStart(ctx context.Context) error {
	for {
		select {
		case <-ctx.Done():
			return ErrStartAborted
		case <-time.After(100 * time.Millisecond):
			if n.downloader.Synchronising() {
				log.Info("Block synchronization progress just started")
				return nil
			}
		}
	}
}

func (n *SyncPoll) waitSyncCompleted(ctx context.Context) error {
	for {
		select {
		case <-ctx.Done():
			return ErrSyncAborted
		case <-time.After(100 * time.Millisecond):
			progress := n.downloader.Progress()

			// If information on highest block has not being retrieved then wait.
			if progress.HighestBlock <= 0 && progress.StartingBlock <= 0 {
				time.Sleep(300 * time.Millisecond)
				continue
			}

			if progress.CurrentBlock >= progress.HighestBlock {
				log.Info("Block synchronization just finished")
				return nil
			}
		}
	}
}