import type { Step, Tour } from "@circuy/contracts";
import { emit, flushEvents } from "./events.js";
import {
	clearProgress,
	markSeen,
	readProgress,
	writeProgress,
} from "./persist.js";
import { destroyHost, hideStep, restoreFocus, showStep } from "./render.js";
import { waitForTarget } from "./resolve.js";

export type EngineStatus = "idle" | "running" | "complete" | "dismissed";

export type Engine = {
	status: () => EngineStatus;
	start: (tour: Tour, resume?: boolean) => Promise<void>;
	dismiss: () => void;
	destroy: () => void;
};

export type EngineOptions = {
	waitTimeoutMs?: number;
	preview?: boolean;
	onFinish?: (reason: "complete" | "dismissed") => void;
};

function stepById(tour: Tour, id: string): Step | undefined {
	return tour.steps.find((step) => step.id === id);
}

function followingId(
	tour: Tour,
	current: Step,
	choiceId?: string,
): string | undefined {
	if (current.type === "question" && choiceId) {
		return current.options.find((option) => option.id === choiceId)?.next;
	}
	if (current.next) return current.next;
	const index = tour.steps.findIndex((step) => step.id === current.id);
	return tour.steps[index + 1]?.id;
}

export function createEngine(
	publicKey: string,
	waitTimeoutMsOrOptions: number | EngineOptions = 5000,
): Engine {
	const options: EngineOptions =
		typeof waitTimeoutMsOrOptions === "number"
			? { waitTimeoutMs: waitTimeoutMsOrOptions }
			: waitTimeoutMsOrOptions;
	const waitTimeoutMs = options.waitTimeoutMs ?? 5000;
	const preview = options.preview === true;
	let status: EngineStatus = "idle";
	let active: Tour | undefined;
	let current: Step | undefined;
	let advanceCleanup: (() => void) | null = null;

	const handlers = {
		onNext: () => {
			void goNext();
		},
		onDismiss: () => dismiss(),
		onChoice: (id: string) => {
			void goNext(id);
		},
	};

	function stopAdvance(): void {
		advanceCleanup?.();
		advanceCleanup = null;
	}

	function bindAdvance(step: Step, target: Element | null): void {
		stopAdvance();
		if (step.type !== "tooltip" || !target) return;
		const advance = step.advance ?? "button";
		if (advance === "element-click") {
			const onClick = () => {
				stopAdvance();
				void goNext();
			};
			target.addEventListener("click", onClick);
			advanceCleanup = () => target.removeEventListener("click", onClick);
		}
		if (advance === "element-input") {
			const onInput = () => {
				stopAdvance();
				void goNext();
			};
			target.addEventListener("input", onInput);
			advanceCleanup = () => target.removeEventListener("input", onInput);
		}
	}

	async function show(step: Step): Promise<void> {
		current = step;
		if (!preview && active) {
			writeProgress(publicKey, active.id, active.version, step.id);
		}
		if (!preview) {
			emit({
				type: "step_view",
				tourId: active?.id ?? "",
				tourVersion: active?.version ?? 0,
				stepId: step.id,
			});
		}

		let target: Element | null = null;
		if (step.type === "tooltip") {
			target = await waitForTarget(step.target, waitTimeoutMs);
			if (!target) {
				if (!preview) {
					emit({
						type: "target_lost",
						tourId: active?.id ?? "",
						tourVersion: active?.version ?? 0,
						stepId: step.id,
					});
				}
				await goNext();
				return;
			}
		}

		showStep(step, target, handlers);
		bindAdvance(step, target);
	}

	async function goNext(choiceId?: string): Promise<void> {
		if (!active || !current || status !== "running") return;
		if (choiceId && !preview) {
			emit({
				type: "choice",
				tourId: active.id,
				tourVersion: active.version,
				stepId: current.id,
				choiceId,
			});
		}
		const next = followingId(active, current, choiceId);
		if (!next) {
			finish("complete");
			return;
		}
		const step = stepById(active, next);
		if (!step) {
			finish("complete");
			return;
		}
		await show(step);
	}

	function finish(reason: "complete" | "dismissed"): void {
		if (status !== "running" || !active) return;
		status = reason;
		stopAdvance();
		hideStep();
		restoreFocus();
		if (!preview) {
			markSeen(publicKey, active.id);
			emit({
				type: reason === "complete" ? "complete" : "dismiss",
				tourId: active.id,
				tourVersion: active.version,
				stepId: current?.id,
			});
			flushEvents();
		}
		current = undefined;
		active = undefined;
		options.onFinish?.(reason);
	}

	async function start(tour: Tour, resume = true): Promise<void> {
		if (status === "running") return;
		status = "running";
		active = tour;
		if (!preview) {
			emit({ type: "start", tourId: tour.id, tourVersion: tour.version });
		}
		const resumed = resume
			? readProgress(publicKey, tour.id, tour.version)
			: null;
		const initial =
			(resumed ? stepById(tour, resumed) : undefined) ?? tour.steps[0];
		if (!initial) {
			finish("complete");
			return;
		}
		await show(initial);
	}

	function dismiss(): void {
		finish("dismissed");
	}

	function destroy(): void {
		stopAdvance();
		hideStep();
		restoreFocus();
		destroyHost();
		if (active && !preview) clearProgress(publicKey, active.id);
		status = "idle";
		active = undefined;
		current = undefined;
	}

	return {
		status: () => status,
		start,
		dismiss,
		destroy,
	};
}
