package drand

import (
	"bytes"
	"context"
	"sync"
	"time"

	dchain "github.com/drand/drand/chain"
	dclient "github.com/drand/drand/client"
	gclient "github.com/drand/drand/cmd/relay-gossip/client"
	dlog "github.com/drand/drand/log"
	"github.com/drand/kyber"
	kzap "github.com/go-kit/kit/log/zap"
	"go.uber.org/zap/zapcore"
	"golang.org/x/xerrors"

	logging "github.com/ipfs/go-log"
	pubsub "github.com/libp2p/go-libp2p-pubsub"

	"github.com/filecoin-project/lotus/build"
	"github.com/filecoin-project/lotus/chain/beacon"
	"github.com/filecoin-project/lotus/chain/types"
	"github.com/filecoin-project/specs-actors/actors/abi"
)

var log = logging.Logger("drand")

var drandServers = []string{
	"https://pl-eu.testnet.drand.sh",
	"https://pl-us.testnet.drand.sh",
	"https://pl-sin.testnet.drand.sh",
}

var drandChain *dchain.Info

func init() {

	var err error
	drandChain, err = dchain.InfoFromJSON(bytes.NewReader([]byte(build.DrandChain)))
	if err != nil {
		panic("could not unmarshal chain info: " + err.Error())
	}
}

type drandPeer struct {
	addr string
	tls  bool
}

func (dp *drandPeer) Address() string {
	return dp.addr
}

func (dp *drandPeer) IsTLS() bool {
	return dp.tls
}

type DrandBeacon struct {
	client dclient.Client

	pubkey kyber.Point

	// seconds
	interval time.Duration

	drandGenTime uint64
	filGenTime   uint64
	filRoundTime uint64

	cacheLk    sync.Mutex
	localCache map[uint64]types.BeaconEntry
}

func NewDrandBeacon(genesisTs, interval uint64, ps *pubsub.PubSub) (*DrandBeacon, error) {
	if genesisTs == 0 {
		panic("what are you doing this cant be zero")
	}

	dlogger := dlog.NewKitLoggerFrom(kzap.NewZapSugarLogger(
		log.SugaredLogger.Desugar(), zapcore.InfoLevel))
	opts := []dclient.Option{
		dclient.WithChainInfo(drandChain),
		dclient.WithHTTPEndpoints(drandServers),
		dclient.WithCacheSize(1024),
		dclient.WithLogger(dlogger),
	}

	if ps != nil {
		opts = append(opts, gclient.WithPubsub(ps))
	} else {
		log.Info("drand beacon without pubsub")
	}

	client, err := dclient.New(opts...)
	if err != nil {
		return nil, xerrors.Errorf("creating drand client")
	}

	go func() {
		// Explicitly Watch until that is fixed in drand
		ch := client.Watch(context.Background())
		for range ch {
		}
		log.Error("dranch Watch bork")
	}()

	db := &DrandBeacon{
		client:     client,
		localCache: make(map[uint64]types.BeaconEntry),
	}

	db.pubkey = drandChain.PublicKey
	db.interval = drandChain.Period
	db.drandGenTime = uint64(drandChain.GenesisTime)
	db.filRoundTime = interval
	db.filGenTime = genesisTs

	return db, nil
}

func (db *DrandBeacon) Entry(ctx context.Context, round uint64) <-chan beacon.Response {
	out := make(chan beacon.Response, 1)
	if round != 0 {
		be := db.getCachedValue(round)
		if be != nil {
			out <- beacon.Response{Entry: *be}
			close(out)
			return out
		}
	}

	go func() {
		start := time.Now()
		log.Infow("start fetching randomness", "round", round)
		resp, err := db.client.Get(ctx, round)

		var br beacon.Response
		if err != nil {
			br.Err = xerrors.Errorf("drand failed Get request: %w", err)
		} else {
			br.Entry.Round = resp.Round()
			br.Entry.Data = resp.Signature()
		}
		log.Infow("done fetching randomness", "round", round, "took", time.Since(start))
		out <- br
		close(out)
	}()

	return out
}
func (db *DrandBeacon) cacheValue(e types.BeaconEntry) {
	db.cacheLk.Lock()
	defer db.cacheLk.Unlock()
	db.localCache[e.Round] = e
}

func (db *DrandBeacon) getCachedValue(round uint64) *types.BeaconEntry {
	db.cacheLk.Lock()
	defer db.cacheLk.Unlock()
	v, ok := db.localCache[round]
	if !ok {
		return nil
	}
	return &v
}

func (db *DrandBeacon) VerifyEntry(curr types.BeaconEntry, prev types.BeaconEntry) error {
	if prev.Round == 0 {
		// TODO handle genesis better
		return nil
	}
	b := &dchain.Beacon{
		PreviousSig: prev.Data,
		Round:       curr.Round,
		Signature:   curr.Data,
	}
	err := dchain.VerifyBeacon(db.pubkey, b)
	if err == nil {
		db.cacheValue(curr)
	}
	return err
}

func (db *DrandBeacon) MaxBeaconRoundForEpoch(filEpoch abi.ChainEpoch, prevEntry types.BeaconEntry) uint64 {
	// TODO: sometimes the genesis time for filecoin is zero and this goes negative
	latestTs := ((uint64(filEpoch) * db.filRoundTime) + db.filGenTime) - db.filRoundTime
	dround := (latestTs - db.drandGenTime) / uint64(db.interval.Seconds())
	return dround
}

var _ beacon.RandomBeacon = (*DrandBeacon)(nil)