package callee

import (
	"context"
	"errors"
	"time"

	"code.justin.tv/sse/malachai/pkg/capabilities"
	"code.justin.tv/sse/malachai/pkg/internal/closer"
	"code.justin.tv/sse/malachai/pkg/log"
)

// ErrCapabilitiesCacheExpired is returned when the local capabilities cache
// is expired and the events client hasn't retrieved an update to refresh the
// cache.
var ErrCapabilitiesCacheExpired = errors.New("local capabilities cache has expired")

const (
	defaultCapabilitiesTTL = 24 * time.Hour
)

type calleeCapabilitiesCache struct {
	Cache         map[string]*capabilities.Capabilities
	LastRetrieved time.Time
}

type calleeCapabilitiesClient struct {
	cache         *calleeCapabilitiesCache
	capabilities  capabilities.Manager
	invalidations chan struct{}
	log           log.S2SLogger

	cfg *calleeCapabilitiesConfig

	closer closer.API
}

func newCalleeCapabilitiesClient(cfg *Config, logger log.S2SLogger, invalidations chan struct{}, closer closer.API) (capClient *calleeCapabilitiesClient, err error) {
	capabilities, err := capabilities.New(cfg.CapabilitiesConfig)
	if err != nil {
		return
	}

	capClient = &calleeCapabilitiesClient{
		capabilities:  capabilities,
		invalidations: invalidations,
		log:           logger,
		cfg:           cfg.calleeCapConfig,
		closer:        closer,
	}
	return
}

type calleeCapabilitiesConfig struct {
	callee          string
	ttl             time.Duration
	capabilitiesTTL time.Duration
	roleArn         string
}

// FillDefaults fills default values
func (cfg *calleeCapabilitiesConfig) FillDefaults() {
	if cfg.ttl == 0 {
		cfg.ttl = 1 * time.Hour
	}

	if cfg.capabilitiesTTL == 0 {
		cfg.capabilitiesTTL = defaultCapabilitiesTTL
	}
}

func (ccc *calleeCapabilitiesClient) get(caller string) (c *capabilities.Capabilities, err error) {
	if time.Now().UTC().Sub(ccc.cache.LastRetrieved) > ccc.cfg.capabilitiesTTL {
		err = ErrCapabilitiesCacheExpired
		return
	}

	c, ok := ccc.cache.Cache[caller]
	if !ok {
		err = capabilities.ErrCapabilitiesNotFound
	}
	return
}

func (ccc *calleeCapabilitiesClient) updateCache() (err error) {
	ctx, cancel := context.WithCancel(ccc.closer)
	defer cancel()
	caps, err := ccc.capabilities.GetForCallee(ctx, ccc.cfg.callee)
	if err != nil {
		return
	}

	ccc.cache = &calleeCapabilitiesCache{
		Cache:         make(map[string]*capabilities.Capabilities),
		LastRetrieved: time.Now().UTC(),
	}
	for _, c := range caps {
		ccc.cache.Cache[c.Caller] = c
	}
	return
}

func (ccc *calleeCapabilitiesClient) start() {
	timer := time.NewTimer(ccc.cfg.ttl)
	for {
		err := ccc.updateCache()
		if err != nil {
			ccc.log.Errorf("error while updating capability cache: %s", err.Error())
		}

		select {
		case <-ccc.closer.Done():
			ccc.log.Debug("exiting due to closed client")
			return
		case <-ccc.invalidations:
			ccc.log.Debug("updating cache due to invalidation")
		case <-timer.C:
			timer.Reset(ccc.cfg.ttl)
			ccc.log.Debug("updating cache due to invalidation timeout")
		}
	}
}
