/**
 * Gestor de sesión SSO para Keycloak
 *
 * Responsabilidades:
 * - Refresh silencioso del access token SOLO cuando el usuario está activo.
 * - Detección de actividad del usuario (evita polling innecesario cuando está idle).
 * - Idle timeout: si el usuario está inactivo, no refrescar → el token y la sesión
 *   expiran en Keycloak de forma natural → siguiente petición 401 → logout.
 * - Logout automático cuando el refresh falla (sesión SSO terminada por Keycloak,
 *   admin forzado, max session alcanzado, etc.).
 * - Verificación de sesión SSO usando el iframe silencioso de Keycloak JS.
 *
 * Flujo de tokens:
 *  - Access token: en memoria (authStore) + cookie no-HttpOnly (password login)
 *                  o en instancia Keycloak JS (SSO flow)
 *  - Refresh token: cookie HttpOnly únicamente (el JS nunca lo toca)
 *  - El refresh se hace server-side via /api-sveltekit/auth/silent-refresh
 */

import { browser } from '$app/environment';
import type Keycloak from 'keycloak-js';

// ─────────────────────────────────────────────────────────
// Eventos DOM personalizados
// ─────────────────────────────────────────────────────────

/** Se emite cuando la sesión está próxima a expirar por inactividad */
export const SESSION_WARNING_EVENT = 'session:warning';
/** Se emite cuando la sesión ha expirado (idle, max-session o refresh fallido) */
export const SESSION_EXPIRED_EVENT = 'session:expired';
/** Se emite cuando el usuario extiende la sesión desde el diálogo de advertencia */
export const SESSION_EXTENDED_EVENT = 'session:extended';
/** Se emite después de un refresh silencioso exitoso */
export const SESSION_TOKEN_REFRESHED_EVENT = 'session:token-refreshed';

// ─────────────────────────────────────────────────────────
// Tipos
// ─────────────────────────────────────────────────────────

export type SessionExpiredReason = 'idle' | 'refresh_failed' | 'keycloak_session_ended' | 'error' | 'manual';

export interface SessionExpiredDetail {
	reason: SessionExpiredReason;
}

export interface SessionWarningDetail {
	remainingMs: number;
}

export interface SessionManagerOptions {
	/**
	 * Segundos antes de la expiración del token para intentar el refresh.
	 * Default: 60
	 */
	refreshBeforeExpirySeconds?: number;

	/**
	 * Tiempo de inactividad (ms) después del cual NO se refresca el token,
	 * permitiendo que la sesión de Keycloak expire de forma natural.
	 * Default: 30 minutos (1_800_000 ms)
	 */
	idleTimeoutMs?: number;

	/**
	 * Milisegundos antes del idle timeout para mostrar el diálogo de advertencia.
	 * Default: 5 minutos (300_000 ms)
	 */
	warningBeforeIdleMs?: number;

	/**
	 * Intervalo (ms) para verificar silenciosamente la sesión SSO de Keycloak.
	 * Solo se usa cuando hay una instancia de Keycloak JS autenticada.
	 * Default: 5 minutos (300_000 ms). 0 para deshabilitar.
	 */
	ssoCheckIntervalMs?: number;

	/** Función para obtener la instancia de Keycloak JS (si usa el SSO flow) */
	getKeycloakInstance?: () => Keycloak | null;

	/** Callback cuando el token se refresca exitosamente */
	onTokenRefreshed?: (newToken: string) => void;

	/** Callback cuando la sesión expira */
	onSessionExpired?: (reason: SessionExpiredReason) => void;
}

// ─────────────────────────────────────────────────────────
// SessionManager
// ─────────────────────────────────────────────────────────

export class SessionManager {
	private opts: Required<SessionManagerOptions>;

	// Timers
	private refreshTimerId: ReturnType<typeof setTimeout> | null = null;
	private idleTimerId: ReturnType<typeof setTimeout> | null = null;
	private warningTimerId: ReturnType<typeof setTimeout> | null = null;
	private ssoCheckIntervalId: ReturnType<typeof setInterval> | null = null;

	// State
	private currentToken: string | null = null;
	private lastActivityAt = Date.now();
	private warningShown = false;
	private isRefreshing = false;
	private destroyed = false;

	// Activity listener cleanups
	private removeListeners: Array<() => void> = [];

	constructor(options: SessionManagerOptions = {}) {
		this.opts = {
			refreshBeforeExpirySeconds: options.refreshBeforeExpirySeconds ?? 60,
			idleTimeoutMs: options.idleTimeoutMs ?? 30 * 60 * 1000,
			warningBeforeIdleMs: options.warningBeforeIdleMs ?? 5 * 60 * 1000,
			ssoCheckIntervalMs: options.ssoCheckIntervalMs ?? 5 * 60 * 1000,
			getKeycloakInstance: options.getKeycloakInstance ?? (() => null),
			onTokenRefreshed: options.onTokenRefreshed ?? (() => {}),
			onSessionExpired: options.onSessionExpired ?? (() => {})
		};
	}

	// ─────────────────────────────────────────────────────
	// Public API
	// ─────────────────────────────────────────────────────

	/**
	 * Inicia el gestor de sesión con el token actual.
	 * Debe llamarse una vez tras la autenticación exitosa.
	 */
	start(initialToken: string): void {
		if (!browser || this.destroyed) return;

		this.currentToken = initialToken;
		this.lastActivityAt = Date.now();
		this.warningShown = false;

		this.setupActivityListeners();
		this.scheduleRefresh(initialToken);
		this.scheduleIdleTimers();
		this.startSsoCheckInterval();
	}

	/**
	 * Actualiza el token en memoria (llamar después de un refresh exitoso externo).
	 */
	updateToken(newToken: string): void {
		if (this.destroyed) return;
		this.currentToken = newToken;
		this.warningShown = false;
		this.cancelRefreshTimer();
		this.scheduleRefresh(newToken);
		this.resetIdleTimers();
	}

	/**
	 * El usuario hizo clic en "Continuar sesión" en el diálogo de advertencia.
	 * Fuerza un refresh inmediato y reinicia los timers de inactividad.
	 */
	extendSession(): void {
		if (this.destroyed) return;
		this.recordActivity();
		if (this.currentToken) {
			void this.doRefresh('extend');
		}
	}

	/** Destruye el gestor y limpia todos los recursos. */
	destroy(): void {
		this.destroyed = true;
		this.cancelRefreshTimer();
		this.cancelIdleTimers();
		this.stopSsoCheckInterval();
		this.teardownActivityListeners();
	}

	// ─────────────────────────────────────────────────────
	// Activity tracking
	// ─────────────────────────────────────────────────────

	private setupActivityListeners(): void {
		const events: (keyof WindowEventMap)[] = [
			'mousedown',
			'mousemove',
			'keydown',
			'scroll',
			'touchstart',
			'click',
			'pointerdown'
		];

		// Limitar actualizaciones de actividad a máximo una por segundo
		let debounceTimer: ReturnType<typeof setTimeout> | null = null;

		const handler = () => {
			if (debounceTimer) return;
			debounceTimer = setTimeout(() => {
				debounceTimer = null;
				this.recordActivity();
			}, 1000);
		};

		events.forEach((event) => {
			window.addEventListener(event, handler, { passive: true });
			this.removeListeners.push(() => window.removeEventListener(event, handler));
		});

		// Al volver a la pestaña, registrar actividad y verificar si hace falta
		// un refresh inmediato (el tiempo puede haber pasado con la pestaña en segundo plano)
		const visibilityHandler = () => {
			if (document.visibilityState === 'visible') {
				this.recordActivity();
				void this.refreshIfExpiringSoon();
			}
		};
		document.addEventListener('visibilitychange', visibilityHandler);
		this.removeListeners.push(() =>
			document.removeEventListener('visibilitychange', visibilityHandler)
		);
	}

	private teardownActivityListeners(): void {
		this.removeListeners.forEach((fn) => fn());
		this.removeListeners = [];
	}

	private recordActivity(): void {
		const wasIdle = this.isIdle();
		this.lastActivityAt = Date.now();

		if (this.warningShown) {
			// El usuario volvió activo → descartar advertencia
			this.warningShown = false;
			this.resetIdleTimers();
			window.dispatchEvent(new CustomEvent(SESSION_EXTENDED_EVENT));
		} else if (wasIdle) {
			// Volvemos de idle → reiniciar timers
			this.resetIdleTimers();
			void this.refreshIfExpiringSoon();
		}
	}

	private isIdle(): boolean {
		return Date.now() - this.lastActivityAt > this.opts.idleTimeoutMs;
	}

	// ─────────────────────────────────────────────────────
	// Token refresh scheduling
	// ─────────────────────────────────────────────────────

	private parseExpiry(token: string): number | null {
		try {
			const payload = JSON.parse(atob(token.split('.')[1].replace(/-/g, '+').replace(/_/g, '/')));
			return typeof payload.exp === 'number' ? payload.exp * 1000 : null;
		} catch {
			return null;
		}
	}

	private scheduleRefresh(token: string): void {
		const expiry = this.parseExpiry(token);
		if (!expiry) return;

		const msUntilRefresh = expiry - Date.now() - this.opts.refreshBeforeExpirySeconds * 1000;

		if (msUntilRefresh <= 0) {
			void this.doRefresh('scheduled');
			return;
		}

		this.refreshTimerId = setTimeout(() => {
			if (!this.destroyed) void this.doRefresh('scheduled');
		}, msUntilRefresh);
	}

	private cancelRefreshTimer(): void {
		if (this.refreshTimerId !== null) {
			clearTimeout(this.refreshTimerId);
			this.refreshTimerId = null;
		}
	}

	/** Refresca el token si le quedan menos de `refreshBeforeExpirySeconds` */
	private async refreshIfExpiringSoon(): Promise<void> {
		if (!this.currentToken) return;
		const expiry = this.parseExpiry(this.currentToken);
		if (!expiry) return;
		if (expiry - Date.now() < this.opts.refreshBeforeExpirySeconds * 1000) {
			await this.doRefresh('on-demand');
		}
	}

	// ─────────────────────────────────────────────────────
	// Idle session timers
	// ─────────────────────────────────────────────────────

	private scheduleIdleTimers(): void {
		this.cancelIdleTimers();
		const now = Date.now();
		const idleAt = this.lastActivityAt + this.opts.idleTimeoutMs;
		const warnAt = idleAt - this.opts.warningBeforeIdleMs;

		const msUntilWarn = warnAt - now;
		const msUntilIdle = idleAt - now;

		if (msUntilWarn > 0) {
			this.warningTimerId = setTimeout(() => {
				if (!this.destroyed && !this.warningShown && this.isIdle() === false) {
					this.showWarning(this.opts.warningBeforeIdleMs);
				}
			}, msUntilWarn);
		}

		if (msUntilIdle > 0) {
			this.idleTimerId = setTimeout(() => {
				if (!this.destroyed && this.isIdle()) {
					this.handleIdleExpiry();
				}
			}, msUntilIdle);
		}
	}

	private cancelIdleTimers(): void {
		if (this.warningTimerId !== null) {
			clearTimeout(this.warningTimerId);
			this.warningTimerId = null;
		}
		if (this.idleTimerId !== null) {
			clearTimeout(this.idleTimerId);
			this.idleTimerId = null;
		}
	}

	private resetIdleTimers(): void {
		this.scheduleIdleTimers();
	}

	private showWarning(remainingMs: number): void {
		this.warningShown = true;
		const detail: SessionWarningDetail = { remainingMs };
		window.dispatchEvent(new CustomEvent(SESSION_WARNING_EVENT, { detail }));
	}

	private handleIdleExpiry(): void {
		const detail: SessionExpiredDetail = { reason: 'idle' };
		window.dispatchEvent(new CustomEvent(SESSION_EXPIRED_EVENT, { detail }));
		this.opts.onSessionExpired('idle');
	}

	// ─────────────────────────────────────────────────────
	// Periodic Keycloak SSO session check (iframe)
	// ─────────────────────────────────────────────────────

	private startSsoCheckInterval(): void {
		if (this.opts.ssoCheckIntervalMs <= 0) return;

		this.ssoCheckIntervalId = setInterval(() => {
			if (!this.destroyed) void this.checkKeycloakSsoSession();
		}, this.opts.ssoCheckIntervalMs);
	}

	private stopSsoCheckInterval(): void {
		if (this.ssoCheckIntervalId !== null) {
			clearInterval(this.ssoCheckIntervalId);
			this.ssoCheckIntervalId = null;
		}
	}

	/**
	 * Comprueba silenciosamente si la sesión SSO de Keycloak sigue activa.
	 * Si el check falla (sesión terminada remotamente) → logout.
	 */
	private async checkKeycloakSsoSession(): Promise<void> {
		const kc = this.opts.getKeycloakInstance();
		if (!kc?.authenticated) return; // Solo aplica al flow SSO con Keycloak JS

		try {
			// updateToken(0) fuerza a Keycloak JS a intentar refrescar via SSO
			// Si la sesión SSO de Keycloak ha sido terminada, lanza un error
			await kc.updateToken(0);
		} catch {
			console.warn('[SessionManager] Keycloak SSO session ended remotely');
			const detail: SessionExpiredDetail = { reason: 'keycloak_session_ended' };
			window.dispatchEvent(new CustomEvent(SESSION_EXPIRED_EVENT, { detail }));
			this.opts.onSessionExpired('keycloak_session_ended');
		}
	}

	// ─────────────────────────────────────────────────────
	// Token refresh execution
	// ─────────────────────────────────────────────────────

	private async doRefresh(reason: string): Promise<void> {
		if (this.destroyed || this.isRefreshing) return;

		// No refrescar automáticamente si el usuario está idle
		// (excepto si es un refresh forzado por "extender sesión")
		if (reason === 'scheduled' && this.isIdle()) {
			console.info('[SessionManager] Omitiendo refresh — usuario inactivo');
			return;
		}

		this.isRefreshing = true;

		try {
			const kc = this.opts.getKeycloakInstance();
			let newToken: string | null = null;

			if (kc?.authenticated) {
				// ── Keycloak JS flow ──────────────────────────────────────────
				// updateToken intenta un silent refresh via iframe con la cookie
				// de sesión SSO de Keycloak.
				// Si el SSO session ha expirado, esto lanzará un error.
				const minValidity = this.opts.refreshBeforeExpirySeconds + 10;
				await kc.updateToken(minValidity);
				newToken = kc.token ?? null;
			} else {
				// ── Password login flow ───────────────────────────────────────
				// Usar el endpoint server-side de SvelteKit que lee el refresh_token
				// desde la cookie HttpOnly (el JS nunca ve el refresh_token).
				newToken = await this.silentRefreshViaCookie();
			}

			if (newToken) {
				this.currentToken = newToken;
				this.cancelRefreshTimer();
				this.scheduleRefresh(newToken);
				this.opts.onTokenRefreshed(newToken);
				window.dispatchEvent(
					new CustomEvent(SESSION_TOKEN_REFRESHED_EVENT, { detail: { token: newToken } })
				);
			} else {
				this.handleRefreshFailure();
			}
		} catch (err) {
			console.error('[SessionManager] Error durante refresh:', err);
			this.handleRefreshFailure();
		} finally {
			this.isRefreshing = false;
		}
	}

	private handleRefreshFailure(): void {
		console.warn('[SessionManager] Refresh fallido — la sesión SSO probablemente expiró');
		const detail: SessionExpiredDetail = { reason: 'refresh_failed' };
		window.dispatchEvent(new CustomEvent(SESSION_EXPIRED_EVENT, { detail }));
		this.opts.onSessionExpired('refresh_failed');
	}

	/**
	 * Llama al endpoint server-side de SvelteKit para realizar el refresh
	 * usando la cookie HttpOnly del refresh_token.
	 *
	 * El servidor lee la cookie, llama a Keycloak, obtiene los nuevos tokens,
	 * actualiza las cookies HttpOnly y devuelve el nuevo access_token al cliente.
	 * El refresh_token NUNCA toca el código JavaScript del cliente.
	 */
	private async silentRefreshViaCookie(): Promise<string | null> {
		try {
			const resp = await fetch('/api-sveltekit/auth/silent-refresh', {
				method: 'POST',
				credentials: 'include', // Envía todas las cookies HttpOnly
				headers: { 'Content-Type': 'application/json' }
			});

			if (!resp.ok) return null;

			const data = await resp.json();
			return (data as { access_token?: string }).access_token ?? null;
		} catch (err) {
			console.error('[SessionManager] Error en silentRefreshViaCookie:', err);
			return null;
		}
	}
}

// ─────────────────────────────────────────────────────────
// Singleton helpers
// ─────────────────────────────────────────────────────────

let _instance: SessionManager | null = null;

/** Obtiene la instancia singleton del SessionManager */
export function getSessionManager(): SessionManager | null {
	return _instance;
}

/**
 * Crea (o recrea) el SessionManager singleton.
 * Destruye la instancia anterior si existe.
 */
export function createSessionManager(options?: SessionManagerOptions): SessionManager {
	if (_instance) {
		_instance.destroy();
	}
	_instance = new SessionManager(options);
	return _instance;
}

/** Destruye el SessionManager singleton y limpia todos los recursos */
export function destroySessionManager(): void {
	if (_instance) {
		_instance.destroy();
		_instance = null;
	}
}
