package fsm // import "go.unistack.org/micro/v3/fsm"

import (
	"context"
	"errors"
	"fmt"
	"sync"
)

var (
	ErrInvalidState = errors.New("does not exists")
	StateEnd        = "end"
)

// Options struct holding fsm options
type Options struct {
	// DryRun mode
	DryRun bool
	// Initial state
	Initial string
	// HooksBefore func slice runs in order before state
	HooksBefore []HookBeforeFunc
	// HooksAfter func slice runs in order after state
	HooksAfter []HookAfterFunc
}

// HookBeforeFunc func signature
type HookBeforeFunc func(ctx context.Context, state string, args interface{})

// HookAfterFunc func signature
type HookAfterFunc func(ctx context.Context, state string, args interface{})

// Option func signature
type Option func(*Options)

// StateOptions holds state options
type StateOptions struct {
	DryRun bool
}

// StateDryRun says that state executes in dry run mode
func StateDryRun(b bool) StateOption {
	return func(o *StateOptions) {
		o.DryRun = b
	}
}

// StateOption func signature
type StateOption func(*StateOptions)

// InitialState sets init state for state machine
func InitialState(initial string) Option {
	return func(o *Options) {
		o.Initial = initial
	}
}

// HookBefore provides hook func slice
func HookBefore(fns ...HookBeforeFunc) Option {
	return func(o *Options) {
		o.HooksBefore = fns
	}
}

// HookAfter provides hook func slice
func HookAfter(fns ...HookAfterFunc) Option {
	return func(o *Options) {
		o.HooksAfter = fns
	}
}

// StateFunc called on state transition and return next step and error
type StateFunc func(ctx context.Context, args interface{}, opts ...StateOption) (string, interface{}, error)

// FSM is a finite state machine
type FSM struct {
	mu          sync.Mutex
	statesMap   map[string]StateFunc
	statesOrder []string
	opts        *Options
	current     string
}

// New creates a new finite state machine having the specified initial state
// with specified options
func New(opts ...Option) *FSM {
	options := &Options{}

	for _, opt := range opts {
		opt(options)
	}

	return &FSM{
		statesMap: map[string]StateFunc{},
		opts:      options,
	}
}

// Current returns the current state
func (f *FSM) Current() string {
	f.mu.Lock()
	defer f.mu.Unlock()
	return f.current
}

// Current returns the current state
func (f *FSM) Reset() {
	f.mu.Lock()
	f.current = f.opts.Initial
	f.mu.Unlock()
}

// State adds state to fsm
func (f *FSM) State(state string, fn StateFunc) {
	f.mu.Lock()
	f.statesMap[state] = fn
	f.statesOrder = append(f.statesOrder, state)
	f.mu.Unlock()
}

// Init initialize fsm and check states

// Start runs state machine with provided data
func (f *FSM) Start(ctx context.Context, args interface{}, opts ...Option) (interface{}, error) {
	var err error
	var ok bool
	var fn StateFunc
	var nstate string

	f.mu.Lock()
	options := f.opts

	for _, opt := range opts {
		opt(options)
	}

	sopts := []StateOption{StateDryRun(options.DryRun)}

	cstate := options.Initial
	states := make(map[string]StateFunc, len(f.statesMap))
	for k, v := range f.statesMap {
		states[k] = v
	}
	f.current = cstate
	f.mu.Unlock()

	for {
		select {
		case <-ctx.Done():
			return nil, ctx.Err()
		default:
			fn, ok = states[cstate]
			if !ok {
				return nil, fmt.Errorf(`state "%s" %w`, cstate, ErrInvalidState)
			}
			f.mu.Lock()
			f.current = cstate
			f.mu.Unlock()
			for _, fn := range options.HooksBefore {
				fn(ctx, cstate, args)
			}
			nstate, args, err = fn(ctx, args, sopts...)
			for _, fn := range options.HooksAfter {
				fn(ctx, cstate, args)
			}
			switch {
			case err != nil:
				return args, err
			case nstate == StateEnd:
				return args, nil
			case nstate == "":
				for idx := range f.statesOrder {
					if f.statesOrder[idx] == cstate && len(f.statesOrder) > idx+1 {
						nstate = f.statesOrder[idx+1]
					}
				}
			}
			cstate = nstate
		}
	}
}