// Copyright 2020 The OPA Authors.  All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.

package file

import (
	"context"
	"os"
	"sync"
	"time"

	"github.com/open-policy-agent/opa/internal/wasm/sdk/opa/errors"
	"github.com/open-policy-agent/opa/v1/bundle"
	"github.com/open-policy-agent/opa/v1/util"

	"github.com/open-policy-agent/opa/internal/wasm/sdk/opa"
)

const (
	// DefaultInterval for re-loading the bundle file.
	DefaultInterval = time.Minute
)

var errNotReady = errors.New(errors.NotReadyErr, "")

// Loader loads a bundle from a file. If started, it loads the bundle
// periodically until closed.
type Loader struct {
	configErr   error // Delayed configuration error, if any.
	initialized bool
	pd          policyData
	filename    string
	interval    time.Duration
	closing     chan struct{} // Signal the request to stop the poller.
	closed      chan struct{} // Signals the successful stopping of the poller.
	logError    func(error)
	mutex       sync.Mutex
}

// policyData captures the functions used in setting the policy and data.
type policyData interface {
	SetPolicyData(ctx context.Context, policy []byte, data *any) error
}

// New constructs a new file loader periodically reloading the bundle
// from a file.
func New(opa *opa.OPA) *Loader {
	return newLoader(opa)
}

// newLoader constructs a newLoader file loader. This is for tests.
func newLoader(pd policyData) *Loader {
	return &Loader{
		pd:       pd,
		interval: DefaultInterval,
		logError: func(error) {},
	}
}

// Init initializes the loader after its construction and
// configuration. If invalid config, will return ErrInvalidConfig.
func (l *Loader) Init() (*Loader, error) {
	if l.configErr != nil {
		return nil, l.configErr
	}

	if l.filename == "" {
		return nil, errors.New(errors.InvalidConfigErr, "missing filename")
	}

	l.initialized = true
	return l, nil
}

// Start starts the periodic loading byt calling Load, failing if the
// bundle loading fails.
func (l *Loader) Start(ctx context.Context) error {
	if !l.initialized {
		return errNotReady
	}

	if err := l.Load(ctx); err != nil {
		return err
	}

	l.closing = make(chan struct{})
	l.closed = make(chan struct{})

	go l.poller()

	return nil
}

// Close stops the loading, releasing all resources.
func (l *Loader) Close() {
	if !l.initialized {
		return
	}

	if l.closing == nil {
		return
	}

	close(l.closing)
	<-l.closed

	l.closing = nil
	l.closed = nil
}

// Load loads the bundle from a file and installs it. The possible
// returned errors are ErrInvalidBundle (in case of an error in
// loading or opening the bundle) and the ones SetPolicyData of OPA
// returns.
func (l *Loader) Load(ctx context.Context) error {
	if !l.initialized {
		return errNotReady
	}

	l.mutex.Lock()
	defer l.mutex.Unlock()

	f, err := os.Open(l.filename)
	if err != nil {
		return errors.New(errors.InvalidBundleErr, err.Error())
	}

	defer f.Close()

	// TODO: Cut the dependency to the OPA bundle package.

	b, err := bundle.NewReader(f).Read()
	if err != nil {
		return errors.New(errors.InvalidBundleErr, err.Error())
	}

	if len(b.WasmModules) == 0 {
		return errors.New(errors.InvalidBundleErr, "missing wasm")
	}

	var data *any
	if b.Data != nil {
		var v any = b.Data
		data = &v
	}

	return l.pd.SetPolicyData(ctx, b.WasmModules[0].Raw, data)
}

// poller periodically downloads the bundle.
func (l *Loader) poller() {
	defer close(l.closed)

	for {
		if err := l.Load(context.Background()); err != nil {
			l.logError(err)
		}

		timer, timerCancel := util.TimerWithCancel(l.interval)
		select {
		case <-timer.C:
		case <-l.closing:
			timerCancel() // explicitly cancel the timer.
			return
		}
	}
}
