type ActionState<T> =
	| { kind: "idle" }
	| { kind: "loading" }
	| { kind: "ready"; value: T }
	| { kind: "failed"; error: string };
type Operation<T, Args extends unknown[] = []> = (...args: Args) => Promise<T>;

const defaultError = (cause: unknown) => (cause instanceof Error ? cause.message : String(cause));

const create = <T, Args extends unknown[] = []>(
	operation: Operation<T, Args>,
	initial: ActionState<T>,
	toError: typeof defaultError
) => {
	let state = $state<ActionState<T>>(initial);
	let revision = 0;
	let args: Args | undefined;

	const run: Operation<void, Args> = async (...nextArgs) => {
		const currentRevision = ++revision;
		args = nextArgs;
		state = { kind: "loading" };
		try {
			const value = await operation(...nextArgs);
			if (currentRevision !== revision) return;
			args = undefined;
			state = { kind: "ready", value };
		} catch (cause) {
			if (currentRevision !== revision) return;
			args = undefined;
			state = { kind: "failed", error: toError(cause) };
		}
	};

	return {
		get data() {
			return state.kind === "ready" ? state.value : undefined;
		},
		get error() {
			return state.kind === "failed" ? state.error : undefined;
		},
		get loading() {
			return state.kind === "loading";
		},
		get args() {
			return state.kind === "loading" ? args : undefined;
		},
		run,
		update(update: (current: T) => T): boolean {
			if (state.kind !== "ready") return false;
			revision++;
			state = { kind: "ready", value: update(state.value) };
			return true;
		}
	};
};

// loads run automatically and rerun when reactive values change:
// const post = createLoad(() => api.getPost(id));
//
// actions only run when called:
// const save = createAction(api.savePost);
// await save.run(draft);
//
// both expose .data, .error, .loading, and .args.
export const createAction = <T, Args extends unknown[] = []>(
	operation: Operation<T, Args>,
	toError: typeof defaultError = defaultError
) => create(operation, { kind: "idle" }, toError);

export const createLoad = <T>(load: Operation<T>, toError: typeof defaultError = defaultError) => {
	const action = create(load, { kind: "loading" }, toError);
	$effect(() => void action.run());
	return action;
};
