/*
Copyright (c) Edgeless Systems GmbH

SPDX-License-Identifier: AGPL-3.0-only
*/

// Package poller implements a poller that can be used to wait for a condition to be met.
// The poller is designed to be a replacement for the azure-sdk-for-go poller
// with exponential backoff and an injectable clock.
// reference: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore@v1.1.1/runtime#Poller .
package poller

import (
	"context"
	"errors"
	"time"

	"k8s.io/utils/clock"
)

// PollUntilDoneOptions provides options for the Poller.
// Used to specify backoff and clock options.
type PollUntilDoneOptions struct {
	StartingBackoff time.Duration
	MaxBackoff      time.Duration
	clock.Clock
}

// NewPollUntilDoneOptions creates a new PollUntilDoneOptions with the default values and a real clock.
func NewPollUntilDoneOptions() *PollUntilDoneOptions {
	return &PollUntilDoneOptions{
		Clock: clock.RealClock{},
	}
}

// Poller is a poller that can be used to wait for a condition to be met.
// The poller is designed to be a replacement for the azure-sdk-for-go poller
// with exponential backoff and an injectable clock.
// reference: https://pkg.go.dev/github.com/Azure/azure-sdk-for-go/sdk/azcore@v1.1.1/runtime#Poller .
type Poller[T any] struct {
	handler PollingHandler[T]
	err     error
	result  *T
	done    bool
}

// New creates a new Poller.
func New[T any](handler PollingHandler[T]) *Poller[T] {
	return &Poller[T]{
		handler: handler,
		result:  new(T),
	}
}

// PollUntilDone polls the handler until the condition is met or the context is cancelled.
func (p *Poller[T]) PollUntilDone(ctx context.Context, options *PollUntilDoneOptions) (T, error) {
	if options == nil {
		options = NewPollUntilDoneOptions()
	}
	if options.MaxBackoff == 0 {
		options.MaxBackoff = time.Minute
	}
	if options.StartingBackoff < time.Second {
		options.StartingBackoff = time.Second
	}
	backoff := options.StartingBackoff
	for {
		timer := options.Clock.NewTimer(backoff)
		err := p.Poll(ctx)
		if err != nil {
			return *new(T), err
		}
		if p.Done() {
			return p.Result(ctx)
		}
		select {
		case <-ctx.Done():
			return *new(T), ctx.Err()
		case <-timer.C():
		}
		if backoff >= options.MaxBackoff/2 {
			backoff = options.MaxBackoff
		} else {
			backoff *= 2
		}
	}
}

// Poll polls the handler.
func (p *Poller[T]) Poll(ctx context.Context) error {
	return p.handler.Poll(ctx)
}

// Done returns true if the condition is met.
func (p *Poller[T]) Done() bool {
	return p.handler.Done()
}

// Result returns the result of the poller if the condition is met.
// If the condition is not met, an error is returned.
func (p *Poller[T]) Result(ctx context.Context) (T, error) {
	if !p.Done() {
		return *new(T), errors.New("poller is in a non-terminal state")
	}
	if p.done {
		// the result has already been retrieved, return the cached value
		if p.err != nil {
			return *new(T), p.err
		}
		return *p.result, nil
	}
	err := p.handler.Result(ctx, p.result)
	p.done = true
	if err != nil {
		p.err = err
		return *new(T), p.err
	}

	return *p.result, nil
}

// PollingHandler is a handler that can be used to poll for a condition to be met.
type PollingHandler[T any] interface {
	Done() bool
	Poll(ctx context.Context) error
	Result(ctx context.Context, out *T) error
}
