package actions

import (
	"errors"
	"fmt"
	"sync/atomic"
	"time"
)

type AsyncAction interface {
	Name() string
	Start(args ActionArgs) error
	CheckFinish(args ActionArgs) (bool, error)
}

// ActionGuard protects the write performed by an action Start call. Polling
// remains outside the guard so long-running async actions do not hold locks.
type ActionGuard func(args ActionArgs, write func() error) error

func CreateActionFromAsync(asynAction AsyncAction, waitInterval, timeout time.Duration) Action {
	return &AsyncWrapAction{
		asynAction:   asynAction,
		waitInterval: waitInterval,
		timeout:      timeout,
	}
}

func CreateGuardedActionFromAsync(asynAction AsyncAction, waitInterval, timeout time.Duration, guard ActionGuard) Action {
	return CreateActionFromAsync(&guardedAsyncAction{AsyncAction: asynAction, guard: guard}, waitInterval, timeout)
}

type guardedAsyncAction struct {
	AsyncAction
	guard ActionGuard
}

func (a *guardedAsyncAction) Start(args ActionArgs) error {
	return a.guard(args, func() error { return a.AsyncAction.Start(args) })
}

func TryAbortAction(action Action) bool {
	if abortable, ok := action.(interface{ TryAbort() bool }); ok {
		return abortable.TryAbort()
	}
	return false
}

type AsyncTimeoutError struct {
	ActionName string
}

func (e *AsyncTimeoutError) Error() string {
	return fmt.Sprintf("async action %q timeout", e.ActionName)
}

func IsAsyncTimeoutError(err error) bool {
	var timeoutErr *AsyncTimeoutError
	return errors.As(err, &timeoutErr)
}

type AsyncWrapAction struct {
	asynAction   AsyncAction
	waitInterval time.Duration
	timeout      time.Duration
	aborting     atomic.Bool
}

func (aw *AsyncWrapAction) Name() string {
	return aw.asynAction.Name()
}

func (aw *AsyncWrapAction) Abort() {
	aw.aborting.Store(true)
}

func (aw *AsyncWrapAction) TryAbort() bool {
	aw.Abort()
	return true
}

func (aw *AsyncWrapAction) Clone() *AsyncWrapAction {
	return &AsyncWrapAction{
		asynAction:   aw.asynAction,
		waitInterval: aw.waitInterval,
		timeout:      aw.timeout,
	}
}

func (aw *AsyncWrapAction) CloneAction() Action {
	return aw.Clone()
}

func (aw *AsyncWrapAction) Invoke(args ActionArgs) error {
	deadline := time.Now().Add(aw.timeout)

	if aw.aborting.Load() {
		return fmt.Errorf("AsyncWrapAction.Invoke: `%v` aborted", aw.asynAction.Name())
	}

	err := aw.asynAction.Start(args)
	if err != nil {
		return err
	}

	for {

		if aw.aborting.Load() {
			return fmt.Errorf("AsyncWrapAction.Invoke: `%v` aborted", aw.asynAction.Name())
		}

		done, err := aw.asynAction.CheckFinish(args)
		if err != nil {
			// TODO: retry?
			return err
		}
		if done {
			break
		}

		if time.Now().After(deadline) {
			return &AsyncTimeoutError{ActionName: aw.asynAction.Name()}
		}
		time.Sleep(aw.waitInterval)

	}
	return nil
}
