import { groupBy } from 'es-toolkit/compat'

export namespace StateMachine {
	export interface Transition<Action extends string | number, State extends string | number> {
		title?: string
		from: State
		to: State
		actions: Action[]
	}

	export type ActionMap<Action extends string | number, Context> = {
		[k in Action]?: (context: Context) => void
	}

	export type StateTransitionMap<Action extends string | number, State extends string | number> = {
		[k in State]?: { on: { [p in State]?: { target: State; actions: Action[] } } }
	}
}

export class Interpreter<Action extends string | number, State extends string | number, Context> {
	private state: State
	private context: Context
	private actions: StateMachine.ActionMap<Action, Context>
	private states: StateMachine.StateTransitionMap<Action, State>

	constructor(
		state: State,
		context: Context,
		states: StateMachine.StateTransitionMap<Action, State>,
		actions: StateMachine.ActionMap<Action, Context>,
	) {
		this.state = state
		this.context = context
		this.states = states
		this.actions = actions
	}

	send(state: State): void {
		// eslint-disable-next-line unicorn-x/no-array-for-each
		this.states[this.state]?.on[state]?.actions.forEach((_action) => {
			this.actions[_action]?.(this.context)
		})
		this.state = state
	}
}

export class MachineBuilderFactory<Action extends string | number, State extends string | number, Context> {
	private actionMap: StateMachine.ActionMap<Action, Context> | undefined
	private transitions: StateMachine.Transition<Action, State>[] = []
	private states: StateMachine.StateTransitionMap<Action, State> | undefined

	withActions(actionMap: StateMachine.ActionMap<Action, Context>): MachineBuilderFactory<Action, State, Context> {
		this.actionMap = actionMap
		return this
	}

	withTransitions(
		transitions: StateMachine.Transition<Action, State>[],
	): MachineBuilderFactory<Action, State, Context> {
		this.transitions.push(...transitions)
		return this
	}

	build(): MachineBuilder<Action, State, Context> {
		if (this.actionMap === undefined) throw new Error('Please define an action map')

		this.states = this.generateStates()
		return new MachineBuilder(this.actionMap, this.states)
	}

	private generateStates(): StateMachine.StateTransitionMap<Action, State> {
		const states: StateMachine.StateTransitionMap<Action, State> = {}
		const groupedTransitions = groupBy(this.transitions, 'from')
		for (const from in groupedTransitions) {
			const fromState = from as State
			const transitions = groupedTransitions[from]
			for (const transition of transitions) {
				if (!states[fromState]) states[fromState] = { on: {} }
				const state = states[fromState]
				state!.on[transition.to] = { target: transition.to, actions: transition.actions }
			}
		}
		return states
	}
}

export class MachineBuilder<Action extends string | number, State extends string | number, Context> {
	private actionMap: StateMachine.ActionMap<Action, Context>
	private states: StateMachine.StateTransitionMap<Action, State>

	constructor(
		actionMap: StateMachine.ActionMap<Action, Context>,
		states: StateMachine.StateTransitionMap<Action, State>,
	) {
		this.actionMap = actionMap
		this.states = states
	}

	interpret(initialState: State, context: Context): Interpreter<Action, State, Context> {
		return new Interpreter(initialState, context, this.states, this.actionMap)
	}
}
