import type { Tour } from "@circuy/contracts";
import { initEvents, listenVisibilityFlush } from "./events.js";
import { type Engine, createEngine } from "./machine.js";
import { pickTour } from "./match.js";
import { onUrlChange } from "./nav.js";
import { readSeen } from "./persist.js";
import { safe, safeAsync } from "./safe.js";
import type { BootOptions, IdentifyAttributes } from "./types.js";

type Command =
	| ["identify", IdentifyAttributes]
	| ["start", string]
	| [string, ...unknown[]];

type QueueFn = {
	(...args: Command): void;
	q?: Command[];
};

declare global {
	interface Window {
		circuy: QueueFn;
	}
}

let bootstrapped = false;
let engine: Engine | undefined;
let options: BootOptions | undefined;
let attrs: IdentifyAttributes = {};
let stopNav: (() => void) | undefined;
let stopFlush: (() => void) | undefined;

function installQueue(): void {
	const pending = Array.isArray(window.circuy?.q) ? window.circuy.q : [];
	const circuy: QueueFn = (...command: Command) => {
		if (bootstrapped) handle(command);
		else {
			circuy.q = circuy.q ?? [];
			circuy.q.push(command);
		}
	};
	circuy.q = pending;
	window.circuy = circuy;
}

function handle(command: Command): void {
	safe(() => {
		if (
			command[0] === "identify" &&
			command[1] &&
			typeof command[1] === "object"
		) {
			attrs = { ...attrs, ...command[1] };
			return;
		}
		if (command[0] === "start" && typeof command[1] === "string" && options) {
			const tour = options.tours.find((item) => item.id === command[1]);
			if (tour) void engine?.start(tour, false);
		}
	}, undefined);
}

function evaluate(): void {
	if (!options || !engine) return;
	if (engine.status() === "running") return;
	const seen = new Set(readSeen(options.key));
	const tour = pickTour(options.tours, window.location.pathname, attrs, (id) =>
		seen.has(id),
	);
	if (tour) void engine.start(tour);
}

export function boot(bootOptions: BootOptions): void {
	safe(() => {
		options = bootOptions;
		initEvents(bootOptions.key, bootOptions.eventsUrl);
		engine?.destroy();
		engine = createEngine(bootOptions.key, bootOptions.waitTimeoutMs);
		stopNav?.();
		stopFlush?.();
		stopNav = onUrlChange(() => {
			void safeAsync(async () => evaluate());
		});
		stopFlush = listenVisibilityFlush();
		bootstrapped = true;
		for (const command of window.circuy?.q ?? []) handle(command);
		if (window.circuy) window.circuy.q = [];
		evaluate();
	}, undefined);
}

function eventsUrlFromTours(toursUrl: string): string | undefined {
	try {
		const resolved = new URL(toursUrl, window.location.href);
		const marker = "/v1/p/";
		const index = resolved.pathname.indexOf(marker);
		if (index === -1) return undefined;
		return `${resolved.origin}${resolved.pathname.slice(0, index)}/v1/events`;
	} catch {
		return undefined;
	}
}

async function loadTours(url: string): Promise<Tour[]> {
	const response = await fetch(url);
	const data: unknown = await response.json();
	let items: unknown[] = [];
	if (Array.isArray(data)) items = data;
	else if (data && typeof data === "object" && "tours" in data) {
		const tours = (data as { tours: unknown }).tours;
		if (Array.isArray(tours)) items = tours;
	}

	const loaded: Tour[] = [];
	for (const item of items) {
		if (!item || typeof item !== "object") continue;
		if ("steps" in item) {
			loaded.push(item as Tour);
			continue;
		}
		if (!("id" in item) || !("version" in item)) continue;
		const id = (item as { id: unknown }).id;
		const version = (item as { version: unknown }).version;
		if (typeof id !== "string" || typeof version !== "number") continue;
		const tourUrl = new URL(`tours/${id}/${version}.json`, url);
		const tourResponse = await fetch(tourUrl);
		const tour = (await tourResponse.json()) as Tour;
		if (tour && typeof tour === "object" && "steps" in tour) {
			loaded.push(tour);
		}
	}
	return loaded;
}

function whenIdle(fn: () => void): void {
	if (typeof requestIdleCallback === "function") {
		requestIdleCallback(() => fn(), { timeout: 2000 });
		return;
	}
	setTimeout(fn, 1);
}

export function bootFromScript(script: HTMLScriptElement): void {
	const key = script.dataset.key;
	if (!key) return;
	const toursUrl = script.dataset.tours;
	whenIdle(() => {
		void safeAsync(async () => {
			const tours = toursUrl ? await loadTours(toursUrl) : [];
			boot({
				key,
				tours,
				eventsUrl:
					script.dataset.eventsUrl ??
					(toursUrl ? eventsUrlFromTours(toursUrl) : undefined),
			});
		});
	});
}

export { createEngine } from "./machine.js";
export type { Engine, EngineOptions } from "./machine.js";
export { emit, onTelemetry, pendingEvents } from "./events.js";
export {
	TEST_ATTRS,
	accessibleName,
	implicitRole,
	resolveCandidate,
	resolveTarget,
	waitForTarget,
} from "./resolve.js";
export { pickTour, matchPath, matchAudience } from "./match.js";
export {
	readProgress,
	writeProgress,
	readSeen,
	markSeen,
	TTL_MS,
} from "./persist.js";
export type {
	BootOptions,
	IdentifyAttributes,
	TelemetryEvent,
	Tour,
} from "./types.js";

installQueue();

if (typeof document !== "undefined") {
	const script = document.currentScript;
	if (script instanceof HTMLScriptElement && script.dataset.key) {
		bootFromScript(script);
	}
}
