import { writable } from 'svelte/store';
import { browser } from '$app/environment';

const TOKEN_KEY = 'eventsnap_jwt';
const PIN_KEY = 'eventsnap_pin';
const USER_ID_KEY = 'eventsnap_user_id';
const DISPLAY_NAME_KEY = 'eventsnap_display_name';

export const isAuthenticated = writable(false);

/**
 * Reactive mirror of `localStorage[PIN_KEY]`. Subscribers (the My Account page) see
 * the PIN change immediately when an SSE `pin-reset` event invalidates it from any
 * route — keeps the displayed PIN consistent with the server hash.
 */
export const currentPin = writable<string | null>(null);

// Guest auth lives in localStorage (persists across sessions — a guest returning to the
// event days later stays signed in). The ADMIN token lives in sessionStorage instead
// (USER_JOURNEYS §11.1): its tighter 1-day lifetime is meant to bound exposure on a shared
// "event laptop", and sessionStorage clears on tab/browser close, which localStorage defeats.
// Reads check sessionStorage FIRST so an active admin token wins over any leftover guest
// token on the same device.
function readAuth(key: string): string | null {
	if (!browser) return null;
	return sessionStorage.getItem(key) ?? localStorage.getItem(key);
}

export function getToken(): string | null {
	return readAuth(TOKEN_KEY);
}

export function getPin(): string | null {
	if (!browser) return null;
	return localStorage.getItem(PIN_KEY);
}

/**
 * Clear the locally-cached recovery PIN. Called when the server resets it (host
 * action) — the cached plaintext no longer matches the bcrypt hash, so showing it
 * would mislead the user.
 */
export function clearPin(): void {
	if (!browser) return;
	localStorage.removeItem(PIN_KEY);
	currentPin.set(null);
}

export function getUserId(): string | null {
	return readAuth(USER_ID_KEY);
}

export function getDisplayName(): string | null {
	return readAuth(DISPLAY_NAME_KEY);
}

export function getExpiry(): Date | null {
	const token = getToken();
	if (!token) return null;
	try {
		const payload = JSON.parse(atob(token.split('.')[1]));
		return payload.exp ? new Date(payload.exp * 1000) : null;
	} catch {
		return null;
	}
}

export function setAuth(
	jwt: string,
	pin: string | null,
	userId: string,
	displayName?: string
): void {
	if (!browser) return;
	// DISPLACE any resident admin session: reads are sessionStorage-first, so a leftover
	// admin token there would otherwise shadow this guest login and hand the guest admin
	// rights on a shared device. Clear the sessionStorage identity so exactly one is resident.
	sessionStorage.removeItem(TOKEN_KEY);
	sessionStorage.removeItem(USER_ID_KEY);
	sessionStorage.removeItem(DISPLAY_NAME_KEY);
	localStorage.setItem(TOKEN_KEY, jwt);
	if (pin) {
		localStorage.setItem(PIN_KEY, pin);
		currentPin.set(pin);
	}
	localStorage.setItem(USER_ID_KEY, userId);
	if (displayName) localStorage.setItem(DISPLAY_NAME_KEY, displayName);
	isAuthenticated.set(true);
	fireSetAuthHooks();
}

/**
 * Persist an ADMIN session in sessionStorage (USER_JOURNEYS §11.1) rather than localStorage,
 * so the elevated credential does not survive a tab/browser close on a shared device — the
 * point of the tighter 1-day admin token. Admins have no recovery PIN. `getToken` reads
 * sessionStorage first, so this wins over any leftover guest token on the same device.
 */
export function setAdminAuth(jwt: string, userId: string, displayName?: string): void {
	if (!browser) return;
	// DISPLACE any resident guest session so exactly one identity is resident (symmetric with
	// setAuth). Keep the guest PIN — it's deliberately preserved for later recovery, and the
	// admin has none. Reads are sessionStorage-first, so the admin token now wins cleanly.
	localStorage.removeItem(TOKEN_KEY);
	localStorage.removeItem(USER_ID_KEY);
	localStorage.removeItem(DISPLAY_NAME_KEY);
	sessionStorage.setItem(TOKEN_KEY, jwt);
	sessionStorage.setItem(USER_ID_KEY, userId);
	if (displayName) sessionStorage.setItem(DISPLAY_NAME_KEY, displayName);
	isAuthenticated.set(true);
	fireSetAuthHooks();
}

// Hook registry: cross-cutting stores (export-status, etc.) register a callback
// here at import-time so they get reset on every clearAuth path — both the
// explicit "Abmelden" button and the api.ts 401 auto-clear. Keeps
// clearAuth the single source of truth without baking dependencies on every
// downstream store into this module (which would create circular imports).
const clearAuthHooks: Array<() => void> = [];
export function onClearAuth(fn: () => void): void {
	clearAuthHooks.push(fn);
}

// The mirror of `onClearAuth`, for stores that must be RE-SEEDED when a new identity
// arrives rather than merely cleared. Without it, anything derived from the token
// survives a logout→login in the same tab: `goto()` is a client-side navigation, so no
// module is re-imported and no `onMount` re-runs, and the previous user's value simply
// stays. Fires after the new token is resident, so hooks can read it.
const setAuthHooks: Array<() => void> = [];
export function onSetAuth(fn: () => void): void {
	setAuthHooks.push(fn);
}

function fireSetAuthHooks(): void {
	for (const fn of setAuthHooks) {
		try {
			fn();
		} catch {
			/* hook failure is non-fatal */
		}
	}
}

export function clearAuth(): void {
	if (!browser) return;
	// Clear from BOTH stores — a guest token lives in localStorage, an admin token in
	// sessionStorage; either could be present, and a stale one must not linger.
	for (const store of [localStorage, sessionStorage]) {
		store.removeItem(TOKEN_KEY);
		store.removeItem(USER_ID_KEY);
		// Clear the display name too — on a shared device the next user shouldn't see
		// the previous guest's name pre-filled on /join.
		store.removeItem(DISPLAY_NAME_KEY);
	}
	// PIN is intentionally kept so the user can recover
	isAuthenticated.set(false);
	// Hooks fire in registration order. Keep them dependency-free of each other —
	// if you ever need ordering, introduce a priority field rather than relying
	// on import-load timing, which is fragile across refactors.
	for (const fn of clearAuthHooks) {
		try {
			fn();
		} catch {
			/* hook failure is non-fatal */
		}
	}
}

export function getRole(): 'guest' | 'host' | 'admin' | null {
	const token = getToken();
	if (!token) return null;
	try {
		const payload = JSON.parse(atob(token.split('.')[1]));
		return payload.role ?? null;
	} catch {
		return null;
	}
}

export function initAuth(): void {
	if (!browser) return;
	isAuthenticated.set(!!getToken());
	currentPin.set(getPin());
}
