import { browser } from '$app/environment';
import { isAuthenticated, session } from './session.svelte';

/**
 * Indexer status, populated from PhotoPrism's WebSocket channel
 * (`/api/v1/ws`). Surfaces in the header pill so the user can see
 * when the library is being scanned — whether triggered by a sidecar
 * folder mutation, a manual reindex from PhotoPrism's own UI, or a
 * scheduled cron.
 *
 * Tolerant by design: PhotoPrism's WS protocol isn't part of any
 * stable contract, so unknown event shapes are ignored rather than
 * thrown. The fields here are derived from the bundled PhotoPrism
 * client's event names (`index`, `indexing`, `library_index`); if
 * the protocol ever changes shape, the pill simply stays idle
 * instead of breaking the app.
 */
interface IndexerState {
	active: boolean;
	/** Human-readable status, e.g. "Indexing", "Index complete". */
	label: string;
	/** Optional sub-text — current file or folder being processed. */
	detail?: string;
}

export const indexer = $state<IndexerState>({
	active: false,
	label: ''
});

let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let backoffMs = 1000;
let clearTimer: ReturnType<typeof setTimeout> | null = null;

// PhotoPrism fires `index.indexing` once per file — that's many events
// per second on a large library. Throttle UI updates from that stream
// to ~150 ms so the pill text doesn't flicker and Svelte isn't re-running
// the render scope on every other frame. Step / completion events bypass
// the throttle (they're already infrequent and the user benefits from
// seeing them land immediately).
const FILE_UPDATE_THROTTLE_MS = 150;
let lastFileUpdateAt = 0;
let pendingFileTimer: ReturnType<typeof setTimeout> | null = null;
let pendingFileName: string | undefined;

function url(): string {
	const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
	return `${proto}//${location.host}/api/v1/ws`;
}

function setActive(label: string, detail?: string): void {
	if (clearTimer !== null) {
		clearTimeout(clearTimer);
		clearTimer = null;
	}
	indexer.active = true;
	indexer.label = label;
	indexer.detail = detail;
}

/**
 * Throttled variant of `setActive` for the per-file `index.indexing`
 * stream. Leading-edge fires immediately so the user sees activity start
 * without delay; subsequent events within FILE_UPDATE_THROTTLE_MS are
 * coalesced into a single trailing-edge update so the displayed filename
 * lands on the *most recent* file rather than some arbitrary one in the
 * middle of the burst.
 */
function setActiveThrottled(label: string, fileName: string | undefined): void {
	pendingFileName = fileName;
	const now = Date.now();
	const since = now - lastFileUpdateAt;
	if (since >= FILE_UPDATE_THROTTLE_MS) {
		lastFileUpdateAt = now;
		setActive(label, pendingFileName);
		pendingFileName = undefined;
		return;
	}
	if (pendingFileTimer !== null) return;
	pendingFileTimer = setTimeout(() => {
		pendingFileTimer = null;
		lastFileUpdateAt = Date.now();
		setActive(label, pendingFileName);
		pendingFileName = undefined;
	}, FILE_UPDATE_THROTTLE_MS - since);
}

function setCompleted(label = 'Index complete'): void {
	// Drop any throttled `index.indexing` update still in flight — once
	// PhotoPrism says "done" we don't want a stale filename to land on
	// top of the completion label a few ms later.
	if (pendingFileTimer !== null) {
		clearTimeout(pendingFileTimer);
		pendingFileTimer = null;
		pendingFileName = undefined;
	}
	indexer.active = false;
	indexer.label = label;
	indexer.detail = undefined;
	// Hold the "complete" label for a few seconds before clearing so the
	// user gets a chance to see it; the pill collapses to nothing once
	// label goes empty.
	if (clearTimer !== null) clearTimeout(clearTimer);
	clearTimer = setTimeout(() => {
		if (!indexer.active) {
			indexer.label = '';
			indexer.detail = undefined;
		}
		clearTimer = null;
	}, 4000);
}

function handleMessage(raw: string): void {
	let msg: unknown;
	try {
		msg = JSON.parse(raw);
	} catch {
		return;
	}
	if (!msg || typeof msg !== 'object') return;
	// PhotoPrism wraps events in `{event, data}`. Some versions nest the
	// payload under `{msg: {event, data}}`; tolerate both.
	const top = msg as Record<string, unknown>;
	const inner = (top.msg as Record<string, unknown> | undefined) ?? top;
	const eventName = inner.event as string | undefined;
	const data = (inner.data ?? {}) as Record<string, unknown>;
	if (!eventName) return;
	// PhotoPrism's WS protocol isn't a stable contract; log the live shape
	// at `debug` (hidden by default in DevTools — toggle "Verbose" to see)
	// so future-us can spot new indexer event names without instrumenting
	// the entire app.
	console.debug('[indexer]', eventName, data);
	switch (eventName) {
		case 'index.indexing': {
			// Per-file event during the scan pass. PhotoPrism emits one
			// of these per file it touches, so this fires often — route
			// it through the throttled setter to keep the pill calm.
			const fileName =
				(data.fileName as string | undefined) ?? (data.baseName as string | undefined);
			setActiveThrottled('Indexing', fileName);
			return;
		}
		case 'index.updating': {
			// Post-file passes (faces, counts, folders, purge, moments)
			// run after the file scan finishes. They're cheap individually
			// but the user sees them so showing the step name keeps the
			// pill informative instead of looking stuck on the last file.
			const step = String(data.step ?? '');
			setActive(step ? `Finalizing — ${step}` : 'Finalizing', undefined);
			return;
		}
		case 'index.completed': {
			const seconds = typeof data.seconds === 'number' ? data.seconds : undefined;
			setCompleted(seconds !== undefined ? `Indexed in ${seconds}s` : 'Index complete');
			return;
		}
		default:
			// Other events (notify.*, log.*, photos.updated, count.*,
			// config.updated) are out of scope for the indexer pill.
			// Intentionally ignored.
			return;
	}
}

function connect(): void {
	if (!browser) return;
	if (!isAuthenticated()) return;
	if (ws) return;
	let sock: WebSocket;
	try {
		sock = new WebSocket(url());
	} catch {
		scheduleReconnect();
		return;
	}
	sock.addEventListener('open', () => {
		backoffMs = 1000;
		// PhotoPrism authenticates the WS channel by reading the
		// `auth_token` cookie set during login, but if the SPA is signed
		// in via the legacy access-token store (no cookie), send an
		// inline auth probe so the server can scope events to this
		// session. Best-effort: failures don't block the listen path.
		try {
			sock.send(
				JSON.stringify({ session: session.id, token: session.accessToken })
			);
		} catch {
			/* best-effort; ignore */
		}
	});
	sock.addEventListener('message', (e) => {
		if (typeof e.data === 'string') handleMessage(e.data);
	});
	sock.addEventListener('close', () => {
		ws = null;
		if (!isAuthenticated()) return;
		scheduleReconnect();
	});
	sock.addEventListener('error', () => {
		// Let `close` drive the reconnect path; logging on every transient
		// error spams the console without adding signal.
	});
	ws = sock;
}

function scheduleReconnect(): void {
	if (reconnectTimer !== null) return;
	reconnectTimer = setTimeout(() => {
		reconnectTimer = null;
		connect();
	}, backoffMs);
	backoffMs = Math.min(backoffMs * 2, 30_000);
}

/** Start listening to PhotoPrism's WS for indexer events. Idempotent —
 *  safe to call multiple times; only one socket is held at a time. */
export function startIndexerWatch(): void {
	if (!browser) return;
	connect();
}

/** Tear down the WS and clear any displayed status. Call on logout. */
export function stopIndexerWatch(): void {
	if (reconnectTimer !== null) {
		clearTimeout(reconnectTimer);
		reconnectTimer = null;
	}
	if (clearTimer !== null) {
		clearTimeout(clearTimer);
		clearTimer = null;
	}
	if (pendingFileTimer !== null) {
		clearTimeout(pendingFileTimer);
		pendingFileTimer = null;
		pendingFileName = undefined;
	}
	if (ws) {
		try {
			ws.close(1000);
		} catch {
			/* close errors are informational only */
		}
		ws = null;
	}
	indexer.active = false;
	indexer.label = '';
	indexer.detail = undefined;
	backoffMs = 1000;
	lastFileUpdateAt = 0;
}
