package polling

import (
	"runtime/debug"
	"sync"
	"time"

	"github.com/matrix-org/go-neb/clients"
	"github.com/matrix-org/go-neb/database"
	"github.com/matrix-org/go-neb/types"
	log "github.com/sirupsen/logrus"
)

// Remember when we first started polling on this service ID. Polling routines will
// continually check this time. If the service gets updated, this will change, prompting
// older instances to die away. If this service gets removed, the time will be 0.
var (
	pollMutex     sync.Mutex
	startPollTime = make(map[string]int64) // ServiceID => unix timestamp
)
var clientPool *clients.Clients

// SetClients sets a pool of clients for passing into OnPoll
func SetClients(clis *clients.Clients) {
	clientPool = clis
}

// Start polling already existing services
func Start() error {
	// Work out which service types require polling
	for _, serviceType := range types.PollingServiceTypes() {
		// Query for all services with said service type
		srvs, err := database.GetServiceDB().LoadServicesByType(serviceType)
		if err != nil {
			return err
		}
		for _, s := range srvs {
			if err := StartPolling(s); err != nil {
				return err
			}
		}
	}
	return nil
}

// StartPolling begins a polling loop for this service.
// If one already exists for this service, it will be instructed to die. The new poll will not wait for this to happen,
// so there may be a brief period of overlap. It is safe to immediately call `StopPolling(service)` to immediately terminate
// this poll.
func StartPolling(service types.Service) error {
	// Set the poll time BEFORE spinning off the goroutine in case the caller immediately stops us. If we don't do this here,
	// we risk them setting the ts to 0 BEFORE we've set the start time, resulting in a poll when one was not intended.
	ts := time.Now().UnixNano()
	setPollStartTime(service, ts)
	go pollLoop(service, ts)
	return nil
}

// StopPolling stops all pollers for this service.
func StopPolling(service types.Service) {
	log.WithFields(log.Fields{
		"service_id":   service.ServiceID(),
		"service_type": service.ServiceType(),
	}).Info("StopPolling")
	setPollStartTime(service, 0)
}

// pollLoop begins the polling loop for this service. Does not return, so call this
// as a goroutine!
func pollLoop(service types.Service, ts int64) {
	logger := log.WithFields(log.Fields{
		"timestamp":    ts,
		"service_id":   service.ServiceID(),
		"service_type": service.ServiceType(),
	})

	defer func() {
		// Kill the poll loop entirely as it is likely that whatever made us panic will
		// make us panic again. We can whine bitterly about it though.
		if r := recover(); r != nil {
			logger.WithField("panic", r).Errorf(
				"pollLoop panicked!\n%s", debug.Stack(),
			)
		}
	}()

	poller, ok := service.(types.Poller)
	if !ok {
		logger.Error("Service is not a Poller.")
		return
	}
	logger.Info("Starting polling loop")
	cli, err := clientPool.Client(service.ServiceUserID())
	if err != nil {
		logger.WithError(err).WithField("user_id", service.ServiceUserID()).Error("Poll setup failed: failed to load client")
		return
	}
	for {
		logger.Info("OnPoll")
		nextTime := poller.OnPoll(cli)
		if pollTimeChanged(service, ts) {
			logger.Info("Terminating poll.")
			break
		}
		// work out how long to sleep
		if nextTime.Unix() == 0 {
			logger.Info("Terminating poll - OnPoll returned 0")
			break
		}
		now := time.Now()
		time.Sleep(nextTime.Sub(now))

		if pollTimeChanged(service, ts) {
			logger.Info("Terminating poll.")
			break
		}
	}
}

// setPollStartTime clobbers the current poll time
func setPollStartTime(service types.Service, startTS int64) {
	pollMutex.Lock()
	defer pollMutex.Unlock()
	startPollTime[service.ServiceID()] = startTS
}

// pollTimeChanged returns true if the poll start time for this service ID is different to the one supplied.
func pollTimeChanged(service types.Service, ts int64) bool {
	pollMutex.Lock()
	defer pollMutex.Unlock()
	return startPollTime[service.ServiceID()] != ts
}