package mockrouting

import (
	"context"
	"math/rand"
	"sync"
	"time"

	"github.com/ipfs/boxo/routing/offline"
	"github.com/ipfs/go-cid"
	ds "github.com/ipfs/go-datastore"
	dssync "github.com/ipfs/go-datastore/sync"
	tnet "github.com/libp2p/go-libp2p-testing/net"
	"github.com/libp2p/go-libp2p/core/peer"
)

// server is the mockrouting.Client's private interface to the routing server
type server interface {
	Announce(peer.AddrInfo, cid.Cid) error
	Providers(cid.Cid) []peer.AddrInfo

	Server
}

// s is an implementation of the private server interface
type s struct {
	delayConf DelayConfig

	lock      sync.RWMutex
	providers map[string]map[peer.ID]providerRecord
}

type providerRecord struct {
	Peer    peer.AddrInfo
	Created time.Time
}

func (rs *s) Announce(p peer.AddrInfo, c cid.Cid) error {
	rs.lock.Lock()
	defer rs.lock.Unlock()

	k := c.Hash().String()

	_, ok := rs.providers[k]
	if !ok {
		rs.providers[k] = make(map[peer.ID]providerRecord)
	}
	rs.providers[k][p.ID] = providerRecord{
		Created: time.Now(),
		Peer:    p,
	}
	return nil
}

func (rs *s) Providers(c cid.Cid) []peer.AddrInfo {
	rs.delayConf.Query.Wait() // before locking
	rs.lock.RLock()
	defer rs.lock.RUnlock()
	k := c.Hash().String()

	var ret []peer.AddrInfo
	records, ok := rs.providers[k]
	if !ok {
		return ret
	}

	for _, r := range records {
		if time.Since(r.Created) > rs.delayConf.ValueVisibility.Get() {
			ret = append(ret, r.Peer)
		}
	}

	for i := range ret {
		j := rand.Intn(i + 1)
		ret[i], ret[j] = ret[j], ret[i]
	}
	return ret
}

func (rs *s) Client(p tnet.Identity) Client {
	return rs.ClientWithDatastore(context.Background(), p, dssync.MutexWrap(ds.NewMapDatastore()))
}

func (rs *s) ClientWithDatastore(_ context.Context, p tnet.Identity, datastore ds.Datastore) Client {
	return &client{
		peer:   p,
		vs:     offline.NewOfflineRouter(datastore, MockValidator{}),
		server: rs,
	}
}
