// Package fsm implements a finite state machine with specific features:
//
// # Operation
//
//   - All data types are parametric and checked: state key, event key, and event data
//   - Low-level (push) mode: clients call its Handle method, feeding events to these calls
//   - High-level (pull) mode: machine receives events from a Feed
//     This enables automatic progress on clock ticks, generating tick events which can be Transition triggers.
//   - Feed for high-level mode is injectable, defaulting to a channel.
//
// # Hooks
//
//   - Events support guards and action hooks.
//   - States support enter and leave hooks
//   - Hooks have a unified signature
//
// # Event payloads
//
//   - Events can carry a typed payload, available to all hooks.
//   - All machine types are parameterized on it, enabling strong typing.
//
// # Contexts
//
//   - Machine carries a context enabling global cancellation
//   - Handlers all take a per-event context enabling individual cancellation
//   - Handler contexts should be either nil, which uses the machine context, or based on that context.
package fsm

import (
	"context"
	"time"
)

type Hook[K comparable, EV any] func(ctx context.Context, event Event[K, EV]) error

type State[SK, EK comparable, EV any] interface {
	Name() SK
	SetAfterEnter([]Hook[EK, EV])
	AfterEnter() []Hook[EK, EV]
	SetBeforeLeave([]Hook[EK, EV])
	BeforeLeave() []Hook[EK, EV]
}

type Event[EK comparable, EV any] interface {
	Name() EK
	Data() EV
}

type RetryableError interface {
	error
	RetryableError()
}

type Transition[SK, EK comparable, EV any] interface {
	SetGuards([]Hook[EK, EV])
	Guards() []Hook[EK, EV]
	SetActions([]Hook[EK, EV])
	Actions() []Hook[EK, EV]
	Next() SK
}

// Matrix describes the set of Transition defining an FSM.
type Matrix[SK, EK comparable, EV any] map[SK]map[EK]Transition[SK, EK, EV]

// BackoffFunc returning <0 means the retry limit has been reached.
type BackoffFunc func() time.Duration

type FSM[SK, EK comparable, EV any] interface {
	Context() context.Context
	Name() string

	SetBackoff(fn BackoffFunc)

	// SetFeed allows the FSM to support pull mode.
	SetFeed(feed Feed[SK, EK, EV])

	// SetMatrix defines the Transition matrix on an FSM.
	SetMatrix(mx Matrix[SK, EK, EV])

	// SetOnUnavailable defines optional behaviour when receiving an event not accepted by the current state.
	// Default is OnErrorIgnore.
	SetOnUnavailable(OnError[SK, EK])
	// SetOnGuardFailure defines optional behaviour when a guard rejects a transition attempt.
	// Default is OnErrorReturn.
	SetOnGuardFailure(OnError[SK, EK])
	// SetOnActionFailure defines optional behaviour when failing an event action.
	// Default is OnErrorReturn.
	SetOnActionFailure(OnError[SK, EK])
	// SetOnEnterFailure defines optional behaviour when a state entry fails.
	// Default is OnErrorReturn.
	SetOnEnterFailure(OnError[SK, EK])
	// SetOnLeaveFailure defines optional behaviour when a state leave fails.
	// Default is OnErrorReturn.
	SetOnLeaveFailure(OnError[SK, EK])

	StartState() State[SK, EK, EV]
	EndState() State[SK, EK, EV]
	ErrorState() State[SK, EK, EV]
}

type PushMachine[SK, EK comparable, EV any] interface {
	FSM[SK, EK, EV]
	Handle(context.Context, Event[EK, EV]) error // If ctx == nil, handling uses the machine context.
}

type PullMachine[SK, EK comparable, EV any] interface {
	FSM[SK, EK, EV]
	Start(ctx context.Context, events chan<- Event[EK, EV]) State[SK, EK, EV]
}

var (
	BackoffInitialDelay    = time.Millisecond
	BackoffMaxAttempts     = 10
	DefaultBackoffAttempts = 0

	// NoBackoff retries without delay, but only up to the maximum number of retries.
	NoBackoff BackoffFunc = func() time.Duration {
		DefaultBackoffAttempts++
		if DefaultBackoffAttempts > BackoffMaxAttempts {
			return -1
		}
		return YoloBackoff()
	}

	// ExponentialBackoff retries with an exponential delay and maximum number of retries.
	ExponentialBackoff BackoffFunc = func() time.Duration {
		d := NoBackoff()
		if d < 0 {
			return d
		}
		return (1 << DefaultBackoffAttempts) * BackoffInitialDelay
	}

	// YoloBackoff retries indefinitely without delay. Not a good idea outside specific test cases.
	YoloBackoff BackoffFunc = func() time.Duration {
		return 0
	}
)

type Feed[SK, EK comparable, EV any] func(fsm FSM[SK, EK, EV]) Event[EK, EV]